├── .eslintrc.yml
├── .github
└── FUNDING.yml
├── COPYING
├── LICENSE
├── LICENSE-atom-autocomplete-php
├── keymaps
└── php-ide-serenata.cson
├── lib
├── AtomConfig.js
├── CancellablePromise.js
├── CodeLensManager.js
├── Config.js
├── ConfigTester.js
├── Main.js
├── PhpInvoker.js
├── ProjectManager.js
├── Proxy.js
├── Refactoring
│ ├── AbstractProvider.js
│ ├── ConstructorGenerationProvider.js
│ ├── ConstructorGenerationProvider
│ │ └── View.js
│ ├── DocblockProvider.js
│ ├── ExtractMethodProvider.js
│ ├── ExtractMethodProvider
│ │ ├── Builder.js
│ │ ├── ParameterParser.js
│ │ └── View.js
│ ├── GetterSetterProvider.js
│ ├── GetterSetterProvider
│ │ └── View.js
│ ├── IntroducePropertyProvider.js
│ ├── OverrideMethodProvider.js
│ ├── OverrideMethodProvider
│ │ └── View.js
│ ├── StubAbstractMethodProvider.js
│ ├── StubAbstractMethodProvider
│ │ └── View.js
│ ├── StubInterfaceMethodProvider.js
│ ├── StubInterfaceMethodProvider
│ │ └── View.js
│ └── Utility
│ │ ├── DocblockBuilder.js
│ │ ├── FunctionBuilder.js
│ │ ├── MultiSelectionView.js
│ │ └── TypeHelper.js
├── SerenataClient.js
├── ServerManager.js
├── ServiceContainer.js
└── UseStatementHelper.js
├── menus
└── menu.cson
├── package-lock.json
├── package.json
└── styles
└── main.less
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | node: true
4 | es6: true
5 | atomtest: true
6 | extends: 'eslint:recommended'
7 | parserOptions:
8 | sourceType: module
9 | ecmaVersion: 2017
10 | rules:
11 | indent:
12 | - error
13 | - 4
14 | linebreak-style:
15 | - error
16 | - unix
17 | quotes:
18 | - error
19 | - single
20 | -
21 | allowTemplateLiterals: true
22 | semi:
23 | - error
24 | - always
25 | no-console:
26 | - off
27 | max-len:
28 | - error
29 | -
30 | code: 120
31 | no-cond-assign:
32 | - off
33 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | #github: [Gert-dev]
4 | #patreon: # Replace with a single Patreon username
5 | #open_collective: # Replace with a single Open Collective username
6 | #ko_fi: # Replace with a single Ko-fi username
7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: Gert-dev
10 | #issuehunt: # Replace with a single IssueHunt username
11 | #otechie: # Replace with a single Otechie username
12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 | custom: ['https://www.paypal.me/gertdev']
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | © 2015 Tom Gerrits
2 |
3 | This program is free software: you can redistribute it and/or modify
4 | it under the terms of the GNU General Public License as published by
5 | the Free Software Foundation, either version 3 of the License, or
6 | (at your option) any later version.
7 |
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 |
13 | You should have received a copy of the GNU General Public License
14 | along with this program. If not, see .
15 |
--------------------------------------------------------------------------------
/LICENSE-atom-autocomplete-php:
--------------------------------------------------------------------------------
1 | This project was forked from atom-autocomplete-php, thus the original code base
2 | was licensed under the MIT license. It can still be found at [1]. The original
3 | license is located below.
4 |
5 | [1] https://github.com/Peekmo/atom-autocomplete-php
6 |
7 | The MIT License (MIT)
8 |
9 | Copyright (c) 2014-2015 Axel Anceau
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy of
12 | this software and associated documentation files (the "Software"), to deal in
13 | the Software without restriction, including without limitation the rights to
14 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
15 | the Software, and to permit persons to whom the Software is furnished to do so,
16 | subject to the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included in all
19 | copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
23 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
24 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
25 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
26 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 |
--------------------------------------------------------------------------------
/keymaps/php-ide-serenata.cson:
--------------------------------------------------------------------------------
1 | # Keybindings require three things to be fully defined: A selector that is
2 | # matched against the focused element, the keystroke and the command to
3 | # execute.
4 | #
5 | # Below is a basic keybinding which registers on all platforms by applying to
6 | # the root workspace element.
7 |
8 | # For more detailed documentation see
9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth
10 | 'atom-text-editor':
11 | 'alt-m': 'php-ide-serenata:extract-method'
12 |
--------------------------------------------------------------------------------
/lib/AtomConfig.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | 'use strict';
4 |
5 | const path = require('path');
6 | const process = require('process');
7 | const mkdirp = require('mkdirp');
8 |
9 | const Config = require('./Config');
10 |
11 | module.exports =
12 |
13 | class AtomConfig extends Config
14 | {
15 | /**
16 | * @inheritdoc
17 | */
18 | constructor(packageName) {
19 | super();
20 |
21 | this.packageName = packageName;
22 | this.configurableProperties = [
23 | 'core.phpExecutionType',
24 | 'core.phpCommand',
25 | 'core.memoryLimit',
26 | 'core.additionalDockerVolumes',
27 | 'general.doNotAskForSupport',
28 | 'general.doNotShowProjectChangeMessage',
29 | 'general.projectOpenCount',
30 | 'refactoring.enable',
31 | ];
32 |
33 | this.attachListeners();
34 | }
35 |
36 | /**
37 | * @inheritdoc
38 | */
39 | load() {
40 | this.set('storagePath', this.getPathToStorageFolderInRidiculousWay());
41 |
42 | this.configurableProperties.forEach((property) => {
43 | this.set(property, atom.config.get(`${this.packageName}.${property}`));
44 | });
45 | }
46 |
47 | /**
48 | * @inheritdoc
49 | */
50 | set(name, value) {
51 | super.set(name, value);
52 |
53 | atom.config.set(`${this.packageName}.${name}`, value);
54 | }
55 |
56 | /**
57 | * Attaches listeners to listen to Atom configuration changes.
58 | */
59 | attachListeners() {
60 | this.configurableProperties.forEach((property) => {
61 | atom.config.onDidChange(`${this.packageName}.${property}`, (data) => {
62 | this.set(property, data.newValue);
63 | });
64 | });
65 | }
66 |
67 | /**
68 | * @return {String}
69 | */
70 | getPathToStorageFolderInRidiculousWay() {
71 | // NOTE: Apparently process.env.ATOM_HOME is not always set for whatever reason and this ridiculous workaround
72 | // is needed to fetch an OS-compliant location to store application data.
73 | let baseFolder = null;
74 |
75 | if (process.env.APPDATA) {
76 | baseFolder = process.env.APPDATA;
77 | } else if (process.platform === 'darwin') {
78 | baseFolder = process.env.HOME + '/Library/Preferences';
79 | } else {
80 | baseFolder = process.env.HOME + '/.cache';
81 | }
82 |
83 | const packageFolder = baseFolder + path.sep + this.packageName;
84 |
85 | mkdirp.sync(packageFolder);
86 |
87 | return packageFolder;
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/lib/CancellablePromise.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Promise that can be cancelled.
7 | */
8 | class CancellablePromise //extends Promise
9 | {
10 | /**
11 | * Constructor.
12 | *
13 | * @param {Callable} executor
14 | * @param {Callable} cancelHandler
15 | */
16 | constructor(executor, cancelHandler)
17 | {
18 | this.isDone = false;
19 | this.promise = new Promise(executor);
20 |
21 | if (cancelHandler) {
22 | this.cancelHandler = cancelHandler;
23 | } else {
24 | this.cancelHandler = () => {
25 | // this.promise.reject('Promise cancelled');
26 | };
27 | }
28 | }
29 |
30 | /**
31 | * Cancels the promise.
32 | */
33 | cancel()
34 | {
35 | if (this.isDone !== true) {
36 | this.cancelHandler.call(this);
37 | }
38 | }
39 |
40 | /**
41 | * @param {callable} onFulfilled
42 | * @param {callable} onRejected
43 | *
44 | * @return {Promise}
45 | */
46 | then(onFulfilled, onRejected = undefined)
47 | {
48 | return this.promise.then(onFulfilled, onRejected);
49 | }
50 |
51 | /**
52 | * @param {callable} onRejected
53 | *
54 | * @return {Promise}
55 | */
56 | catch(onRejected)
57 | {
58 | return this.promise.catch(onRejected);
59 | }
60 |
61 | /**
62 | * @param {*} value
63 | *
64 | * @return {Promise}
65 | */
66 | resolve(value)
67 | {
68 | this.isDone = true;
69 |
70 | return this.promise.resolve(value);
71 | }
72 |
73 | /**
74 | * @param {*} reason
75 | *
76 | * @return {Promise}
77 | */
78 | reject(reason)
79 | {
80 | this.isDone = true;
81 |
82 | return this.promise.reject(reason);
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/lib/CodeLensManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Manages visual information around code lenses returned by the server.
7 | */
8 | class CodeLensManager
9 | {
10 | /**
11 | * Constructor.
12 | */
13 | constructor() {
14 | this.markers = {};
15 | this.annotations = {};
16 | }
17 |
18 | /**
19 | * @param {TextEditor} editor
20 | * @param {Array} codeLenses
21 | * @param {Callable} executeCommandHandler
22 | */
23 | process(editor, codeLenses, executeCommandHandler) {
24 | this.removeMarkers(editor);
25 |
26 | const {Convert} = require('atom-languageclient');
27 |
28 | // You cannot have multiple markers on the exact same line, you need to combine them into one element and
29 | // have one marker, so do some grouping up front.
30 | const codeLensesGroupedByLine = codeLenses.reduce((accumulator, codeLens) => {
31 | const range = Convert.lsRangeToAtomRange(codeLens.range);
32 |
33 | if (!accumulator.has(range.start.row)) {
34 | accumulator.set(range.start.row, new Array());
35 | }
36 |
37 | accumulator.get(range.start.row).push(codeLens);
38 |
39 | return accumulator;
40 | }, new Map());
41 |
42 | codeLensesGroupedByLine.forEach((codeLenses, line) => {
43 | this.processForLine(editor, codeLenses, line, executeCommandHandler);
44 | });
45 | }
46 |
47 | /**
48 | * @param {TextEditor} editor
49 | * @param {Array} codeLenses
50 | * @param {Number} line
51 | * @param {Callable} executeCommandHandler
52 | */
53 | processForLine(editor, codeLenses, line, executeCommandHandler) {
54 | const {Range, Point} = require('atom');
55 | const {Convert} = require('atom-languageclient');
56 |
57 | const marker = this.registerMarker(editor, new Range(new Point(line, 0), new Point(line, 1)), {
58 | invalidate : 'touch',
59 | });
60 |
61 | const codeLensLineElement = document.createElement('div');
62 | codeLensLineElement.classList.add('php-ide-serenata-code-lens-wrapper');
63 |
64 | let charactersTaken = 0;
65 |
66 | codeLenses.forEach((codeLens) => {
67 | if (!codeLens.command) {
68 | // To support this, one would have to send a resolve request and show some sort of placeholder
69 | // beforehand, as we wouldn't know what title to show yet.
70 | throw new Error('Code lenses with unresolved commands are currently not supported');
71 | }
72 |
73 | const range = Convert.lsRangeToAtomRange(codeLens.range);
74 | const paddingSpacesNeeded = range.start.column - charactersTaken;
75 |
76 | // Having one marker per line (see above) means that we need to do padding ourselves when multiple code
77 | // lenses are present. This can happen in cases where multiple properties are on one line, and more than one
78 | // of them is an override. ot great, but it gets the job done.
79 | const paddingSpanElement = document.createElement('span');
80 | paddingSpanElement.innerHTML = ' '.repeat(paddingSpacesNeeded);
81 |
82 | const anchorElement = document.createElement('a');
83 | anchorElement.innerHTML = codeLens.command.title;
84 | anchorElement.classList.add('badge');
85 | anchorElement.classList.add('badge-small');
86 | anchorElement.href = '#';
87 | anchorElement.addEventListener('click', () => {
88 | executeCommandHandler({
89 | command: codeLens.command.command,
90 | arguments: codeLens.command.arguments,
91 | });
92 | });
93 |
94 | charactersTaken += paddingSpacesNeeded + anchorElement.innerHTML.length ;
95 |
96 | const wrapperElement = document.createElement('div');
97 | wrapperElement.classList.add('php-ide-serenata-code-lens');
98 | // wrapperElement.style.marginLeft = range.start.column + 'em';
99 | wrapperElement.appendChild(paddingSpanElement);
100 | wrapperElement.appendChild(anchorElement);
101 |
102 | codeLensLineElement.appendChild(wrapperElement);
103 | });
104 |
105 | editor.decorateMarker(marker, {
106 | type: 'block',
107 | item: codeLensLineElement,
108 | });
109 | }
110 |
111 | /**
112 | * @param {TextEditor} editor
113 | * @param {Range} range
114 | * @param {Object} options
115 | *
116 | * @return {Object}
117 | */
118 | registerMarker(editor, range, options) {
119 | const marker = editor.markBufferRange(range, options);
120 |
121 | if (!(editor.id in this.markers)) {
122 | this.markers[editor.id] = [];
123 | }
124 |
125 | this.markers[editor.id].push(marker);
126 |
127 | return marker;
128 | }
129 |
130 | /**
131 | * @param {TextEditor} editor
132 | */
133 | removeMarkers(editor) {
134 | for (let i in this.markers[editor.id]) {
135 | const marker = this.markers[editor.id][i];
136 | marker.destroy();
137 | }
138 |
139 | this.markers[editor.id] = [];
140 | }
141 | };
142 |
--------------------------------------------------------------------------------
/lib/Config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Abstract base class for managing configurations.
7 | */
8 | class Config {
9 | constructor() {
10 | this.listeners = {};
11 |
12 | this.data = {
13 | 'core.phpExecutionType' : 'host',
14 | 'core.phpCommand' : null,
15 | 'core.memoryLimit' : 2048,
16 | 'core.additionalDockerVolumes' : [],
17 |
18 | 'general.doNotAskForSupport' : false,
19 | 'general.doNotShowProjectChangeMessage' : false,
20 | 'general.projectOpenCount' : 0,
21 |
22 | 'refactoring.enable' : true,
23 | };
24 | }
25 |
26 | load() {
27 | throw new Error('This method is abstract and must be implemented!');
28 | }
29 |
30 | onDidChange(name, callback) {
31 | if (!(name in this.listeners)) {
32 | this.listeners[name] = [];
33 | }
34 |
35 | this.listeners[name].push(callback);
36 | }
37 |
38 | get(name) {
39 | return this.data[name];
40 | }
41 |
42 | set(name, value) {
43 | this.data[name] = value;
44 |
45 | if (name in this.listeners) {
46 | this.listeners[name].map((listener) => {
47 | listener(value, name);
48 | });
49 | }
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/lib/ConfigTester.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Tests the user's PHP setup to see if it is properly usable.
7 | */
8 | class ConfigTester
9 | {
10 | /**
11 | * Constructor.
12 | *
13 | * @param {PhpInvoker} phpInvoker
14 | */
15 | constructor(phpInvoker) {
16 | this.phpInvoker = phpInvoker;
17 | }
18 |
19 | /**
20 | * @return {Promise}
21 | */
22 | test() {
23 | return new Promise((resolve) => {
24 | const process = this.phpInvoker.invoke(['-v']);
25 |
26 | process.on('close', (code) => {
27 | if (code === 0) {
28 | resolve(true);
29 | return;
30 | }
31 |
32 | resolve(false);
33 | });
34 | });
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/lib/Main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ServiceContainer = require('./ServiceContainer');
4 |
5 | const container = new ServiceContainer();
6 |
7 | module.exports = container.getSerenataClient();
8 |
--------------------------------------------------------------------------------
/lib/PhpInvoker.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | 'use strict';
4 |
5 | module.exports =
6 |
7 | /**
8 | * Invokes PHP.
9 | */
10 | class PhpInvoker
11 | {
12 | /**
13 | * Constructor.
14 | *
15 | * @param {Config} config
16 | */
17 | constructor(config) {
18 | this.config = config;
19 | }
20 |
21 | /**
22 | * Invokes PHP.
23 | *
24 | * NOTE: The composer:1.6.4 image uses the Alpine version of the "PHP 7.x" image of PHP, which at the time of
25 | * writing is PHP 7.2. The most important part is that the PHP version used for Composer installations is the same
26 | * as the one used for actually running the server to avoid outdated or too recent dependencies.
27 | *
28 | * @param {Array} parameters
29 | * @param {Array} additionalDockerRunParameters
30 | * @param {Object} options
31 | * @param {String} dockerImage
32 | *
33 | * @return {Process}
34 | */
35 | invoke(parameters, additionalDockerRunParameters = [], options = {}, dockerImage = 'composer:1.6.4') {
36 | const child_process = require('child_process');
37 | const executionType = this.config.get('core.phpExecutionType');
38 |
39 | if (executionType === 'host') {
40 | return child_process.spawn(this.config.get('core.phpCommand'), parameters, options);
41 | }
42 |
43 | let command = 'docker';
44 |
45 | if (executionType === 'podman') {
46 | additionalDockerRunParameters = ['-net=host'].concat(additionalDockerRunParameters);
47 | }
48 |
49 | let dockerParameters = this.getDockerRunParameters(dockerImage, additionalDockerRunParameters);
50 |
51 | dockerParameters = dockerParameters.concat(parameters);
52 |
53 | if (executionType === 'docker') {
54 | command = 'docker';
55 | } else if (executionType === 'docker-polkit') {
56 | dockerParameters = [command].concat(dockerParameters);
57 | command = 'pkexec';
58 | } else if (executionType === 'podman') {
59 | /*
60 | Add this to the PHP execution type config option in package.json to test this:
61 |
62 | {
63 | "value": "podman",
64 | "description": "Use a PHP container via Podman, avoiding privilege escalation entirely (experimental)"
65 | }
66 |
67 | Note that podman doesn't work yet because it doesn't support rootless port binding yet at the time of
68 | writing. I don't believe Serenata needs any special ports below 1024 or any other things that requires
69 | root, so if they implement support for this, we should be able to easily support it.
70 | */
71 | command = 'podman';
72 | } else {
73 | throw new Error('Unknown executionType "' + executionType + '" received');
74 | }
75 |
76 | const process = child_process.spawn(command, dockerParameters);
77 |
78 | console.debug(command, dockerParameters);
79 |
80 | // NOTE: Uncomment this to test failures
81 | process.stdout.on('data', data => {
82 | console.debug('STDOUT', data.toString());
83 | });
84 |
85 | process.stderr.on('data', data => {
86 | console.debug('STDERR', data.toString());
87 | });
88 |
89 | return process;
90 | }
91 |
92 | /**
93 | * @param {Array} additionalDockerRunParameters
94 | *
95 | * @return {Array}
96 | */
97 | getDockerRunParameters(dockerImage, additionalDockerRunParameters) {
98 | const parameters = ['run', '--rm=true'];
99 | const object = this.getPathsToMountInDockerContainer();
100 |
101 | for (const src in object) {
102 | const dest = object[src];
103 | parameters.push('-v');
104 | parameters.push(src + ':' + dest);
105 | }
106 |
107 | return parameters.concat(additionalDockerRunParameters).concat([dockerImage, 'php']);
108 | }
109 |
110 | /**
111 | * @return {Object}
112 | */
113 | getPathsToMountInDockerContainer() {
114 | const paths = {};
115 | paths[this.config.get('storagePath')] = this.config.get('storagePath');
116 |
117 | for (const path of this.config.get('core.additionalDockerVolumes')) {
118 | const parts = path.split(':');
119 |
120 | paths[parts[0]] = parts[1];
121 | }
122 |
123 | for (const path of atom.project.getPaths()) {
124 | paths[path] = path;
125 | }
126 |
127 | return this.normalizeVolumePaths(paths);
128 | }
129 |
130 | /**
131 | * @param {Object} pathMap
132 | *
133 | * @return {Object}
134 | */
135 | normalizeVolumePaths(pathMap) {
136 | const newPathMap = {};
137 |
138 | for (let src in pathMap) {
139 | const dest = pathMap[src];
140 | newPathMap[this.normalizeVolumePath(src)] = this.normalizeVolumePath(dest);
141 | }
142 |
143 | return newPathMap;
144 | }
145 |
146 | /**
147 | * @param {String} path
148 | *
149 | * @return {String}
150 | */
151 | normalizeVolumePath(path) {
152 | const os = require('os');
153 |
154 | if (os.platform() !== 'win32') {
155 | return path;
156 | }
157 |
158 | const matches = path.match(/^([A-Z]+):(.+)$/);
159 |
160 | if (matches !== null) {
161 | // On Windows, paths for Docker volumes are specified as /c/Path/To/Item.
162 | path = `/${matches[1].toLowerCase()}${matches[2]}`;
163 | }
164 |
165 | return path.replace(/\\/g, '/');
166 | }
167 |
168 | /**
169 | * @param {String} path
170 | *
171 | * @return {String}
172 | */
173 | denormalizeVolumePath(path) {
174 | const os = require('os');
175 |
176 | if (os.platform() !== 'win32') {
177 | return path;
178 | }
179 |
180 | const matches = path.match(/^\/([a-z]+)\/(.+)$/);
181 |
182 | if (matches !== null) {
183 | // On Windows, paths for Docker volumes are specified as /c/Path/To/Item.
184 | path = matches[1].toUpperCase() + ':\\' + matches[2];
185 | }
186 |
187 | return path.replace(/\//g, '\\');
188 | }
189 |
190 | /**
191 | * Retrieves a path normalized for the current platform *and* runtime.
192 | *
193 | * On Windows, we still need UNIX paths if we are using Docker as runtime,
194 | * but not if we are using the host PHP.
195 | *
196 | * @param {String} path
197 | *
198 | * @return {String}
199 | */
200 | normalizePlatformAndRuntimePath(path) {
201 | if (this.config.get('core.phpExecutionType') === 'host') {
202 | return path;
203 | }
204 |
205 | return this.normalizeVolumePath(path);
206 | }
207 |
208 | /**
209 | * Retrieves a path denormalized for the current platform *and* runtime.
210 | *
211 | * On Windows, this converts Docker paths back to Windows paths.
212 | *
213 | * @param {String} path
214 | *
215 | * @return {String}
216 | */
217 | denormalizePlatformAndRuntimePath(path) {
218 | if (this.config.get('core.phpExecutionType') === 'host') {
219 | return path;
220 | }
221 |
222 | return this.denormalizeVolumePath(path);
223 | }
224 | };
225 |
--------------------------------------------------------------------------------
/lib/ProjectManager.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | 'use strict';
4 |
5 | const fs = require('fs');
6 | const mkdirp = require('mkdirp');
7 |
8 | module.exports =
9 |
10 | class ProjectManager
11 | {
12 | /**
13 | * @param {Object} proxy
14 | */
15 | constructor(proxy, config) {
16 | this.proxy = proxy;
17 | this.config = config;
18 | this.activeProject = null;
19 | }
20 |
21 | /**
22 | * Sets up the specified project for usage with the Serenata server.
23 | *
24 | * @param {String} mainFolder
25 | */
26 | setUpProject(mainFolder) {
27 | // const process = require('process');
28 | //
29 | // // Great, URI's are supposed to be file:// + host (optional) + path, which on UNIX systems becomes something
30 | // // like file:///my/folder if the host is omitted. Microsoft decided to do it differently and does
31 | // // file:///c:/my/path instead of file://C:/my/path, so add an extra slash and lower case the drive letter.
32 | // if (process.platform === 'win32') {
33 | // mainFolder = '/' + mainFolder.substr(0, 1).toLowerCase() + mainFolder.substr(1);
34 | // }
35 |
36 | const path = require('path');
37 | const crypto = require('crypto');
38 |
39 | const md5 = crypto.createHash('md5');
40 | const configFileFolderPath = mainFolder + '/.serenata';
41 | const configFilePath = configFileFolderPath + '/config.json';
42 |
43 | // NOTE: I wanted to place the index inside the .serenata folder, but it turns out that is a very bad idea.
44 | // Pulsar will start firing massive amounts of change requests, due to it watching the database file, every time
45 | // it is modified. We can disable this for our package, but not for other packages, which will still receive
46 | // these events en masse uselessly, which not only prevents any other responses from being handled in the
47 | // meantime, it also spikes CPU usage.
48 | const indexDatabaseUri = 'file://' + path.join(
49 | this.config.get('storagePath'),
50 | 'index-' + md5.update(mainFolder).digest('hex') + '.sqlite'
51 | );
52 |
53 | if (fs.existsSync(configFilePath)) {
54 | throw new Error(
55 | 'The currently active project was already initialized. To prevent existing settings from being ' +
56 | 'lost, the request has been aborted.'
57 | );
58 | }
59 |
60 | const template =
61 | `{
62 | "uris": [
63 | "file://${mainFolder.replace(/\\/g, '\\\\')}"
64 | ],
65 | "indexDatabaseUri": "${indexDatabaseUri.replace(/\\/g, '\\\\')}",
66 | "phpVersion": 7.3,
67 | "excludedPathExpressions": [],
68 | "fileExtensions": [
69 | "php"
70 | ]
71 | }`;
72 |
73 | mkdirp.sync(configFileFolderPath);
74 | fs.writeFileSync(configFilePath, template);
75 | }
76 |
77 | /**
78 | * @param {String} projectFolder
79 | *
80 | * @return {Boolean}
81 | */
82 | shouldStartForProject(projectFolder) {
83 | return fs.existsSync(this.getConfigFilePath(projectFolder));
84 | }
85 |
86 | /**
87 | * @param {String} projectFolder
88 | */
89 | tryLoad(projectFolder) {
90 | if (!this.shouldStartForProject(projectFolder)) {
91 | return;
92 | }
93 |
94 | this.load(projectFolder);
95 | }
96 |
97 | /**
98 | * @param {String} projectFolder
99 | */
100 | load(projectFolder) {
101 | const path = this.getConfigFilePath(projectFolder);
102 |
103 | try {
104 | this.activeProject = JSON.parse(fs.readFileSync(path));
105 | } catch (e) {
106 | const message =
107 | 'Loading project configuration in **' + path + '** failed. It likely contains syntax errors. \n \n' +
108 |
109 | 'The error message returned was:\n \n' +
110 |
111 | '```' + e + '```';
112 |
113 | atom.notifications.addError('Serenata - Loading Project Failed', {
114 | description: message,
115 | dismissable: true
116 | });
117 |
118 | throw new Error('Loading project at "' + path + '" failed due to it not being valid JSON');
119 | }
120 | }
121 |
122 | /**
123 | * @param {String} projectFolder
124 | *
125 | * @return {String}
126 | */
127 | getConfigFilePath(projectFolder) {
128 | return projectFolder + '/.serenata/config.json';
129 | }
130 |
131 | /**
132 | * @return {Object|null}
133 | */
134 | getCurrentProjectSettings() {
135 | return this.activeProject;
136 | }
137 | };
138 |
--------------------------------------------------------------------------------
/lib/Proxy.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const net = require('net');
4 |
5 | module.exports =
6 |
7 | class Proxy {
8 | /**
9 | * Constructor.
10 | *
11 | * @param {Config} config
12 | * @param {PhpInvoker} phpInvoker
13 | */
14 | constructor(config, phpInvoker) {
15 | this.serverPath = null;
16 | this.config = config;
17 | this.phpInvoker = phpInvoker;
18 | this.port = this.generateRandomServerPort();
19 | }
20 |
21 | /**
22 | * Spawns the PHP socket server process.
23 | *
24 | * @param {Number} port
25 | *
26 | * @return {Promise}
27 | */
28 | async spawnPhpServer(port) {
29 | const memoryLimit = this.config.get('core.memoryLimit');
30 | const socketHost = this.config.get('core.phpExecutionType') === 'host' ? '127.0.0.1' : '0.0.0.0';
31 |
32 | const parameters = [
33 | // Enable this to debug or profile using Xdebug. You will also need to allow Xdebug below.
34 | //'-d zend_extension=/usr/lib/php/modules/xdebug.so',
35 | //'-d xdebug.profiler_enable=On',
36 | // // '-d xdebug.gc_stats_enable=On',
37 | //'-d xdebug.profiler_output_dir=/tmp',
38 |
39 | `-d memory_limit=${memoryLimit}M`,
40 | this.phpInvoker.normalizePlatformAndRuntimePath(this.serverPath) + 'distribution.phar',
41 | `--uri=tcp://${socketHost}:${port}`
42 | ];
43 |
44 | const additionalDockerRunParameters = [
45 | '-p', `127.0.0.1:${port}:${port}`
46 | ];
47 |
48 | const options = {
49 | // Enable this to debug or profile as well.
50 | // env: {
51 | // SERENATA_ALLOW_XDEBUG: 1
52 | // }
53 | };
54 |
55 | const process = this.phpInvoker.invoke(parameters, additionalDockerRunParameters, options);
56 |
57 | return new Promise((resolve, reject) => {
58 | process.stdout.on('data', data => {
59 | const message = data.toString();
60 |
61 | console.debug('The PHP server has something to say:', message);
62 |
63 | if (message.indexOf('Starting server bound') !== -1) {
64 | // Assume the server has successfully spawned the moment it says it's listening.
65 | resolve(process);
66 | }
67 | });
68 |
69 | process.stderr.on('data', data => {
70 | console.error('The PHP server has errors to report:', data.toString());
71 | });
72 |
73 | process.on('close', (code) => {
74 | if (code === 2) {
75 | console.error(`Port ${port} is already taken`);
76 | } else if (code !== 0 && code !== null) {
77 | const detail =
78 | 'Serenata unexpectedly closed. Either something caused the process to stop, it crashed, ' +
79 | 'or the socket closed. In case of the first two, you should see additional output ' +
80 | 'indicating this is the case and you can report a bug. If there is no additional output, ' +
81 | 'you may be missing the right dependencies or extensions or the server may have run out ' +
82 | 'of memory (you can increase it via the settings screen).';
83 |
84 | console.error(detail);
85 | }
86 |
87 | reject();
88 | });
89 | });
90 | }
91 |
92 | /**
93 | * @return {Number}
94 | */
95 | generateRandomServerPort() {
96 | const minPort = 10000;
97 | const maxPort = 40000;
98 |
99 | return Math.floor((Math.random() * (maxPort - minPort)) + minPort);
100 | }
101 |
102 | /**
103 | * @return {Promise}
104 | */
105 | async getSocketConnection() {
106 | const phpServer = await this.spawnPhpServer(this.port);
107 |
108 | return new Promise((resolve) => {
109 | const client = net.createConnection({port: this.port}, () => {
110 | resolve([client, phpServer]);
111 | });
112 |
113 | client.setNoDelay(true);
114 | client.on('error', this.onSocketError.bind(this));
115 | });
116 | }
117 |
118 | /**
119 | * @param {Object} error
120 | */
121 | onSocketError(error) {
122 | // Do nothing here, this should silence socket errors such as ECONNRESET. After this is called, the socket
123 | // will be closed and all handling is performed there.
124 | console.debug(
125 | 'The socket connection notified us of an error (this is normal if the server is shutdown).',
126 | error
127 | );
128 | }
129 |
130 | /**
131 | * @param {String} serverPath
132 | */
133 | setServerPath(serverPath) {
134 | this.serverPath = serverPath;
135 | }
136 | };
137 |
--------------------------------------------------------------------------------
/lib/Refactoring/AbstractProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | module.exports =
10 |
11 | /**
12 | * Base class for providers.
13 | */
14 | class AbstractProvider {
15 | /**
16 | * Constructor.
17 | */
18 | constructor() {
19 | /**
20 | * The service (that can be used to query the source code and contains utility methods).
21 | */
22 | this.service = null;
23 |
24 | /**
25 | * Service to insert snippets into the editor.
26 | */
27 | this.snippetManager = null;
28 | }
29 |
30 | /**
31 | * Initializes this provider.
32 | *
33 | * @param {mixed} service
34 | */
35 | activate(service) {
36 | this.service = service;
37 | const dependentPackage = 'language-php';
38 |
39 | // It could be that the dependent package is already active, in that case we can continue immediately.
40 | // If not, we'll need to wait for the listener to be invoked
41 | if (atom.packages.isPackageActive(dependentPackage)) {
42 | this.doActualInitialization();
43 | }
44 |
45 | atom.packages.onDidActivatePackage(packageData => {
46 | if (packageData.name !== dependentPackage) { return; }
47 |
48 | return this.doActualInitialization();
49 | });
50 |
51 | return atom.packages.onDidDeactivatePackage(packageData => {
52 | if (packageData.name !== dependentPackage) { return; }
53 |
54 | return this.deactivate();
55 | });
56 | }
57 |
58 | /**
59 | * Does the actual initialization.
60 | */
61 | doActualInitialization() {
62 | atom.workspace.observeTextEditors(editor => {
63 | if (/text.html.php$/.test(editor.getGrammar().scopeName)) {
64 | return this.registerEvents(editor);
65 | }
66 | });
67 |
68 | // When you go back to only have one pane the events are lost, so need to re-register.
69 | atom.workspace.onDidDestroyPane(() => {
70 | const panes = atom.workspace.getPanes();
71 |
72 | if (panes.length === 1) {
73 | return this.registerEventsForPane(panes[0]);
74 | }
75 | });
76 |
77 | // Having to re-register events as when a new pane is created the old panes lose the events.
78 | return atom.workspace.onDidAddPane(observedPane => {
79 | const panes = atom.workspace.getPanes();
80 |
81 | const result = [];
82 |
83 | for (const pane of panes) {
84 | if (pane !== observedPane) {
85 | result.push(this.registerEventsForPane(pane));
86 | } else {
87 | result.push(undefined);
88 | }
89 | }
90 |
91 | return result;
92 | });
93 | }
94 |
95 | /**
96 | * Registers the necessary event handlers for the editors in the specified pane.
97 | *
98 | * @param {Pane} pane
99 | */
100 | registerEventsForPane(pane) {
101 | const result = [];
102 |
103 | for (const paneItem of pane.items) {
104 | if (atom.workspace.isTextEditor(paneItem)) {
105 | if (/text.html.php$/.test(paneItem.getGrammar().scopeName)) {
106 | result.push(this.registerEvents(paneItem));
107 | } else {
108 | result.push(undefined);
109 | }
110 | } else {
111 | result.push(undefined);
112 | }
113 | }
114 |
115 | return result;
116 | }
117 |
118 | /**
119 | * Deactives the provider.
120 | */
121 | deactivate() {}
122 |
123 | /**
124 | * Retrieves intention providers (by default, the intentions menu shows when the user presses alt-enter).
125 | *
126 | * This method should be overwritten by subclasses.
127 | *
128 | * @return {array}
129 | */
130 | getIntentionProviders() {
131 | return [];
132 | }
133 |
134 | /**
135 | * Registers the necessary event handlers.
136 | *
137 | * @param {TextEditor} _ TextEditor to register events to.
138 | */
139 | // eslint-disable-next-line no-unused-vars
140 | registerEvents(_) {}
141 |
142 | /**
143 | * Sets the snippet manager
144 | *
145 | * @param {Object} @snippetManager
146 | */
147 | setSnippetManager(snippetManager) {
148 | this.snippetManager = snippetManager;
149 | }
150 |
151 | /**
152 | * @return {Number|null}
153 | */
154 | getCurrentProjectPhpVersion() {
155 | const projectSettings = this.service.getCurrentProjectSettings();
156 |
157 | if (projectSettings != null) {
158 | return projectSettings.phpVersion;
159 | }
160 |
161 | return null;
162 | }
163 |
164 | /**
165 | * @param {Array} functionParameters
166 | * @param {String} editorUri
167 | * @param {Position} bufferPosition
168 | *
169 | * @return {Array}
170 | */
171 | async localizeFunctionParameterTypeHints(functionParameters, editorUri, bufferPosition) {
172 | await Promise.all(functionParameters.map(async (parameter) => {
173 | if (!parameter.typeHint) {
174 | return parameter.typeHint;
175 | }
176 |
177 | parameter.typeHint = await this.service.localizeType(
178 | editorUri,
179 | bufferPosition,
180 | parameter.typeHint,
181 | 'classlike'
182 | );
183 |
184 | return parameter;
185 | }));
186 | }
187 |
188 | /**
189 | * @param {Array} functionParameters
190 | * @param {String} editorUri
191 | * @param {Position} bufferPosition
192 | *
193 | * @return {Array}
194 | */
195 | async localizeFunctionParametersTypes(functionParameters, editorUri, bufferPosition) {
196 | await Promise.all(functionParameters.map(async (parameter) => {
197 | await this.localizeFunctionParameterTypes(parameter, editorUri, bufferPosition);
198 | }));
199 | }
200 |
201 | /**
202 | * @param {Object} functionParameter
203 | * @param {String} editorUri
204 | * @param {Position} bufferPosition
205 | *
206 | * @return {Array}
207 | */
208 | async localizeFunctionParameterTypes(functionParameter, editorUri, bufferPosition, property = 'types') {
209 | await Promise.all(functionParameter[property].map(async (type) => {
210 | if (!type.type) {
211 | return type.type;
212 | }
213 |
214 | type.type = await this.service.localizeType(
215 | editorUri,
216 | bufferPosition,
217 | type.type,
218 | 'classlike'
219 | );
220 |
221 | return type;
222 | }));
223 | }
224 | };
225 |
--------------------------------------------------------------------------------
/lib/Refactoring/ConstructorGenerationProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | /**
14 | * Provides docblock generation and maintenance capabilities.
15 | */
16 | class ConstructorGenerationProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} typeHelper
19 | * @param {Object} functionBuilder
20 | * @param {Object} docblockBuilder
21 | */
22 | constructor(typeHelper, functionBuilder, docblockBuilder) {
23 | super();
24 |
25 | /**
26 | * The view that allows the user to select the properties to add to the constructor as parameters.
27 | */
28 | this.selectionView = null;
29 |
30 | /**
31 | * Aids in building functions.
32 | */
33 | this.functionBuilder = functionBuilder;
34 |
35 | /**
36 | * The docblock builder.
37 | */
38 | this.docblockBuilder = docblockBuilder;
39 |
40 | /**
41 | * The type helper.
42 | */
43 | this.typeHelper = typeHelper;
44 | }
45 |
46 | /**
47 | * @inheritdoc
48 | */
49 | deactivate() {
50 | super.deactivate();
51 |
52 | if (this.selectionView) {
53 | this.selectionView.destroy();
54 | this.selectionView = null;
55 | }
56 | }
57 |
58 | /**
59 | * @inheritdoc
60 | */
61 | getIntentionProviders() {
62 | return [{
63 | grammarScopes: ['source.php'],
64 | getIntentions: ({textEditor, bufferPosition}) => {
65 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
66 |
67 | return this.getIntentions(textEditor, bufferPosition);
68 | }
69 | }];
70 | }
71 |
72 | /**
73 | * @param {TextEditor} editor
74 | * @param {Point} triggerPosition
75 | */
76 | getIntentions(editor, triggerPosition) {
77 | const successHandler = currentClassName => {
78 | if ((currentClassName == null)) { return []; }
79 |
80 | const nestedSuccessHandler = classInfo => {
81 | if ((classInfo == null)) { return []; }
82 |
83 | return [{
84 | priority : 100,
85 | icon : 'gear',
86 | title : 'Generate Constructor',
87 |
88 | selected : () => {
89 | const items = [];
90 | const promises = [];
91 |
92 | const localTypesResolvedHandler = results => {
93 | let resultIndex = 0;
94 |
95 | for (const item of items) {
96 | for (const type of item.types) {
97 | type.type = results[resultIndex++];
98 | }
99 | }
100 |
101 | const tabText = editor.getTabText();
102 | const indentationLevel = editor.indentationForBufferRow(triggerPosition.row);
103 |
104 | return this.generateConstructor(
105 | editor,
106 | triggerPosition,
107 | items,
108 | tabText,
109 | indentationLevel,
110 | atom.config.get(
111 | 'editor.preferredLineLength',
112 | editor.getLastCursor().getScopeDescriptor()
113 | )
114 | );
115 | };
116 |
117 | if (classInfo.properties.length === 0) {
118 | return localTypesResolvedHandler([]);
119 |
120 | } else {
121 | // Ensure all types are localized to the use statements of this file, the original
122 | // types will be relative to the original file (which may not be the same).
123 | // The FQCN works but is long and there may be a local use statement that can be used
124 | // to shorten it.
125 | for (let name in classInfo.properties) {
126 | const property = classInfo.properties[name];
127 | items.push({
128 | name,
129 | types : property.types
130 | });
131 |
132 | for (const type of property.types) {
133 | if (this.typeHelper.isClassType(type.type)) {
134 | promises.push(this.service.localizeType(
135 | 'file://' + editor.getPath(),
136 | triggerPosition,
137 | type.type,
138 | 'classlike'
139 | )
140 | );
141 |
142 | } else {
143 | promises.push(Promise.resolve(type.type));
144 | }
145 | }
146 | }
147 |
148 | return Promise.all(promises).then(localTypesResolvedHandler, failureHandler);
149 | }
150 | }
151 | }];
152 | };
153 |
154 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
155 | };
156 |
157 | var failureHandler = () => [];
158 |
159 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
160 | }
161 |
162 | /**
163 | * @param {TextEditor} editor
164 | * @param {Point} triggerPosition
165 | * @param {Array} items
166 | * @param {String} tabText
167 | * @param {Number} indentationLevel
168 | * @param {Number} maxLineLength
169 | */
170 | generateConstructor(editor, triggerPosition, items, tabText, indentationLevel, maxLineLength) {
171 | const metadata = {
172 | editor,
173 | position: triggerPosition,
174 | tabText,
175 | indentationLevel,
176 | maxLineLength
177 | };
178 |
179 | if (items.length > 0) {
180 | this.getSelectionView().setItems(items);
181 | this.getSelectionView().setMetadata(metadata);
182 | this.getSelectionView().storeFocusedElement();
183 | return this.getSelectionView().present();
184 |
185 | } else {
186 | return this.onConfirm([], metadata);
187 | }
188 | }
189 |
190 | /**
191 | * Called when the selection of properties is cancelled.
192 | */
193 | onCancel() {}
194 |
195 | /**
196 | * Called when the selection of properties is confirmed.
197 | *
198 | * @param {Array} selectedItems
199 | * @param {Object|null} metadata
200 | */
201 | onConfirm(selectedItems, metadata) {
202 | const statements = [];
203 | const parameters = [];
204 | const docblockParameters = [];
205 |
206 | for (const item of selectedItems) {
207 | const typeSpecification = this.typeHelper.buildTypeSpecificationFromTypeArray(item.types);
208 | const parameterTypeHint = this.typeHelper.getTypeHintForTypeSpecification(typeSpecification);
209 |
210 | parameters.push({
211 | name : `$${item.name}`,
212 | typeHint : parameterTypeHint ? parameterTypeHint.typeHint : null,
213 | defaultValue : parameterTypeHint ?
214 | (parameterTypeHint.shouldSetDefaultValueToNull ? 'null' : null) :
215 | null
216 | });
217 |
218 | docblockParameters.push({
219 | name : `$${item.name}`,
220 | type : item.types.length > 0 ? typeSpecification : 'mixed'
221 | });
222 |
223 | statements.push(`$this->${item.name} = $${item.name};`);
224 | }
225 |
226 | if (statements.length === 0) {
227 | statements.push('');
228 | }
229 |
230 | const functionText = this.functionBuilder
231 | .makePublic()
232 | .setIsStatic(false)
233 | .setIsAbstract(false)
234 | .setName('__construct')
235 | .setReturnType(null)
236 | .setParameters(parameters)
237 | .setStatements(statements)
238 | .setTabText(metadata.tabText)
239 | .setIndentationLevel(metadata.indentationLevel)
240 | .setMaxLineLength(metadata.maxLineLength)
241 | .build();
242 |
243 | const docblockText = this.docblockBuilder.buildForMethod(
244 | docblockParameters,
245 | null,
246 | false,
247 | metadata.tabText.repeat(metadata.indentationLevel)
248 | );
249 |
250 | const text = docblockText.trimLeft() + functionText;
251 |
252 | return metadata.editor.getBuffer().insert(metadata.position, text);
253 | }
254 |
255 | /**
256 | * @return {Builder}
257 | */
258 | getSelectionView() {
259 | if ((this.selectionView == null)) {
260 | const View = require('./ConstructorGenerationProvider/View');
261 |
262 | this.selectionView = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
263 | this.selectionView.setLoading('Loading class information...');
264 | this.selectionView.setEmptyMessage('No properties found.');
265 | }
266 |
267 | return this.selectionView;
268 | }
269 | };
270 |
--------------------------------------------------------------------------------
/lib/Refactoring/ConstructorGenerationProvider/View.js:
--------------------------------------------------------------------------------
1 | const MultiSelectionView = require('../Utility/MultiSelectionView');
2 |
3 | module.exports =
4 |
5 | class View extends MultiSelectionView {};
6 |
--------------------------------------------------------------------------------
/lib/Refactoring/DocblockProvider.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS207: Consider shorter variations of null checks
5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
6 | */
7 | const {Point} = require('atom');
8 |
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | //#*
14 | // Provides docblock generation and maintenance capabilities.
15 | //#
16 | class DocblockProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} typeHelper
19 | * @param {Object} docblockBuilder
20 | */
21 | constructor(typeHelper, docblockBuilder) {
22 | super();
23 |
24 | /**
25 | * The docblock builder.
26 | */
27 | this.docblockBuilder = docblockBuilder;
28 |
29 | /**
30 | * The type helper.
31 | */
32 | this.typeHelper = typeHelper;
33 | }
34 |
35 | /**
36 | * @inheritdoc
37 | */
38 | getIntentionProviders() {
39 | return [{
40 | grammarScopes: [
41 | 'entity.name.type.class.php',
42 | 'entity.name.type.interface.php',
43 | 'entity.name.type.trait.php'
44 | ],
45 | getIntentions: ({textEditor, bufferPosition}) => {
46 | const nameRange = textEditor.bufferRangeForScopeAtCursor('entity.name.type');
47 |
48 | if ((nameRange == null)) { return []; }
49 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
50 |
51 | const name = textEditor.getTextInBufferRange(nameRange);
52 |
53 | return this.getClasslikeIntentions(textEditor, bufferPosition, name);
54 | }
55 | }, {
56 | grammarScopes: ['entity.name.function.php', 'support.function.magic.php'],
57 | getIntentions: ({textEditor, bufferPosition}) => {
58 | let nameRange = textEditor.bufferRangeForScopeAtCursor('entity.name.function.php');
59 |
60 | if ((nameRange == null)) {
61 | nameRange = textEditor.bufferRangeForScopeAtCursor('support.function.magic.php');
62 | }
63 |
64 | if ((nameRange == null)) { return []; }
65 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
66 |
67 | const name = textEditor.getTextInBufferRange(nameRange);
68 |
69 | return this.getFunctionlikeIntentions(textEditor, bufferPosition, name);
70 | }
71 | }, {
72 | grammarScopes: ['variable.other.php'],
73 | getIntentions: ({textEditor, bufferPosition}) => {
74 | const nameRange = textEditor.bufferRangeForScopeAtCursor('variable.other.php');
75 |
76 | if ((nameRange == null)) { return []; }
77 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
78 |
79 | const name = textEditor.getTextInBufferRange(nameRange);
80 |
81 | return this.getPropertyIntentions(textEditor, bufferPosition, name);
82 | }
83 | }, {
84 | grammarScopes: ['constant.other.php'],
85 | getIntentions: ({textEditor, bufferPosition}) => {
86 | const nameRange = textEditor.bufferRangeForScopeAtCursor('constant.other.php');
87 |
88 | if ((nameRange == null)) { return []; }
89 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
90 |
91 | const name = textEditor.getTextInBufferRange(nameRange);
92 |
93 | return this.getConstantIntentions(textEditor, bufferPosition, name);
94 | }
95 | }];
96 | }
97 |
98 | /**
99 | * @inheritdoc
100 | */
101 | deactivate() {
102 | super.deactivate();
103 |
104 | if (this.docblockBuilder) {
105 | //@docblockBuilder.destroy()
106 | this.docblockBuilder = null;
107 | }
108 | }
109 |
110 | /**
111 | * @param {TextEditor} editor
112 | * @param {Point} triggerPosition
113 | * @param {String} name
114 | */
115 | getClasslikeIntentions(editor, triggerPosition, name) {
116 | const failureHandler = () => [];
117 |
118 | const successHandler = resolvedType => {
119 | const nestedSuccessHandler = classInfo => {
120 | const intentions = [];
121 |
122 | if ((classInfo == null)) { return intentions; }
123 |
124 | if (!classInfo.hasDocblock) {
125 | if (classInfo.hasDocumentation) {
126 | intentions.push({
127 | priority : 100,
128 | icon : 'gear',
129 | title : 'Generate Docblock (inheritDoc)',
130 |
131 | selected : () => {
132 | return this.generateDocblockInheritance(editor, triggerPosition);
133 | }
134 | });
135 | }
136 |
137 | intentions.push({
138 | priority : 100,
139 | icon : 'gear',
140 | title : 'Generate Docblock',
141 |
142 | selected : () => {
143 | return this.generateClasslikeDocblockFor(editor, classInfo);
144 | }
145 | });
146 | }
147 |
148 | return intentions;
149 | };
150 |
151 | return this.service.getClassInfo(resolvedType).then(nestedSuccessHandler, failureHandler);
152 | };
153 |
154 | return this.service.resolveType('file://' + editor.getPath(), triggerPosition, name, 'classlike')
155 | .then(successHandler, failureHandler);
156 | }
157 |
158 | /**
159 | * @param {TextEditor} editor
160 | * @param {Object} classData
161 | */
162 | generateClasslikeDocblockFor(editor, classData) {
163 | const zeroBasedStartLine = classData.range.start.line;
164 |
165 | const indentationLevel = editor.indentationForBufferRow(zeroBasedStartLine);
166 |
167 | const docblock = this.docblockBuilder.buildByLines(
168 | [],
169 | editor.getTabText().repeat(indentationLevel)
170 | );
171 |
172 | return editor.getBuffer().insert(new Point(zeroBasedStartLine, -1), docblock);
173 | }
174 |
175 | /**
176 | * @param {TextEditor} editor
177 | * @param {Point} triggerPosition
178 | * @param {String} name
179 | */
180 | getFunctionlikeIntentions(editor, triggerPosition, name) {
181 | const failureHandler = () => {
182 | return [];
183 | };
184 |
185 | const successHandler = currentClassName => {
186 | let nestedSuccessHandler;
187 | const helperFunction = functionlikeData => {
188 | const intentions = [];
189 |
190 | if (!functionlikeData) { return intentions; }
191 |
192 | if (!functionlikeData.hasDocblock) {
193 | if (functionlikeData.hasDocumentation) {
194 | intentions.push({
195 | priority : 100,
196 | icon : 'gear',
197 | title : 'Generate Docblock (inheritDoc)',
198 |
199 | selected : () => {
200 | return this.generateDocblockInheritance(editor, triggerPosition);
201 | }
202 | });
203 | }
204 |
205 | intentions.push({
206 | priority : 100,
207 | icon : 'gear',
208 | title : 'Generate Docblock',
209 |
210 | selected : async () => {
211 | await this.generateFunctionlikeDocblockFor(editor, functionlikeData);
212 | }
213 | });
214 | }
215 |
216 | return intentions;
217 | };
218 |
219 | if (currentClassName) {
220 | nestedSuccessHandler = classInfo => {
221 | if (!(name in classInfo.methods)) { return []; }
222 | return helperFunction(classInfo.methods[name]);
223 | };
224 |
225 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
226 |
227 | } else {
228 | nestedSuccessHandler = globalFunctions => {
229 | if (!(name in globalFunctions)) { return []; }
230 | return helperFunction(globalFunctions[name]);
231 | };
232 |
233 | return this.service.getGlobalFunctions().then(nestedSuccessHandler, failureHandler);
234 | }
235 | };
236 |
237 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
238 | }
239 |
240 | /**
241 | * @param {TextEditor} editor
242 | * @param {Object} data
243 | */
244 | async generateFunctionlikeDocblockFor(editor, data) {
245 | const zeroBasedStartLine = data.range.start.line;
246 |
247 | await this.localizeFunctionParametersTypes(
248 | data.parameters,
249 | 'file://' + editor.getPath(),
250 | new Point(data.range.start.line, data.range.start.character),
251 | );
252 |
253 | const parameters = await Promise.all(data.parameters.map(async (parameter) => {
254 | let type = 'mixed';
255 |
256 | if (parameter.types.length > 0) {
257 | type = this.typeHelper.buildTypeSpecificationFromTypeArray(parameter.types);
258 | }
259 |
260 | let name = '';
261 |
262 | if (parameter.isReference) {
263 | name += '&';
264 | }
265 |
266 | name += `$${parameter.name}`;
267 |
268 | if (parameter.isVariadic) {
269 | name = `...${name}`;
270 | type += '[]';
271 | }
272 |
273 | return {
274 | name,
275 | type
276 | };
277 | }));
278 |
279 | const indentationLevel = editor.indentationForBufferRow(zeroBasedStartLine);
280 |
281 | let returnType = null;
282 |
283 | if ((data.returnTypes.length > 0) && (data.name !== '__construct')) {
284 | await this.localizeFunctionParameterTypes(
285 | data,
286 | 'file://' + editor.getPath(),
287 | new Point(data.range.start.line, data.range.start.character),
288 | 'returnTypes'
289 | );
290 |
291 | returnType = this.typeHelper.buildTypeSpecificationFromTypeArray(data.returnTypes);
292 | }
293 |
294 | const docblock = this.docblockBuilder.buildForMethod(
295 | parameters,
296 | returnType,
297 | false,
298 | editor.getTabText().repeat(indentationLevel)
299 | );
300 |
301 | return editor.getBuffer().insert(new Point(zeroBasedStartLine, -1), docblock);
302 | }
303 |
304 | /**
305 | * @param {TextEditor} editor
306 | * @param {Point} triggerPosition
307 | * @param {String} name
308 | */
309 | getPropertyIntentions(editor, triggerPosition, name) {
310 | const failureHandler = () => {
311 | return [];
312 | };
313 |
314 | const successHandler = currentClassName => {
315 | if ((currentClassName == null)) { return []; }
316 |
317 | const nestedSuccessHandler = classInfo => {
318 | name = name.substr(1);
319 |
320 | if (!(name in classInfo.properties)) { return []; }
321 |
322 | const propertyData = classInfo.properties[name];
323 |
324 | if ((propertyData == null)) { return; }
325 |
326 | const intentions = [];
327 |
328 | if (!propertyData) { return intentions; }
329 |
330 | if (!propertyData.hasDocblock) {
331 | if (propertyData.hasDocumentation) {
332 | intentions.push({
333 | priority : 100,
334 | icon : 'gear',
335 | title : 'Generate Docblock (inheritDoc)',
336 |
337 | selected : () => {
338 | return this.generateDocblockInheritance(editor, triggerPosition);
339 | }
340 | });
341 | }
342 |
343 | intentions.push({
344 | priority : 100,
345 | icon : 'gear',
346 | title : 'Generate Docblock',
347 |
348 | selected : () => {
349 | return this.generatePropertyDocblockFor(editor, propertyData);
350 | }
351 | });
352 | }
353 |
354 | return intentions;
355 | };
356 |
357 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
358 | };
359 |
360 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
361 | }
362 |
363 | /**
364 | * @param {TextEditor} editor
365 | * @param {Object} data
366 | */
367 | generatePropertyDocblockFor(editor, data) {
368 | const zeroBasedStartLine = data.range.start.line;
369 |
370 | const indentationLevel = editor.indentationForBufferRow(zeroBasedStartLine);
371 |
372 | let type = 'mixed';
373 |
374 | if (data.types.length > 0) {
375 | type = this.typeHelper.buildTypeSpecificationFromTypeArray(data.types);
376 | }
377 |
378 | const docblock = this.docblockBuilder.buildForProperty(
379 | type,
380 | false,
381 | editor.getTabText().repeat(indentationLevel)
382 | );
383 |
384 | return editor.getBuffer().insert(new Point(zeroBasedStartLine, -1), docblock);
385 | }
386 |
387 | /**
388 | * @param {TextEditor} editor
389 | * @param {Point} triggerPosition
390 | * @param {String} name
391 | */
392 | getConstantIntentions(editor, triggerPosition, name) {
393 | const failureHandler = () => {
394 | return [];
395 | };
396 |
397 | const successHandler = currentClassName => {
398 | let nestedSuccessHandler;
399 | const helperFunction = constantData => {
400 | const intentions = [];
401 |
402 | if (!constantData) { return intentions; }
403 |
404 | if (!constantData.hasDocblock) {
405 | intentions.push({
406 | priority : 100,
407 | icon : 'gear',
408 | title : 'Generate Docblock',
409 |
410 | selected : () => {
411 | return this.generateConstantDocblockFor(editor, constantData);
412 | }
413 | });
414 | }
415 |
416 | return intentions;
417 | };
418 |
419 | if (currentClassName) {
420 | nestedSuccessHandler = classInfo => {
421 | if (!(name in classInfo.constants)) { return []; }
422 | return helperFunction(classInfo.constants[name]);
423 | };
424 |
425 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
426 |
427 | } else {
428 | nestedSuccessHandler = globalConstants => {
429 | if (!(name in globalConstants)) { return []; }
430 | return helperFunction(globalConstants[name]);
431 | };
432 |
433 | return this.service.getGlobalConstants().then(nestedSuccessHandler, failureHandler);
434 | }
435 | };
436 |
437 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
438 | }
439 |
440 | /**
441 | * @param {TextEditor} editor
442 | * @param {Object} data
443 | */
444 | generateConstantDocblockFor(editor, data) {
445 | const zeroBasedStartLine = data.range.start.line;
446 |
447 | const indentationLevel = editor.indentationForBufferRow(zeroBasedStartLine);
448 |
449 | let type = 'mixed';
450 |
451 | if (data.types.length > 0) {
452 | type = this.typeHelper.buildTypeSpecificationFromTypeArray(data.types);
453 | }
454 |
455 | const docblock = this.docblockBuilder.buildForProperty(
456 | type,
457 | false,
458 | editor.getTabText().repeat(indentationLevel)
459 | );
460 |
461 | return editor.getBuffer().insert(new Point(zeroBasedStartLine, -1), docblock);
462 | }
463 |
464 | /**
465 | * @param {TextEditor} editor
466 | * @param {Point} triggerPosition
467 | */
468 | generateDocblockInheritance(editor, triggerPosition) {
469 | const indentationLevel = editor.indentationForBufferRow(triggerPosition.row);
470 |
471 | const docblock = this.docblockBuilder.buildByLines(
472 | ['@inheritDoc'],
473 | editor.getTabText().repeat(indentationLevel)
474 | );
475 |
476 | return editor.getBuffer().insert(new Point(triggerPosition.row, -1), docblock);
477 | }
478 | };
479 |
--------------------------------------------------------------------------------
/lib/Refactoring/ExtractMethodProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const {Range} = require('atom');
10 |
11 | const AbstractProvider = require('./AbstractProvider');
12 |
13 | module.exports =
14 |
15 | //#*
16 | // Provides method extraction capabilities.
17 | //#
18 | class ExtractMethodProvider extends AbstractProvider {
19 | /**
20 | * @param {Object} builder
21 | */
22 | constructor(builder) {
23 | super();
24 |
25 | /**
26 | * View that the user interacts with when extracting code.
27 | *
28 | * @type {Object}
29 | */
30 | this.view = null;
31 |
32 | /**
33 | * Builder used to generate the new method.
34 | *
35 | * @type {Object}
36 | */
37 | this.builder = builder;
38 | }
39 |
40 | /**
41 | * @inheritdoc
42 | */
43 | activate(service) {
44 | super.activate(service);
45 |
46 | return atom.commands.add('atom-text-editor', { 'php-ide-serenata:extract-method': () => {
47 | return this.executeCommand();
48 | }
49 | }
50 | );
51 | }
52 |
53 | /**
54 | * @inheritdoc
55 | */
56 | deactivate() {
57 | super.deactivate();
58 |
59 | if (this.view) {
60 | this.view.destroy();
61 | this.view = null;
62 | }
63 | }
64 |
65 | /**
66 | * @inheritdoc
67 | */
68 | getIntentionProviders() {
69 | return [{
70 | grammarScopes: ['source.php'],
71 | getIntentions: () => {
72 | const activeTextEditor = atom.workspace.getActiveTextEditor();
73 |
74 | if (!activeTextEditor) { return []; }
75 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
76 |
77 | const selection = activeTextEditor.getSelectedBufferRange();
78 |
79 | // Checking if a selection has been made
80 | if (
81 | (selection.start.row === selection.end.row) &&
82 | (selection.start.column === selection.end.column)
83 | ) {
84 | return [];
85 | }
86 |
87 | return [
88 | {
89 | priority : 200,
90 | icon : 'git-branch',
91 | title : 'Extract Method',
92 |
93 | selected : () => {
94 | return this.executeCommand();
95 | }
96 | }
97 | ];
98 | }
99 | }];
100 | }
101 |
102 | /**
103 | * Executes the extraction.
104 | */
105 | executeCommand() {
106 | const activeTextEditor = atom.workspace.getActiveTextEditor();
107 |
108 | if (!activeTextEditor) { return; }
109 |
110 | const tabText = activeTextEditor.getTabText();
111 |
112 | const selection = activeTextEditor.getSelectedBufferRange();
113 |
114 | // Checking if a selection has been made
115 | if ((selection.start.row === selection.end.row) && (selection.start.column === selection.end.column)) {
116 | atom.notifications.addInfo('Serenata', {
117 | description: 'Please select the code to extract and try again.'
118 | });
119 |
120 | return;
121 | }
122 |
123 | const line = activeTextEditor.lineTextForBufferRow(selection.start.row);
124 |
125 | const findSingleTab = new RegExp(`(${tabText})`, 'g');
126 |
127 | const matches = (line.match(findSingleTab) || []).length;
128 |
129 | // If the first line doesn't have any tabs then add one.
130 | let highlightedText = activeTextEditor.getTextInBufferRange(selection);
131 | const selectedBufferFirstLine = highlightedText.split('\n')[0];
132 |
133 | if ((selectedBufferFirstLine.match(findSingleTab) || []).length === 0) {
134 | highlightedText = `${tabText}` + highlightedText;
135 | }
136 |
137 | // Replacing double indents with one, so it can be shown in the preview area of panel.
138 | const multipleTabTexts = Array(matches).fill(`${tabText}`);
139 | const findMultipleTab = new RegExp(`^${multipleTabTexts.join('')}`, 'mg');
140 | const reducedHighlightedText = highlightedText.replace(findMultipleTab, `${tabText}`);
141 |
142 | this.builder.setEditor(activeTextEditor);
143 | this.builder.setMethodBody(reducedHighlightedText);
144 |
145 | this.getView().storeFocusedElement();
146 | return this.getView().present();
147 | }
148 |
149 | /**
150 | * Called when the user has cancel the extraction in the modal.
151 | */
152 | onCancel() {
153 | return this.builder.cleanUp();
154 | }
155 |
156 | /**
157 | * Called when the user has confirmed the extraction in the modal.
158 | *
159 | * @param {Object} settings
160 | *
161 | * @see ParameterParser.buildMethod for structure of settings
162 | */
163 | onConfirm(settings) {
164 | const successHandler = methodCall => {
165 | const activeTextEditor = atom.workspace.getActiveTextEditor();
166 |
167 | const selectedBufferRange = activeTextEditor.getSelectedBufferRange();
168 |
169 | const highlightedBufferPosition = selectedBufferRange.end;
170 |
171 | let row = 0;
172 |
173 | // eslint-disable-next-line no-constant-condition
174 | while (true) {
175 | row++;
176 | const descriptions = activeTextEditor.scopeDescriptorForBufferPosition(
177 | [highlightedBufferPosition.row + row, activeTextEditor.getTabLength()]
178 | );
179 | const indexOfDescriptor = descriptions.scopes.indexOf('punctuation.section.scope.end.php');
180 | if ((indexOfDescriptor > -1) || (row === activeTextEditor.getLineCount())) { break; }
181 | }
182 |
183 | row = highlightedBufferPosition.row + row;
184 |
185 | const line = activeTextEditor.lineTextForBufferRow(row);
186 |
187 | const endOfLine = line != null ? line.length : undefined;
188 |
189 | let replaceRange = [
190 | [row, 0],
191 | [row, endOfLine]
192 | ];
193 |
194 | const previousText = activeTextEditor.getTextInBufferRange(replaceRange);
195 |
196 | settings.tabs = true;
197 |
198 | const nestedSuccessHandler = newMethodBody => {
199 | settings.tabs = false;
200 |
201 | this.builder.cleanUp();
202 |
203 | return activeTextEditor.transact(() => {
204 | // Matching current indentation
205 | const selectedText = activeTextEditor.getSelectedText();
206 | let spacing = selectedText.match(/^\s*/);
207 | if (spacing !== null) {
208 | spacing = spacing[0];
209 | }
210 |
211 | activeTextEditor.insertText(spacing + methodCall);
212 |
213 | // Remove any extra new lines between functions
214 | const nextLine = activeTextEditor.lineTextForBufferRow(row + 1);
215 | if (nextLine === '') {
216 | activeTextEditor.setSelectedBufferRange(
217 | [
218 | [row + 1, 0],
219 | [row + 1, 1]
220 | ]
221 | );
222 | activeTextEditor.deleteLine();
223 | }
224 |
225 |
226 | // Re working out range as inserting method call will delete some
227 | // lines and thus offsetting this
228 | row -= selectedBufferRange.end.row - selectedBufferRange.start.row;
229 |
230 | if (this.snippetManager != null) {
231 | activeTextEditor.setCursorBufferPosition([row + 1, 0]);
232 |
233 | const body = `\n${newMethodBody}`;
234 |
235 | const result = this.getTabStopsForBody(body);
236 |
237 | const snippet = {
238 | body,
239 | lineCount: result.lineCount,
240 | tabStopList: result.tabStops
241 | };
242 |
243 | snippet.tabStopList.toArray = () => {
244 | return snippet.tabStopList;
245 | };
246 |
247 | return this.snippetManager.insertSnippet(
248 | snippet,
249 | activeTextEditor
250 | );
251 | } else {
252 | // Re working out range as inserting method call will delete some
253 | // lines and thus offsetting this
254 | row -= selectedBufferRange.end.row - selectedBufferRange.start.row;
255 |
256 | replaceRange = [
257 | [row, 0],
258 | [row, (line != null ? line.length : undefined)]
259 | ];
260 |
261 | return activeTextEditor.setTextInBufferRange(
262 | replaceRange,
263 | `${previousText}\n\n${newMethodBody}`
264 | );
265 | }
266 | });
267 | };
268 |
269 | const nestedFailureHandler = () => {
270 | settings.tabs = false;
271 | };
272 |
273 | return this.builder.buildMethod(settings).then(nestedSuccessHandler, nestedFailureHandler);
274 | };
275 |
276 | const failureHandler = () => {};
277 | // Do nothing.
278 |
279 | return this.builder.buildMethodCall(settings.methodName).then(successHandler, failureHandler);
280 | }
281 |
282 | /**
283 | * Gets all the tab stops and line count for the body given
284 | *
285 | * @param {String} body
286 | *
287 | * @return {Object}
288 | */
289 | getTabStopsForBody(body) {
290 | const lines = body.split('\n');
291 | let row = 0;
292 | let lineCount = 0;
293 | let tabStops = [];
294 | const tabStopIndex = {};
295 |
296 | for (const line of lines) {
297 | var match;
298 | const regex = /(\[[\w ]*?\])(\s*\$[a-zA-Z0-9_]+)?/g;
299 | // Get tab stops by looping through all matches
300 | while ((match = regex.exec(line)) !== null) {
301 | let key = match[2]; // 2nd capturing group (variable name)
302 | const range = new Range(
303 | [row, match.index],
304 | [row, match.index + match[1].length]
305 | );
306 |
307 | if (key !== undefined) {
308 | key = key.trim();
309 | if (tabStopIndex[key] !== undefined) {
310 | tabStopIndex[key].push(range);
311 | } else {
312 | tabStopIndex[key] = [range];
313 | }
314 | } else {
315 | tabStops.push([range]);
316 | }
317 | }
318 |
319 | row++;
320 | lineCount++;
321 | }
322 |
323 | for (const objectKey of Object.keys(tabStopIndex)) {
324 | tabStops.push(tabStopIndex[objectKey]);
325 | }
326 |
327 | tabStops = tabStops.sort(this.sortTabStops);
328 |
329 | return {
330 | tabStops,
331 | lineCount
332 | };
333 | }
334 |
335 | /**
336 | * Sorts the tab stops by their row and column
337 | *
338 | * @param {Array} a
339 | * @param {Array} b
340 | *
341 | * @return {Integer}
342 | */
343 | sortTabStops(a, b) {
344 | // Grabbing first range in the array
345 | a = a[0];
346 | b = b[0];
347 |
348 | // b is before a in the rows
349 | if (a.start.row > b.start.row) {
350 | return 1;
351 | }
352 |
353 | // a is before b in the rows
354 | if (a.start.row < b.start.row) {
355 | return -1;
356 | }
357 |
358 | // On same line but b is before a
359 | if (a.start.column > b.start.column) {
360 | return 1;
361 | }
362 |
363 | // On same line but a is before b
364 | if (a.start.column < b.start.column) {
365 | return -1;
366 | }
367 |
368 | // Same position
369 | return 0;
370 | }
371 |
372 | /**
373 | * @return {View}
374 | */
375 | getView() {
376 | if ((this.view == null)) {
377 | const View = require('./ExtractMethodProvider/View');
378 |
379 | this.view = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
380 | this.view.setBuilder(this.builder);
381 | }
382 |
383 | return this.view;
384 | }
385 | };
386 |
--------------------------------------------------------------------------------
/lib/Refactoring/ExtractMethodProvider/Builder.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const {Range} = require('atom');
10 |
11 | module.exports =
12 |
13 | class Builder {
14 | /**
15 | * Constructor.
16 | *
17 | * @param {Object} parameterParser
18 | * @param {Object} docblockBuilder
19 | * @param {Object} functionBuilder
20 | * @param {Object} typeHelper
21 | */
22 | constructor(parameterParser, docblockBuilder, functionBuilder, typeHelper) {
23 | /**
24 | * The body of the new method that will be shown in the preview area.
25 | *
26 | * @type {String}
27 | */
28 | this.methodBody = '';
29 |
30 | /**
31 | * The tab string that is used by the current editor.
32 | *
33 | * @type {String}
34 | */
35 | this.tabText = '';
36 |
37 | /**
38 | * @type {Number}
39 | */
40 | this.indentationLevel = null;
41 |
42 | /**
43 | * @type {Number}
44 | */
45 | this.maxLineLength = null;
46 |
47 | /**
48 | * The php-ide-serenata service.
49 | *
50 | * @type {Service}
51 | */
52 | this.service = null;
53 |
54 | /**
55 | * A range of the selected/highlighted area of code to analyse.
56 | *
57 | * @type {Range}
58 | */
59 | this.selectedBufferRange = null;
60 |
61 | /**
62 | * The text editor to be analysing.
63 | *
64 | * @type {TextEditor}
65 | */
66 | this.editor = null;
67 |
68 | /**
69 | * The parameter parser that will work out the parameters the
70 | * selectedBufferRange will need.
71 | *
72 | * @type {Object}
73 | */
74 | this.parameterParser = parameterParser;
75 |
76 | /**
77 | * All the variables to return
78 | *
79 | * @type {Array}
80 | */
81 | this.returnVariables = null;
82 |
83 | /**
84 | * @type {Object}
85 | */
86 | this.docblockBuilder = docblockBuilder;
87 |
88 | /**
89 | * @type {Object}
90 | */
91 | this.functionBuilder = functionBuilder;
92 |
93 | /**
94 | * @type {Object}
95 | */
96 | this.typeHelper = typeHelper;
97 | }
98 |
99 | /**
100 | * Sets the method body to use in the preview.
101 | *
102 | * @param {String} text
103 | */
104 | setMethodBody(text) {
105 | this.methodBody = text;
106 | }
107 |
108 | /**
109 | * The tab string to use when generating the new method.
110 | *
111 | * @param {String} tab
112 | */
113 | setTabText(tab) {
114 | this.tabText = tab;
115 | }
116 |
117 | /**
118 | * @param {Number} indentationLevel
119 | */
120 | setIndentationLevel(indentationLevel) {
121 | this.indentationLevel = indentationLevel;
122 | }
123 |
124 | /**
125 | * @param {Number} maxLineLength
126 | */
127 | setMaxLineLength(maxLineLength) {
128 | this.maxLineLength = maxLineLength;
129 | }
130 |
131 | /**
132 | * Set the php-ide-serenata service to be used.
133 | *
134 | * @param {Service} service
135 | */
136 | setService(service) {
137 | this.service = service;
138 | this.parameterParser.setService(service);
139 | }
140 |
141 | /**
142 | * Set the selectedBufferRange to analyse.
143 | *
144 | * @param {Range} range [description]
145 | */
146 | setSelectedBufferRange(range) {
147 | this.selectedBufferRange = range;
148 | }
149 |
150 | /**
151 | * Set the TextEditor to be used when analysing the selectedBufferRange
152 | *
153 | * @param {TextEditor} editor [description]
154 | */
155 | setEditor(editor) {
156 | this.editor = editor;
157 | this.setTabText(editor.getTabText());
158 | this.setIndentationLevel(1);
159 | this.setMaxLineLength(
160 | atom.config.get('editor.preferredLineLength',
161 | editor.getLastCursor().getScopeDescriptor())
162 | );
163 | this.setSelectedBufferRange(editor.getSelectedBufferRange());
164 | }
165 |
166 | /**
167 | * Builds the new method from the selectedBufferRange and settings given.
168 | *
169 | * The settings parameter should be an object with these properties:
170 | * - methodName (string)
171 | * - visibility (string) ['private', 'protected', 'public']
172 | * - tabs (boolean)
173 | * - generateDocs (boolean)
174 | * - arraySyntax (string) ['word', 'brackets']
175 | * - generateDocPlaceholders (boolean)
176 | *
177 | * @param {Object} settings
178 | *
179 | * @return {Promise}
180 | */
181 | buildMethod(settings) {
182 | const successHandler = parameters => {
183 | if (this.returnVariables === null) {
184 | this.returnVariables = this.workOutReturnVariables(this.parameterParser.getVariableDeclarations());
185 | }
186 |
187 | const tabText = settings.tabs ? this.tabText : '';
188 | const totalIndentation = tabText.repeat(this.indentationLevel);
189 |
190 | const statements = [];
191 |
192 | for (const statement of this.methodBody.split('\n')) {
193 | const newStatement = statement.substr(totalIndentation.length);
194 |
195 | statements.push(newStatement);
196 | }
197 |
198 | let returnTypeHintSpecification = 'void';
199 | let returnStatement = this.buildReturnStatement(this.returnVariables, settings.arraySyntax);
200 |
201 | if (returnStatement != null) {
202 | if (this.returnVariables.length === 1) {
203 | returnTypeHintSpecification = this.returnVariables[0].types.join('|');
204 |
205 | } else {
206 | returnTypeHintSpecification = 'array';
207 | }
208 |
209 | returnStatement = returnStatement.substr(totalIndentation.length);
210 |
211 | statements.push('');
212 | statements.push(returnStatement);
213 | }
214 |
215 | const functionParameters = parameters.map(parameter => {
216 | const typeHintInfo = this.typeHelper.getTypeHintForDocblockTypes(parameter.types);
217 |
218 | return {
219 | name : parameter.name,
220 | typeHint : (typeHintInfo != null) && settings.typeHinting ? typeHintInfo.typeHint : null,
221 | defaultValue : (typeHintInfo != null) && typeHintInfo.isNullable ? 'null' : null
222 | };
223 | });
224 |
225 | const docblockParameters = parameters.map(parameter => {
226 | const typeSpecification = this.typeHelper.buildTypeSpecificationFromTypes(parameter.types);
227 |
228 | return {
229 | name : parameter.name,
230 | type : typeSpecification.length > 0 ? typeSpecification : '[type]'
231 | };
232 | });
233 |
234 | this.functionBuilder
235 | .setIsStatic(false)
236 | .setIsAbstract(false)
237 | .setName(settings.methodName)
238 | .setReturnType(this.typeHelper.getReturnTypeHintForTypeSpecification(returnTypeHintSpecification))
239 | .setParameters(functionParameters)
240 | .setStatements(statements)
241 | .setIndentationLevel(this.indentationLevel)
242 | .setTabText(tabText)
243 | .setMaxLineLength(this.maxLineLength);
244 |
245 | if (settings.visibility === 'public') {
246 | this.functionBuilder.makePublic();
247 |
248 | } else if (settings.visibility === 'protected') {
249 | this.functionBuilder.makeProtected();
250 |
251 | } else if (settings.visibility === 'private') {
252 | this.functionBuilder.makePrivate();
253 |
254 | } else {
255 | this.functionBuilder.makeGlobal();
256 | }
257 |
258 | let docblockText = '';
259 |
260 | if (settings.generateDocs) {
261 | let returnType = 'void';
262 |
263 | if ((this.returnVariables !== null) && (this.returnVariables.length > 0)) {
264 | returnType = '[type]';
265 |
266 | if (this.returnVariables.length > 1) {
267 | returnType = 'array';
268 |
269 | } else if ((this.returnVariables.length === 1) && (this.returnVariables[0].types.length > 0)) {
270 | returnType = this.typeHelper.buildTypeSpecificationFromTypes(this.returnVariables[0].types);
271 | }
272 | }
273 |
274 | docblockText = this.docblockBuilder.buildForMethod(
275 | docblockParameters,
276 | returnType,
277 | settings.generateDescPlaceholders,
278 | totalIndentation
279 | );
280 | }
281 |
282 | return docblockText + this.functionBuilder.build();
283 | };
284 |
285 | const failureHandler = () => null;
286 |
287 | return this.parameterParser.findParameters(this.editor, this.selectedBufferRange)
288 | .then(successHandler, failureHandler);
289 | }
290 |
291 | /**
292 | * Build the line that calls the new method and the variable the method
293 | * to be assigned to.
294 | *
295 | * @param {String} methodName
296 | * @param {String} variable [Optional]
297 | *
298 | * @return {Promise}
299 | */
300 | buildMethodCall(methodName, variable) {
301 | const successHandler = parameters => {
302 | const parameterNames = parameters.map(item => item.name);
303 |
304 | let methodCall = `$this->${methodName}(${parameterNames.join(', ')});`;
305 |
306 | if (variable !== undefined) {
307 | methodCall = `$${variable} = ${methodCall}`;
308 | } else {
309 | if (this.returnVariables !== null) {
310 | if (this.returnVariables.length === 1) {
311 | methodCall = `${this.returnVariables[0].name} = ${methodCall}`;
312 | } else if (this.returnVariables.length > 1) {
313 | const variables = this.returnVariables.reduce(function(previous, current) {
314 | if (typeof previous !== 'string') {
315 | previous = previous.name;
316 | }
317 |
318 | return previous + ', ' + current.name;
319 | });
320 |
321 | methodCall = `list(${variables}) = ${methodCall}`;
322 | }
323 | }
324 | }
325 |
326 | return methodCall;
327 | };
328 |
329 | const failureHandler = () => null;
330 |
331 | return this.parameterParser.findParameters(this.editor, this.selectedBufferRange)
332 | .then(successHandler, failureHandler);
333 | }
334 |
335 | /**
336 | * Performs any clean up needed with the builder.
337 | */
338 | cleanUp() {
339 | this.returnVariables = null;
340 | return this.parameterParser.cleanUp();
341 | }
342 |
343 | /**
344 | * Works out which variables need to be returned from the new method.
345 | *
346 | * @param {Array} variableDeclarations
347 | *
348 | * @return {Array}
349 | */
350 | workOutReturnVariables(variableDeclarations) {
351 | const startPoint = this.selectedBufferRange.end;
352 | const scopeRange = this.parameterParser.getRangeForCurrentScope(this.editor, startPoint);
353 |
354 | const lookupRange = new Range(startPoint, scopeRange.end);
355 |
356 | const textAfterExtraction = this.editor.getTextInBufferRange(lookupRange);
357 | const allVariablesAfterExtraction = textAfterExtraction.match(/\$[a-zA-Z0-9]+/g);
358 |
359 | if (allVariablesAfterExtraction === null) { return null; }
360 |
361 | variableDeclarations = variableDeclarations.filter(variable => {
362 | if (allVariablesAfterExtraction.includes(variable.name)) { return true; }
363 | return false;
364 | });
365 |
366 | return variableDeclarations;
367 | }
368 |
369 | /**
370 | * Builds the return statement for the new method.
371 | *
372 | * @param {Array} variableDeclarations
373 | * @param {String} arrayType ['word', 'brackets']
374 | *
375 | * @return {String|null}
376 | */
377 | buildReturnStatement(variableDeclarations, arrayType) {
378 | if (arrayType == null) { arrayType = 'word'; }
379 | if (variableDeclarations != null) {
380 | if (variableDeclarations.length === 1) {
381 | return `${this.tabText}return ${variableDeclarations[0].name};`;
382 |
383 | } else if (variableDeclarations.length > 1) {
384 | let variables = variableDeclarations.reduce(function(previous, current) {
385 | if (typeof previous !== 'string') {
386 | previous = previous.name;
387 | }
388 |
389 | return previous + ', ' + current.name;
390 | });
391 |
392 | if (arrayType === 'brackets') {
393 | variables = `[${variables}]`;
394 |
395 | } else {
396 | variables = `array(${variables})`;
397 | }
398 |
399 | return `${this.tabText}return ${variables};`;
400 | }
401 | }
402 |
403 | return null;
404 | }
405 |
406 | /**
407 | * Checks if the new method will be returning any values.
408 | *
409 | * @return {Boolean}
410 | */
411 | hasReturnValues() {
412 | return (this.returnVariables !== null) && (this.returnVariables.length > 0);
413 | }
414 |
415 | /**
416 | * Returns if there are multiple return values.
417 | *
418 | * @return {Boolean}
419 | */
420 | hasMultipleReturnValues() {
421 | return (this.returnVariables !== null) && (this.returnVariables.length > 1);
422 | }
423 | };
424 |
--------------------------------------------------------------------------------
/lib/Refactoring/ExtractMethodProvider/ParameterParser.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS202: Simplify dynamic range loops
5 | * DS207: Consider shorter variations of null checks
6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
7 | */
8 | const {Point, Range} = require('atom');
9 |
10 | module.exports =
11 |
12 | class ParameterParser {
13 | /**
14 | * Constructor
15 | *
16 | * @param {Object} typeHelper
17 | */
18 | constructor(typeHelper) {
19 | /**
20 | * Service object from the php-ide-serenata service
21 | *
22 | * @type {Service}
23 | */
24 | this.service = null;
25 |
26 | /**
27 | * @type {Object}
28 | */
29 | this.typeHelper = typeHelper;
30 |
31 | /**
32 | * List of all the variable declarations that have been process
33 | *
34 | * @type {Array}
35 | */
36 | this.variableDeclarations = [];
37 |
38 | /**
39 | * The selected range that we are scanning for parameters in.
40 | *
41 | * @type {Range}
42 | */
43 | this.selectedBufferRange = null;
44 | }
45 |
46 | /**
47 | * @param {Object} service
48 | */
49 | setService(service) {
50 | this.service = service;
51 | }
52 |
53 | /**
54 | * Takes the editor and the range and loops through finding all the
55 | * parameters that will be needed if this code was to be moved into
56 | * its own function
57 | *
58 | * @param {TextEditor} editor
59 | * @param {Range} selectedBufferRange
60 | *
61 | * @return {Promise}
62 | */
63 | findParameters(editor, selectedBufferRange) {
64 | this.selectedBufferRange = selectedBufferRange;
65 |
66 | let parameters = [];
67 |
68 | editor.scanInBufferRange(/\$[a-zA-Z0-9_]+/g, selectedBufferRange, element => {
69 | // Making sure we matched a variable and not a variable within a string
70 | const descriptions = editor.scopeDescriptorForBufferPosition(element.range.start);
71 | const indexOfDescriptor = descriptions.scopes.indexOf('variable.other.php');
72 | if (indexOfDescriptor > -1) {
73 | return parameters.push({
74 | name: element.matchText,
75 | range: element.range
76 | });
77 | }
78 | });
79 |
80 | const regexFilters = [
81 | {
82 | name: 'Foreach loops',
83 | regex: /as\s(\$[a-zA-Z0-9_]+)(?:\s=>\s(\$[a-zA-Z0-9_]+))?/g
84 | },
85 | {
86 | name: 'For loops',
87 | regex: /for\s*\(\s*(\$[a-zA-Z0-9_]+)\s*=/g
88 | },
89 | {
90 | name: 'Try catch',
91 | regex: /catch(?:\(|\s)+.*?(\$[a-zA-Z0-9_]+)/g
92 | },
93 | {
94 | name: 'Closure',
95 | regex: /function(?:\s)*?\((?:\$).*?\)/g
96 | },
97 | {
98 | name: 'Variable declarations',
99 | regex: /(\$[a-zA-Z0-9]+)\s*?=(?!>|=)/g
100 | }
101 | ];
102 |
103 | const getTypePromises = [];
104 | const variableDeclarations = [];
105 |
106 | for (const filter of regexFilters) {
107 | editor.backwardsScanInBufferRange(filter.regex, selectedBufferRange, element => {
108 | const variables = element.matchText.match(/\$[a-zA-Z0-9]+/g);
109 | const startPoint = new Point(element.range.end.row, 0);
110 | const scopeRange = this.getRangeForCurrentScope(editor, startPoint);
111 |
112 | if (filter.name === 'Variable declarations') {
113 | let chosenParameter = null;
114 | for (const parameter of parameters) {
115 | if (element.range.containsRange(parameter.range)) {
116 | chosenParameter = parameter;
117 | break;
118 | }
119 | }
120 |
121 | if (chosenParameter !== null) {
122 | getTypePromises.push((this.getTypesForParameter(editor, chosenParameter)));
123 | variableDeclarations.push(chosenParameter);
124 | }
125 | }
126 |
127 | return variables.map((variable) => {
128 | (parameters = parameters.filter(parameter => {
129 | if (parameter.name !== variable) {
130 | return true;
131 | }
132 | if (scopeRange.containsRange(parameter.range)) {
133 | // If variable declaration is after parameter then it's
134 | // still needed in parameters.
135 | if (element.range.start.row > parameter.range.start.row) {
136 | return true;
137 | }
138 | if ((element.range.start.row === parameter.range.start.row) &&
139 | (element.range.start.column > parameter.range.start.column)) {
140 | return true;
141 | }
142 |
143 | return false;
144 | }
145 |
146 | return true;
147 | }));
148 | });
149 | });
150 | }
151 |
152 | this.variableDeclarations = this.makeUnique(variableDeclarations);
153 |
154 | parameters = this.makeUnique(parameters);
155 |
156 | // Grab the variable types of the parameters.
157 | const promises = [];
158 |
159 | parameters.forEach(parameter => {
160 | // Removing $this from parameters as this doesn't need to be passed in.
161 | if (parameter.name === '$this') { return; }
162 |
163 | return promises.push(this.getTypesForParameter(editor, parameter));
164 | });
165 |
166 | const returnFirstResultHandler = resultArray => resultArray[0];
167 |
168 | return Promise.all([Promise.all(promises), Promise.all(getTypePromises)]).then(returnFirstResultHandler);
169 | }
170 |
171 | /**
172 | * Takes the current buffer position and returns a range of the current
173 | * scope that the buffer position is in.
174 | *
175 | * For example this could be the code within an if statement or closure.
176 | *
177 | * @param {TextEditor} editor
178 | * @param {Point} bufferPosition
179 | *
180 | * @return {Range}
181 | */
182 | getRangeForCurrentScope(editor, bufferPosition) {
183 | let descriptions, i, indexOfDescriptor, line, row;
184 | let asc;
185 | let asc2, end;
186 | let startScopePoint = null;
187 | let endScopePoint = null;
188 |
189 | // Tracks any extra scopes that might exist inside the scope we are
190 | // looking for.
191 | let childScopes = 0;
192 |
193 | // First walk back until we find the start of the current scope.
194 | for (
195 | ({ row } = bufferPosition), asc = bufferPosition.row <= 0;
196 | asc ? row <= 0 : row >= 0;
197 | asc ? row++ : row--
198 | ) {
199 | var asc1;
200 | line = editor.lineTextForBufferRow(row);
201 |
202 | if (!line) { continue; }
203 |
204 | const lastIndex = line.length - 1;
205 |
206 | for (i = lastIndex, asc1 = lastIndex <= 0; asc1 ? i <= 0 : i >= 0; asc1 ? i++ : i--) {
207 | descriptions = editor.scopeDescriptorForBufferPosition(
208 | [row, i]
209 | );
210 |
211 | indexOfDescriptor = descriptions.scopes.indexOf('punctuation.section.scope.end.php');
212 | if (indexOfDescriptor > -1) {
213 | childScopes++;
214 | }
215 |
216 | indexOfDescriptor = descriptions.scopes.indexOf('punctuation.section.scope.begin.php');
217 | if (indexOfDescriptor > -1) {
218 | childScopes--;
219 |
220 | if (childScopes === -1) {
221 | startScopePoint = new Point(row, 0);
222 | break;
223 | }
224 | }
225 | }
226 |
227 | if (startScopePoint != null) { break; }
228 | }
229 |
230 | if (startScopePoint === null) {
231 | startScopePoint = new Point(0, 0);
232 | }
233 |
234 | childScopes = 0;
235 |
236 | // Walk forward until we find the end of the current scope
237 | for (
238 | ({ row } = startScopePoint), end = editor.getLineCount(), asc2 = startScopePoint.row <= end;
239 | asc2 ? row <= end : row >= end;
240 | asc2 ? row++ : row--
241 | ) {
242 | var asc3, end1;
243 | line = editor.lineTextForBufferRow(row);
244 |
245 | if (!line) { continue; }
246 |
247 | let startIndex = 0;
248 |
249 | if (startScopePoint.row === row) {
250 | startIndex = line.length - 1;
251 | }
252 |
253 | for (
254 | i = startIndex, end1 = line.length - 1, asc3 = startIndex <= end1;
255 | asc3 ? i <= end1 : i >= end1;
256 | asc3 ? i++ : i--
257 | ) {
258 | descriptions = editor.scopeDescriptorForBufferPosition(
259 | [row, i]
260 | );
261 |
262 | indexOfDescriptor = descriptions.scopes.indexOf('punctuation.section.scope.begin.php');
263 | if (indexOfDescriptor > -1) {
264 | childScopes++;
265 | }
266 |
267 | indexOfDescriptor = descriptions.scopes.indexOf('punctuation.section.scope.end.php');
268 | if (indexOfDescriptor > -1) {
269 | if (childScopes > 0) {
270 | childScopes--;
271 | }
272 |
273 | if (childScopes === 0) {
274 | endScopePoint = new Point(row, i + 1);
275 | break;
276 | }
277 | }
278 | }
279 |
280 | if (endScopePoint != null) { break; }
281 | }
282 |
283 | return new Range(startScopePoint, endScopePoint);
284 | }
285 |
286 | /**
287 | * Takes an array of parameters and removes any parameters that appear more
288 | * that once with the same name.
289 | *
290 | * @param {Array} array
291 | *
292 | * @return {Array}
293 | */
294 | makeUnique(array) {
295 | return array.filter(function(filterItem, pos, self) {
296 | for (let i = 0, end = self.length - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
297 | if (self[i].name !== filterItem.name) {
298 | continue;
299 | }
300 |
301 | return pos === i;
302 | }
303 | return true;
304 | });
305 | }
306 | /**
307 | * Generates the key used to store the parameters in the cache.
308 | *
309 | * @param {TextEditor} editor
310 | * @param {Range} selectedBufferRange
311 | *
312 | * @return {String}
313 | */
314 | buildKey(editor, selectedBufferRange) {
315 | return editor.getPath() + JSON.stringify(selectedBufferRange);
316 | }
317 |
318 | /**
319 | * Gets the type for the parameter given.
320 | *
321 | * @param {TextEditor} editor
322 | * @param {Object} parameter
323 | *
324 | * @return {Promise}
325 | */
326 | getTypesForParameter(editor, parameter) {
327 | const successHandler = types => {
328 | parameter.types = types;
329 |
330 | const typeResolutionPromises = [];
331 | const path = 'file://' + editor.getPath();
332 |
333 | const localizeTypeSuccessHandler = localizedType => {
334 | return localizedType;
335 | };
336 |
337 | const localizeTypeFailureHandler = () => null;
338 |
339 | for (const fqcn of parameter.types) {
340 | if (this.typeHelper.isClassType(fqcn)) {
341 | const typeResolutionPromise = this.service.localizeType(
342 | path,
343 | this.selectedBufferRange.end,
344 | fqcn,
345 | 'classlike'
346 | );
347 |
348 | typeResolutionPromises.push(typeResolutionPromise.then(
349 | localizeTypeSuccessHandler,
350 | localizeTypeFailureHandler
351 | )
352 | );
353 |
354 | } else {
355 | typeResolutionPromises.push(Promise.resolve(fqcn));
356 | }
357 | }
358 |
359 | const combineResolvedTypesHandler = function(processedTypeArray) {
360 | parameter.types = processedTypeArray;
361 |
362 | return parameter;
363 | };
364 |
365 | return Promise.all(typeResolutionPromises).then(
366 | combineResolvedTypesHandler,
367 | combineResolvedTypesHandler
368 | );
369 | };
370 |
371 | const failureHandler = () => {
372 | return null;
373 | };
374 |
375 | return this.service.deduceTypesAt(parameter.name, editor, this.selectedBufferRange.end).then(
376 | successHandler,
377 | failureHandler
378 | );
379 | }
380 |
381 | /**
382 | * Returns all the variable declarations that have been parsed.
383 | *
384 | * @return {Array}
385 | */
386 | getVariableDeclarations() {
387 | return this.variableDeclarations;
388 | }
389 |
390 | /**
391 | * Clean up any data from previous usage
392 | */
393 | cleanUp() {
394 | this.variableDeclarations = [];
395 | }
396 | };
397 |
--------------------------------------------------------------------------------
/lib/Refactoring/ExtractMethodProvider/View.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const {$, TextEditorView, View} = require('atom-space-pen-views');
10 |
11 | module.exports =
12 |
13 | class ExtractMethodView extends View {
14 | /**
15 | * Constructor.
16 | *
17 | * @param {Callback} onDidConfirm
18 | * @param {Callback} onDidCancel
19 | */
20 | constructor(onDidConfirm, onDidCancel = null) {
21 | super();
22 |
23 | /**
24 | * The callback to invoke when the user confirms his selections.
25 | *
26 | * @type {Callback}
27 | */
28 | this.onDidConfirm = onDidConfirm;
29 |
30 | /**
31 | * The callback to invoke when the user cancels the view.
32 | *
33 | * @type {Callback}
34 | */
35 | this.onDidCancel = onDidCancel;
36 |
37 | /**
38 | * Settings of how to generate new method that will be passed to the parser
39 | *
40 | * @type {Object}
41 | */
42 | this.settings = {
43 | generateDocs: true,
44 | methodName: '',
45 | visibility: 'private',
46 | tabs: false,
47 | arraySyntax: 'brackets',
48 | typeHinting: true,
49 | generateDescPlaceholders: false
50 | };
51 |
52 | /**
53 | * Builder to use when generating preview area
54 | *
55 | * @type {Builder}
56 | */
57 | this.builder = null;
58 | }
59 |
60 | /**
61 | * Content to be displayed when this view is shown.
62 | */
63 | static content() {
64 | return this.div({class: 'php-ide-serenata-refactoring-extract-method'}, () => {
65 | this.div({outlet: 'methodNameForm'}, () => {
66 | this.subview('methodNameEditor', new TextEditorView(
67 | { mini: true, placeholderText: 'Enter a method name' }
68 | ));
69 | this.div(
70 | { class: 'text-error error-message hide error-message--method-name' },
71 | 'You must enter a method name!'
72 | );
73 | return this.div({class: 'settings-view'}, () => {
74 | return this.div({class: 'section-body'}, () => {
75 | this.div({class: 'control-group'}, () => {
76 | return this.div({class: 'controls'}, () => {
77 | return this.label({class: 'control-label'}, () => {
78 | this.div({class: 'setting-title'}, 'Access Modifier');
79 | return this.select({
80 | outlet: 'accessMethodsInput',
81 | class: 'form-control'
82 | }, () => {
83 | this.option({value: 'public'}, 'Public');
84 | this.option({value: 'protected'}, 'Protected');
85 | return this.option({value: 'private', selected: 'selected'}, 'Private');
86 | });
87 | });
88 | });
89 | });
90 | this.div({class: 'control-group'}, () => {
91 | return this.label({class: 'control-label'}, () => {
92 | this.div({class: 'setting-title'}, 'Documentation');
93 | this.div({class: 'controls'}, () => {
94 | return this.div({class: 'checkbox'}, () => {
95 | return this.label(() => {
96 | this.input({
97 | outlet: 'generateDocInput',
98 | type: 'checkbox',
99 | checked: true
100 | });
101 | return this.div({class: 'setting-title'}, 'Generate documentation');
102 | });
103 | });
104 | });
105 | return this.div({class: 'controls generate-docs-control'}, () => {
106 | return this.div({class: 'checkbox'}, () => {
107 | return this.label(() => {
108 | this.input({
109 | outlet: 'generateDescPlaceholdersInput',
110 | type: 'checkbox'
111 | });
112 | return this.div(
113 | { class: 'setting-title' },
114 | 'Generate description placeholders'
115 | );
116 | });
117 | });
118 | });
119 | });
120 | });
121 | this.div({class: 'control-group'}, () => {
122 | return this.label({class: 'control-label'}, () => {
123 | this.div({class: 'setting-title'}, 'Type hinting');
124 | return this.div({class: 'controls'}, () => {
125 | return this.div({class: 'checkbox'}, () => {
126 | return this.label(() => {
127 | this.input({
128 | outlet: 'generateTypeHints',
129 | type: 'checkbox',
130 | checked: true
131 | });
132 | return this.div({class: 'setting-title'}, 'Generate type hints');
133 | });
134 | });
135 | });
136 | });
137 | });
138 | this.div({class: 'return-multiple-control control-group'}, () => {
139 | return this.label({class: 'control-label'}, () => {
140 | this.div({class: 'setting-title'}, 'Method styling');
141 | return this.div({class: 'controls'}, () => {
142 | return this.div({class: 'checkbox'}, () => {
143 | return this.label(() => {
144 | this.input({
145 | outlet: 'arraySyntax',
146 | type: 'checkbox',
147 | checked: true
148 | });
149 | return this.div(
150 | { class: 'setting-title' },
151 | 'Use PHP 5.4 array syntax (square brackets)'
152 | );
153 | });
154 | });
155 | });
156 | });
157 | });
158 | return this.div({class: 'control-group'}, () => {
159 | return this.div({class: 'controls'}, () => {
160 | return this.label({class: 'control-label'}, () => {
161 | this.div({class: 'setting-title'}, 'Preview');
162 | return this.div({class: 'preview-area'}, () => {
163 | return this.subview(
164 | 'previewArea',
165 | new TextEditorView(),
166 | { class: 'preview-area' }
167 | );
168 | });
169 | });
170 | });
171 | });
172 | });
173 | });
174 | });
175 | return this.div({class: 'button-bar'}, () => {
176 | this.button(
177 | { class: 'btn btn-error inline-block-tight pull-left icon icon-circle-slash button--cancel' },
178 | 'Cancel'
179 | );
180 | this.button(
181 | { class: 'btn btn-success inline-block-tight pull-right icon icon-gear button--confirm' },
182 | 'Extract'
183 | );
184 | return this.div({class: 'clear-float'});
185 | });
186 | });
187 | }
188 |
189 | /**
190 | * @inheritdoc
191 | */
192 | initialize() {
193 | atom.commands.add(this.element, {
194 | 'core:confirm': event => {
195 | this.confirm();
196 | return event.stopPropagation();
197 | },
198 | 'core:cancel': event => {
199 | this.cancel();
200 | return event.stopPropagation();
201 | }
202 | }
203 | );
204 |
205 | this.on('click', 'button', event => {
206 | if ($(event.target).hasClass('button--confirm')) { this.confirm(); }
207 | if ($(event.target).hasClass('button--cancel')) { return this.cancel(); }
208 | });
209 |
210 | this.methodNameEditor.getModel().onDidChange(() => {
211 | this.settings.methodName = this.methodNameEditor.getText();
212 | $('.php-ide-serenata-refactoring-extract-method .error-message--method-name').addClass('hide');
213 |
214 | return this.refreshPreviewArea();
215 | });
216 |
217 | $(this.accessMethodsInput[0]).change(event => {
218 | this.settings.visibility = $(event.target).val();
219 | return this.refreshPreviewArea();
220 | });
221 |
222 | $(this.generateDocInput[0]).change(() => {
223 | this.settings.generateDocs = !this.settings.generateDocs;
224 | if (this.settings.generateDocs === true) {
225 | $('.php-ide-serenata-refactoring-extract-method .generate-docs-control').removeClass('hide');
226 | } else {
227 | $('.php-ide-serenata-refactoring-extract-method .generate-docs-control').addClass('hide');
228 | }
229 |
230 |
231 | return this.refreshPreviewArea();
232 | });
233 |
234 | $(this.generateDescPlaceholdersInput[0]).change(() => {
235 | this.settings.generateDescPlaceholders = !this.settings.generateDescPlaceholders;
236 | return this.refreshPreviewArea();
237 | });
238 |
239 | $(this.generateTypeHints[0]).change(() => {
240 | this.settings.typeHinting = !this.settings.typeHinting;
241 | return this.refreshPreviewArea();
242 | });
243 |
244 | $(this.arraySyntax[0]).change(() => {
245 | if (this.settings.arraySyntax === 'word') {
246 | this.settings.arraySyntax = 'brackets';
247 | } else {
248 | this.settings.arraySyntax = 'word';
249 | }
250 | return this.refreshPreviewArea();
251 | });
252 |
253 | if (this.panel == null) { this.panel = atom.workspace.addModalPanel({item: this, visible: false}); }
254 |
255 | const previewAreaTextEditor = this.previewArea.getModel();
256 | previewAreaTextEditor.setGrammar(atom.grammars.grammarForScopeName('text.html.php'));
257 |
258 | this.on('click', document, event => {
259 | return event.stopPropagation();
260 | });
261 |
262 | return $(document).on('click', () => {
263 | if (this.panel != null ? this.panel.isVisible() : undefined) {
264 | return this.cancel();
265 | }
266 | });
267 | }
268 |
269 | /**
270 | * Destroys the view and cleans up.
271 | */
272 | destroy() {
273 | this.panel.destroy();
274 | this.panel = null;
275 | }
276 |
277 | /**
278 | * Shows the view and refreshes the preview area with the current settings.
279 | */
280 | present() {
281 | this.panel.show();
282 | this.methodNameEditor.focus();
283 | return this.methodNameEditor.setText('');
284 | }
285 |
286 | /**
287 | * Hides the panel.
288 | */
289 | hide() {
290 | if (this.panel != null) {
291 | this.panel.hide();
292 | }
293 | return this.restoreFocus();
294 | }
295 |
296 | /**
297 | * Called when the user confirms the extraction and will then call
298 | * onDidConfirm, if set.
299 | */
300 | confirm() {
301 | if (this.settings.methodName === '') {
302 | $('.php-ide-serenata-refactoring-extract-method .error-message--method-name').removeClass('hide');
303 | return false;
304 | }
305 |
306 | if (this.onDidConfirm) {
307 | this.onDidConfirm(this.getSettings());
308 | }
309 |
310 | return this.hide();
311 | }
312 |
313 | /**
314 | * Called when the user cancels the extraction and will then call
315 | * onDidCancel, if set.
316 | */
317 | cancel() {
318 | if (this.onDidCancel) {
319 | this.onDidCancel();
320 | }
321 |
322 | return this.hide();
323 | }
324 |
325 | /**
326 | * Updates the preview area using the current setttings.
327 | */
328 | refreshPreviewArea() {
329 | if (!this.panel.isVisible()) { return; }
330 |
331 | const successHandler = methodBody => {
332 | if (this.builder.hasReturnValues()) {
333 | if (this.builder.hasMultipleReturnValues()) {
334 | $('.php-ide-serenata-refactoring-extract-method .return-multiple-control').removeClass('hide');
335 | } else {
336 | $('.php-ide-serenata-refactoring-extract-method .return-multiple-control').addClass('hide');
337 | }
338 |
339 | $('.php-ide-serenata-refactoring-extract-method .return-control').removeClass('hide');
340 | } else {
341 | $('.php-ide-serenata-refactoring-extract-method .return-control').addClass('hide');
342 | $('.php-ide-serenata-refactoring-extract-method .return-multiple-control').addClass('hide');
343 | }
344 |
345 | return this.previewArea.getModel().getBuffer().setText(` {
53 | return this.executeCommand(true, false);
54 | }
55 | }
56 | );
57 |
58 | atom.commands.add('atom-workspace', { 'php-ide-serenata-refactoring:generate-setter': () => {
59 | return this.executeCommand(false, true);
60 | }
61 | }
62 | );
63 |
64 | return atom.commands.add(
65 | 'atom-workspace',
66 | { 'php-ide-serenata-refactoring:generate-getter-setter-pair': () => {
67 | return this.executeCommand(true, true);
68 | } }
69 | );
70 | }
71 |
72 | /**
73 | * @inheritdoc
74 | */
75 | deactivate() {
76 | super.deactivate();
77 |
78 | if (this.selectionView) {
79 | this.selectionView.destroy();
80 | this.selectionView = null;
81 | }
82 | }
83 |
84 | /**
85 | * @inheritdoc
86 | */
87 | getIntentionProviders() {
88 | return [{
89 | grammarScopes: ['source.php'],
90 | getIntentions: () => {
91 | const successHandler = currentClassName => {
92 | if (!currentClassName) { return []; }
93 |
94 | return [
95 | {
96 | priority : 100,
97 | icon : 'gear',
98 | title : 'Generate Getter And Setter Pair(s)',
99 |
100 | selected : () => {
101 | return this.executeCommand(true, true);
102 | }
103 | },
104 |
105 | {
106 | priority : 100,
107 | icon : 'gear',
108 | title : 'Generate Getter(s)',
109 |
110 | selected : () => {
111 | return this.executeCommand(true, false);
112 | }
113 | },
114 |
115 | {
116 | priority : 100,
117 | icon : 'gear',
118 | title : 'Generate Setter(s)',
119 |
120 | selected : () => {
121 | return this.executeCommand(false, true);
122 | }
123 | }
124 | ];
125 | };
126 |
127 | const failureHandler = () => [];
128 |
129 | const activeTextEditor = atom.workspace.getActiveTextEditor();
130 |
131 | if (!activeTextEditor) { return []; }
132 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
133 |
134 | return this.service.determineCurrentClassName(
135 | activeTextEditor,
136 | activeTextEditor.getCursorBufferPosition()
137 | ).then(successHandler, failureHandler);
138 | }
139 | }];
140 | }
141 |
142 | /**
143 | * Executes the generation.
144 | *
145 | * @param {boolean} enableGetterGeneration
146 | * @param {boolean} enableSetterGeneration
147 | */
148 | executeCommand(enableGetterGeneration, enableSetterGeneration) {
149 | const activeTextEditor = atom.workspace.getActiveTextEditor();
150 |
151 | if (!activeTextEditor) { return; }
152 |
153 | this.getSelectionView().setMetadata({editor: activeTextEditor});
154 | this.getSelectionView().storeFocusedElement();
155 | this.getSelectionView().present();
156 |
157 | const successHandler = currentClassName => {
158 | if (!currentClassName) { return; }
159 |
160 | const nestedSuccessHandler = classInfo => {
161 | const enabledItems = [];
162 | const disabledItems = [];
163 |
164 | const indentationLevel = activeTextEditor.indentationForBufferRow(
165 | activeTextEditor.getCursorBufferPosition().row
166 | );
167 |
168 | for (let name in classInfo.properties) {
169 | const property = classInfo.properties[name];
170 | const getterName = `get${name.substr(0, 1).toUpperCase()}${name.substr(1)}`;
171 | const setterName = `set${name.substr(0, 1).toUpperCase()}${name.substr(1)}`;
172 |
173 | const getterExists = getterName in classInfo.methods ? true : false;
174 | const setterExists = setterName in classInfo.methods ? true : false;
175 |
176 | const data = {
177 | name,
178 | types : property.types,
179 | needsGetter : enableGetterGeneration,
180 | needsSetter : enableSetterGeneration,
181 | getterName,
182 | setterName,
183 | tabText : activeTextEditor.getTabText(),
184 | indentationLevel,
185 | maxLineLength : atom.config.get(
186 | 'editor.preferredLineLength',
187 | activeTextEditor.getLastCursor().getScopeDescriptor()
188 | )
189 | };
190 |
191 | if ((enableGetterGeneration && enableSetterGeneration && getterExists && setterExists) ||
192 | (enableGetterGeneration && getterExists) ||
193 | (enableSetterGeneration && setterExists)) {
194 | data.className = 'php-ide-serenata-refactoring-strikethrough';
195 | disabledItems.push(data);
196 |
197 | } else {
198 | data.className = '';
199 | enabledItems.push(data);
200 | }
201 | }
202 |
203 | return this.getSelectionView().setItems(enabledItems.concat(disabledItems));
204 | };
205 |
206 | const nestedFailureHandler = () => {
207 | return this.getSelectionView().setItems([]);
208 | };
209 |
210 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, nestedFailureHandler);
211 | };
212 |
213 | const failureHandler = () => {
214 | return this.getSelectionView().setItems([]);
215 | };
216 |
217 | return this.service.determineCurrentClassName(activeTextEditor, activeTextEditor.getCursorBufferPosition())
218 | .then(successHandler, failureHandler);
219 | }
220 |
221 | /**
222 | * Indicates if the specified type is a class type or not.
223 | *
224 | * @return {bool}
225 | */
226 | isClassType(type) {
227 | if (type.substr(0, 1).toUpperCase() === type.substr(0, 1)) { return true; } else { return false; }
228 | }
229 |
230 | /**
231 | * Called when the selection of properties is cancelled.
232 | *
233 | * @param {Object|null} metadata
234 | */
235 | onCancel() {}
236 |
237 | /**
238 | * Called when the selection of properties is confirmed.
239 | *
240 | * @param {array} selectedItems
241 | * @param {Object|null} metadata
242 | */
243 | async onConfirm(selectedItems, metadata) {
244 | const itemOutputs = [];
245 | const editorUri = 'file://' + metadata.editor.getPath();
246 | const bufferPosition = metadata.editor.getCursorBufferPosition();
247 |
248 | for (const item of selectedItems) {
249 | if (item.needsGetter) {
250 | itemOutputs.push(await this.generateGetterForItem(item, editorUri, bufferPosition));
251 | }
252 |
253 | if (item.needsSetter) {
254 | itemOutputs.push(await this.generateSetterForItem(item, editorUri, bufferPosition));
255 | }
256 | }
257 |
258 | const output = itemOutputs.join('\n').trim();
259 |
260 | return metadata.editor.getBuffer().insert(bufferPosition, output);
261 | }
262 |
263 | /**
264 | * Generates a getter for the specified selected item.
265 | *
266 | * @param {Object} item
267 | * @param {String} editorUri
268 | * @param {Point} bufferPosition
269 | *
270 | * @return {string}
271 | */
272 | async generateGetterForItem(item, editorUri, bufferPosition) {
273 | await this.localizeFunctionParameterTypes(item, editorUri, bufferPosition);
274 |
275 | const typeSpecification = this.typeHelper.buildTypeSpecificationFromTypeArray(item.types);
276 |
277 | const statements = [
278 | `return $this->${item.name};`
279 | ];
280 |
281 | const functionText = this.functionBuilder
282 | .makePublic()
283 | .setIsStatic(false)
284 | .setIsAbstract(false)
285 | .setName(item.getterName)
286 | .setReturnType(this.typeHelper.getReturnTypeHintForTypeSpecification(typeSpecification))
287 | .setParameters([])
288 | .setStatements(statements)
289 | .setTabText(item.tabText)
290 | .setIndentationLevel(item.indentationLevel)
291 | .setMaxLineLength(item.maxLineLength)
292 | .build();
293 |
294 | const docblockText = this.docblockBuilder.buildForMethod(
295 | [],
296 | typeSpecification,
297 | false,
298 | item.tabText.repeat(item.indentationLevel)
299 | );
300 |
301 | return docblockText + functionText;
302 | }
303 |
304 | /**
305 | * Generates a setter for the specified selected item.
306 | *
307 | * @param {Object} item
308 | *
309 | * @return {string}
310 | */
311 | generateSetterForItem(item) {
312 | const typeSpecification = this.typeHelper.buildTypeSpecificationFromTypeArray(item.types);
313 | const parameterTypeHint = this.typeHelper.getTypeHintForTypeSpecification(typeSpecification);
314 |
315 | const statements = [
316 | `$this->${item.name} = $${item.name};`,
317 | 'return $this;'
318 | ];
319 |
320 | const parameters = [
321 | {
322 | name : `$${item.name}`,
323 | typeHint : parameterTypeHint.typeHint,
324 | defaultValue : parameterTypeHint.shouldSetDefaultValueToNull ? 'null' : null
325 | }
326 | ];
327 |
328 | const functionText = this.functionBuilder
329 | .makePublic()
330 | .setIsStatic(false)
331 | .setIsAbstract(false)
332 | .setName(item.setterName)
333 | .setReturnType(null)
334 | .setParameters(parameters)
335 | .setStatements(statements)
336 | .setTabText(item.tabText)
337 | .setIndentationLevel(item.indentationLevel)
338 | .setMaxLineLength(item.maxLineLength)
339 | .build();
340 |
341 | const docblockText = this.docblockBuilder.buildForMethod(
342 | [{name : `$${item.name}`, type : typeSpecification}],
343 | 'static',
344 | false,
345 | item.tabText.repeat(item.indentationLevel)
346 | );
347 |
348 | return docblockText + functionText;
349 | }
350 |
351 | /**
352 | * @return {Builder}
353 | */
354 | getSelectionView() {
355 | if ((this.selectionView == null)) {
356 | const View = require('./GetterSetterProvider/View');
357 |
358 | this.selectionView = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
359 | this.selectionView.setLoading('Loading class information...');
360 | this.selectionView.setEmptyMessage('No properties found.');
361 | }
362 |
363 | return this.selectionView;
364 | }
365 | };
366 |
--------------------------------------------------------------------------------
/lib/Refactoring/GetterSetterProvider/View.js:
--------------------------------------------------------------------------------
1 | const MultiSelectionView = require('../Utility/MultiSelectionView');
2 |
3 | module.exports =
4 |
5 | class View extends MultiSelectionView {};
6 |
--------------------------------------------------------------------------------
/lib/Refactoring/IntroducePropertyProvider.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS207: Consider shorter variations of null checks
5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
6 | */
7 | const {Point} = require('atom');
8 |
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | //#*
14 | // Provides property generation for non-existent properties.
15 | //#
16 | class IntroducePropertyProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} docblockBuilder
19 | */
20 | constructor(docblockBuilder) {
21 | super();
22 |
23 | /**
24 | * The docblock builder.
25 | */
26 | this.docblockBuilder = docblockBuilder;
27 | }
28 |
29 | /**
30 | * @inheritdoc
31 | */
32 | getIntentionProviders() {
33 | return [{
34 | grammarScopes: ['variable.other.property.php'],
35 | getIntentions: ({textEditor, bufferPosition}) => {
36 | const nameRange = textEditor.bufferRangeForScopeAtCursor('variable.other.property');
37 |
38 | if ((nameRange == null)) { return []; }
39 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
40 |
41 | const name = textEditor.getTextInBufferRange(nameRange);
42 |
43 | return this.getIntentions(textEditor, bufferPosition, name);
44 | }
45 | }];
46 | }
47 |
48 | /**
49 | * @param {TextEditor} editor
50 | * @param {Point} triggerPosition
51 | * @param {String} name
52 | */
53 | getIntentions(editor, triggerPosition, name) {
54 | const failureHandler = () => {
55 | return [];
56 | };
57 |
58 | const successHandler = currentClassName => {
59 | if ((currentClassName == null)) { return []; }
60 |
61 | const nestedSuccessHandler = classInfo => {
62 | const intentions = [];
63 |
64 | if (!classInfo) { return intentions; }
65 |
66 | if (!(name in classInfo.properties)) {
67 | intentions.push({
68 | priority : 100,
69 | icon : 'gear',
70 | title : 'Introduce New Property',
71 |
72 | selected : () => {
73 | return this.introducePropertyFor(editor, classInfo, name);
74 | }
75 | });
76 | }
77 |
78 | return intentions;
79 | };
80 |
81 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
82 | };
83 |
84 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
85 | }
86 |
87 | /**
88 | * @param {TextEditor} editor
89 | * @param {Object} classData
90 | * @param {String} name
91 | */
92 | introducePropertyFor(editor, classData, name) {
93 | const indentationLevel = editor.indentationForBufferRow(classData.range.start.line) + 1;
94 |
95 | const tabText = editor.getTabText().repeat(indentationLevel);
96 |
97 | const docblock = this.docblockBuilder.buildForProperty(
98 | 'mixed',
99 | false,
100 | tabText
101 | );
102 |
103 | const property = `${tabText}protected $${name};\n\n`;
104 |
105 | const point = this.findLocationToInsertProperty(editor, classData);
106 |
107 | return editor.getBuffer().insert(point, docblock + property);
108 | }
109 |
110 |
111 | /**
112 | * @param {TextEditor} editor
113 | * @param {Object} classData
114 | *
115 | * @return {Point}
116 | */
117 | findLocationToInsertProperty(editor, classData) {
118 | let startLine = null;
119 |
120 | // Try to place the new property underneath the existing properties.
121 | for (let name in classData.properties) {
122 | const propertyData = classData.properties[name];
123 | if (propertyData.declaringStructure.name === classData.name) {
124 | startLine = propertyData.range.end.line + 2;
125 | }
126 | }
127 |
128 | if ((startLine == null)) {
129 | // Ensure we don't end up somewhere in the middle of the class definition if it spans multiple lines.
130 | const lineCount = editor.getLineCount();
131 |
132 | for (
133 | let line = (classData.range.start.line + 1),
134 | end = lineCount,
135 | asc = (classData.range.start.line + 1) <= end;
136 | asc ? line <= end : line >= end;
137 | asc ? line++ : line--
138 | ) {
139 | const lineText = editor.lineTextForBufferRow(line);
140 |
141 | if ((lineText == null)) { continue; }
142 |
143 | for (
144 | let i = 0, end1 = lineText.length - 1, asc1 = 0 <= end1;
145 | asc1 ? i <= end1 : i >= end1;
146 | asc1 ? i++ : i--
147 | ) {
148 | if (lineText[i] === '{') {
149 | startLine = line + 1;
150 | break;
151 | }
152 | }
153 |
154 | if (startLine != null) { break; }
155 | }
156 | }
157 |
158 | if ((startLine == null)) {
159 | startLine = classData.range.start.line + 2;
160 | }
161 |
162 | return new Point(startLine, -1);
163 | }
164 | };
165 |
--------------------------------------------------------------------------------
/lib/Refactoring/OverrideMethodProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | //#*
14 | // Provides the ability to implement interface methods.
15 | //#
16 | class OverrideMethodProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} docblockBuilder
19 | * @param {Object} functionBuilder
20 | */
21 | constructor(docblockBuilder, functionBuilder) {
22 | super();
23 |
24 | /**
25 | * The view that allows the user to select the properties to generate for.
26 | */
27 | this.selectionView = null;
28 |
29 | /**
30 | * @type {Object}
31 | */
32 | this.docblockBuilder = docblockBuilder;
33 |
34 | /**
35 | * @type {Object}
36 | */
37 | this.functionBuilder = functionBuilder;
38 | }
39 |
40 | /**
41 | * @inheritdoc
42 | */
43 | deactivate() {
44 | super.deactivate();
45 |
46 | if (this.selectionView) {
47 | this.selectionView.destroy();
48 | this.selectionView = null;
49 | }
50 | }
51 |
52 | /**
53 | * @inheritdoc
54 | */
55 | getIntentionProviders() {
56 | return [{
57 | grammarScopes: ['source.php'],
58 | getIntentions: ({textEditor, bufferPosition}) => {
59 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
60 |
61 | return this.getStubInterfaceMethodIntentions(textEditor, bufferPosition);
62 | }
63 | }];
64 | }
65 |
66 | /**
67 | * @param {TextEditor} editor
68 | * @param {Point} triggerPosition
69 | */
70 | getStubInterfaceMethodIntentions(editor, triggerPosition) {
71 | const failureHandler = () => [];
72 |
73 | const successHandler = currentClassName => {
74 | if (!currentClassName) { return []; }
75 |
76 | const nestedSuccessHandler = classInfo => {
77 | if (!classInfo) { return []; }
78 |
79 | const items = [];
80 |
81 | for (let name in classInfo.methods) {
82 | const method = classInfo.methods[name];
83 | const data = {
84 | name,
85 | method
86 | };
87 |
88 | // Interface methods can already be stubbed via StubInterfaceMethodProvider.
89 | if (method.declaringStructure.type === 'interface') { continue; }
90 |
91 | // Abstract methods can already be stubbed via StubAbstractMethodProvider.
92 | if (method.isAbstract) { continue; }
93 |
94 | if (method.declaringStructure.name !== classInfo.name) {
95 | items.push(data);
96 | }
97 | }
98 |
99 | if (items.length === 0) { return []; }
100 |
101 | this.getSelectionView().setItems(items);
102 |
103 | return [
104 | {
105 | priority : 100,
106 | icon : 'link',
107 | title : 'Override Method(s)',
108 |
109 | selected : () => {
110 | return this.executeStubInterfaceMethods(editor);
111 | }
112 | }
113 | ];
114 | };
115 |
116 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
117 | };
118 |
119 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
120 | }
121 |
122 | /**
123 | * @param {TextEditor} editor
124 | * @param {Point} triggerPosition
125 | */
126 | executeStubInterfaceMethods(editor) {
127 | this.getSelectionView().setMetadata({editor});
128 | this.getSelectionView().storeFocusedElement();
129 | return this.getSelectionView().present();
130 | }
131 |
132 | /**
133 | * Called when the selection of properties is cancelled.
134 | */
135 | onCancel() {}
136 |
137 | /**
138 | * Called when the selection of properties is confirmed.
139 | *
140 | * @param {array} selectedItems
141 | * @param {Object|null} metadata
142 | */
143 | async onConfirm(selectedItems, metadata) {
144 | const itemOutputs = [];
145 |
146 | const tabText = metadata.editor.getTabText();
147 | const editorUri = 'file://' + metadata.editor.getPath();
148 | const bufferPosition = metadata.editor.getCursorBufferPosition();
149 | const indentationLevel = metadata.editor.indentationForBufferRow(bufferPosition.row);
150 | const maxLineLength = atom.config.get(
151 | 'editor.preferredLineLength',
152 | metadata.editor.getLastCursor().getScopeDescriptor()
153 | );
154 |
155 | for (const item of selectedItems) {
156 | const stub = await this.generateStubForInterfaceMethod(
157 | item.method,
158 | tabText,
159 | indentationLevel,
160 | maxLineLength,
161 | editorUri,
162 | bufferPosition
163 | );
164 |
165 | itemOutputs.push(stub);
166 | }
167 |
168 | const output = itemOutputs.join('\n').trim();
169 |
170 | return metadata.editor.insertText(output);
171 | }
172 |
173 | /**
174 | * Generates an override for the specified selected data.
175 | *
176 | * @param {Object} data
177 | * @param {String} tabText
178 | * @param {Number} indentationLevel
179 | * @param {Number} maxLineLength
180 | * @param {String} editorUri
181 | * @param {Position} bufferPosition
182 | *
183 | * @return {Promise}
184 | */
185 | async generateStubForInterfaceMethod(
186 | data,
187 | tabText,
188 | indentationLevel,
189 | maxLineLength,
190 | editorUri,
191 | bufferPosition
192 | ) {
193 | const parameterNames = data.parameters.map(item => `$${item.name}`);
194 |
195 | const hasReturnValue = this.hasReturnValue(data);
196 |
197 | let parentCallStatement = '';
198 |
199 | if (hasReturnValue) {
200 | parentCallStatement += '$value = ';
201 | }
202 |
203 | parentCallStatement += `parent::${data.name}(`;
204 | parentCallStatement += parameterNames.join(', ');
205 | parentCallStatement += ');';
206 |
207 | const statements = [
208 | parentCallStatement,
209 | '',
210 | '// TODO'
211 | ];
212 |
213 | if (hasReturnValue) {
214 | statements.push('');
215 | statements.push('return $value;');
216 | }
217 |
218 | await this.localizeFunctionParameterTypeHints(data.parameters, editorUri, bufferPosition);
219 |
220 | const functionText = this.functionBuilder
221 | .setFromRawMethodData(data)
222 | .setIsAbstract(false)
223 | .setStatements(statements)
224 | .setTabText(tabText)
225 | .setIndentationLevel(indentationLevel)
226 | .setMaxLineLength(maxLineLength)
227 | .build();
228 |
229 | const docblockText = this.docblockBuilder.buildByLines(['@inheritDoc'], tabText.repeat(indentationLevel));
230 |
231 | return docblockText + functionText;
232 | }
233 |
234 | /**
235 | * @param {Object} data
236 | *
237 | * @return {Boolean}
238 | */
239 | hasReturnValue(data) {
240 | if (data.name === '__construct') { return false; }
241 | if (data.returnTypes.length === 0) { return false; }
242 | if ((data.returnTypes.length === 1) && (data.returnTypes[0].type === 'void')) { return false; }
243 |
244 | return true;
245 | }
246 |
247 | /**
248 | * @return {Builder}
249 | */
250 | getSelectionView() {
251 | if ((this.selectionView == null)) {
252 | const View = require('./OverrideMethodProvider/View');
253 |
254 | this.selectionView = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
255 | this.selectionView.setLoading('Loading class information...');
256 | this.selectionView.setEmptyMessage('No overridable methods found.');
257 | }
258 |
259 | return this.selectionView;
260 | }
261 | };
262 |
--------------------------------------------------------------------------------
/lib/Refactoring/OverrideMethodProvider/View.js:
--------------------------------------------------------------------------------
1 | const MultiSelectionView = require('../Utility/MultiSelectionView');
2 |
3 | module.exports =
4 |
5 | class View extends MultiSelectionView {};
6 |
--------------------------------------------------------------------------------
/lib/Refactoring/StubAbstractMethodProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | //#*
14 | // Provides the ability to stub abstract methods.
15 | //#
16 | class StubAbstractMethodProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} docblockBuilder
19 | * @param {Object} functionBuilder
20 | */
21 | constructor(docblockBuilder, functionBuilder) {
22 | super();
23 |
24 | /**
25 | * The view that allows the user to select the properties to generate for.
26 | */
27 | this.selectionView = null;
28 |
29 | /**
30 | * @type {Object}
31 | */
32 | this.docblockBuilder = docblockBuilder;
33 |
34 | /**
35 | * @type {Object}
36 | */
37 | this.functionBuilder = functionBuilder;
38 | }
39 |
40 | /**
41 | * @inheritdoc
42 | */
43 | deactivate() {
44 | super.deactivate();
45 |
46 | if (this.selectionView) {
47 | this.selectionView.destroy();
48 | this.selectionView = null;
49 | }
50 | }
51 |
52 | /**
53 | * @inheritdoc
54 | */
55 | getIntentionProviders() {
56 | return [{
57 | grammarScopes: ['source.php'],
58 | getIntentions: ({textEditor, bufferPosition}) => {
59 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
60 |
61 | return this.getStubInterfaceMethodIntentions(textEditor, bufferPosition);
62 | }
63 | }];
64 | }
65 |
66 | /**
67 | * @param {TextEditor} editor
68 | * @param {Point} triggerPosition
69 | */
70 | getStubInterfaceMethodIntentions(editor, triggerPosition) {
71 | const failureHandler = () => [];
72 |
73 | const successHandler = currentClassName => {
74 | if (!currentClassName) { return []; }
75 |
76 | const nestedSuccessHandler = classInfo => {
77 | if (!classInfo) { return []; }
78 |
79 | const items = [];
80 |
81 | for (let name in classInfo.methods) {
82 | const method = classInfo.methods[name];
83 | const data = {
84 | name,
85 | method
86 | };
87 |
88 | if (method.isAbstract) {
89 | items.push(data);
90 | }
91 | }
92 |
93 | if (items.length === 0) { return []; }
94 |
95 | this.getSelectionView().setItems(items);
96 |
97 | return [
98 | {
99 | priority : 100,
100 | icon : 'link',
101 | title : 'Stub Unimplemented Abstract Method(s)',
102 |
103 | selected : () => {
104 | return this.executeStubInterfaceMethods(editor);
105 | }
106 | }
107 | ];
108 | };
109 |
110 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
111 | };
112 |
113 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
114 | }
115 |
116 | /**
117 | * @param {TextEditor} editor
118 | * @param {Point} triggerPosition
119 | */
120 | executeStubInterfaceMethods(editor) {
121 | this.getSelectionView().setMetadata({editor});
122 | this.getSelectionView().storeFocusedElement();
123 | return this.getSelectionView().present();
124 | }
125 |
126 | /**
127 | * Called when the selection of properties is cancelled.
128 | */
129 | onCancel() {}
130 |
131 | /**
132 | * Called when the selection of properties is confirmed.
133 | *
134 | * @param {array} selectedItems
135 | * @param {Object|null} metadata
136 | */
137 | async onConfirm(selectedItems, metadata) {
138 | const itemOutputs = [];
139 |
140 | const tabText = metadata.editor.getTabText();
141 | const editorUri = 'file://' + metadata.editor.getPath();
142 | const bufferPosition = metadata.editor.getCursorBufferPosition();
143 | const indentationLevel = metadata.editor.indentationForBufferRow(bufferPosition.row);
144 | const maxLineLength = atom.config.get(
145 | 'editor.preferredLineLength',
146 | metadata.editor.getLastCursor().getScopeDescriptor()
147 | );
148 |
149 | for (const item of selectedItems) {
150 | const stub = await this.generateStubForInterfaceMethod(
151 | item.method,
152 | tabText,
153 | indentationLevel,
154 | maxLineLength,
155 | editorUri,
156 | bufferPosition
157 | );
158 |
159 | itemOutputs.push(stub);
160 | }
161 |
162 | const output = itemOutputs.join('\n').trim();
163 |
164 | return metadata.editor.insertText(output);
165 | }
166 |
167 | /**
168 | * Generates a stub for the specified selected data.
169 | *
170 | * @param {Object} data
171 | * @param {String} tabText
172 | * @param {Number} indentationLevel
173 | * @param {Number} maxLineLength
174 | * @param {String} editorUri
175 | * @param {Position} bufferPosition
176 | *
177 | * @return {string}
178 | */
179 | async generateStubForInterfaceMethod(
180 | data,
181 | tabText,
182 | indentationLevel,
183 | maxLineLength,
184 | editorUri,
185 | bufferPosition
186 | ) {
187 | const statements = [
188 | 'throw new \\LogicException(\'Not implemented\'); // TODO'
189 | ];
190 |
191 | await this.localizeFunctionParameterTypeHints(data.parameters, editorUri, bufferPosition);
192 |
193 | const functionText = this.functionBuilder
194 | .setFromRawMethodData(data)
195 | .setIsAbstract(false)
196 | .setStatements(statements)
197 | .setTabText(tabText)
198 | .setIndentationLevel(indentationLevel)
199 | .setMaxLineLength(maxLineLength)
200 | .build();
201 |
202 | const docblockText = this.docblockBuilder.buildByLines(['@inheritDoc'], tabText.repeat(indentationLevel));
203 |
204 | return docblockText + functionText;
205 | }
206 |
207 | /**
208 | * @return {Builder}
209 | */
210 | getSelectionView() {
211 | if ((this.selectionView == null)) {
212 | const View = require('./StubAbstractMethodProvider/View');
213 |
214 | this.selectionView = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
215 | this.selectionView.setLoading('Loading class information...');
216 | this.selectionView.setEmptyMessage('No unimplemented abstract methods found.');
217 | }
218 |
219 | return this.selectionView;
220 | }
221 | };
222 |
--------------------------------------------------------------------------------
/lib/Refactoring/StubAbstractMethodProvider/View.js:
--------------------------------------------------------------------------------
1 | const MultiSelectionView = require('../Utility/MultiSelectionView');
2 |
3 | module.exports =
4 |
5 | class View extends MultiSelectionView {};
6 |
--------------------------------------------------------------------------------
/lib/Refactoring/StubInterfaceMethodProvider.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const AbstractProvider = require('./AbstractProvider');
10 |
11 | module.exports =
12 |
13 | //#*
14 | // Provides the ability to stub interface methods.
15 | //#
16 | class StubInterfaceMethodProvider extends AbstractProvider {
17 | /**
18 | * @param {Object} docblockBuilder
19 | * @param {Object} functionBuilder
20 | */
21 | constructor(docblockBuilder, functionBuilder) {
22 | super();
23 |
24 | /**
25 | * The view that allows the user to select the properties to generate for.
26 | */
27 | this.selectionView = null;
28 |
29 | /**
30 | * @type {Object}
31 | */
32 | this.docblockBuilder = docblockBuilder;
33 |
34 | /**
35 | * @type {Object}
36 | */
37 | this.functionBuilder = functionBuilder;
38 | }
39 |
40 | /**
41 | * @inheritdoc
42 | */
43 | deactivate() {
44 | super.deactivate();
45 |
46 | if (this.selectionView) {
47 | this.selectionView.destroy();
48 | this.selectionView = null;
49 | }
50 | }
51 |
52 | /**
53 | * @inheritdoc
54 | */
55 | getIntentionProviders() {
56 | return [{
57 | grammarScopes: ['source.php'],
58 | getIntentions: ({textEditor, bufferPosition}) => {
59 | if ((this.getCurrentProjectPhpVersion() == null)) { return []; }
60 |
61 | return this.getStubInterfaceMethodIntentions(textEditor, bufferPosition);
62 | }
63 | }];
64 | }
65 |
66 | /**
67 | * @param {TextEditor} editor
68 | * @param {Point} triggerPosition
69 | */
70 | getStubInterfaceMethodIntentions(editor, triggerPosition) {
71 | const failureHandler = () => [];
72 |
73 | const successHandler = currentClassName => {
74 | if (!currentClassName) { return []; }
75 |
76 | const nestedSuccessHandler = classInfo => {
77 | if (!classInfo) { return []; }
78 |
79 | const items = [];
80 |
81 | for (let name in classInfo.methods) {
82 | const method = classInfo.methods[name];
83 | const data = {
84 | name,
85 | method
86 | };
87 |
88 | if (
89 | (method.declaringStructure.type === 'interface') &&
90 | ((method.implementations != null ? method.implementations.length : undefined) === 0)
91 | ) {
92 | items.push(data);
93 | }
94 | }
95 |
96 | if (items.length === 0) { return []; }
97 |
98 | this.getSelectionView().setItems(items);
99 |
100 | return [
101 | {
102 | priority : 100,
103 | icon : 'link',
104 | title : 'Stub Unimplemented Interface Method(s)',
105 |
106 | selected : () => {
107 | return this.executeStubInterfaceMethods(editor);
108 | }
109 | }
110 | ];
111 | };
112 |
113 | return this.service.getClassInfo(currentClassName).then(nestedSuccessHandler, failureHandler);
114 | };
115 |
116 | return this.service.determineCurrentClassName(editor, triggerPosition).then(successHandler, failureHandler);
117 | }
118 |
119 | /**
120 | * @param {TextEditor} editor
121 | * @param {Point} triggerPosition
122 | */
123 | executeStubInterfaceMethods(editor) {
124 | this.getSelectionView().setMetadata({editor});
125 | this.getSelectionView().storeFocusedElement();
126 | return this.getSelectionView().present();
127 | }
128 |
129 | /**
130 | * Called when the selection of properties is cancelled.
131 | */
132 | onCancel() {}
133 |
134 | /**
135 | * Called when the selection of properties is confirmed.
136 | *
137 | * @param {array} selectedItems
138 | * @param {Object|null} metadata
139 | */
140 | async onConfirm(selectedItems, metadata) {
141 | const itemOutputs = [];
142 |
143 | const tabText = metadata.editor.getTabText();
144 | const editorUri = 'file://' + metadata.editor.getPath();
145 | const bufferPosition = metadata.editor.getCursorBufferPosition();
146 | const indentationLevel = metadata.editor.indentationForBufferRow(bufferPosition.row);
147 | const maxLineLength = atom.config.get(
148 | 'editor.preferredLineLength',
149 | metadata.editor.getLastCursor().getScopeDescriptor()
150 | );
151 |
152 | for (const item of selectedItems) {
153 | const stub = await this.generateStubForInterfaceMethod(
154 | item.method,
155 | tabText,
156 | indentationLevel,
157 | maxLineLength,
158 | editorUri,
159 | bufferPosition
160 | );
161 |
162 | itemOutputs.push(stub);
163 | }
164 |
165 | const output = itemOutputs.join('\n').trim();
166 |
167 | return metadata.editor.insertText(output);
168 | }
169 |
170 | /**
171 | * Generates a stub for the specified selected data.
172 | *
173 | * @param {Object} data
174 | * @param {String} tabText
175 | * @param {Number} indentationLevel
176 | * @param {Number} maxLineLength
177 | * @param {String} editorUri
178 | * @param {Position} bufferPosition
179 | *
180 | * @return {string}
181 | */
182 | async generateStubForInterfaceMethod(
183 | data,
184 | tabText,
185 | indentationLevel,
186 | maxLineLength,
187 | editorUri,
188 | bufferPosition
189 | ) {
190 | const statements = [
191 | 'throw new \\LogicException(\'Not implemented\'); // TODO'
192 | ];
193 |
194 | await this.localizeFunctionParameterTypeHints(data.parameters, editorUri, bufferPosition);
195 |
196 | const functionText = this.functionBuilder
197 | .setFromRawMethodData(data)
198 | .setStatements(statements)
199 | .setTabText(tabText)
200 | .setIndentationLevel(indentationLevel)
201 | .setMaxLineLength(maxLineLength)
202 | .build();
203 |
204 | const docblockText = this.docblockBuilder.buildByLines(['@inheritDoc'], tabText.repeat(indentationLevel));
205 |
206 | return docblockText + functionText;
207 | }
208 |
209 | /**
210 | * @return {Builder}
211 | */
212 | getSelectionView() {
213 | if ((this.selectionView == null)) {
214 | const View = require('./StubInterfaceMethodProvider/View');
215 |
216 | this.selectionView = new View(this.onConfirm.bind(this), this.onCancel.bind(this));
217 | this.selectionView.setLoading('Loading class information...');
218 | this.selectionView.setEmptyMessage('No unimplemented interface methods found.');
219 | }
220 |
221 | return this.selectionView;
222 | }
223 | };
224 |
--------------------------------------------------------------------------------
/lib/Refactoring/StubInterfaceMethodProvider/View.js:
--------------------------------------------------------------------------------
1 | const MultiSelectionView = require('../Utility/MultiSelectionView');
2 |
3 | module.exports =
4 |
5 | class View extends MultiSelectionView {};
6 |
--------------------------------------------------------------------------------
/lib/Refactoring/Utility/DocblockBuilder.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS207: Consider shorter variations of null checks
5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
6 | */
7 | module.exports =
8 |
9 | class DocblockBuilder {
10 | /**
11 | * @param {Array} parameters
12 | * @param {String|null} returnType
13 | * @param {boolean} generateDescriptionPlaceholders
14 | * @param {String} tabText
15 | *
16 | * @return {String}
17 | */
18 | buildForMethod(parameters, returnType, generateDescriptionPlaceholders, tabText) {
19 | if (generateDescriptionPlaceholders == null) { generateDescriptionPlaceholders = true; }
20 | if (tabText == null) { tabText = ''; }
21 | const lines = [];
22 |
23 | if (generateDescriptionPlaceholders) {
24 | lines.push('[Short description of the method]');
25 | }
26 |
27 | if (parameters.length > 0) {
28 | let descriptionPlaceholder = '';
29 |
30 | if (generateDescriptionPlaceholders) {
31 | lines.push('');
32 |
33 | descriptionPlaceholder = ' [Description]';
34 | }
35 |
36 | // Determine the necessary padding.
37 | const parameterTypeLengths = parameters.map(function(item) {
38 | if (item.type) { return item.type.length; } else { return 0; }
39 | });
40 |
41 | const parameterNameLengths = parameters.map(function(item) {
42 | if (item.name) { return item.name.length; } else { return 0; }
43 | });
44 |
45 | const longestTypeLength = Math.max(...parameterTypeLengths);
46 | const longestNameLength = Math.max(...parameterNameLengths);
47 |
48 | // Generate parameter lines.
49 | for (const parameter of parameters) {
50 | const typePadding = longestTypeLength - parameter.type.length;
51 | const variablePadding = longestNameLength - parameter.name.length;
52 |
53 | const type = parameter.type + ' '.repeat(typePadding);
54 | const variable = parameter.name + ' '.repeat(variablePadding);
55 |
56 | lines.push(`@param ${type} ${variable}${descriptionPlaceholder}`);
57 | }
58 | }
59 |
60 | if ((returnType != null) && (returnType !== 'void')) {
61 | if (generateDescriptionPlaceholders || (parameters.length > 0)) {
62 | lines.push('');
63 | }
64 |
65 | lines.push(`@return ${returnType}`);
66 | }
67 |
68 | return this.buildByLines(lines, tabText);
69 | }
70 |
71 | /**
72 | * @param {String|null} type
73 | * @param {boolean} generateDescriptionPlaceholders
74 | * @param {String} tabText
75 | *
76 | * @return {String}
77 | */
78 | buildForProperty(type, generateDescriptionPlaceholders, tabText) {
79 | if (generateDescriptionPlaceholders == null) { generateDescriptionPlaceholders = true; }
80 | if (tabText == null) { tabText = ''; }
81 | const lines = [];
82 |
83 | if (generateDescriptionPlaceholders) {
84 | lines.push('[Short description of the property]');
85 | lines.push('');
86 | }
87 |
88 | lines.push(`@var ${type}`);
89 |
90 | return this.buildByLines(lines, tabText);
91 | }
92 |
93 | /**
94 | * @param {Array} lines
95 | * @param {String} tabText
96 | *
97 | * @return {String}
98 | */
99 | buildByLines(lines, tabText) {
100 | if (tabText == null) { tabText = ''; }
101 | let docs = this.buildLine('/**', tabText);
102 |
103 | if (lines.length === 0) {
104 | // Ensure we always have at least one line.
105 | lines.push('');
106 | }
107 |
108 | for (const line of lines) {
109 | docs += this.buildDocblockLine(line, tabText);
110 | }
111 |
112 | docs += this.buildLine(' */', tabText);
113 |
114 | return docs;
115 | }
116 |
117 | /**
118 | * @param {String} content
119 | * @param {String} tabText
120 | *
121 | * @return {String}
122 | */
123 | buildDocblockLine(content, tabText) {
124 | if (tabText == null) { tabText = ''; }
125 | content = ` * ${content}`;
126 |
127 | return this.buildLine(content.trimRight(), tabText);
128 | }
129 |
130 | /**
131 | * @param {String} content
132 | * @param {String} tabText
133 | *
134 | * @return {String}
135 | */
136 | buildLine(content, tabText) {
137 | if (tabText == null) { tabText = ''; }
138 | return `${tabText}${content}\n`;
139 | }
140 | };
141 |
--------------------------------------------------------------------------------
/lib/Refactoring/Utility/FunctionBuilder.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS207: Consider shorter variations of null checks
4 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
5 | */
6 | module.exports =
7 |
8 | class FunctionBuilder {
9 | constructor() {
10 | /**
11 | * The access modifier (null if none).
12 | */
13 | this.accessModifier = null;
14 |
15 | /**
16 | * Whether the method is static or not.
17 | */
18 | this.isStatic = false;
19 |
20 | /**
21 | * Whether the method is abstract or not.
22 | */
23 | this.isAbstract = null;
24 |
25 | /**
26 | * The name of the function.
27 | */
28 | this.name = null;
29 |
30 | /**
31 | * The return type of the function. This could be set when generating PHP >= 7 methods.
32 | */
33 | this.returnType = null;
34 |
35 | /**
36 | * The parameters of the function (a list of objects).
37 | */
38 | this.parameters = [];
39 |
40 | /**
41 | * A list of statements to place in the body of the function.
42 | */
43 | this.statements = [];
44 |
45 | /**
46 | * The tab text to insert on each line.
47 | */
48 | this.tabText = '';
49 |
50 | /**
51 | * The indentation level.
52 | */
53 | this.indentationLevel = null;
54 |
55 | /**
56 | * The indentation level.
57 | *
58 | * @var {Number|null}
59 | */
60 | this.maxLineLength = null;
61 | }
62 |
63 | /**
64 | * Makes the method public.
65 | *
66 | * @return {FunctionBuilder}
67 | */
68 | makePublic() {
69 | this.accessModifier = 'public';
70 | return this;
71 | }
72 |
73 | /**
74 | * Makes the method private.
75 | *
76 | * @return {FunctionBuilder}
77 | */
78 | makePrivate() {
79 | this.accessModifier = 'private';
80 | return this;
81 | }
82 |
83 | /**
84 | * Makes the method protected.
85 | *
86 | * @return {FunctionBuilder}
87 | */
88 | makeProtected() {
89 | this.accessModifier = 'protected';
90 | return this;
91 | }
92 |
93 | /**
94 | * Makes the method global (i.e. no access modifier is added).
95 | *
96 | * @return {FunctionBuilder}
97 | */
98 | makeGlobal() {
99 | this.accessModifier = null;
100 | return this;
101 | }
102 |
103 | /**
104 | * Sets whether the method is static or not.
105 | *
106 | * @param {bool} isStatic
107 | *
108 | * @return {FunctionBuilder}
109 | */
110 | setIsStatic(isStatic) {
111 | this.isStatic = isStatic;
112 | return this;
113 | }
114 |
115 | /**
116 | * Sets whether the method is abstract or not.
117 | *
118 | * @param {bool} isAbstract
119 | *
120 | * @return {FunctionBuilder}
121 | */
122 | setIsAbstract(isAbstract) {
123 | this.isAbstract = isAbstract;
124 | return this;
125 | }
126 |
127 | /**
128 | * Sets the name of the function.
129 | *
130 | * @param {String} name
131 | *
132 | * @return {FunctionBuilder}
133 | */
134 | setName(name) {
135 | this.name = name;
136 | return this;
137 | }
138 |
139 | /**
140 | * Sets the return type.
141 | *
142 | * @param {String|null} returnType
143 | *
144 | * @return {FunctionBuilder}
145 | */
146 | setReturnType(returnType) {
147 | this.returnType = returnType;
148 | return this;
149 | }
150 |
151 | /**
152 | * Sets the parameters to add.
153 | *
154 | * @param {Array} parameters
155 | *
156 | * @return {FunctionBuilder}
157 | */
158 | setParameters(parameters) {
159 | this.parameters = parameters;
160 | return this;
161 | }
162 |
163 | /**
164 | * Adds a parameter to the parameter list.
165 | *
166 | * @param {Object} parameter
167 | *
168 | * @return {FunctionBuilder}
169 | */
170 | addParameter(parameter) {
171 | this.parameters.push(parameter);
172 | return this;
173 | }
174 |
175 | /**
176 | * Sets the statements to add.
177 | *
178 | * @param {Array} statements
179 | *
180 | * @return {FunctionBuilder}
181 | */
182 | setStatements(statements) {
183 | this.statements = statements;
184 | return this;
185 | }
186 |
187 | /**
188 | * Adds a statement to the body of the function.
189 | *
190 | * @param {String} statement
191 | *
192 | * @return {FunctionBuilder}
193 | */
194 | addStatement(statement) {
195 | this.statements.push(statement);
196 | return this;
197 | }
198 |
199 | /**
200 | * Sets the tab text to prepend to each line.
201 | *
202 | * @param {String} tabText
203 | *
204 | * @return {FunctionBuilder}
205 | */
206 | setTabText(tabText) {
207 | this.tabText = tabText;
208 | return this;
209 | }
210 |
211 | /**
212 | * Sets the indentation level to use. The tab text is repeated this many times for each line.
213 | *
214 | * @param {Number} indentationLevel
215 | *
216 | * @return {FunctionBuilder}
217 | */
218 | setIndentationLevel(indentationLevel) {
219 | this.indentationLevel = indentationLevel;
220 | return this;
221 | }
222 |
223 | /**
224 | * Sets the maximum length a single line may occupy. After this, text will wrap.
225 | *
226 | * This primarily influences parameter lists, which will automatically be split over multiple lines if the
227 | * parameter list would otherwise exceed the maximum length.
228 | *
229 | * @param {Number|null} maxLineLength The length or null to disable the maximum.
230 | *
231 | * @return {FunctionBuilder}
232 | */
233 | setMaxLineLength(maxLineLength) {
234 | this.maxLineLength = maxLineLength;
235 | return this;
236 | }
237 |
238 | /**
239 | * Sets the parameters of the builder based on raw method data from the base service.
240 | *
241 | * @param {Object} data
242 | *
243 | * @return {FunctionBuilder}
244 | */
245 | setFromRawMethodData(data) {
246 | if (data.isPublic) {
247 | this.makePublic();
248 |
249 | } else if (data.isProtected) {
250 | this.makeProtected();
251 |
252 | } else if (data.isPrivate) {
253 | this.makePrivate();
254 |
255 | } else {
256 | this.makeGlobal();
257 | }
258 |
259 | this.setName(data.name);
260 | this.setIsStatic(data.isStatic);
261 | this.setIsAbstract(data.isAbstract);
262 | this.setReturnType(data.returnTypeHint);
263 |
264 | const parameters = [];
265 |
266 | for (const parameter of data.parameters) {
267 | parameters.push({
268 | name : `$${parameter.name}`,
269 | typeHint : parameter.typeHint,
270 | isVariadic : parameter.isVariadic,
271 | isReference : parameter.isReference,
272 | defaultValue : parameter.defaultValue
273 | });
274 | }
275 |
276 | this.setParameters(parameters);
277 |
278 | return this;
279 | }
280 |
281 | /**
282 | * Builds the method using the preconfigured settings.
283 | *
284 | * @return {String}
285 | */
286 | build() {
287 | let output = '';
288 |
289 | const signature = this.buildSignature(false);
290 |
291 | if ((this.maxLineLength != null) && (signature.length > this.maxLineLength)) {
292 | output += this.buildSignature(true);
293 | output += ' {\n';
294 |
295 | } else {
296 | output += signature + '\n';
297 | output += this.buildLine('{');
298 | }
299 |
300 | for (const statement of this.statements) {
301 | output += this.tabText + this.buildLine(statement);
302 | }
303 |
304 | output += this.buildLine('}');
305 |
306 | return output;
307 | }
308 |
309 | /**
310 | * @param {Boolean} isMultiLine
311 | *
312 | * @return {String}
313 | */
314 | buildSignature(isMultiLine) {
315 | let signatureLine = '';
316 |
317 | if (this.isAbstract) {
318 | signatureLine += 'abstract ';
319 | }
320 |
321 | if (this.accessModifier != null) {
322 | signatureLine += `${this.accessModifier} `;
323 | }
324 |
325 | if (this.isStatic) {
326 | signatureLine += 'static ';
327 | }
328 |
329 | signatureLine += `function ${this.name}(`;
330 |
331 | const parameters = [];
332 |
333 | for (const parameter of this.parameters) {
334 | let parameterText = '';
335 |
336 | if (parameter.typeHint != null) {
337 | parameterText += `${parameter.typeHint} `;
338 | }
339 |
340 | if (parameter.isVariadic) {
341 | parameterText += '...';
342 | }
343 |
344 | if (parameter.isReference) {
345 | parameterText += '&';
346 | }
347 |
348 | parameterText += `${parameter.name}`;
349 |
350 | if (parameter.defaultValue != null) {
351 | parameterText += ` = ${parameter.defaultValue}`;
352 | }
353 |
354 | parameters.push(parameterText);
355 | }
356 |
357 | if (!isMultiLine) {
358 | signatureLine += parameters.join(', ');
359 | signatureLine += ')';
360 |
361 | signatureLine = this.addTabText(signatureLine);
362 |
363 | } else {
364 | signatureLine = this.buildLine(signatureLine);
365 |
366 | for (let i in parameters) {
367 | let parameter = parameters[i];
368 |
369 | if (i < (parameters.length - 1)) {
370 | parameter += ',';
371 | }
372 |
373 | signatureLine += this.buildLine(parameter, this.indentationLevel + 1);
374 | }
375 |
376 | signatureLine += this.addTabText(')');
377 | }
378 |
379 | if (this.returnType != null) {
380 | signatureLine += `: ${this.returnType}`;
381 | }
382 |
383 | return signatureLine;
384 | }
385 |
386 | /**
387 | * @param {String} content
388 | * @param {Number|null} indentationLevel
389 | *
390 | * @return {String}
391 | */
392 | buildLine(content, indentationLevel = null) {
393 | return this.addTabText(content, indentationLevel) + '\n';
394 | }
395 |
396 | /**
397 | * @param {String} content
398 | * @param {Number|null} indentationLevel
399 | *
400 | * @return {String}
401 | */
402 | addTabText(content, indentationLevel = null) {
403 | if ((indentationLevel == null)) {
404 | ({ indentationLevel } = this);
405 | }
406 |
407 | const tabText = this.tabText.repeat(indentationLevel);
408 |
409 | return `${tabText}${content}`;
410 | }
411 | };
412 |
--------------------------------------------------------------------------------
/lib/Refactoring/Utility/MultiSelectionView.js:
--------------------------------------------------------------------------------
1 | /* global atom */
2 |
3 | /*
4 | * decaffeinate suggestions:
5 | * DS102: Remove unnecessary code created because of implicit returns
6 | * DS207: Consider shorter variations of null checks
7 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
8 | */
9 | const {$, $$, SelectListView} = require('atom-space-pen-views');
10 |
11 | module.exports =
12 |
13 | /**
14 | * An extension on SelectListView from atom-space-pen-views that allows multiple selections.
15 | */
16 | class MultiSelectionView extends SelectListView {
17 | /**
18 | * Constructor.
19 | *
20 | * @param {Callback} onDidConfirm
21 | * @param {Callback} onDidCancel
22 | */
23 | constructor(onDidConfirm, onDidCancel = null) {
24 | super();
25 |
26 | /**
27 | * The callback to invoke when the user confirms his selections.
28 | */
29 | this.onDidConfirm = onDidConfirm;
30 |
31 | /**
32 | * The callback to invoke when the user cancels the view.
33 | */
34 | this.onDidCancel = onDidCancel;
35 |
36 | /**
37 | * Metadata to pass to the callbacks.
38 | */
39 | this.metadata = null;
40 |
41 | /**
42 | * The message to display when there are no results.
43 | */
44 | this.emptyMessage = null;
45 |
46 | /**
47 | * Items that are currently selected.
48 | */
49 | this.selectedItems = [];
50 | }
51 |
52 | /**
53 | * @inheritdoc
54 | */
55 | initialize() {
56 | super.initialize();
57 |
58 | this.addClass('php-ide-serenata-refactoring-multi-selection-view');
59 | this.list.addClass('mark-active');
60 |
61 | if (this.panel == null) {
62 | this.panel = atom.workspace.addModalPanel({ item: this, visible: false });
63 | }
64 |
65 | return this.createWidgets();
66 | }
67 |
68 | /**
69 | * Destroys the view and cleans up.
70 | */
71 | destroy() {
72 | this.panel.destroy();
73 | this.panel = null;
74 | }
75 |
76 | /**
77 | * Creates additional for the view.
78 | */
79 | createWidgets() {
80 | const checkboxBar = $$(function() {
81 | return this.div({class: 'checkbox-bar settings-view'}, () => {
82 | return this.div({class: 'controls'}, () => {
83 | return this.div({class: 'block text-line'}, () => {
84 | return this.label(
85 | { class: 'icon icon-info' },
86 | 'Tip: The order in which items are selected determines the order of the output.'
87 | );
88 | });
89 | });
90 | });
91 | });
92 |
93 | checkboxBar.appendTo(this);
94 |
95 | // Ensure that button clicks are actually handled.
96 | this.on('mousedown', ({target}) => {
97 | if ($(target).hasClass('checkbox-input')) { return false; }
98 | if ($(target).hasClass('checkbox-label-text')) { return false; }
99 | });
100 |
101 | const cancelButtonText = this.getCancelButtonText();
102 | const confirmButtonText = this.getConfirmButtonText();
103 |
104 | const buttonBar = $$(function() {
105 | return this.div({class: 'button-bar'}, () => {
106 | this.button({
107 | class: 'btn btn-error inline-block-tight pull-left icon icon-circle-slash button--cancel'
108 | }, cancelButtonText);
109 | this.button({
110 | class: 'btn btn-success inline-block-tight pull-right icon icon-gear button--confirm'
111 | }, confirmButtonText);
112 | return this.div({ class: 'clear-float' });
113 | });
114 | });
115 |
116 | buttonBar.appendTo(this);
117 |
118 | this.on('click', 'button', event => {
119 | if ($(event.target).hasClass('button--confirm')) { this.confirmedByButton(); }
120 | if ($(event.target).hasClass('button--cancel')) { return this.cancel(); }
121 | });
122 |
123 | this.on('keydown', event => {
124 | // Shift + Return
125 | if ((event.keyCode === 13) && (event.shiftKey === true)) {
126 | return this.confirmedByButton();
127 | }
128 | });
129 |
130 | // Ensure that button clicks are actually handled.
131 | return this.on('mousedown', ({target}) => {
132 | if ($(target).hasClass('btn')) { return false; }
133 | });
134 | }
135 |
136 | /**
137 | * @inheritdoc
138 | */
139 | viewForItem(item) {
140 | const classes = ['list-item'];
141 |
142 | if (item.className) {
143 | classes.push(item.className);
144 | }
145 |
146 | if (item.isSelected) {
147 | classes.push('active');
148 | }
149 |
150 | const className = classes.join(' ');
151 | const displayText = item.name;
152 |
153 | return `\
154 | ${displayText}\
155 | `;
156 | }
157 |
158 | /**
159 | * @inheritdoc
160 | */
161 | getFilterKey() {
162 | return 'name';
163 | }
164 |
165 | /**
166 | * Retrieves the text to display on the cancel button.
167 | *
168 | * @return {string}
169 | */
170 | getCancelButtonText() {
171 | return 'Cancel';
172 | }
173 |
174 | /**
175 | * Retrieves the text to display on the confirmation button.
176 | *
177 | * @return {string}
178 | */
179 | getConfirmButtonText() {
180 | return 'Generate';
181 | }
182 |
183 | /**
184 | * Retrieves the message that is displayed when there are no results.
185 | *
186 | * @return {string}
187 | */
188 | getEmptyMessage() {
189 | if (this.emptyMessage != null) {
190 | return this.emptyMessage;
191 | }
192 |
193 | return super.getEmptyMessage();
194 | }
195 |
196 | /**
197 | * Sets the message that is displayed when there are no results.
198 | *
199 | * @param {string} emptyMessage
200 | */
201 | setEmptyMessage(emptyMessage) {
202 | this.emptyMessage = emptyMessage;
203 | }
204 |
205 | /**
206 | * Retrieves the metadata to pass to the callbacks.
207 | *
208 | * @return {Object|null}
209 | */
210 | getMetadata() {
211 | return this.metadata;
212 | }
213 |
214 | /**
215 | * Sets the metadata to pass to the callbacks.
216 | *
217 | * @param {Object|null} metadata
218 | */
219 | setMetadata(metadata) {
220 | this.metadata = metadata;
221 | }
222 |
223 | /**
224 | * @inheritdoc
225 | */
226 | setItems(items) {
227 | let i = 0;
228 |
229 | for (const item of items) {
230 | item.index = i++;
231 | }
232 |
233 | super.setItems(items);
234 |
235 | this.selectedItems = [];
236 | }
237 |
238 | /**
239 | * @inheritdoc
240 | */
241 | confirmed(item) {
242 | let index;
243 | item.isSelected = !item.isSelected;
244 |
245 | if (item.isSelected) {
246 | this.selectedItems.push(item);
247 |
248 | } else {
249 | index = this.selectedItems.indexOf(item);
250 |
251 | if (index >= 0) {
252 | this.selectedItems.splice(index, 1);
253 | }
254 | }
255 |
256 | const selectedItem = this.getSelectedItem();
257 | index = selectedItem ? selectedItem.index : 0;
258 |
259 | this.populateList();
260 |
261 | return this.selectItemView(this.list.find(`li:nth(${index})`));
262 | }
263 |
264 | /**
265 | * Invoked when the user confirms his selections by pressing the confirmation button.
266 | */
267 | confirmedByButton() {
268 | this.invokeOnDidConfirm();
269 | this.restoreFocus();
270 | return this.panel.hide();
271 | }
272 |
273 | /**
274 | * Invokes the on did confirm handler with the correct arguments (if it is set).
275 | */
276 | invokeOnDidConfirm() {
277 | if (this.onDidConfirm) {
278 | return this.onDidConfirm(this.selectedItems, this.getMetadata());
279 | }
280 | }
281 |
282 | /**
283 | * @inheritdoc
284 | */
285 | cancelled() {
286 | if (this.onDidCancel) {
287 | this.onDidCancel(this.getMetadata());
288 | }
289 |
290 | this.restoreFocus();
291 | return this.panel.hide();
292 | }
293 |
294 | /**
295 | * Presents the view to the user.
296 | */
297 | present() {
298 | this.panel.show();
299 | return this.focusFilterEditor();
300 | }
301 | };
302 |
--------------------------------------------------------------------------------
/lib/Refactoring/Utility/TypeHelper.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS207: Consider shorter variations of null checks
5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
6 | */
7 | module.exports =
8 |
9 | class TypeHelper {
10 | constructor() {
11 | /**
12 | * @var {Object|null} service
13 | */
14 | this.service = null;
15 | }
16 |
17 | /**
18 | * @param {Object} service
19 | */
20 | setService(service) {
21 | this.service = service;
22 | }
23 |
24 | /**
25 | * @return {Number}
26 | */
27 | getCurrentProjectPhpVersion() {
28 | const projectSettings = this.service.getCurrentProjectSettings();
29 |
30 | if (projectSettings != null) {
31 | return projectSettings.phpVersion;
32 | }
33 |
34 | return 5.2; // Assume lowest supported version
35 | }
36 |
37 | /**
38 | * @param {String|null} typeSpecification
39 | *
40 | * @return {Object|null}
41 | */
42 | getReturnTypeHintForTypeSpecification(typeSpecification) {
43 | if (this.getCurrentProjectPhpVersion() < 7.0) { return null; }
44 |
45 | const returnTypeHint = this.getTypeHintForTypeSpecification(typeSpecification);
46 |
47 | if ((returnTypeHint == null) || returnTypeHint.shouldSetDefaultValueToNull) {
48 | return null;
49 | }
50 |
51 | return returnTypeHint.typeHint;
52 | }
53 |
54 | /**
55 | * @param {String|null} typeSpecification
56 | *
57 | * @return {Object|null}
58 | */
59 | getTypeHintForTypeSpecification(typeSpecification) {
60 | const types = this.getDocblockTypesFromDocblockTypeSpecification(typeSpecification);
61 |
62 | return this.getTypeHintForDocblockTypes(types);
63 | }
64 |
65 | /**
66 | * @param {String|null} typeSpecification
67 | *
68 | * @return {Array}
69 | */
70 | getDocblockTypesFromDocblockTypeSpecification(typeSpecification) {
71 | if ((typeSpecification == null)) { return []; }
72 | return typeSpecification.split('|');
73 | }
74 |
75 | /**
76 | * @param {Array} types
77 | * @param {boolean} allowPhp7
78 | *
79 | * @return {Object|null}
80 | */
81 | getTypeHintForDocblockTypes(types) {
82 | let isNullable = false;
83 |
84 | types = types.filter(type => {
85 | if (type === 'null') {
86 | isNullable = true;
87 | }
88 |
89 | return type !== 'null';
90 | });
91 |
92 | let typeHint = null;
93 | let previousTypeHint = null;
94 |
95 | for (const type of types) {
96 | typeHint = this.getTypeHintForDocblockType(type);
97 |
98 | if ((previousTypeHint != null) && (typeHint !== previousTypeHint)) {
99 | // Several different type hints are necessary, we can't provide a common denominator.
100 | return null;
101 | }
102 |
103 | previousTypeHint = typeHint;
104 | }
105 |
106 | const data = {
107 | typeHint,
108 | shouldSetDefaultValueToNull : false
109 | };
110 |
111 | if ((typeHint == null)) { return data; }
112 | if (!isNullable) { return data; }
113 |
114 | const currentPhpVersion = this.getCurrentProjectPhpVersion();
115 |
116 | if (currentPhpVersion >= 7.1) {
117 | data.typeHint = `?${typeHint}`;
118 | data.shouldSetDefaultValueToNull = false;
119 |
120 | } else {
121 | data.shouldSetDefaultValueToNull = true;
122 | }
123 |
124 | return data;
125 | }
126 |
127 | /**
128 | * @param {String|null} type
129 | *
130 | * @return {String|null}
131 | */
132 | getTypeHintForDocblockType(type) {
133 | if ((type == null)) { return null; }
134 | if (this.isClassType(type)) { return type; }
135 | return this.getScalarTypeHintForDocblockType(type);
136 | }
137 |
138 | /**
139 | * @param {String|null} type
140 | *
141 | * @return {boolean}
142 | */
143 | isClassType(type) {
144 | if (this.getScalarTypeHintForDocblockType(type) === false) { return true; } else { return false; }
145 | }
146 |
147 | /**
148 | * @param {String|null} type
149 | *
150 | * @return {String|null|false} Null if the type is recognized, but there is no type hint available, false of the
151 | * type is not recognized at all, and the type hint itself if it is recognized and there is a type hint.
152 | */
153 | getScalarTypeHintForDocblockType(type) {
154 | if ((type == null)) { return null; }
155 |
156 | const phpVersion = this.getCurrentProjectPhpVersion();
157 |
158 | if (phpVersion >= 7.1) {
159 | if (type === 'iterable') { return 'iterable'; }
160 | if (type === 'void') { return 'void'; }
161 |
162 | } else if (phpVersion >= 7.0) {
163 | if (type === 'string') { return 'string'; }
164 | if (type === 'int') { return 'int'; }
165 | if (type === 'bool') { return 'bool'; }
166 | if (type === 'float') { return 'float'; }
167 | if (type === 'resource') { return 'resource'; }
168 | if (type === 'false') { return 'bool'; }
169 | if (type === 'true') { return 'bool'; }
170 |
171 | } else {
172 | if (type === 'string') { return null; }
173 | if (type === 'int') { return null; }
174 | if (type === 'bool') { return null; }
175 | if (type === 'float') { return null; }
176 | if (type === 'resource') { return null; }
177 | if (type === 'false') { return null; }
178 | if (type === 'true') { return null; }
179 | }
180 |
181 | if (type === 'array') { return 'array'; }
182 | if (type === 'callable') { return 'callable'; }
183 | if (type === 'self') { return 'self'; }
184 | if (type === 'static') { return 'self'; }
185 | if (/^.+\[\]$/.test(type)) { return 'array'; }
186 |
187 | if (type === 'object') { return null; }
188 | if (type === 'mixed') { return null; }
189 | if (type === 'void') { return null; }
190 | if (type === 'null') { return null; }
191 | if (type === 'parent') { return null; }
192 | if (type === '$this') { return null; }
193 |
194 | return false;
195 | }
196 |
197 | /**
198 | * Takes a type list (list of type objects) and turns them into a single docblock type specification.
199 | *
200 | * @param {Array} typeList
201 | *
202 | * @return {String}
203 | */
204 | buildTypeSpecificationFromTypeArray(typeList) {
205 | const typeNames = typeList.map(type => type.type);
206 |
207 | return this.buildTypeSpecificationFromTypes(typeNames);
208 | }
209 |
210 | /**
211 | * Takes a list of type names and turns them into a single docblock type specification.
212 | *
213 | * @param {Array} typeNames
214 | *
215 | * @return {String}
216 | */
217 | buildTypeSpecificationFromTypes(typeNames) {
218 | if (typeNames.length === 0) { return 'mixed'; }
219 |
220 | return typeNames.join('|');
221 | }
222 | };
223 |
--------------------------------------------------------------------------------
/lib/ServerManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Handles management of the (PHP) server that is needed to handle the server side.
7 | */
8 | class ServerManager
9 | {
10 | /**
11 | * @param {Object} phpInvoker
12 | * @param {String} folder
13 | */
14 | constructor(phpInvoker, folder) {
15 | this.phpInvoker = phpInvoker;
16 | this.folder = folder;
17 |
18 | this.distributionJobNumber = '735379565';
19 | }
20 |
21 | /**
22 | * @return Promise
23 | */
24 | async install() {
25 | const download = require('download');
26 |
27 | // TODO: Serenata offers PHARs for each PHP version it supports, but for now we can get away with using the
28 | // lowest PHP version, as newer versions are backwards compatible enough.
29 | await download(
30 | `https://gitlab.com/Serenata/Serenata/-/jobs/${this.distributionJobNumber}` +
31 | `/artifacts/raw/bin/distribution.phar`,
32 | this.phpInvoker.normalizePlatformAndRuntimePath(this.getServerSourcePath()),
33 | {
34 | filename: 'distribution.phar',
35 | }
36 | );
37 |
38 | return new Promise((resolve, reject) => {
39 | const fs = require('fs');
40 |
41 | fs.writeFile(this.getVersionSpecificationFilePath(), this.versionSpecification, (error) => {
42 | if (error) {
43 | reject(new Error(error.message));
44 | } else {
45 | resolve();
46 | }
47 | });
48 | });
49 | }
50 |
51 | /**
52 | * @return {Boolean}
53 | */
54 | isInstalled() {
55 | const fs = require('fs');
56 |
57 | return fs.existsSync(this.getVersionSpecificationFilePath());
58 | }
59 |
60 | /**
61 | * @return {String}
62 | */
63 | getServerSourcePath() {
64 | if (this.folder === null || this.folder.length === 0) {
65 | throw new Error('Failed producing a usable server source folder path');
66 | } else if (this.folder === '/') {
67 | // Can never be too careful with dynamic path generation (and recursive deletes).
68 | throw new Error('Nope, I\'m not going to use your filesystem root');
69 | }
70 |
71 | return this.folder;
72 | }
73 |
74 | /**
75 | * @return {String}
76 | */
77 | getServerExecutablePath() {
78 | const path = require('path');
79 |
80 | return path.join(this.getServerSourcePath(), 'distribution.phar');
81 | }
82 |
83 | /**
84 | * @return {String}
85 | */
86 | getVersionSpecificationFilePath() {
87 | const path = require('path');
88 |
89 | return path.join(this.folder, this.distributionJobNumber);
90 | }
91 | };
92 |
--------------------------------------------------------------------------------
/lib/ServiceContainer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports =
4 |
5 | /**
6 | * Container that provides instances of application services.
7 | */
8 | class ServiceContainer
9 | {
10 | constructor() {
11 | this.packageName = 'php-ide-serenata';
12 |
13 | this.configuration = null;
14 | this.phpInvoker = null;
15 | this.proxy = null;
16 | this.composerService = null;
17 | this.serverManager = null;
18 | this.useStatementHelper = null;
19 | this.projectManager = null;
20 | this.codeLensManager = null;
21 | this.serenataClient = null;
22 | }
23 |
24 | getConfiguration() {
25 | if ((this.configuration === null)) {
26 | const AtomConfig = require('./AtomConfig');
27 | this.configuration = new AtomConfig(this.packageName);
28 | this.configuration.load();
29 | }
30 |
31 | return this.configuration;
32 | }
33 |
34 | getPhpInvoker() {
35 | if ((this.phpInvoker === null)) {
36 | const PhpInvoker = require('./PhpInvoker');
37 | this.phpInvoker = new PhpInvoker(this.getConfiguration());
38 | }
39 |
40 | return this.phpInvoker;
41 | }
42 |
43 | getProxy() {
44 | if ((this.proxy === null)) {
45 | const Proxy = require('./Proxy');
46 | this.proxy = new Proxy(this.getConfiguration(), this.getPhpInvoker());
47 | this.proxy.setServerPath(this.getServerManager().getServerSourcePath());
48 | }
49 |
50 | return this.proxy;
51 | }
52 |
53 | getServerManager() {
54 | if ((this.serverManager === null)) {
55 | const ServerManager = require('./ServerManager');
56 | this.serverManager = new ServerManager(
57 | this.getPhpInvoker(),
58 | this.getConfiguration().get('storagePath') + '/'
59 | );
60 | }
61 |
62 | return this.serverManager;
63 | }
64 |
65 | getUseStatementHelper() {
66 | if ((this.useStatementHelper === null)) {
67 | const UseStatementHelper = require('./UseStatementHelper');
68 | this.useStatementHelper = new UseStatementHelper(true);
69 | }
70 |
71 | return this.useStatementHelper;
72 | }
73 |
74 | getProjectManager() {
75 | if (this.projectManager === null) {
76 | const ProjectManager = require('./ProjectManager');
77 | this.projectManager = new ProjectManager(this.getProxy(), this.getConfiguration());
78 | }
79 |
80 | return this.projectManager;
81 | }
82 |
83 | getCodeLensManager() {
84 | if (this.codeLensManager === null) {
85 | const CodeLensManager = require('./CodeLensManager');
86 | this.codeLensManager = new CodeLensManager();
87 | }
88 |
89 | return this.codeLensManager;
90 | }
91 |
92 | getSerenataClient() {
93 | if (this.serenataClient === null) {
94 | const SerenataClient = require('./SerenataClient');
95 | this.serenataClient = new SerenataClient(
96 | this,
97 | this.packageName
98 | );
99 | }
100 |
101 | return this.serenataClient;
102 | }
103 | };
104 |
--------------------------------------------------------------------------------
/lib/UseStatementHelper.js:
--------------------------------------------------------------------------------
1 | /*
2 | * decaffeinate suggestions:
3 | * DS102: Remove unnecessary code created because of implicit returns
4 | * DS104: Avoid inline assignments
5 | * DS202: Simplify dynamic range loops
6 | * DS205: Consider reworking code to avoid use of IIFEs
7 | * DS207: Consider shorter variations of null checks
8 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
9 | */
10 | module.exports =
11 |
12 | //#*
13 | // Contains convenience methods for dealing with use statements.
14 | //#
15 | class UseStatementHelper {
16 | /**
17 | * @param {Boolean} allowAdditionalNewlines
18 | */
19 | constructor(allowAdditionalNewlines) {
20 | /**
21 | * Regular expression that will search for a structure (class, interface, trait, ...).
22 | *
23 | * @var {RegExp}
24 | */
25 | this.structureStartRegex = /(?:abstract class|class|trait|interface)\s+(\w+)/;
26 |
27 | /**
28 | * Regular expression that will search for a use statement.
29 | *
30 | * @var {RegExp}
31 | */
32 | this.useStatementRegex = /(?:use)(?:[^\w\\])([\w\\]+)(?![\w\\])(?:(?:[ ]+as[ ]+)(\w+))?(?:;)/;
33 |
34 | /**
35 | * Whether to allow adding additional newlines to attempt to group use statements.
36 | *
37 | * @var {Boolean}
38 | */
39 | this.allowAdditionalNewlines = allowAdditionalNewlines;
40 | }
41 |
42 | /**
43 | * @param {Boolean} allowAdditionalNewlines
44 | */
45 | setAllowAdditionalNewlines(allowAdditionalNewlines) {
46 | this.allowAdditionalNewlines = allowAdditionalNewlines;
47 | }
48 |
49 | /**
50 | * Add the use for the given class if not already added.
51 | *
52 | * @param {TextEditor} editor Pulsar text editor.
53 | * @param {String} className Name of the class to add.
54 | *
55 | * @return {Number} The amount of lines added (including newlines), so you can reliably and easily offset your
56 | * rows. This could be zero if a use statement was already present.
57 | */
58 | addUseClass(editor, className) {
59 | let i, line, matches, scopeDescriptor;
60 | let asc, end;
61 | let asc1, end1;
62 | let bestUseRow = 0;
63 | let placeBelow = true;
64 | let doNewLine = true;
65 | const lineCount = editor.getLineCount();
66 | let previousMatchThatSharedNamespacePrefixRow = null;
67 |
68 | // First see if the use statement is already present. The next loop stops early (and can't do this).
69 | for (i = 0, end = lineCount - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
70 | line = editor.lineTextForBufferRow(i).trim();
71 |
72 | if (line.length === 0) { continue; }
73 |
74 | scopeDescriptor = editor.scopeDescriptorForBufferPosition([i, line.length]).getScopeChain();
75 |
76 | if (scopeDescriptor.indexOf('.comment') >= 0) {
77 | continue;
78 | }
79 |
80 | if (line.match(this.structureStartRegex)) { break; }
81 |
82 | if (matches = this.useStatementRegex.exec(line)) {
83 | if ((matches[1] === className) || ((matches[1][0] === '\\') &&
84 | (matches[1].substr(1) === className))) {
85 | return 0;
86 | }
87 | }
88 | }
89 |
90 | // Determine an appropriate location to place the use statement.
91 | for (i = 0, end1 = lineCount - 1, asc1 = 0 <= end1; asc1 ? i <= end1 : i >= end1; asc1 ? i++ : i--) {
92 | line = editor.lineTextForBufferRow(i).trim();
93 |
94 | if (line.length === 0) { continue; }
95 |
96 | scopeDescriptor = editor.scopeDescriptorForBufferPosition([i, line.length]).getScopeChain();
97 |
98 | if (scopeDescriptor.indexOf('.comment') >= 0) {
99 | continue;
100 | }
101 |
102 | if (line.match(this.structureStartRegex)) { break; }
103 |
104 | if (line.indexOf('namespace ') >= 0) {
105 | bestUseRow = i;
106 | }
107 |
108 | if (matches = this.useStatementRegex.exec(line)) {
109 | bestUseRow = i;
110 |
111 | placeBelow = true;
112 | const shareCommonNamespacePrefix = this.doShareCommonNamespacePrefix(className, matches[1]);
113 |
114 | doNewLine = !shareCommonNamespacePrefix;
115 |
116 | if (this.scoreClassName(className, matches[1]) <= 0) {
117 | placeBelow = false;
118 |
119 | // Normally we keep going until the sorting indicates we should stop, and then place the use
120 | // statement above the 'incorrect' match, but if the previous use statement was a use statement
121 | // that has the same namespace, we want to ensure we stick close to it instead of creating
122 | // additional newlines (which the item from the same namespace already placed).
123 | if (previousMatchThatSharedNamespacePrefixRow != null) {
124 | placeBelow = true;
125 | doNewLine = false;
126 | bestUseRow = previousMatchThatSharedNamespacePrefixRow;
127 | }
128 |
129 | break;
130 | }
131 |
132 | previousMatchThatSharedNamespacePrefixRow = shareCommonNamespacePrefix ? i : null;
133 | }
134 | }
135 |
136 | // Insert the use statement itself.
137 | let lineEnding = editor.getBuffer().lineEndingForRow(0);
138 |
139 | if (!this.allowAdditionalNewlines) {
140 | doNewLine = false;
141 | }
142 |
143 | if (!lineEnding) {
144 | lineEnding = '\n';
145 | }
146 |
147 | let textToInsert = '';
148 |
149 | if (doNewLine && placeBelow) {
150 | textToInsert += lineEnding;
151 | }
152 |
153 | textToInsert += `use ${className};` + lineEnding;
154 |
155 | if (doNewLine && !placeBelow) {
156 | textToInsert += lineEnding;
157 | }
158 |
159 | const lineToInsertAt = bestUseRow + (placeBelow ? 1 : 0);
160 | editor.setTextInBufferRange([[lineToInsertAt, 0], [lineToInsertAt, 0]], textToInsert);
161 |
162 | return (1 + (doNewLine ? 1 : 0));
163 | }
164 |
165 | /**
166 | * Returns a boolean indicating if the specified class names share a common namespace prefix.
167 | *
168 | * @param {String} firstClassName
169 | * @param {String} secondClassName
170 | *
171 | * @return {Boolean}
172 | */
173 | doShareCommonNamespacePrefix(firstClassName, secondClassName) {
174 | const firstClassNameParts = firstClassName.split('\\');
175 | const secondClassNameParts = secondClassName.split('\\');
176 |
177 | firstClassNameParts.pop();
178 | secondClassNameParts.pop();
179 |
180 | if (firstClassNameParts.join('\\') === secondClassNameParts.join('\\')) {
181 | return true;
182 | }
183 |
184 | return false;
185 | }
186 |
187 | /**
188 | * Scores the first class name against the second, indicating how much they 'match' each other. This can be used
189 | * to e.g. find an appropriate location to place a class in an existing list of classes.
190 | *
191 | * @param {String} firstClassName
192 | * @param {String} secondClassName
193 | *
194 | * @return {Number} A floating point number that represents the score.
195 | */
196 | scoreClassName(firstClassName, secondClassName) {
197 | const firstClassNameParts = firstClassName.split('\\');
198 | const secondClassNameParts = secondClassName.split('\\');
199 |
200 | const collator = new Intl.Collator;
201 | const maxLength = Math.min(firstClassNameParts.length, secondClassNameParts.length);
202 |
203 | // Always sort unqualified imports before everything else.
204 | if (firstClassNameParts.length !== secondClassNameParts.length) {
205 | if (firstClassNameParts.length <= 1) {
206 | return -1;
207 | } else if (secondClassNameParts.length <= 1) {
208 | return 1;
209 | }
210 | }
211 |
212 | for (let i = 0, end = maxLength - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
213 | if (firstClassNameParts[i] !== secondClassNameParts[i]) {
214 | // For use statements that only differ in the last segment (with a common namespace segment),
215 | // sort the last part by length so we get a neat gradually expanding half of a christmas tree.
216 | if (firstClassNameParts[i].length !== secondClassNameParts[i].length &&
217 | firstClassNameParts.length === secondClassNameParts.length &&
218 | i === (maxLength - 1)
219 | ) {
220 | return firstClassNameParts[i].length > secondClassNameParts[i].length ? 1 : -1;
221 | }
222 |
223 | return collator.compare(firstClassNameParts[i], secondClassNameParts[i]);
224 | }
225 | }
226 |
227 | return firstClassNameParts.length > secondClassNameParts.length ? 1 : -1;
228 | }
229 |
230 | /**
231 | * Sorts the use statements in the specified file according to the same algorithm used by 'addUseClass'.
232 | *
233 | * @param {TextEditor} editor
234 | */
235 | sortUseStatements(editor) {
236 | let endLine = null;
237 | let startLine = null;
238 | const useStatements = [];
239 |
240 | for (let i = 0, end = editor.getLineCount(), asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
241 | var matches;
242 | const lineText = editor.lineTextForBufferRow(i);
243 |
244 | endLine = i;
245 |
246 | if (!lineText || (lineText.trim() === '')) {
247 | continue;
248 |
249 | } else if (matches = this.useStatementRegex.exec(lineText)) {
250 | if (!startLine) {
251 | startLine = i;
252 | }
253 |
254 | let text = matches[1];
255 |
256 | if (matches[2] != null) {
257 | text += ` as ${matches[2]}`;
258 | }
259 |
260 | useStatements.push(text);
261 |
262 | // We still do the regex check here to prevent continuing when there are no use statements at all.
263 | } else if (startLine || this.structureStartRegex.test(lineText)) {
264 | break;
265 | }
266 | }
267 |
268 | if (useStatements.length === 0) { return; }
269 |
270 | return editor.transact(() => {
271 | editor.setTextInBufferRange([[startLine, 0], [endLine, 0]], '');
272 |
273 | return (() => {
274 | const result = [];
275 | for (let useStatement of useStatements) {
276 | // The leading slash is unnecessary, not recommended, and messes up sorting, take it out.
277 | if (useStatement[0] === '\\') {
278 | useStatement = useStatement.substr(1);
279 | }
280 |
281 | result.push(this.addUseClass(editor, useStatement, this.allowAdditionalNewlines));
282 | }
283 | return result;
284 | })();
285 | });
286 | }
287 | };
288 |
--------------------------------------------------------------------------------
/menus/menu.cson:
--------------------------------------------------------------------------------
1 | 'menu': [
2 | {
3 | 'label': 'Packages'
4 | 'submenu': [
5 | {
6 | 'label': 'Serenata',
7 | 'submenu': [
8 | {'label': 'Set Up Current Project', 'command': 'php-ide-serenata:set-up-current-project'},
9 | {'label': '(Re)index Project', 'command': 'php-ide-serenata:index-project'},
10 | {'label': 'Forcibly (Re)index Project', 'command': 'php-ide-serenata:force-index-project'},
11 | {'type' : 'separator'},
12 | {'label': 'Sort Use Statements', 'command': 'php-ide-serenata:sort-use-statements'},
13 | ]
14 | }
15 | ]
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-ide-serenata",
3 | "main": "./lib/Main",
4 | "version": "5.5.1",
5 | "description": "PHP language support for Pulsar-IDE via the Serenata server",
6 | "repository": "git@github.com:Gert-dev/php-ide-serenata",
7 | "homepage": "https://serenata.gitlab.io/",
8 | "license": "GPL-3.0-or-later",
9 | "engines": {
10 | "atom": ">=1.26.0 <2.0.0"
11 | },
12 | "providedServices": {
13 | "intentions:list": {
14 | "versions": {
15 | "1.0.0": "provideIntentions"
16 | }
17 | },
18 | "definitions": {
19 | "versions": {
20 | "0.1.0": "provideDefinitions"
21 | }
22 | },
23 | "autocomplete.provider": {
24 | "versions": {
25 | "4.0.0": "provideAutocomplete"
26 | }
27 | },
28 | "outline-view": {
29 | "versions": {
30 | "0.1.0": "provideOutlines"
31 | }
32 | },
33 | "code-highlight": {
34 | "versions": {
35 | "0.1.0": "provideCodeHighlight"
36 | }
37 | }
38 | },
39 | "consumedServices": {
40 | "snippets": {
41 | "versions": {
42 | "0.1.0": "setSnippetManager"
43 | }
44 | },
45 | "linter-indie": {
46 | "versions": {
47 | "2.0.0": "consumeLinterV2"
48 | }
49 | },
50 | "atom-ide-busy-signal": {
51 | "versions": {
52 | "0.1.0": "consumeBusySignal"
53 | }
54 | },
55 | "datatip": {
56 | "versions": {
57 | "0.1.0": "consumeDatatip"
58 | }
59 | },
60 | "signature-help": {
61 | "versions": {
62 | "0.1.0": "consumeSignatureHelp"
63 | }
64 | },
65 | "console": {
66 | "versions": {
67 | "0.1.0": "consumeConsole"
68 | }
69 | }
70 | },
71 | "dependencies": {
72 | "atom-languageclient": "^1.16.0",
73 | "atom-package-deps": "^5.0",
74 | "atom-space-pen-views": "^2.2",
75 | "download": "^7.1",
76 | "mkdirp": "^0.5.5"
77 | },
78 | "package-deps": [
79 | "atom-ide-ui",
80 | "intentions"
81 | ],
82 | "keywords": [
83 | "serenata",
84 | "php",
85 | "ide",
86 | "integration",
87 | "autocompletion",
88 | "refactoring",
89 | "docblock",
90 | "generator"
91 | ],
92 | "devDependencies": {
93 | "eslint": "^6.8.0"
94 | },
95 | "configSchema": {
96 | "core": {
97 | "type": "object",
98 | "order": 1,
99 | "properties": {
100 | "phpExecutionType": {
101 | "title": "PHP execution type",
102 | "description": "How to start PHP, which is needed to start the server in turn. \n \n 'Use PHP on the host' uses a PHP binary installed on your local machine. 'Use PHP container via Docker' requires Docker and uses a PHP container to start the server with. Using PolicyKit allows Linux users that are not part of the Docker group to enter their password via an authentication dialog to temporarily escalate privileges so the Docker daemon can be invoked once to start the server. \n \n You can use the php-ide-serenata:test-configuration command to test your setup. \n \n When using containers, project paths open in Pulsar are automatically mounted into the container at the same path. If you want to specify more exotic paths for Serenata to index in your project file, you have to ensure these are mounted in the container as well. \n \n Requires a restart after changing. \n \n",
103 | "type": "string",
104 | "default": "host",
105 | "order": 1,
106 | "enum": [
107 | {
108 | "value": "host",
109 | "description": "Use PHP on the host"
110 | },
111 | {
112 | "value": "docker",
113 | "description": "Use a PHP container via Docker (experimental)"
114 | },
115 | {
116 | "value": "docker-polkit",
117 | "description": "Use a PHP container via Docker, using PolicyKit for privilege escalation (experimental, Linux only)"
118 | },
119 | {
120 | "value": "podman",
121 | "description": "Use a PHP container via Podman, avoiding privilege escalation entirely (experimental, Linux only)"
122 | }
123 | ]
124 | },
125 | "phpCommand": {
126 | "title": "PHP command",
127 | "description": "The path to your PHP binary (e.g. /usr/bin/php, php, ...). Only applies if you've selected \"Use PHP on the host\" above. \n \n Requires a restart after changing.",
128 | "type": "string",
129 | "default": "php",
130 | "order": 2
131 | },
132 | "memoryLimit": {
133 | "title": "Memory limit (in MB)",
134 | "description": "The memory limit to set for the PHP process. The PHP process uses the available memory for in-memory caching as well, so it should not be too low. On the other hand, it shouldn't be growing very large, so setting it to -1 is probably a bad idea as an infinite loop bug might take down your system. The default should suit most projects, from small to large. \n \n Requires a restart after changing.",
135 | "type": "integer",
136 | "default": 2048,
137 | "order": 3
138 | },
139 | "additionalDockerVolumes": {
140 | "title": "Additional Docker volumes",
141 | "description": "Additional paths to mount as Docker volumes. Only applies when using Docker to run the server. Separate these using comma's, where each item follows the format \"src:dest\" as the Docker -v flag uses. \n \n Requires a restart after changing.",
142 | "type": "array",
143 | "default": [],
144 | "order": 4,
145 | "items": {
146 | "type": "string"
147 | }
148 | }
149 | }
150 | },
151 | "refactoring": {
152 | "type": "object",
153 | "order": 3,
154 | "properties": {
155 | "enable": {
156 | "title": "Enable",
157 | "description": "When enabled, refactoring actions will be available via the intentions package.",
158 | "type": "boolean",
159 | "default": true,
160 | "order": 1
161 | }
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/styles/main.less:
--------------------------------------------------------------------------------
1 | // The ui-variables file is provided by base themes provided by Pulsar.
2 | //
3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less
4 | // for a full listing of what's available.
5 | @import "ui-variables";
6 | @import "octicon-utf-codes";
7 |
8 | .php-ide-serenata-autocompletion-strike, .php-ide-serenata-autocompletion-strike .word {
9 | text-decoration: line-through;
10 | }
11 |
12 | .php-ide-serenata-autocompletion-suggestion {
13 | &.php-ide-serenata-autocompletion-has-additional-icons {
14 | .icon-container {
15 | padding-right: 0;
16 | }
17 | }
18 |
19 | .left-label {
20 | .icon {
21 | float: left;
22 | padding-top: 0.25em;
23 | padding-left: 0.5em;
24 | padding-right: 2em;
25 | margin-right: 0.75em;
26 | }
27 | }
28 |
29 | .right-label {
30 | float: right;
31 | }
32 | }
33 |
34 | .php-ide-serenata-refactoring-strikethrough {
35 | text-decoration: line-through;
36 | }
37 |
38 | .php-ide-serenata-refactoring-multi-selection-view {
39 | .list-item {
40 |
41 | }
42 |
43 | .checkbox {
44 | font-size: x-small;
45 | }
46 |
47 | .text-line {
48 | // margin-top: 0.5em;
49 | }
50 |
51 | .button-bar {
52 | // margin-top: 0.5em;
53 |
54 | .clear-float {
55 | clear: both;
56 | }
57 | }
58 | }
59 |
60 | .php-ide-serenata-refactoring-extract-method {
61 | .section-body, .form-control, .control-label, .control-group, .controls {
62 | width: 100%;
63 | }
64 | .preview-area {
65 | atom-text-editor .editor-contents--private {
66 | max-height: 400px !important;
67 | }
68 | }
69 | // Should change to reflect the padding of the theme instead of hardcoded.
70 | .block {
71 | margin-top: 4px;
72 | }
73 | .pull-right {
74 | .inline-block {
75 | margin-left: 9px;
76 | }
77 | }
78 |
79 | .checkbox {
80 | // Removing any extra margining to the checkboxes (e.g. Atom Material)
81 | margin-top: 5px;
82 | margin-bottom: 5px;
83 | }
84 |
85 | .settings-view .section-body {
86 | margin-top: 0px;
87 | }
88 |
89 | .button-bar {
90 | .clear-float {
91 | clear: both;
92 | }
93 | }
94 | }
95 |
96 | .php-ide-serenata-code-lens {
97 | // a {
98 | // &:extend(.badge);
99 | // &:extend(.badge-small);
100 | // }
101 |
102 | display: inline-block;
103 | }
104 |
105 | .php-ide-serenata-code-lens-wrapper {
106 | }
107 |
--------------------------------------------------------------------------------