├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── keymaps
└── atom-autocomplete-php.cson
├── lib
├── annotation
│ ├── abstract-provider.coffee
│ ├── annotation-manager.coffee
│ ├── method-provider.coffee
│ └── property-provider.coffee
├── autocompletion
│ ├── abstract-provider.coffee
│ ├── autocompletion-manager.coffee
│ ├── class-provider.coffee
│ ├── constant-provider.coffee
│ ├── function-provider.coffee
│ ├── member-provider.coffee
│ └── variable-provider.coffee
├── config.coffee
├── goto
│ ├── abstract-provider.coffee
│ ├── class-provider.coffee
│ ├── function-provider.coffee
│ ├── goto-manager.coffee
│ └── property-provider.coffee
├── peekmo-php-atom-autocomplete.coffee
├── services
│ ├── attached-popover.coffee
│ ├── namespace.coffee
│ ├── php-file-parser.coffee
│ ├── php-proxy.coffee
│ ├── plugin-manager.coffee
│ ├── popover.coffee
│ ├── status-error-autocomplete.coffee
│ ├── status-in-progress.coffee
│ └── use-statement.coffee
├── tooltip
│ ├── abstract-provider.coffee
│ ├── class-provider.coffee
│ ├── function-provider.coffee
│ ├── property-provider.coffee
│ └── tooltip-manager.coffee
└── views
│ └── class-list-view.coffee
├── package.json
├── php
├── Config.php
├── ErrorHandler.php
├── parser.php
├── providers
│ ├── AutocompleteProvider.php
│ ├── ClassMapRefresh.php
│ ├── ClassProvider.php
│ ├── ConstantsProvider.php
│ ├── DocParamProvider.php
│ ├── FunctionsProvider.php
│ ├── MethodsProvider.php
│ └── ProviderInterface.php
└── services
│ ├── DocParser.php
│ ├── FileParser.php
│ └── Tools.php
├── spec
└── peekmo-php-atom-autocomplete-spec.coffee
└── styles
└── peekmo-php-atom-autocomplete.less
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 | /php/tmp.php
5 | /indexes/
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.25.0
2 | * You can now reindex the project with the command "reindexing project" (the project is reindexed by default when you open a new window of your project)
3 |
4 | ## 0.24.0
5 | * Default keybinding for goto-backtrack changed to ctrl-shift-g (to not override windows/linux keybindings)
6 | * Ability to change the key to press with "click" in package settings
7 |
8 | ## 0.23.0
9 | * Multiple bug fixes
10 | * Import use of a selected class outside completion by @fuelingtheweb (see #338) (shortcut : ctrl-alt-u)
11 |
12 | ## 0.22.0
13 | * Support for `@property` and `@method` annotations (#318)
14 |
15 | ## 0.21.0
16 | * Support for atom 1.13 (fix deprecations)
17 |
18 | ## 0.20.0
19 | * Support completion and goto on drupal 6/7 functions
20 |
21 | ## 0.19.0
22 | * Multiple bug fixes
23 | * Function's return value type hint parsed (PHP7)
24 |
25 | ## 0.18.0
26 | * Plugin configuration is now more interactive. If there's an error, you'll know it without doing the command
27 |
28 | ## 0.17.0
29 | * [internal] String parameters are keep in autocomplete in order to be able to change the function return (needed for symfony2 support)
30 |
31 | ## 0.16.0
32 | * Support for external services (see atom-symfony2) package
33 | * Doc refactoring
34 |
35 | ## 0.15.0
36 | * Fail message when saving are now just in developer console by default (you can change this in settings)
37 | * A progress bar appears in status bar when there's a classes' indexing in progress
38 | * Uses are now ordered. You can even have new line between them (see plugin settings)
39 | * Annotations on class properties
40 |
41 | ## 0.14.0
42 | * Support configuration checking on Windows
43 | * Lot of code refactoring (many bug resolved, but perhaps some new :()
44 | * Tooltips on classes, interfaces
45 | * Constants provider
46 | * Autocomplete from type hints in closures
47 |
48 | ## 0.13.0
49 | * Custom tooltip management (does not rely on atom's one anymore)
50 |
51 | ## 0.12.0
52 | * Support for {@inheritdoc} && {@inheritDoc} as the only one comment (symfony2 style)
53 | * Bugfixes for non PHP projects
54 | * Bugfixes on Docblock parser
55 |
56 | ## 0.11.0
57 | * Bugfixes on Goto
58 | * Major refactor in the code of the plugin itself
59 |
60 | ## 0.10.0
61 | * Autocomplete in catch() {} #91
62 | * Comments "@var $var Class" now supported for completion
63 |
64 | ## 0.9.0
65 | * Many bugfixes and improvements for tooltips (from @hotoiledgoblinsack)
66 | * Basic autocomplete on "new" keyword (e.g :
67 | $x = new \DateTime();
68 | $x->{autocomplete}
69 | )
70 |
71 | ## 0.8.0
72 | * Tooltips on methods and attributes
73 | * Strikethrough style to deprecated methods
74 |
75 | ## 0.7.0
76 | * Goto class properties
77 | * Goto bugfixes
78 |
79 | ## 0.6.0
80 | * Goto command on first level of methods, and classes (#42 by @CWDN)
81 | * Fix namespace on the same line as PHP tag
82 |
83 | ## 0.5.0
84 | * Support for Windows
85 |
86 | ## 0.4.0
87 | * Completion on local variables
88 | * Bug fixes
89 |
90 | ## 0.3.0
91 | * Completion $this on multiline
92 | * Bug fixes
93 |
94 | ## 0.2.0
95 | * Completion on parent::
96 | * Completion on self::
97 | * Bug fixes
98 |
99 | ## 0.1.0
100 | * Completion on classNames
101 | * Completion on $this->
102 | * Completion on static methods
103 | * Namespace management
104 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | Here is a list of every people to thank, because without them, the package would not be what it is today.
2 |
3 | - Tom Gerrits
4 | - [Chris Normansell (@CWDN)](https://github.com/CWDN)
5 | - [Guillaume Perréal (@Adirelle)](https://github.com/Adirelle)
6 | - [Vincent Klaiber (@vinkla)](https://github.com/vinkla)
7 | - [@Benoth](https://github.com/Benoth)
8 | - [Axel Anceau (@Peekmo)](https://github.com/Peekmo)
9 |
10 | And **you** who are using the plugin and reporting issues ;)
11 |
12 | Thanks to everyone.
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2015 Axel Anceau
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # atom-autocomplete-php
2 |
3 | [](https://gitter.im/Peekmo/atom-autocomplete-php?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 |
5 | atom-autocomplete-php provides autocompletion for the PHP language for projects that use Composer for dependency management. What cool things can you expect?
6 | * Autocompletion of class members, built-in constants, built-in PHP functions, ...
7 | * Autocompletion of class names and automatic adding of `use` statements where needed.
8 | * Alt-clicking class members, class names, etc. to navigate to their definition.
9 | * Annotations in the gutter for methods that are overrides or interface implementations.
10 | * Tooltips for methods, classes, etc. that display information about the item itself.
11 | * IntellJ-style variable annotations `/** @var MyType $var */` as well as `/** @var $var MyType */`.
12 | * Shortcut variable annotations (must appear right above the respective variable) `/** @var MyType */`.
13 | * Add use statement of class under cursor (ctrl-alt-u)
14 | * ...
15 |
16 | ## What do I need to do to make it work?
17 | Currently the following limitations or restrictions are present:
18 | * You must use [Composer](https://getcomposer.org/) for dependency management.
19 | * You must have PHP 5.5+ with xml extension installed.
20 | * You must follow the PSR standards (for the names of classes, methods, namespacing, etc.).
21 | * You must write proper docblocks for your methods. There currently is no standard around this, but we try to follow the draft PSR-5 standard (which, in turn, is mostly inspired by phpDocumentor's implementation). Minimum requirements for proper autocompletion:
22 | * `@return` statements for functions and methods.
23 | * `@param` statements for functions and methods.
24 | * `@var` statements for properties in classes.
25 | * (Type hints in functions and methods will also be checked.)
26 |
27 | Some features may or may not work outside these restrictions. Composer is primarily used for its classmap, to fetch a list of classes that are present in your codebase. Reflection is used to fetch information about classes.
28 |
29 | The package also requires a one time setup, To configure the plugin, click on "package" in your preferences, and select "settings" on atom-autocomplete-php plugin.
30 |
31 | - **Command to use composer** : it's highly recommended to write here the full path to your composer.phar bin. E.G on unix systems, it could be /usr/local/bin/composer. Using an alias is not recommended at all!
32 | - **Command php** : Command to execute PHP cli in your console. (php by default on unix systems). If it doesn't work, put here the full path to your PHP bin.
33 | - **Autoload file** : Write here, a coma separated list of all the different path to the autoload files. By default, it's "vendor/autoload.php" for composer projects ;)
34 | - **Classmap files** : All paths to PHP files that returns an array of "className" => "fullPath to the file where the class is located". The default one for composer is vendor/composer/autoload_classmap.php
35 |
36 | You can test your configuration by using a command (cmd - shift - p) : ```Atom Autocomplete Php : Configuration```
37 |
38 | ### Linux
39 | 
40 |
41 |
42 | ### Windows (WAMP and ComposerSetup)
43 | 
44 |
45 |
46 | ## CMS integration
47 | * Built-in support for Drupal 6/7 functions
48 |
49 | ## Framework integration
50 | * [Symfony2 plugin](https://github.com/Peekmo/atom-symfony2)
51 |
52 | ## What Does Not Work?
53 | * Most of the issue reports indicate things that are missing, but autocompletion should be working fairly well in general.
54 |
55 | ### Won't Fix (For Now)
56 | * "Go to definition" will take you to the incorrect location if a class is using a method with the exact same name as one in its own direct traits. You will be taken to the trait method instead of the class method (the latter should take precedence). See also issue #177.
57 | * `static` and `self` behave mostly like `$this` and can access non-static methods when used in non-static contexts. See also issue #101.
58 |
59 | ### Conflicts
60 | * This package has known conflicts with other PHP autocomplete packages. Disable this one or the other one to avoid some errors.
61 |
62 | ## What's Next & Contributing
63 | Keep in mind that this plugin is under active development. If you find a bug, please, open an issue with more information on how to reproduce. Feel free to contribute ;)
64 |
65 | 
66 |
--------------------------------------------------------------------------------
/keymaps/atom-autocomplete-php.cson:
--------------------------------------------------------------------------------
1 | 'atom-text-editor':
2 | 'ctrl-alt-n': 'atom-autocomplete-php:namespace'
3 | 'ctrl-alt-g': 'atom-autocomplete-php:goto'
4 | 'ctrl-shift-g': 'atom-autocomplete-php:goto-backtrack'
5 | 'ctrl-alt-u': 'atom-autocomplete-php:import-use-statement'
6 |
--------------------------------------------------------------------------------
/lib/annotation/abstract-provider.coffee:
--------------------------------------------------------------------------------
1 | {Range, Point, TextEditor} = require 'atom'
2 |
3 | SubAtom = require 'sub-atom'
4 |
5 | AttachedPopover = require '../services/attached-popover'
6 |
7 | module.exports =
8 |
9 | class AbstractProvider
10 | # The regular expression that a line must match in order for it to be checked if it requires an annotation.
11 | regex: null
12 | markers: []
13 | subAtoms: []
14 |
15 | ###*
16 | * Initializes this provider.
17 | ###
18 | init: () ->
19 | @$ = require 'jquery'
20 | @parser = require '../services/php-file-parser'
21 |
22 | atom.workspace.observeTextEditors (editor) =>
23 | editor.onDidSave (event) =>
24 | @rescan(editor)
25 |
26 | @registerAnnotations editor
27 | @registerEvents editor
28 |
29 | # When you go back to only have 1 pane the events are lost, so need to re-register.
30 | atom.workspace.onDidDestroyPane (pane) =>
31 | panes = atom.workspace.getPanes()
32 |
33 | if panes.length == 1
34 | for paneItem in panes[0].items
35 | if paneItem instanceof TextEditor
36 | @registerEvents paneItem
37 |
38 | # Having to re-register events as when a new pane is created the old panes lose the events.
39 | atom.workspace.onDidAddPane (observedPane) =>
40 | panes = atom.workspace.getPanes()
41 |
42 | for pane in panes
43 | if pane == observedPane
44 | continue
45 |
46 | for paneItem in pane.items
47 | if paneItem instanceof TextEditor
48 | @registerEvents paneItem
49 |
50 | ###*
51 | * Deactives the provider.
52 | ###
53 | deactivate: () ->
54 | @removeAnnotations()
55 |
56 | ###*
57 | * Registers event handlers.
58 | *
59 | * @param {TextEditor} editor TextEditor to register events to.
60 | ###
61 | registerEvents: (editor) ->
62 | if editor.getGrammar().scopeName.match /text.html.php$/
63 | # Ticket #107 - Mouseout isn't generated until the mouse moves, even when scrolling (with the keyboard or
64 | # mouse). If the element goes out of the view in the meantime, its HTML element disappears, never removing
65 | # it.
66 | editor.onDidDestroy () =>
67 | @removePopover()
68 |
69 | editor.onDidStopChanging () =>
70 | @removePopover()
71 |
72 | textEditorElement = atom.views.getView(editor)
73 |
74 | @$(textEditorElement).find('.horizontal-scrollbar').on 'scroll', () =>
75 | @removePopover()
76 |
77 | @$(textEditorElement).find('.vertical-scrollbar').on 'scroll', () =>
78 | @removePopover()
79 |
80 | ###*
81 | * Registers the annotations.
82 | *
83 | * @param {TextEditor} editor The editor to search through.
84 | ###
85 | registerAnnotations: (editor) ->
86 | text = editor.getText()
87 | rows = text.split('\n')
88 | @subAtoms[editor.getLongTitle()] = new SubAtom
89 |
90 | for rowNum,row of rows
91 | while (match = @regex.exec(row))
92 | @placeAnnotation(editor, rowNum, row, match)
93 |
94 | ###*
95 | * Places an annotation at the specified line and row text.
96 | *
97 | * @param {TextEditor} editor
98 | * @param {int} row
99 | * @param {String} rowText
100 | * @param {Array} match
101 | ###
102 | placeAnnotation: (editor, row, rowText, match) ->
103 | annotationInfo = @extractAnnotationInfo(editor, row, rowText, match)
104 |
105 | if not annotationInfo
106 | return
107 |
108 | range = new Range(
109 | new Point(parseInt(row), 0),
110 | new Point(parseInt(row), rowText.length)
111 | )
112 |
113 | # For Atom 1.3 or greater, maintainHistory can only be applied to entire
114 | # marker layers. Layers don't exist in earlier versions, hence the
115 | # conditional logic.
116 | if typeof editor.addMarkerLayer is 'function'
117 | @markerLayers ?= new WeakMap
118 | unless markerLayer = @markerLayers.get(editor)
119 | markerLayer = editor.addMarkerLayer(maintainHistory: true)
120 | @markerLayers.set(editor, markerLayer)
121 |
122 | marker = (markerLayer ? editor).markBufferRange(range)
123 |
124 | decoration = editor.decorateMarker(marker, {
125 | type: 'line-number',
126 | class: annotationInfo.lineNumberClass
127 | })
128 |
129 | longTitle = editor.getLongTitle()
130 |
131 | if @markers[longTitle] == undefined
132 | @markers[longTitle] = []
133 |
134 | @markers[longTitle].push(marker)
135 |
136 | @registerAnnotationEventHandlers(editor, row, annotationInfo)
137 |
138 | ###*
139 | * Exracts information about the annotation match.
140 | *
141 | * @param {TextEditor} editor
142 | * @param {int} row
143 | * @param {String} rowText
144 | * @param {Array} match
145 | ###
146 | extractAnnotationInfo: (editor, row, rowText, match) ->
147 |
148 | ###*
149 | * Registers annotation event handlers for the specified row.
150 | *
151 | * @param {TextEditor} editor
152 | * @param {int} row
153 | * @param {Object} annotationInfo
154 | ###
155 | registerAnnotationEventHandlers: (editor, row, annotationInfo) ->
156 | textEditorElement = atom.views.getView(editor)
157 | gutterContainerElement = @$(textEditorElement).find('.gutter-container')
158 |
159 | do (editor, gutterContainerElement, annotationInfo) =>
160 | longTitle = editor.getLongTitle()
161 | selector = '.line-number' + '.' + annotationInfo.lineNumberClass + '[data-buffer-row=' + row + '] .icon-right'
162 |
163 | @subAtoms[longTitle].add gutterContainerElement, 'mouseover', selector, (event) =>
164 | @handleMouseOver(event, editor, annotationInfo)
165 |
166 | @subAtoms[longTitle].add gutterContainerElement, 'mouseout', selector, (event) =>
167 | @handleMouseOut(event, editor, annotationInfo)
168 |
169 | @subAtoms[longTitle].add gutterContainerElement, 'click', selector, (event) =>
170 | @handleMouseClick(event, editor, annotationInfo)
171 |
172 | ###*
173 | * Handles the mouse over event on an annotation.
174 | *
175 | * @param {jQuery.Event} event
176 | * @param {TextEditor} editor
177 | * @param {Object} annotationInfo
178 | ###
179 | handleMouseOver: (event, editor, annotationInfo) ->
180 | if annotationInfo.tooltipText
181 | @removePopover()
182 |
183 | @attachedPopover = new AttachedPopover(event.target)
184 | @attachedPopover.setText(annotationInfo.tooltipText)
185 | @attachedPopover.show()
186 |
187 | ###*
188 | * Handles the mouse out event on an annotation.
189 | *
190 | * @param {jQuery.Event} event
191 | * @param {TextEditor} editor
192 | * @param {Object} annotationInfo
193 | ###
194 | handleMouseOut: (event, editor, annotationInfo) ->
195 | @removePopover()
196 |
197 | ###*
198 | * Handles the mouse click event on an annotation.
199 | *
200 | * @param {jQuery.Event} event
201 | * @param {TextEditor} editor
202 | * @param {Object} annotationInfo
203 | ###
204 | handleMouseClick: (event, editor, annotationInfo) ->
205 |
206 | ###*
207 | * Removes the existing popover, if any.
208 | ###
209 | removePopover: () ->
210 | if @attachedPopover
211 | @attachedPopover.dispose()
212 | @attachedPopover = null
213 |
214 | ###*
215 | * Removes any annotations that were created.
216 | *
217 | * @param {TextEditor} editor The editor to search through.
218 | ###
219 | removeAnnotations: (editor) ->
220 | if editor?
221 | for i,marker of @markers[editor.getLongTitle()]
222 | marker.destroy()
223 | @markers[editor.getLongTitle()] = []
224 | @subAtoms[editor.getLongTitle()]?.dispose()
225 | else
226 | for i,name of @markers
227 | for i,marker of @markers[name]
228 | marker.destroy()
229 | @markers = []
230 | for i, subAtom of @subAtoms
231 | subAtom.dispose()
232 | @subAtoms = []
233 |
234 | ###*
235 | * Rescans the editor, updating all annotations.
236 | *
237 | * @param {TextEditor} editor The editor to search through.
238 | ###
239 | rescan: (editor) ->
240 | @removeAnnotations(editor)
241 | @registerAnnotations(editor)
242 |
--------------------------------------------------------------------------------
/lib/annotation/annotation-manager.coffee:
--------------------------------------------------------------------------------
1 | MethodProvider = require './method-provider.coffee'
2 | PropertyProvider = require './property-provider.coffee'
3 |
4 | module.exports =
5 |
6 | class AnnotationManager
7 | providers: []
8 |
9 | ###*
10 | * Initializes the tooltip providers.
11 | ###
12 | init: () ->
13 | @providers.push new MethodProvider()
14 | @providers.push new PropertyProvider()
15 |
16 | for provider in @providers
17 | provider.init(@)
18 |
19 | ###*
20 | * Deactivates the tooltip providers.
21 | ###
22 | deactivate: () ->
23 | for provider in @providers
24 | provider.deactivate()
25 |
--------------------------------------------------------------------------------
/lib/annotation/method-provider.coffee:
--------------------------------------------------------------------------------
1 | AbstractProvider = require './abstract-provider'
2 |
3 | module.exports =
4 |
5 | # Provides annotations for overriding methods and implementations of interface methods.
6 | class FunctionProvider extends AbstractProvider
7 | regex: /(\s*(?:public|protected|private)\s+function\s+)(\w+)\s*\(/g
8 |
9 | ###*
10 | * @inheritdoc
11 | ###
12 | extractAnnotationInfo: (editor, row, rowText, match) ->
13 | currentClass = @parser.getFullClassName(editor)
14 |
15 | propertyName = match[2]
16 |
17 | context = @parser.getMemberContext(editor, propertyName, null, currentClass)
18 |
19 | if not context or (not context.override and not context.implementation)
20 | return null
21 |
22 | extraData = null
23 | tooltipText = ''
24 | lineNumberClass = ''
25 |
26 | # NOTE: We deliberately show the declaring class here, not the structure (which could be a trait).
27 | if context.override
28 | extraData = context.override
29 | lineNumberClass = 'override'
30 | tooltipText = 'Overrides method from ' + extraData.declaringClass.name
31 |
32 | else
33 | extraData = context.implementation
34 | lineNumberClass = 'implementation'
35 | tooltipText = 'Implements method for ' + extraData.declaringClass.name
36 |
37 | return {
38 | lineNumberClass : lineNumberClass
39 | tooltipText : tooltipText
40 | extraData : extraData
41 | }
42 |
43 | ###*
44 | * @inheritdoc
45 | ###
46 | handleMouseClick: (event, editor, annotationInfo) ->
47 | atom.workspace.open(annotationInfo.extraData.declaringStructure.filename, {
48 | initialLine : annotationInfo.extraData.startLine - 1,
49 | searchAllPanes : true
50 | })
51 |
52 | ###*
53 | * @inheritdoc
54 | ###
55 | removePopover: () ->
56 | if @attachedPopover
57 | @attachedPopover.dispose()
58 | @attachedPopover = null
59 |
--------------------------------------------------------------------------------
/lib/annotation/property-provider.coffee:
--------------------------------------------------------------------------------
1 | AbstractProvider = require './abstract-provider'
2 |
3 | module.exports =
4 |
5 | # Provides annotations for overriding property.
6 | class FunctionProvider extends AbstractProvider
7 | regex: /(\s*(?:public|protected|private)\s+\$)(\w+)\s+/g
8 |
9 | ###*
10 | * @inheritdoc
11 | ###
12 | extractAnnotationInfo: (editor, row, rowText, match) ->
13 | currentClass = @parser.getFullClassName(editor)
14 |
15 | propertyName = match[2]
16 |
17 | context = @parser.getMemberContext(editor, propertyName, null, currentClass)
18 |
19 | if not context or not context.override
20 | return null
21 |
22 | # NOTE: We deliberately show the declaring class here, not the structure (which could be a trait).
23 | return {
24 | lineNumberClass : 'override'
25 | tooltipText : 'Overrides property from ' + context.override.declaringClass.name
26 | extraData : context.override
27 | }
28 |
29 | ###*
30 | * @inheritdoc
31 | ###
32 | handleMouseClick: (event, editor, annotationInfo) ->
33 | atom.workspace.open(annotationInfo.extraData.declaringStructure.filename, {
34 | # initialLine : annotationInfo.startLine - 1,
35 | searchAllPanes : true
36 | })
37 |
--------------------------------------------------------------------------------
/lib/autocompletion/abstract-provider.coffee:
--------------------------------------------------------------------------------
1 | parser = require "../services/php-file-parser.coffee"
2 |
3 | module.exports =
4 |
5 | # Abstract base class for autocompletion providers.
6 | class AbstractProvider
7 | regex: ''
8 | selector: '.source.php'
9 |
10 | inclusionPriority: 10
11 |
12 | disableForSelector: '.source.php .comment, .source.php .string'
13 |
14 | ###*
15 | * Initializes this provider.
16 | ###
17 | init: () ->
18 |
19 | ###*
20 | * Deactives the provider.
21 | ###
22 | deactivate: () ->
23 |
24 | ###*
25 | * Entry point of all request from autocomplete-plus
26 | * Calls @fetchSuggestion in the provider if allowed
27 | * @return array Suggestions
28 | ###
29 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
30 | new Promise (resolve) =>
31 | resolve(@fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix}))
32 |
33 | ###*
34 | * Builds a snippet for a PHP function
35 | * @param {string} word Function name
36 | * @param {array} elements All arguments for the snippet (parameters, optionals)
37 | * @return string The snippet
38 | ###
39 | getFunctionSnippet: (word, elements) ->
40 | body = word + "("
41 | lastIndex = 0
42 |
43 | # Non optional elements
44 | for arg, index in elements.parameters
45 | body += ", " if index != 0
46 | body += "${" + (index+1) + ":" + arg + "}"
47 | lastIndex = index+1
48 |
49 | # Optional elements. One big same snippet
50 | if elements.optionals.length > 0
51 | body += " ${" + (lastIndex + 1) + ":["
52 | body += ", " if lastIndex != 0
53 |
54 | lastIndex += 1
55 |
56 | for arg, index in elements.optionals
57 | body += ", " if index != 0
58 | body += arg
59 | body += "]}"
60 |
61 | body += ")"
62 |
63 | # Ensure the user ends up after the inserted text when he's done cycling through the parameters with tab.
64 | body += "$0"
65 |
66 | return body
67 |
68 | ###*
69 | * Builds the signature for a PHP function
70 | * @param {string} word Function name
71 | * @param {array} elements All arguments for the signature (parameters, optionals)
72 | * @return string The signature
73 | ###
74 | getFunctionSignature: (word, element) ->
75 | snippet = @getFunctionSnippet(word, element)
76 |
77 | # Just strip out the placeholders.
78 | signature = snippet.replace(/\$\{\d+:([^\}]+)\}/g, '$1')
79 |
80 | return signature[0 .. -3]
81 |
82 | ###*
83 | * Get prefix from bufferPosition and @regex
84 | * @return string
85 | ###
86 | getPrefix: (editor, bufferPosition) ->
87 | # Get the text for the line up to the triggered buffer position
88 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition])
89 |
90 | # Match the regex to the line, and return the match
91 | matches = line.match(@regex)
92 |
93 | # Looking for the correct match
94 | if matches?
95 | for match in matches
96 | start = bufferPosition.column - match.length
97 | if start >= 0
98 | word = editor.getTextInBufferRange([[bufferPosition.row, bufferPosition.column - match.length], bufferPosition])
99 | if word == match
100 | # Not really nice hack.. But non matching groups take the first word before. So I remove it.
101 | # Necessary to have completion juste next to a ( or [ or {
102 | if match[0] == '{' or match[0] == '(' or match[0] == '['
103 | match = match.substring(1)
104 |
105 | return match
106 |
107 | return ''
108 |
--------------------------------------------------------------------------------
/lib/autocompletion/autocompletion-manager.coffee:
--------------------------------------------------------------------------------
1 | ClassProvider = require './class-provider.coffee'
2 | MemberProvider = require './member-provider.coffee'
3 | ConstantProvider = require './constant-provider.coffee'
4 | VariableProvider = require './variable-provider.coffee'
5 | FunctionProvider = require './function-provider.coffee'
6 |
7 | module.exports =
8 |
9 | class AutocompletionManager
10 | providers: []
11 |
12 | ###*
13 | * Initializes the autocompletion providers.
14 | ###
15 | init: () ->
16 | @providers.push new ConstantProvider()
17 | @providers.push new VariableProvider()
18 | @providers.push new FunctionProvider()
19 | @providers.push new ClassProvider()
20 | @providers.push new MemberProvider()
21 |
22 | for provider in @providers
23 | provider.init(@)
24 |
25 | ###*
26 | * Deactivates the autocompletion providers.
27 | ###
28 | deactivate: () ->
29 | for provider in @providers
30 | provider.deactivate()
31 |
32 | ###*
33 | * Deactivates the autocompletion providers.
34 | ###
35 | getProviders: () ->
36 | @providers
37 |
--------------------------------------------------------------------------------
/lib/autocompletion/class-provider.coffee:
--------------------------------------------------------------------------------
1 | fuzzaldrin = require 'fuzzaldrin'
2 | exec = require "child_process"
3 |
4 | config = require "../config.coffee"
5 | proxy = require "../services/php-proxy.coffee"
6 | parser = require "../services/php-file-parser.coffee"
7 | AbstractProvider = require "./abstract-provider"
8 |
9 | module.exports =
10 |
11 | # Autocompletion for class names (e.g. after the new or use keyword).
12 | class ClassProvider extends AbstractProvider
13 | classes = []
14 | disableForSelector: '.source.php .string'
15 |
16 | ###*
17 | * Get suggestions from the provider (@see provider-api)
18 | * @return array
19 | ###
20 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
21 | # "new" keyword or word starting with capital letter
22 | @regex = /((?:new|use)?(?:[^a-z0-9_])\\?(?:[A-Z][a-zA-Z_\\]*)+)/g
23 |
24 | prefix = @getPrefix(editor, bufferPosition)
25 | return unless prefix.length
26 |
27 | @classes = proxy.classes()
28 | return unless @classes?.autocomplete?
29 |
30 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]])
31 | insertParameterList = if characterAfterPrefix == '(' then false else true
32 |
33 | suggestions = @findSuggestionsForPrefix(prefix.trim(), insertParameterList)
34 | return unless suggestions.length
35 | return suggestions
36 |
37 | ###*
38 | * Get suggestions from the provider for a single word (@see provider-api)
39 | * @return array
40 | ###
41 | fetchSuggestionsFromWord: (word) ->
42 | @classes = proxy.classes()
43 | return unless @classes?.autocomplete?
44 |
45 | suggestions = @findSuggestionsForPrefix(word)
46 | return unless suggestions.length
47 | return suggestions
48 |
49 | ###*
50 | * Returns suggestions available matching the given prefix
51 | * @param {string} prefix Prefix to match.
52 | * @param {bool} insertParameterList Whether to insert a list of parameters for methods.
53 | * @return array
54 | ###
55 | findSuggestionsForPrefix: (prefix, insertParameterList = true) ->
56 | # Get rid of the leading "new" or "use" keyword
57 | instantiation = false
58 | use = false
59 |
60 | if prefix.indexOf("new \\") != -1
61 | instantiation = true
62 | prefix = prefix.replace /new \\/, ''
63 | else if prefix.indexOf("new ") != -1
64 | instantiation = true
65 | prefix = prefix.replace /new /, ''
66 | else if prefix.indexOf("use ") != -1
67 | use = true
68 | prefix = prefix.replace /use /, ''
69 |
70 | if prefix.indexOf("\\") == 0
71 | prefix = prefix.substring(1, prefix.length)
72 |
73 | # Filter the words using fuzzaldrin
74 | words = fuzzaldrin.filter @classes.autocomplete, prefix
75 |
76 | # Builds suggestions for the words
77 | suggestions = []
78 |
79 | for word in words when word isnt prefix
80 | classInfo = @classes.mapping[word]
81 |
82 | # Just print classes with constructors with "new"
83 | if instantiation and @classes.mapping[word].methods.constructor.has
84 | args = classInfo.methods.constructor.args
85 |
86 | suggestions.push
87 | text: word,
88 | type: 'class',
89 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else ''
90 | snippet: if insertParameterList then @getFunctionSnippet(word, args) else null
91 | displayText: @getFunctionSignature(word, args)
92 | data:
93 | kind: 'instantiation',
94 | prefix: prefix,
95 | replacementPrefix: prefix
96 |
97 | else if use
98 | suggestions.push
99 | text: word,
100 | type: 'class',
101 | prefix: prefix,
102 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else ''
103 | replacementPrefix: prefix,
104 | data:
105 | kind: 'use'
106 |
107 | # Not instantiation => not printing constructor params
108 | else
109 | suggestions.push
110 | text: word,
111 | type: 'class',
112 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else ''
113 | data:
114 | kind: 'static',
115 | prefix: prefix,
116 | replacementPrefix: prefix
117 |
118 | return suggestions
119 |
120 | ###*
121 | * Adds the missing use if needed
122 | * @param {TextEditor} editor
123 | * @param {Position} triggerPosition
124 | * @param {object} suggestion
125 | ###
126 | onDidInsertSuggestion: ({editor, triggerPosition, suggestion}) ->
127 | return unless suggestion.data?.kind
128 |
129 | if suggestion.data.kind == 'instantiation' or suggestion.data.kind == 'static'
130 | editor.transact () =>
131 | linesAdded = parser.addUseClass(editor, suggestion.text, config.config.insertNewlinesForUseStatements, config.config.ensureNewLineAfterNamespace)
132 |
133 | # Removes namespace from classname
134 | if linesAdded != null
135 | name = suggestion.text
136 | splits = name.split('\\')
137 |
138 | nameLength = splits[splits.length-1].length
139 | startColumn = triggerPosition.column - suggestion.data.prefix.length
140 | row = triggerPosition.row + linesAdded
141 |
142 | if suggestion.data.kind == 'instantiation'
143 | endColumn = startColumn + name.length - nameLength - splits.length + 1
144 |
145 | else
146 | endColumn = startColumn + name.length - nameLength
147 |
148 | editor.setTextInBufferRange([
149 | [row, startColumn],
150 | [row, endColumn] # Because when selected there's not \ (why?)
151 | ], "")
152 |
153 | ###*
154 | * Adds the missing use if needed without removing text from editor
155 | * @param {TextEditor} editor
156 | * @param {object} suggestion
157 | ###
158 | onSelectedClassSuggestion: ({editor, suggestion}) ->
159 | return unless suggestion.data?.kind
160 |
161 | if suggestion.data.kind == 'instantiation' or suggestion.data.kind == 'static'
162 | editor.transact () =>
163 | linesAdded = parser.addUseClass(editor, suggestion.text, config.config.insertNewlinesForUseStatements, config.config.ensureNewLineAfterNamespace)
164 |
--------------------------------------------------------------------------------
/lib/autocompletion/constant-provider.coffee:
--------------------------------------------------------------------------------
1 | fuzzaldrin = require 'fuzzaldrin'
2 |
3 | proxy = require "../services/php-proxy.coffee"
4 | parser = require "../services/php-file-parser.coffee"
5 | AbstractProvider = require "./abstract-provider"
6 |
7 | config = require "../config.coffee"
8 |
9 | module.exports =
10 |
11 | # Autocompletion for internal PHP constants.
12 | class ConstantProvider extends AbstractProvider
13 | constants: []
14 |
15 | ###*
16 | * Get suggestions from the provider (@see provider-api)
17 | * @return array
18 | ###
19 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
20 | # not preceded by a > (arrow operator), a $ (variable start), ...
21 | @regex = /(?:(?:^|[^\w\$_\>]))([A-Z_]+)(?![\w\$_\>])/g
22 |
23 | prefix = @getPrefix(editor, bufferPosition)
24 | return unless prefix.length
25 |
26 | @constants = proxy.constants()
27 | return unless @constants?.names?
28 |
29 | suggestions = @findSuggestionsForPrefix(prefix.trim())
30 | return unless suggestions.length
31 | return suggestions
32 |
33 | ###*
34 | * Returns suggestions available matching the given prefix
35 | * @param {string} prefix Prefix to match
36 | * @return array
37 | ###
38 | findSuggestionsForPrefix: (prefix) ->
39 | # Filter the words using fuzzaldrin
40 | words = fuzzaldrin.filter @constants.names, prefix
41 |
42 | # Builds suggestions for the words
43 | suggestions = []
44 | for word in words
45 | for element in @constants.values[word]
46 | suggestions.push
47 | text: word,
48 | type: 'constant',
49 | description: 'Built-in PHP constant.'
50 |
51 | return suggestions
52 |
--------------------------------------------------------------------------------
/lib/autocompletion/function-provider.coffee:
--------------------------------------------------------------------------------
1 | fuzzaldrin = require 'fuzzaldrin'
2 |
3 | proxy = require "../services/php-proxy.coffee"
4 | parser = require "../services/php-file-parser.coffee"
5 | AbstractProvider = require "./abstract-provider"
6 |
7 | config = require "../config.coffee"
8 |
9 | module.exports =
10 |
11 | # Autocompletion for internal PHP functions.
12 | class FunctionProvider extends AbstractProvider
13 | functions: []
14 |
15 | ###*
16 | * Get suggestions from the provider (@see provider-api)
17 | * @return array
18 | ###
19 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
20 | # not preceded by a > (arrow operator), a $ (variable start), ...
21 | @regex = /(?:(?:^|[^\w\$_\>]))([a-zA-Z_]+)(?![\w\$_\>])/g
22 |
23 | prefix = @getPrefix(editor, bufferPosition)
24 | return unless prefix.length
25 |
26 | @functions = proxy.functions()
27 | return unless @functions?.names?
28 |
29 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]])
30 | insertParameterList = if characterAfterPrefix == '(' then false else true
31 |
32 | suggestions = @findSuggestionsForPrefix(prefix.trim(), insertParameterList)
33 | return unless suggestions.length
34 | return suggestions
35 |
36 | ###*
37 | * Returns suggestions available matching the given prefix.
38 | *
39 | * @param {string} prefix Prefix to match.
40 | * @param {bool} insertParameterList Whether to insert a list of parameters.
41 | *
42 | * @return {Array}
43 | ###
44 | findSuggestionsForPrefix: (prefix, insertParameterList = true) ->
45 | # Filter the words using fuzzaldrin
46 | words = fuzzaldrin.filter @functions.names, prefix
47 |
48 | # Builds suggestions for the words
49 | suggestions = []
50 | for word in words
51 | for element in @functions.values[word]
52 | returnValueParts = if element.args.return?.type then element.args.return.type.split('\\') else []
53 | returnValue = returnValueParts[returnValueParts.length - 1]
54 |
55 | suggestion =
56 | text: word,
57 | type: 'function',
58 | description: if element.isInternal then 'Built-in PHP function.' else (if element.args.descriptions.short? then element.args.descriptions.short else '')
59 | className: if element.args.deprecated then 'php-atom-autocomplete-strike' else ''
60 | snippet: if insertParameterList then @getFunctionSnippet(word, element.args) else null
61 | displayText: @getFunctionSignature(word, element.args)
62 | replacementPrefix: prefix
63 | leftLabel: returnValue
64 |
65 | if element.isInternal
66 | suggestion.descriptionMoreURL = config.config.php_documentation_base_url.functions + word
67 |
68 | suggestions.push suggestion
69 |
70 |
71 | return suggestions
72 |
--------------------------------------------------------------------------------
/lib/autocompletion/member-provider.coffee:
--------------------------------------------------------------------------------
1 | fuzzaldrin = require 'fuzzaldrin'
2 | exec = require "child_process"
3 |
4 | proxy = require "../services/php-proxy.coffee"
5 | parser = require "../services/php-file-parser.coffee"
6 | AbstractProvider = require "./abstract-provider"
7 |
8 | module.exports =
9 |
10 | # Autocompletion for members of variables such as after ->, ::.
11 | class MemberProvider extends AbstractProvider
12 | methods: []
13 |
14 | ###*
15 | * Get suggestions from the provider (@see provider-api)
16 | * @return array
17 | ###
18 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
19 | # Autocompletion for class members, i.e. after a ::, ->, ...
20 | @regex = /(?:(?:[a-zA-Z0-9_]*)\s*(?:\(.*\))?\s*(?:->|::)\s*)+([a-zA-Z0-9_]*)/g
21 |
22 | prefix = @getPrefix(editor, bufferPosition)
23 | return unless prefix.length
24 |
25 | elements = parser.getStackClasses(editor, bufferPosition)
26 | return unless elements?
27 |
28 | className = parser.parseElements(editor, bufferPosition, elements)
29 | return unless className?
30 |
31 | elements = prefix.split(/(->|::)/)
32 |
33 | # We only autocomplete after splitters, so there must be at least one word, one splitter, and another word
34 | # (the latter which could be empty).
35 | return unless elements.length > 2
36 |
37 | currentClass = parser.getFullClassName(editor)
38 | currentClassParents = []
39 |
40 | if currentClass
41 | classInfo = proxy.methods(currentClass)
42 | currentClassParents = if classInfo?.parents then classInfo?.parents else []
43 |
44 | mustBeStatic = false
45 |
46 | if elements[elements.length - 2] == '::' and elements[elements.length - 3].trim() != 'parent'
47 | mustBeStatic = true
48 |
49 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]])
50 | insertParameterList = if characterAfterPrefix == '(' then false else true
51 |
52 | suggestions = @findSuggestionsForPrefix(className, elements[elements.length-1].trim(), (element) =>
53 | # See also ticket #127.
54 | return false if mustBeStatic and not element.isStatic
55 | return false if element.isPrivate and element.declaringClass.name != currentClass
56 | return false if element.isProtected and element.declaringClass.name != currentClass and element.declaringClass.name not in currentClassParents
57 |
58 | # Constants are only available when statically accessed.
59 | return false if not element.isMethod and not element.isProperty and not mustBeStatic
60 |
61 | return true
62 | , insertParameterList)
63 |
64 | return unless suggestions.length
65 | return suggestions
66 |
67 | ###*
68 | * Returns suggestions available matching the given prefix
69 | * @param {string} className The name of the class to show members of.
70 | * @param {string} prefix Prefix to match (may be left empty to list all members).
71 | * @param {callback} filterCallback A callback that should return true if the item should be added to the
72 | * suggestions list.
73 | * @param {bool} insertParameterList Whether to insert a list of parameters for methods.
74 | * @return array
75 | ###
76 | findSuggestionsForPrefix: (className, prefix, filterCallback, insertParameterList = true) ->
77 | methods = proxy.methods(className)
78 |
79 | if not methods?.names
80 | return []
81 |
82 | # Filter the words using fuzzaldrin
83 | words = fuzzaldrin.filter(methods.names, prefix)
84 |
85 | # Builds suggestions for the words
86 | suggestions = []
87 |
88 | for word in words
89 | element = methods.values[word]
90 |
91 | if element not instanceof Array
92 | element = [element]
93 |
94 | for ele in element
95 | if filterCallback and not filterCallback(ele)
96 | continue
97 |
98 | # Ensure we don't get very long return types by just showing the last part.
99 | snippet = null
100 | displayText = word
101 | returnValueParts = if ele.args.return?.type then ele.args.return.type.split('\\') else []
102 | returnValue = returnValueParts[returnValueParts.length - 1]
103 |
104 | if ele.isMethod
105 | type = 'method'
106 | snippet = if insertParameterList then @getFunctionSnippet(word, ele.args) else null
107 | displayText = @getFunctionSignature(word, ele.args)
108 |
109 | else if ele.isProperty
110 | type = 'property'
111 |
112 | else
113 | type = 'constant'
114 |
115 | suggestions.push
116 | text : word,
117 | type : type
118 | snippet : snippet
119 | displayText : displayText
120 | leftLabel : returnValue
121 | description : if ele.args.descriptions.short? then ele.args.descriptions.short else ''
122 | className : if ele.args.deprecated then 'php-atom-autocomplete-strike' else ''
123 |
124 | return suggestions
125 |
--------------------------------------------------------------------------------
/lib/autocompletion/variable-provider.coffee:
--------------------------------------------------------------------------------
1 | fuzzaldrin = require 'fuzzaldrin'
2 |
3 | parser = require "../services/php-file-parser.coffee"
4 | AbstractProvider = require "./abstract-provider"
5 |
6 | module.exports =
7 |
8 | # Autocomplete for local variable names.
9 | class VariableProvider extends AbstractProvider
10 | variables: []
11 |
12 | ###*
13 | * Get suggestions from the provider (@see provider-api)
14 | * @return array
15 | ###
16 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
17 | # "new" keyword or word starting with capital letter
18 | @regex = /(\$[a-zA-Z_]*)/g
19 |
20 | prefix = @getPrefix(editor, bufferPosition)
21 | return unless prefix.length
22 |
23 | @variables = parser.getAllVariablesInFunction(editor, bufferPosition)
24 | return unless @variables.length
25 |
26 | suggestions = @findSuggestionsForPrefix(prefix.trim())
27 | return unless suggestions.length
28 | return suggestions
29 |
30 | ###*
31 | * Returns suggestions available matching the given prefix
32 | * @param {string} prefix Prefix to match
33 | * @return array
34 | ###
35 | findSuggestionsForPrefix: (prefix) ->
36 | # Filter the words using fuzzaldrin
37 | words = fuzzaldrin.filter @variables, prefix
38 |
39 | # Builds suggestions for the words
40 | suggestions = []
41 | for word in words
42 | suggestions.push
43 | text: word,
44 | type: 'variable',
45 | replacementPrefix: prefix
46 |
47 | return suggestions
48 |
--------------------------------------------------------------------------------
/lib/config.coffee:
--------------------------------------------------------------------------------
1 | fs = require 'fs'
2 | namespace = require './services/namespace.coffee'
3 | useStatement = require './services/use-statement.coffee'
4 | StatusInProgress = require "./services/status-in-progress.coffee"
5 | StatusErrorAutocomplete = require "./services/status-error-autocomplete.coffee"
6 |
7 | module.exports =
8 |
9 | config: {}
10 | statusInProgress: null
11 | statusErrorAutocomplete: null
12 |
13 | ###*
14 | * Get plugin configuration
15 | ###
16 | getConfig: () ->
17 | # See also https://secure.php.net/urlhowto.php .
18 | @config['php_documentation_base_url'] = {
19 | functions: 'https://secure.php.net/function.'
20 | }
21 |
22 | @config['composer'] = atom.config.get('atom-autocomplete-php.binComposer')
23 | @config['php'] = atom.config.get('atom-autocomplete-php.binPhp')
24 | @config['autoload'] = atom.config.get('atom-autocomplete-php.autoloadPaths')
25 | @config['gotoKey'] = atom.config.get('atom-autocomplete-php.gotoKey')
26 | @config['classmap'] = atom.config.get('atom-autocomplete-php.classMapFiles')
27 | @config['packagePath'] = atom.packages.resolvePackagePath('atom-autocomplete-php')
28 | @config['verboseErrors'] = atom.config.get('atom-autocomplete-php.verboseErrors')
29 | @config['insertNewlinesForUseStatements'] = atom.config.get('atom-autocomplete-php.insertNewlinesForUseStatements')
30 | @config['ensureNewLineAfterNamespace'] = atom.config.get('atom-autocomplete-php.ensureNewLineAfterNamespace')
31 |
32 | ###*
33 | * Writes configuration in "php lib" folder
34 | ###
35 | writeConfig: () ->
36 | @getConfig()
37 |
38 | files = ""
39 | for file in @config.autoload
40 | files += "'#{file}',"
41 |
42 | classmaps = ""
43 | for classmap in @config.classmap
44 | classmaps += "'#{classmap}',"
45 |
46 | text = " '#{@config.composer}',
49 | 'php' => '#{@config.php}',
50 | 'autoload' => array(#{files}),
51 | 'classmap' => array(#{classmaps})
52 | );
53 | "
54 |
55 | fs.writeFileSync(@config.packagePath + '/php/tmp.php', text)
56 |
57 | ###*
58 | * Tests the user's PHP and Composer configuration.
59 | * @return {bool}
60 | ###
61 | testConfig: (interactive) ->
62 | @getConfig()
63 |
64 | exec = require "child_process"
65 | testResult = exec.spawnSync(@config.php, ["-v"])
66 |
67 | errorTitle = 'atom-autocomplete-php - Incorrect setup!'
68 | errorMessage = 'Either PHP or Composer is not correctly set up and as a result PHP autocompletion will not work. ' +
69 | 'Please visit the settings screen to correct this error. If you are not specifying an absolute path for PHP or ' +
70 | 'Composer, make sure they are in your PATH.
71 | Feel free to look package\'s README for configuration examples'
72 |
73 | if testResult.status = null or testResult.status != 0
74 | atom.notifications.addError(errorTitle, {'detail': errorMessage})
75 | return false
76 |
77 | # Test Composer.
78 | testResult = exec.spawnSync(@config.php, [@config.composer, "--version"])
79 |
80 | if testResult.status = null or testResult.status != 0
81 | testResult = exec.spawnSync(@config.composer, ["--version"])
82 |
83 | # Try executing Composer directly.
84 | if testResult.status = null or testResult.status != 0
85 | atom.notifications.addError(errorTitle, {'detail': errorMessage})
86 | return false
87 |
88 | if interactive
89 | atom.notifications.addSuccess('atom-autocomplete-php - Success', {'detail': 'Configuration OK !'})
90 |
91 | return true
92 |
93 | ###*
94 | * Init function called on package activation
95 | * Register config events and write the first config
96 | ###
97 | init: () ->
98 | @statusInProgress = new StatusInProgress
99 | @statusInProgress.hide()
100 |
101 | @statusErrorAutocomplete = new StatusErrorAutocomplete
102 | @statusErrorAutocomplete.hide()
103 |
104 | # Command for namespaces
105 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:namespace': =>
106 | namespace.createNamespace(atom.workspace.getActivePaneItem())
107 |
108 | # Command for importing use statement
109 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:import-use-statement': =>
110 | useStatement.importUseStatement(atom.workspace.getActivePaneItem())
111 |
112 | # Command to reindex the current project
113 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:reindex-project': ->
114 | proxy = require './services/php-proxy.coffee'
115 | proxy.refresh()
116 |
117 | # Command to test configuration
118 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:configuration': =>
119 | @testConfig(true)
120 |
121 | @writeConfig()
122 |
123 | atom.config.onDidChange 'atom-autocomplete-php.binPhp', () =>
124 | @writeConfig()
125 | @testConfig(true)
126 |
127 | atom.config.onDidChange 'atom-autocomplete-php.binComposer', () =>
128 | @writeConfig()
129 | @testConfig(true)
130 |
131 | atom.config.onDidChange 'atom-autocomplete-php.autoloadPaths', () =>
132 | @writeConfig()
133 |
134 | atom.config.onDidChange 'atom-autocomplete-php.gotoKey', () =>
135 | @writeConfig()
136 |
137 | atom.config.onDidChange 'atom-autocomplete-php.classMapFiles', () =>
138 | @writeConfig()
139 |
140 | atom.config.onDidChange 'atom-autocomplete-php.verboseErrors', () =>
141 | @writeConfig()
142 |
143 | atom.config.onDidChange 'atom-autocomplete-php.insertNewlinesForUseStatements', () =>
144 | @writeConfig()
145 |
146 | atom.config.onDidChange 'atom-autocomplete-php.ensureNewLineAfterNamespace', () =>
147 | @writeConfig()
148 |
--------------------------------------------------------------------------------
/lib/goto/abstract-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | SubAtom = require 'sub-atom'
4 | config = require '../config.coffee'
5 |
6 | module.exports =
7 |
8 | class AbstractProvider
9 | allMarkers: []
10 | hoverEventSelectors: ''
11 | clickEventSelectors: ''
12 | manager: {}
13 | gotoRegex: ''
14 | jumpWord: ''
15 |
16 | ###*
17 | * Initialisation of Gotos
18 | *
19 | * @param {GotoManager} manager The manager that stores this goto. Used mainly for backtrack registering.
20 | ###
21 | init: (manager) ->
22 | @subAtom = new SubAtom
23 |
24 | @$ = require 'jquery'
25 | @parser = require '../services/php-file-parser'
26 | @fuzzaldrin = require 'fuzzaldrin'
27 |
28 | @manager = manager
29 |
30 | atom.workspace.observeTextEditors (editor) =>
31 | editor.onDidSave (event) =>
32 | @rescanMarkers(editor)
33 |
34 | @registerMarkers editor
35 | @registerEvents editor
36 |
37 | atom.workspace.onDidChangeActivePaneItem (paneItem) =>
38 | if paneItem instanceof TextEditor && @jumpWord != '' && @jumpWord != undefined
39 | @jumpTo(paneItem, @jumpWord)
40 | @jumpWord = ''
41 |
42 | # When you go back to only have 1 pane the events are lost, so need to re-register.
43 | atom.workspace.onDidDestroyPane (pane) =>
44 | panes = atom.workspace.getPanes()
45 |
46 | if panes.length == 1
47 | for paneItem in panes[0].items
48 | if paneItem instanceof TextEditor
49 | @registerEvents paneItem
50 |
51 | # Having to re-register events as when a new pane is created the old panes lose the events.
52 | atom.workspace.onDidAddPane (observedPane) =>
53 | panes = atom.workspace.getPanes()
54 |
55 | for pane in panes
56 | if pane == observedPane
57 | continue
58 |
59 | for paneItem in pane.items
60 | if paneItem instanceof TextEditor
61 | @registerEvents paneItem
62 |
63 | ###*
64 | * Deactives the goto feature.
65 | ###
66 | deactivate: () ->
67 | @subAtom.dispose()
68 | allMarkers = []
69 |
70 | ###*
71 | * Goto from the current cursor position in the editor.
72 | *
73 | * @param {TextEditor} editor TextEditor to pull term from.
74 | ###
75 | gotoFromEditor: (editor) ->
76 | if editor.getGrammar().scopeName.match /text.html.php$/
77 | position = editor.getCursorBufferPosition()
78 | term = @parser.getFullWordFromBufferPosition(editor, position)
79 |
80 | termParts = term.split(/(?:\-\>|::)/)
81 | term = termParts.pop().replace('(', '')
82 |
83 | @gotoFromWord(editor, term)
84 |
85 | ###*
86 | * Goto from the term given.
87 | *
88 | * @param {TextEditor} editor TextEditor to search for namespace of term.
89 | * @param {string} term Term to search for.
90 | ###
91 | gotoFromWord: (editor, term) ->
92 |
93 | ###*
94 | * Registers the mouse events for alt-click.
95 | *
96 | * @param {TextEditor} editor TextEditor to register events to.
97 | ###
98 | registerEvents: (editor) ->
99 | if editor.getGrammar().scopeName.match /text.html.php$/
100 | textEditorElement = atom.views.getView(editor)
101 | scrollViewElement = @$(textEditorElement).find('.scroll-view')
102 |
103 | @subAtom.add scrollViewElement, 'mousemove', @hoverEventSelectors, (event) =>
104 | return unless @isGotoKeyPressed(event)
105 |
106 | selector = @getSelectorFromEvent(event)
107 |
108 | return unless selector
109 |
110 | @$(selector).css('border-bottom', '1px solid ' + @$(selector).css('color'))
111 | @$(selector).css('cursor', 'pointer')
112 |
113 | @isHovering = true
114 |
115 | @subAtom.add scrollViewElement, 'mouseout', @hoverEventSelectors, (event) =>
116 | return unless @isHovering
117 |
118 | selector = @getSelectorFromEvent(event)
119 |
120 | return unless selector
121 |
122 | @$(selector).css('border-bottom', '')
123 | @$(selector).css('cursor', '')
124 |
125 | @isHovering = false
126 |
127 | @subAtom.add scrollViewElement, 'click', @clickEventSelectors, (event) =>
128 | selector = @getSelectorFromEvent(event)
129 |
130 | if selector == null || @isGotoKeyPressed(event) == false
131 | return
132 |
133 | if event.handled != true
134 | @gotoFromWord(editor, @$(selector).text())
135 | event.handled = true
136 |
137 | # This is needed to be able to alt-click class names inside comments (docblocks).
138 | editor.onDidChangeCursorPosition (event) =>
139 | return unless @isHovering
140 |
141 | markerProperties =
142 | containsBufferPosition: event.newBufferPosition
143 |
144 | markers = event.cursor.editor.findMarkers markerProperties
145 |
146 | for key,marker of markers
147 | for allKey,allMarker of @allMarkers[editor.getLongTitle()]
148 | if marker.id == allMarker.id
149 | @gotoFromWord(event.cursor.editor, marker.getProperties().term)
150 | break
151 |
152 | ###*
153 | * Check if the key binded to the goto with click is pressed or not (according to the settings)
154 | *
155 | * @param {Object} event JS event
156 | *
157 | * @return {Boolean}
158 | ###
159 | isGotoKeyPressed: (event) ->
160 | switch config.config.gotoKey
161 | when 'ctrl'then return event.ctrlKey
162 | when 'alt' then return event.altKey
163 | when 'cmd' then return event.metaKey
164 | else return false
165 |
166 | ###*
167 | * Register any markers that you need.
168 | *
169 | * @param {TextEditor} editor The editor to search through.
170 | ###
171 | registerMarkers: (editor) ->
172 |
173 | ###*
174 | * Removes any markers previously created by registerMarkers.
175 | *
176 | * @param {TextEditor} editor The editor to search through.
177 | ###
178 | cleanMarkers: (editor) ->
179 |
180 | ###*
181 | * Rescans the editor, updating all markers.
182 | *
183 | * @param {TextEditor} editor The editor to search through.
184 | ###
185 | rescanMarkers: (editor) ->
186 | @cleanMarkers(editor)
187 | @registerMarkers(editor)
188 |
189 | ###*
190 | * Gets the correct selector when a selector is clicked.
191 | *
192 | * @param {jQuery.Event} event A jQuery event.
193 | *
194 | * @return {object|null} A selector to be used with jQuery.
195 | ###
196 | getSelectorFromEvent: (event) ->
197 | return event.currentTarget
198 |
199 | ###*
200 | * Returns whether this goto is able to jump using the term.
201 | *
202 | * @param {string} term Term to check.
203 | *
204 | * @return {boolean} Whether a jump is possible.
205 | ###
206 | canGoto: (term) ->
207 | return term.match(@gotoRegex)?.length > 0
208 |
209 | ###*
210 | * Gets the regex used when looking for a word within the editor.
211 | *
212 | * @param {string} term Term being search.
213 | *
214 | * @return {regex} Regex to be used.
215 | ###
216 | getJumpToRegex: (term) ->
217 |
218 | ###*
219 | * Jumps to a word within the editor
220 | * @param {TextEditor} editor The editor that has the function in.
221 | * @param {string} word The word to find and then jump to.
222 | * @return {boolean} Whether the finding was successful.
223 | ###
224 | jumpTo: (editor, word) ->
225 | bufferPosition = @parser.findBufferPositionOfWord(editor, word, @getJumpToRegex(word))
226 |
227 | if bufferPosition == null
228 | return false
229 |
230 | # Small delay to wait for when a editor is being created.
231 | setTimeout(() ->
232 | editor.setCursorBufferPosition(bufferPosition, {
233 | autoscroll: false
234 | })
235 |
236 | # Separated these as the autoscroll on setCursorBufferPosition didn't work as well.
237 | editor.scrollToScreenPosition(editor.screenPositionForBufferPosition(bufferPosition), {
238 | center: true
239 | })
240 | , 100)
241 |
--------------------------------------------------------------------------------
/lib/goto/class-provider.coffee:
--------------------------------------------------------------------------------
1 | AbstractProvider = require './abstract-provider'
2 |
3 | module.exports =
4 |
5 | class ClassProvider extends AbstractProvider
6 | hoverEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class, .syntax--comment-clickable .syntax--region'
7 | clickEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class'
8 | gotoRegex: /^\\?[A-Z][A-za-z0-9_]*(\\[A-Z][A-Za-z0-9_])*$/
9 |
10 | ###*
11 | * Goto the class from the term given.
12 | *
13 | * @param {TextEditor} editor TextEditor to search for namespace of term.
14 | * @param {string} term Term to search for.
15 | ###
16 | gotoFromWord: (editor, term) ->
17 | if term == undefined || term.indexOf('$') == 0
18 | return
19 |
20 | term = @parser.getFullClassName(editor, term)
21 |
22 | proxy = require '../services/php-proxy.coffee'
23 | classesResponse = proxy.classes()
24 |
25 | return unless classesResponse.autocomplete
26 |
27 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition())
28 |
29 | # See what matches we have for this class name.
30 | matches = @fuzzaldrin.filter(classesResponse.autocomplete, term)
31 |
32 | if matches[0] == term
33 | regexMatches = /(?:\\)(\w+)$/i.exec(matches[0])
34 |
35 | if regexMatches == null || regexMatches.length == 0
36 | @jumpWord = matches[0]
37 |
38 | else
39 | @jumpWord = regexMatches[1]
40 |
41 | classInfo = proxy.methods(matches[0])
42 |
43 | atom.workspace.open(classInfo.filename, {
44 | searchAllPanes: true
45 | })
46 |
47 | ###*
48 | * Gets the correct selector when a class or namespace is clicked.
49 | *
50 | * @param {jQuery.Event} event A jQuery event.
51 | *
52 | * @return {object|null} A selector to be used with jQuery.
53 | ###
54 | getSelectorFromEvent: (event) ->
55 | return @parser.getClassSelectorFromEvent(event)
56 |
57 | ###*
58 | * Goes through all the lines within the editor looking for classes within comments. More specifically if they have
59 | * @var, @param or @return prefixed.
60 | *
61 | * @param {TextEditor} editor The editor to search through.
62 | ###
63 | registerMarkers: (editor) ->
64 | text = editor.getText()
65 | rows = text.split('\n')
66 |
67 | for key,row of rows
68 | regex = /@param|@var|@return|@throws|@see/gi
69 |
70 | if regex.test(row)
71 | @addMarkerToCommentLine row.split(' '), parseInt(key), editor, true
72 |
73 | ###*
74 | * Removes any markers previously created by registerMarkers.
75 | *
76 | * @param {TextEditor} editor The editor to search through
77 | ###
78 | cleanMarkers: (editor) ->
79 | for i,marker of @allMarkers[editor.getLongTitle()]
80 | marker.destroy()
81 |
82 | @allMarkers = []
83 |
84 | ###*
85 | * Analyses the words array given for any classes and then creates a marker for them.
86 | *
87 | * @param {array} words The array of words to check.
88 | * @param {int} rowIndex The current row the words are on within the editor.
89 | * @param {TextEditor} editor The editor the words are from.
90 | * @param {bool} shouldBreak Flag to say whether the search should break after finding 1 class.
91 | * @param {int} currentIndex = 0 The current column index the search is on.
92 | * @param {int} offset = 0 Any offset that should be applied when creating the marker.
93 | ###
94 | addMarkerToCommentLine: (words, rowIndex, editor, shouldBreak, currentIndex = 0, offset = 0) ->
95 | for key,value of words
96 | regex = /^\\?([A-Za-z0-9_]+)\\?([A-Za-zA-Z_\\]*)?/g
97 | keywordRegex = /^(array|object|bool|string|static|null|boolean|void|int|integer|mixed|callable)$/gi
98 |
99 | if value && regex.test(value) && keywordRegex.test(value) == false
100 | if value.includes('|')
101 | @addMarkerToCommentLine value.split('|'), rowIndex, editor, false, currentIndex, parseInt(key)
102 |
103 | else
104 | range = [[rowIndex, currentIndex + parseInt(key) + offset], [rowIndex, currentIndex + parseInt(key) + value.length + offset]];
105 |
106 | marker = editor.markBufferRange(range)
107 |
108 | markerProperties =
109 | term: value
110 |
111 | marker.setProperties markerProperties
112 |
113 | options =
114 | type: 'highlight'
115 | class: 'comment-clickable comment'
116 |
117 | if !marker.isDestroyed()
118 | editor.decorateMarker marker, options
119 |
120 | if @allMarkers[editor.getLongTitle()] == undefined
121 | @allMarkers[editor.getLongTitle()] = []
122 |
123 | @allMarkers[editor.getLongTitle()].push(marker)
124 |
125 | if shouldBreak == true
126 | break
127 |
128 | currentIndex += value.length;
129 |
130 | ###*
131 | * Gets the regex used when looking for a word within the editor
132 | *
133 | * @param {string} term Term being search.
134 | *
135 | * @return {regex} Regex to be used.
136 | ###
137 | getJumpToRegex: (term) ->
138 | return ///^(class|interface|abstract class|trait)\ +#{term}///i
139 |
--------------------------------------------------------------------------------
/lib/goto/function-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | AbstractProvider = require './abstract-provider'
4 |
5 | module.exports =
6 |
7 | class FunctionProvider extends AbstractProvider
8 | hoverEventSelectors: '.syntax--function-call'
9 | clickEventSelectors: '.syntax--function-call'
10 | gotoRegex: /(?:(?:[a-zA-Z0-9_]*)\s*(?:\(.*\))?\s*(?:->|::)\s*)+([a-zA-Z0-9_]*)/
11 |
12 | ###*
13 | * Goto the class from the term given.
14 | *
15 | * @param {TextEditor} editor TextEditor to search for namespace of term.
16 | * @param {string} term Term to search for.
17 | ###
18 | gotoFromWord: (editor, term) ->
19 | bufferPosition = editor.getCursorBufferPosition()
20 |
21 | calledClass = @parser.getCalledClass(editor, term, bufferPosition)
22 |
23 | if not calledClass
24 | return
25 |
26 | currentClass = @parser.getFullClassName(editor)
27 |
28 | if currentClass == calledClass && @jumpTo(editor, term)
29 | @manager.addBackTrack(editor.getPath(), bufferPosition)
30 | return
31 |
32 | value = @parser.getMemberContext(editor, term, bufferPosition, calledClass)
33 |
34 | if not value
35 | return
36 |
37 | atom.workspace.open(value.declaringStructure.filename, {
38 | initialLine : (value.startLine - 1),
39 | searchAllPanes : true
40 | })
41 |
42 | @manager.addBackTrack(editor.getPath(), bufferPosition)
43 |
44 | ###*
45 | * Gets the regex used when looking for a word within the editor
46 | *
47 | * @param {string} term Term being search.
48 | *
49 | * @return {regex} Regex to be used.
50 | ###
51 | getJumpToRegex: (term) ->
52 | return ///function\ +#{term}(\ +|\()///i
53 |
--------------------------------------------------------------------------------
/lib/goto/goto-manager.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | ClassProvider = require './class-provider.coffee'
4 | FunctionProvider = require './function-provider.coffee'
5 | PropertyProvider = require './property-provider.coffee'
6 |
7 | parser = require '../services/php-file-parser.coffee'
8 |
9 | module.exports =
10 |
11 | class GotoManager
12 | providers: []
13 | trace: []
14 |
15 | ###*
16 | * Initialisation of all the providers and commands for goto
17 | ###
18 | init: () ->
19 | @providers.push new ClassProvider()
20 | @providers.push new FunctionProvider()
21 | @providers.push new PropertyProvider()
22 |
23 | for provider in @providers
24 | provider.init(@)
25 |
26 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:goto-backtrack': =>
27 | @backTrack(atom.workspace.getActivePaneItem())
28 |
29 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:goto': =>
30 | @goto(atom.workspace.getActivePaneItem())
31 |
32 | ###*
33 | * Deactivates the goto functionaility
34 | ###
35 | deactivate: () ->
36 | for provider in @providers
37 | provider.deactivate()
38 |
39 | ###*
40 | * Adds a backtrack step to the stack.
41 | *
42 | * @param {string} fileName The file where the jump took place.
43 | * @param {BufferPosition} bufferPosition The buffer position the cursor was last on.
44 | ###
45 | addBackTrack: (fileName, bufferPosition) ->
46 | @trace.push({
47 | file: fileName,
48 | position: bufferPosition
49 | })
50 |
51 | ###*
52 | * Pops one of the stored back tracks and jump the user to its position.
53 | *
54 | * @param {TextEditor} editor The current editor.
55 | ###
56 | backTrack: (editor) ->
57 | if @trace.length == 0
58 | return
59 |
60 | lastTrace = @trace.pop()
61 |
62 | if editor instanceof TextEditor && editor.getPath() == lastTrace.file
63 | editor.setCursorBufferPosition(lastTrace.position, {
64 | autoscroll: false
65 | })
66 |
67 | # Separated these as the autoscroll on setCursorBufferPosition
68 | # didn't work as well.
69 | editor.scrollToScreenPosition(editor.screenPositionForBufferPosition(lastTrace.position), {
70 | center: true
71 | })
72 |
73 | else
74 | atom.workspace.open(lastTrace.file, {
75 | searchAllPanes: true,
76 | initialLine: lastTrace.position[0]
77 | initialColumn: lastTrace.position[1]
78 | })
79 |
80 | ###*
81 | * Takes the editor and jumps using one of the providers.
82 | *
83 | * @param {TextEditor} editor Current active editor
84 | ###
85 | goto: (editor) ->
86 | fullTerm = parser.getFullWordFromBufferPosition(editor, editor.getCursorBufferPosition())
87 |
88 | for provider in @providers
89 | if provider.canGoto(fullTerm)
90 | provider.gotoFromEditor(editor)
91 | break
92 |
--------------------------------------------------------------------------------
/lib/goto/property-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | AbstractProvider = require './abstract-provider'
4 |
5 | module.exports =
6 |
7 | class PropertyProvider extends AbstractProvider
8 | hoverEventSelectors: '.syntax--property'
9 | clickEventSelectors: '.syntax--property'
10 | gotoRegex: /^(\$\w+)?((->|::)\w+)+/
11 |
12 | ###*
13 | * Goto the property from the term given.
14 | *
15 | * @param {TextEditor} editor TextEditor to search for namespace of term.
16 | * @param {string} term Term to search for.
17 | ###
18 | gotoFromWord: (editor, term) ->
19 | bufferPosition = editor.getCursorBufferPosition()
20 |
21 | calledClass = @parser.getCalledClass(editor, term, bufferPosition)
22 |
23 | if not calledClass
24 | return
25 |
26 | currentClass = @parser.getFullClassName(editor)
27 |
28 | if currentClass == calledClass && @jumpTo(editor, term)
29 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition())
30 | return
31 |
32 | value = @parser.getMemberContext(editor, term, bufferPosition, calledClass)
33 |
34 | if not value
35 | return
36 |
37 | atom.workspace.open(value.declaringStructure.filename, {
38 | searchAllPanes: true
39 | })
40 |
41 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition())
42 | @jumpWord = term
43 |
44 | ###*
45 | * Gets the regex used when looking for a word within the editor
46 | *
47 | * @param {string} term Term being search.
48 | *
49 | * @return {regex} Regex to be used.
50 | ###
51 | getJumpToRegex: (term) ->
52 | return ///(protected|public|private|static)\ +\$#{term}///i
53 |
--------------------------------------------------------------------------------
/lib/peekmo-php-atom-autocomplete.coffee:
--------------------------------------------------------------------------------
1 | GotoManager = require "./goto/goto-manager.coffee"
2 | TooltipManager = require "./tooltip/tooltip-manager.coffee"
3 | AnnotationManager = require "./annotation/annotation-manager.coffee"
4 | AutocompletionManager = require "./autocompletion/autocompletion-manager.coffee"
5 | StatusInProgress = require "./services/status-in-progress.coffee"
6 | config = require './config.coffee'
7 | proxy = require './services/php-proxy.coffee'
8 | parser = require './services/php-file-parser.coffee'
9 | plugins = require './services/plugin-manager.coffee'
10 |
11 | module.exports =
12 | config:
13 | binComposer:
14 | title: 'Command to use composer'
15 | description: 'This plugin depends on composer in order to work. Specify the path
16 | to your composer bin (e.g : bin/composer, composer.phar, composer)'
17 | type: 'string'
18 | default: '/usr/local/bin/composer'
19 | order: 1
20 |
21 | binPhp:
22 | title: 'Command php'
23 | description: 'This plugin use php CLI in order to work. Please specify your php
24 | command ("php" on UNIX systems)'
25 | type: 'string'
26 | default: 'php'
27 | order: 2
28 |
29 | autoloadPaths:
30 | title: 'Autoloader file'
31 | description: 'Relative path to the files of autoload.php from composer (or an other one). You can specify multiple
32 | paths (comma separated) if you have different paths for some projects.'
33 | type: 'array'
34 | default: ['vendor/autoload.php', 'autoload.php']
35 | order: 3
36 |
37 | gotoKey:
38 | title: 'Goto key'
39 | description: 'Key to use with "click" to use goto. By default "alt" (because on macOS, ctrl + click is like right click)'
40 | type: 'string'
41 | default: 'alt'
42 | enum: ['alt', 'ctrl', 'cmd']
43 | order: 4
44 |
45 | classMapFiles:
46 | title: 'Classmap files'
47 | description: 'Relative path to the files that contains a classmap (array with "className" => "fileName"). By default
48 | on composer it\'s vendor/composer/autoload_classmap.php'
49 | type: 'array'
50 | default: ['vendor/composer/autoload_classmap.php', 'autoload/ezp_kernel.php']
51 | order: 5
52 |
53 | insertNewlinesForUseStatements:
54 | title: 'Insert newlines for use statements.'
55 | description: 'When enabled, the plugin will add additional newlines before or after an automatically added
56 | use statement when it can\'t add them nicely to an existing group. This results in more cleanly
57 | separated use statements but will create additional vertical whitespace.'
58 | type: 'boolean'
59 | default: false
60 | order: 6
61 |
62 | ensureNewLineAfterNamespace:
63 | title: 'Ensure new line after namespace.'
64 | description: 'When enabled, the plugin will add a new line before the use statement when it comes directly after the namespace.'
65 | type: 'boolean'
66 | default: false
67 | order: 6
68 |
69 | verboseErrors:
70 | title: 'Errors on file saving showed'
71 | description: 'When enabled, you\'ll have a notification once an error occured on autocomplete. Otherwise, the message will just be logged in developer console'
72 | type: 'boolean'
73 | default: false
74 | order: 7
75 |
76 | activate: ->
77 | config.testConfig()
78 | config.init()
79 |
80 | @autocompletionManager = new AutocompletionManager()
81 | @autocompletionManager.init()
82 |
83 | @gotoManager = new GotoManager()
84 | @gotoManager.init()
85 |
86 | @tooltipManager = new TooltipManager()
87 | @tooltipManager.init()
88 |
89 | @annotationManager = new AnnotationManager()
90 | @annotationManager.init()
91 |
92 | proxy.init()
93 |
94 | deactivate: ->
95 | @gotoManager.deactivate()
96 | @tooltipManager.deactivate()
97 | @annotationManager.deactivate()
98 | @autocompletionManager.deactivate()
99 | proxy.deactivate()
100 |
101 | consumeStatusBar: (statusBar) ->
102 | config.statusInProgress.initialize(statusBar)
103 | config.statusInProgress.attach()
104 |
105 | config.statusErrorAutocomplete.initialize(statusBar)
106 | config.statusErrorAutocomplete.attach()
107 |
108 | consumePlugin: (plugin) ->
109 | plugins.plugins.push(plugin)
110 |
111 | provideAutocompleteTools: ->
112 | @services =
113 | proxy: proxy
114 | parser: parser
115 |
116 | return @services
117 |
118 | getProvider: ->
119 | return @autocompletionManager.getProviders()
120 |
--------------------------------------------------------------------------------
/lib/services/attached-popover.coffee:
--------------------------------------------------------------------------------
1 | Popover = require './popover'
2 |
3 | module.exports =
4 |
5 | class AttachedPopover extends Popover
6 | ###
7 | NOTE: The reason we do not use Atom's native tooltip is because it is attached to an element, which caused
8 | strange problems such as tickets #107 and #72. This implementation uses the same CSS classes and transitions but
9 | handles the displaying manually as we don't want to attach/detach, we only want to temporarily display a popover
10 | on mouseover.
11 | ###
12 | timeoutId: null
13 | elementToAttachTo: null
14 |
15 | ###*
16 | * Constructor.
17 | *
18 | * @param {HTMLElement} elementToAttachTo The element to show the popover over.
19 | * @param {int} delay How long the mouse has to hover over the elment before the popover shows
20 | * up (in miliiseconds).
21 | ###
22 | constructor: (@elementToAttachTo, delay = 500) ->
23 | super()
24 |
25 | ###*
26 | * Destructor.
27 | *
28 | ###
29 | destructor: () ->
30 | if @timeoutId
31 | clearTimeout(@timeoutId)
32 | @timeoutId = null
33 |
34 | super()
35 |
36 | ###*
37 | * Shows the popover with the specified text.
38 | *
39 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip.
40 | ###
41 | show: (fadeInTime = 100) ->
42 | coordinates = @elementToAttachTo.getBoundingClientRect();
43 |
44 | centerOffset = ((coordinates.right - coordinates.left) / 2)
45 |
46 | x = coordinates.left - (@$(@getElement()).width() / 2) + centerOffset
47 | y = coordinates.bottom
48 |
49 | super(x, y, fadeInTime)
50 |
51 | ###*
52 | * Shows the popover with the specified text after the specified delay (in miliiseconds). Calling this method
53 | * multiple times will cancel previous show requests and restart.
54 | *
55 | * @param {int} delay The delay before the tooltip shows up (in milliseconds).
56 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip.
57 | ###
58 | showAfter: (delay, fadeInTime = 100) ->
59 | @timeoutId = setTimeout(() =>
60 | @show(fadeInTime)
61 | , delay)
62 |
--------------------------------------------------------------------------------
/lib/services/namespace.coffee:
--------------------------------------------------------------------------------
1 | ###*
2 | * PHP files namespace management
3 | ###
4 |
5 | module.exports =
6 |
7 | ###*
8 | * Add the good namespace to the given file
9 | * @param {TextEditor} editor
10 | ###
11 | createNamespace: (editor) ->
12 | proxy = require './php-proxy.coffee'
13 |
14 | composer = proxy.composer()
15 | autoloaders = []
16 |
17 | if not composer
18 | return
19 |
20 | # Get elements from composer.json
21 | for psr, autoload of composer.autoload
22 | for namespace, src of autoload
23 | if namespace.endsWith("\\")
24 | namespace = namespace.substr(0, namespace.length-1)
25 |
26 | autoloaders[src] = namespace
27 |
28 | if composer["autoload-dev"]
29 | for psr, autoload of composer["autoload-dev"]
30 | for namespace, src of autoload
31 | if namespace.endsWith("\\")
32 | namespace = namespace.substr(0, namespace.length-1)
33 |
34 | autoloaders[src] = namespace
35 |
36 | # Get the current path of the file
37 | path = editor.getPath()
38 | for directory in atom.project.getDirectories()
39 | if path.indexOf(directory.path) == 0
40 | path = path.substr(directory.path.length+1)
41 | break
42 |
43 | # Path with \ replaced by / to be ok with composer.json
44 | path = path.replace(/\\/g, '/')
45 |
46 | # Get the root namespace
47 | namespace = null
48 | for src, name of autoloaders
49 | if path.indexOf(src) == 0
50 | path = path.substr(src.length)
51 | namespace = name
52 | break
53 |
54 | # No namespace found ? Let's leave
55 | if namespace == null
56 | return
57 |
58 | # If the path starts with "/", we remove it
59 | if path.indexOf("/") == 0
60 | path = path.substr(1)
61 |
62 | elements = path.split('/')
63 |
64 | # Build the namespace
65 | index = 1
66 | for element in elements
67 | if element == "" or index == elements.length
68 | continue
69 |
70 | namespace = if namespace == "" then element else namespace + "\\" + element
71 | index++
72 |
73 | text = editor.getText()
74 | index = 0
75 |
76 | # Search for the good place to write the namespace
77 | lines = text.split('\n')
78 | for line in lines
79 | line = line.trim()
80 |
81 | # If we found class keyword, we are not in namespace space, so return
82 | if line.indexOf('namespace ') == 0
83 | editor.setTextInBufferRange([[index,0], [index+1, 0]], "namespace #{namespace};\n")
84 | return
85 | else if line.trim() != "" and line.trim().indexOf("") != 0
86 | editor.setTextInBufferRange([[index,0], [index, 0]], "namespace #{namespace};\n\n")
87 | return
88 |
89 | index += 1
90 |
91 | editor.setTextInBufferRange([[2 ,0], [2, 0]], "namespace #{namespace};\n\n")
92 |
--------------------------------------------------------------------------------
/lib/services/php-proxy.coffee:
--------------------------------------------------------------------------------
1 | exec = require "child_process"
2 | process = require "process"
3 | config = require "../config.coffee"
4 | md5 = require 'md5'
5 | fs = require 'fs'
6 |
7 | module.exports =
8 | data:
9 | methods: [],
10 | autocomplete: [],
11 | composer: null
12 |
13 | currentProcesses: []
14 |
15 | ###*
16 | * Executes a command to PHP proxy
17 | * @param {string} command Command to execute
18 | * @param {boolean} async Must be async or not
19 | * @param {array} options Options for the command
20 | * @param {boolean} noparser Do not use php/parser.php
21 | * @return {array} Json of the response
22 | ###
23 | execute: (command, async, options, noparser) ->
24 | options = {} if not options
25 | processKey = command.join("_")
26 |
27 | for directory in atom.project.getDirectories()
28 | for c in command
29 | c.replace(/\\/g, '\\\\')
30 |
31 | if not async
32 | try
33 | # avoid multiple processes of the same command
34 | if not @currentProcesses[processKey]?
35 | @currentProcesses[processKey] = true
36 |
37 | args = [__dirname + "/../../php/parser.php", directory.path].concat(command)
38 | if noparser
39 | args = command
40 | stdout = exec.spawnSync(config.config.php, args, options).output[1].toString('ascii')
41 |
42 | delete @currentProcesses[processKey]
43 |
44 | if noparser
45 | res =
46 | result: stdout
47 | else
48 | res = JSON.parse(stdout)
49 | catch err
50 | console.log err
51 | res =
52 | error: err
53 |
54 | if !res
55 | return []
56 |
57 | if res.error?
58 | @printError(res.error)
59 |
60 | return res
61 | else
62 | if not @currentProcesses[processKey]?
63 | config.statusErrorAutocomplete.update("Autocomplete failure", false)
64 |
65 | if processKey.indexOf("--refresh") != -1
66 | config.statusInProgress.update("Indexing...", true)
67 |
68 | args = [__dirname + "/../../php/parser.php", directory.path].concat(command)
69 | if noparser
70 | args = command
71 |
72 | @currentProcesses[processKey] = exec.spawn(config.config.php, args, options)
73 | @currentProcesses[processKey].on("exit", (exitCode) =>
74 | delete @currentProcesses[processKey]
75 | )
76 |
77 | commandData = ''
78 | @currentProcesses[processKey].stdout.on("data", (data) =>
79 | commandData += data.toString()
80 | )
81 |
82 | @currentProcesses[processKey].on("close", () =>
83 | if processKey.indexOf("--functions") != -1
84 | try
85 | @data.functions = JSON.parse(commandData)
86 | catch err
87 | config.statusErrorAutocomplete.update("Autocomplete failure", true)
88 |
89 | if processKey.indexOf("--refresh") != -1
90 | config.statusInProgress.update("Indexing...", false)
91 | )
92 |
93 | ###*
94 | * Reads an index by its name (file in indexes/index.[name].json)
95 | * @param {string} name Name of the index to read
96 | ###
97 | readIndex: (name) ->
98 | for directory in atom.project.getDirectories()
99 | crypt = md5(directory.path)
100 | path = __dirname + "/../../indexes/" + crypt + "/index." + name + ".json"
101 | try
102 | fs.accessSync(path, fs.F_OK | fs.R_OK)
103 | catch err
104 | return []
105 |
106 | options =
107 | encoding: 'UTF-8'
108 | return JSON.parse(fs.readFileSync(path, options))
109 |
110 | break
111 |
112 | ###*
113 | * Open and read the composer.json file in the current folder
114 | ###
115 | readComposer: () ->
116 | for directory in atom.project.getDirectories()
117 | path = "#{directory.path}/composer.json"
118 |
119 | try
120 | fs.accessSync(path, fs.F_OK | fs.R_OK)
121 | catch err
122 | continue
123 |
124 | options =
125 | encoding: 'UTF-8'
126 | @data.composer = JSON.parse(fs.readFileSync(path, options))
127 | return @data.composer
128 |
129 | console.log 'Unable to find composer.json file or to open it. The plugin will not work as expected. It only works on composer project'
130 | throw "Error"
131 |
132 | ###*
133 | * Throw a formatted error
134 | * @param {object} error Error to show
135 | ###
136 | printError:(error) ->
137 | @data.error = true
138 | message = error.message
139 |
140 | #if error.file? and error.line?
141 | #message = message + ' [from file ' + error.file + ' - Line ' + error.line + ']'
142 |
143 | #throw new Error(message)
144 |
145 | ###*
146 | * Clear all cache of the plugin
147 | ###
148 | clearCache: () ->
149 | @data =
150 | error: false,
151 | autocomplete: [],
152 | methods: [],
153 | composer: null
154 |
155 | # Fill the functions array because it can take times
156 | @functions()
157 |
158 | ###*
159 | * Autocomplete for classes name
160 | * @return {array}
161 | ###
162 | classes: () ->
163 | return @readIndex('classes')
164 |
165 | ###*
166 | * Returns composer.json file
167 | * @return {Object}
168 | ###
169 | composer: () ->
170 | return @readComposer()
171 |
172 | ###*
173 | * Autocomplete for internal PHP constants
174 | * @return {array}
175 | ###
176 | constants: () ->
177 | if not @data.constants?
178 | res = @execute(["--constants"], false)
179 | @data.constants = res
180 |
181 | return @data.constants
182 |
183 | ###*
184 | * Autocomplete for internal PHP functions
185 | *
186 | * @return {array}
187 | ###
188 | functions: () ->
189 | if not @data.functions?
190 | @execute(["--functions"], true)
191 |
192 | return @data.functions
193 |
194 | ###*
195 | * Autocomplete for methods & properties of a class
196 | * @param {string} className Class complete name (with namespace)
197 | * @return {array}
198 | ###
199 | methods: (className) ->
200 | if not @data.methods[className]?
201 | res = @execute(["--methods","#{className}"], false)
202 | @data.methods[className] = res
203 |
204 | return @data.methods[className]
205 |
206 | ###*
207 | * Autocomplete for methods & properties of a class
208 | * @param {string} className Class complete name (with namespace)
209 | * @return {array}
210 | ###
211 | autocomplete: (className, name) ->
212 | cacheKey = className + "." + name
213 |
214 | if not @data.autocomplete[cacheKey]?
215 | res = @execute(["--autocomplete", className, name], false)
216 | @data.autocomplete[cacheKey] = res
217 |
218 | return @data.autocomplete[cacheKey]
219 |
220 | ###*
221 | * Returns params from the documentation of the given function
222 | *
223 | * @param {string} className
224 | * @param {string} functionName
225 | ###
226 | docParams: (className, functionName) ->
227 | res = @execute(["--doc-params", "#{className}", "#{functionName}"], false)
228 | return res
229 |
230 | ###*
231 | * Refresh the full index or only for the given classPath
232 | * @param {string} classPath Full path (dir) of the class to refresh
233 | ###
234 | refresh: (classPath) ->
235 | if not classPath?
236 | @execute(["--refresh"], true)
237 | else
238 | @execute(["--refresh", "#{classPath}"], true)
239 |
240 | ###*
241 | * Method called on plugin activation
242 | ###
243 | init: () ->
244 | @refresh()
245 | atom.workspace.observeTextEditors (editor) =>
246 | editor.onDidSave((event) =>
247 | # Only .php file
248 | if editor.getGrammar().scopeName.match /text.html.php$/
249 | @clearCache()
250 |
251 | # For Windows - Replace \ in class namespace to / because
252 | # composer use / instead of \
253 | path = event.path
254 | for directory in atom.project.getDirectories()
255 | if path.indexOf(directory.path) == 0
256 | classPath = path.substr(0, directory.path.length+1)
257 | path = path.substr(directory.path.length+1)
258 | break
259 |
260 | @refresh(classPath + path.replace(/\\/g, '/'))
261 | )
262 |
263 | atom.config.onDidChange 'atom-autocomplete-php.binPhp', () =>
264 | @clearCache()
265 |
266 | atom.config.onDidChange 'atom-autocomplete-php.binComposer', () =>
267 | @clearCache()
268 |
269 | atom.config.onDidChange 'atom-autocomplete-php.autoloadPaths', () =>
270 | @clearCache()
271 |
272 | ###*
273 | * Function called when plugin is deactivate
274 | * Cleanup every request in progress (#330)
275 | ###
276 | deactivate: () ->
277 | for key, process of @currentProcesses
278 | process.kill()
279 |
--------------------------------------------------------------------------------
/lib/services/plugin-manager.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | plugins: []
3 |
--------------------------------------------------------------------------------
/lib/services/popover.coffee:
--------------------------------------------------------------------------------
1 | {Disposable} = require 'atom'
2 |
3 | module.exports =
4 |
5 | class Popover extends Disposable
6 | element: null
7 |
8 | ###*
9 | * Constructor.
10 | ###
11 | constructor: () ->
12 | @$ = require 'jquery'
13 |
14 | @element = document.createElement('div')
15 | @element.className = 'tooltip bottom fade php-atom-autocomplete-popover'
16 | @element.innerHTML = ""
17 |
18 | document.body.appendChild(@element)
19 |
20 | super @destructor
21 |
22 | ###*
23 | * Destructor.
24 | ###
25 | destructor: () ->
26 | @hide()
27 | document.body.removeChild(@element)
28 |
29 | ###*
30 | * Retrieves the HTML element containing the popover.
31 | *
32 | * @return {HTMLElement}
33 | ###
34 | getElement: () ->
35 | return @element
36 |
37 | ###*
38 | * sets the text to display.
39 | *
40 | * @param {string} text
41 | ###
42 | setText: (text) ->
43 | @$('.tooltip-inner', @element).html(
44 | '' + text.replace(/\n\n/g, '
') + '
'
45 | )
46 |
47 | ###*
48 | * Shows a popover at the specified location with the specified text and fade in time.
49 | *
50 | * @param {int} x The X coordinate to show the popover at (left).
51 | * @param {int} y The Y coordinate to show the popover at (top).
52 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip.
53 | ###
54 | show: (x, y, fadeInTime = 100) ->
55 | @$(@element).css('left', x + 'px')
56 | @$(@element).css('top', y + 'px')
57 |
58 | @$(@element).addClass('in')
59 | @$(@element).css('opacity', 100)
60 | @$(@element).css('display', 'block')
61 |
62 | ###*
63 | * Hides the tooltip, if it is displayed.
64 | ###
65 | hide: () ->
66 | @$(@element).removeClass('in')
67 | @$(@element).css('opacity', 0)
68 | @$(@element).css('display', 'none')
69 |
--------------------------------------------------------------------------------
/lib/services/status-error-autocomplete.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 |
3 | ##*
4 | # Progress bar in the status bar
5 | ##
6 | class StatusErrorAutocomplete
7 | actions: []
8 |
9 | constructor: ->
10 | @span = document.createElement("span")
11 | @span.className = "inline-block text-subtle"
12 | @span.innerHTML = "Autocomplete failure"
13 |
14 | @container = document.createElement("div")
15 | @container.className = "inline-block"
16 |
17 | @subcontainer = document.createElement("div")
18 | @subcontainer.className = "block"
19 | @container.appendChild(@subcontainer)
20 |
21 | @subcontainer.appendChild(@span)
22 |
23 | initialize: (@statusBar) ->
24 |
25 | update: (text, show) ->
26 | if show
27 | @container.className = "inline-block"
28 | @span.innerHTML = text
29 | @actions.push(text)
30 | else
31 | @actions.forEach((value, index) ->
32 | if value == text
33 | @actions.splice(index, 1)
34 | , @)
35 |
36 | if @actions.length == 0
37 | @hide()
38 | else
39 | @span.innerHTML = @actions[0]
40 |
41 | hide: ->
42 | @container.className = 'hidden'
43 |
44 | attach: ->
45 | @tile = @statusBar.addRightTile(item: @container, priority: 20)
46 |
47 | detach: ->
48 | @tile.destroy()
49 |
--------------------------------------------------------------------------------
/lib/services/status-in-progress.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 |
3 | ##*
4 | # Progress bar in the status bar
5 | ##
6 | class StatusInProgress
7 | actions: []
8 |
9 | constructor: ->
10 | @span = document.createElement("span")
11 | @span.className = "inline-block text-subtle"
12 | @span.innerHTML = "Indexing.."
13 |
14 | @progress = document.createElement("progress")
15 |
16 | @container = document.createElement("div")
17 | @container.className = "inline-block"
18 |
19 | @subcontainer = document.createElement("div")
20 | @subcontainer.className = "block"
21 | @container.appendChild(@subcontainer)
22 |
23 | @subcontainer.appendChild(@progress)
24 | @subcontainer.appendChild(@span)
25 |
26 | initialize: (@statusBar) ->
27 |
28 | update: (text, show) ->
29 | if show
30 | @container.className = "inline-block"
31 | @span.innerHTML = text
32 | @actions.push(text)
33 | else
34 | @actions.forEach((value, index) ->
35 | if value == text
36 | @actions.splice(index, 1)
37 | , @)
38 |
39 | if @actions.length == 0
40 | @hide()
41 | else
42 | @span.innerHTML = @actions[0]
43 |
44 | hide: ->
45 | @container.className = 'hidden'
46 |
47 | attach: ->
48 | @tile = @statusBar.addRightTile(item: @container, priority: 19)
49 |
50 | detach: ->
51 | @tile.destroy()
52 |
--------------------------------------------------------------------------------
/lib/services/use-statement.coffee:
--------------------------------------------------------------------------------
1 | ###*
2 | * PHP import use statement
3 | ###
4 |
5 | module.exports =
6 |
7 | ###*
8 | * Import use statement for class under cursor
9 | * @param {TextEditor} editor
10 | ###
11 | importUseStatement: (editor) ->
12 | ClassProvider = require '../autocompletion/class-provider.coffee'
13 | provider = new ClassProvider()
14 | word = editor.getWordUnderCursor()
15 | regex = new RegExp('\\\\' + word + '$');
16 |
17 | suggestions = provider.fetchSuggestionsFromWord(word)
18 | return unless suggestions
19 |
20 | suggestions = suggestions.filter((suggestion) ->
21 | return suggestion.text == word || regex.test(suggestion.text)
22 | )
23 |
24 | return unless suggestions.length
25 |
26 | if suggestions.length < 2
27 | return provider.onSelectedClassSuggestion {editor, suggestion: suggestions.shift()}
28 |
29 | ClassListView = require '../views/class-list-view'
30 |
31 | return new ClassListView(suggestions, ({name}) ->
32 | suggestion = suggestions.filter((suggestion) ->
33 | return suggestion.text == name
34 | ).shift()
35 | provider.onSelectedClassSuggestion {editor, suggestion}
36 | )
37 |
--------------------------------------------------------------------------------
/lib/tooltip/abstract-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | SubAtom = require 'sub-atom'
4 | AttachedPopover = require '../services/attached-popover'
5 |
6 | module.exports =
7 |
8 | class AbstractProvider
9 | hoverEventSelectors: ''
10 |
11 | ###*
12 | * Initializes this provider.
13 | ###
14 | init: () ->
15 | @$ = require 'jquery'
16 | @parser = require '../services/php-file-parser'
17 |
18 | @subAtom = new SubAtom
19 |
20 | atom.workspace.observeTextEditors (editor) =>
21 | @registerEvents editor
22 |
23 | # When you go back to only have one pane the events are lost, so need to re-register.
24 | atom.workspace.onDidDestroyPane (pane) =>
25 | panes = atom.workspace.getPanes()
26 |
27 | if panes.length == 1
28 | for paneItem in panes[0].items
29 | if paneItem instanceof TextEditor
30 | @registerEvents paneItem
31 |
32 | # Having to re-register events as when a new pane is created the old panes lose the events.
33 | atom.workspace.onDidAddPane (observedPane) =>
34 | panes = atom.workspace.getPanes()
35 |
36 | for pane in panes
37 | if pane == observedPane
38 | continue
39 |
40 | for paneItem in pane.items
41 | if paneItem instanceof TextEditor
42 | @registerEvents paneItem
43 |
44 | ###*
45 | * Deactives the provider.
46 | ###
47 | deactivate: () ->
48 | @subAtom.dispose()
49 | @removePopover()
50 |
51 | ###*
52 | * Registers the necessary event handlers.
53 | *
54 | * @param {TextEditor} editor TextEditor to register events to.
55 | ###
56 | registerEvents: (editor) ->
57 | if editor.getGrammar().scopeName.match /text.html.php$/
58 | textEditorElement = atom.views.getView(editor)
59 | scrollViewElement = @$(textEditorElement).find('.scroll-view')
60 |
61 | @subAtom.add scrollViewElement, 'mouseover', @hoverEventSelectors, (event) =>
62 | if @timeout
63 | clearTimeout(@timeout)
64 |
65 | selector = @getSelectorFromEvent(event)
66 |
67 | if selector == null
68 | return
69 |
70 | editorViewComponent = atom.views.getView(editor).component
71 |
72 | # Ticket #140 - In rare cases the component is null.
73 | if editorViewComponent
74 | cursorPosition = editorViewComponent.screenPositionForMouseEvent(event)
75 |
76 | @removePopover()
77 | @showPopoverFor(editor, selector, cursorPosition)
78 |
79 | @subAtom.add scrollViewElement, 'mouseout', @hoverEventSelectors, (event) =>
80 | @removePopover()
81 |
82 | # Ticket #107 - Mouseout isn't generated until the mouse moves, even when scrolling (with the keyboard or
83 | # mouse). If the element goes out of the view in the meantime, its HTML element disappears, never removing
84 | # it.
85 | editor.onDidDestroy () =>
86 | @removePopover()
87 |
88 | editor.onDidStopChanging () =>
89 | @removePopover()
90 |
91 | @$(textEditorElement).find('.horizontal-scrollbar').on 'scroll', () =>
92 | @removePopover()
93 |
94 | @$(textEditorElement).find('.vertical-scrollbar').on 'scroll', () =>
95 | @removePopover()
96 |
97 | ###*
98 | * Shows a popover containing the documentation of the specified element located at the specified location.
99 | *
100 | * @param {TextEditor} editor TextEditor containing the elemment.
101 | * @param {string} element The element to search for.
102 | * @param {Point} bufferPosition The cursor location the element is at.
103 | * @param {int} delay How long to wait before the popover shows up.
104 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip.
105 | ###
106 | showPopoverFor: (editor, element, bufferPosition, delay = 500, fadeInTime = 100) ->
107 | term = @$(element).text()
108 | tooltipText = @getTooltipForWord(editor, term, bufferPosition)
109 |
110 | if tooltipText?.length > 0
111 | popoverElement = @getPopoverElementFromSelector(element)
112 |
113 | @attachedPopover = new AttachedPopover(popoverElement)
114 | @attachedPopover.setText('' + tooltipText + '
')
115 | @attachedPopover.showAfter(delay, fadeInTime)
116 |
117 | ###*
118 | * Removes the popover, if it is displayed.
119 | ###
120 | removePopover: () ->
121 | if @attachedPopover
122 | @attachedPopover.dispose()
123 | @attachedPopover = null
124 |
125 | ###*
126 | * Retrieves a tooltip for the word given.
127 | *
128 | * @param {TextEditor} editor TextEditor to search for namespace of term.
129 | * @param {string} term Term to search for.
130 | * @param {Point} bufferPosition The cursor location the term is at.
131 | ###
132 | getTooltipForWord: (editor, term, bufferPosition) ->
133 |
134 | ###*
135 | * Gets the correct selector when a selector is clicked.
136 | * @param {jQuery.Event} event A jQuery event.
137 | * @return {object|null} A selector to be used with jQuery.
138 | ###
139 | getSelectorFromEvent: (event) ->
140 | return event.currentTarget
141 |
142 | ###*
143 | * Gets the correct element to attach the popover to from the retrieved selector.
144 | * @param {jQuery.Event} event A jQuery event.
145 | * @return {object|null} A selector to be used with jQuery.
146 | ###
147 | getPopoverElementFromSelector: (selector) ->
148 | return selector
149 |
--------------------------------------------------------------------------------
/lib/tooltip/class-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | proxy = require './abstract-provider'
4 | AbstractProvider = require './abstract-provider'
5 |
6 | module.exports =
7 |
8 | class ClassProvider extends AbstractProvider
9 | hoverEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class, .syntax--comment-clickable .syntax--region'
10 |
11 | ###*
12 | * Retrieves a tooltip for the word given.
13 | * @param {TextEditor} editor TextEditor to search for namespace of term.
14 | * @param {string} term Term to search for.
15 | * @param {Point} bufferPosition The cursor location the term is at.
16 | ###
17 | getTooltipForWord: (editor, term, bufferPosition) ->
18 | fullClassName = @parser.getFullClassName(editor, term)
19 |
20 | proxy = require '../services/php-proxy.coffee'
21 | classInfo = proxy.methods(fullClassName)
22 |
23 | if not classInfo or not classInfo.wasFound
24 | return
25 |
26 | type = ''
27 |
28 | if classInfo.isClass
29 | type = (if classInfo.isAbstract then 'abstract ' else '') + 'class'
30 |
31 | else if classInfo.isTrait
32 | type = 'trait'
33 |
34 | else if classInfo.isInterface
35 | type = 'interface'
36 |
37 | # Create a useful description to show in the tooltip.
38 | description = ''
39 |
40 | description += ""
41 | description += type + ' ' + '' + classInfo.shortName + ' — ' + classInfo.class
42 | description += '
'
43 |
44 | # Show the summary (short description).
45 | description += ''
46 | description += (if classInfo.args.descriptions.short then classInfo.args.descriptions.short else '(No documentation available)')
47 | description += '
'
48 |
49 | # Show the (long) description.
50 | if classInfo.args.descriptions.long?.length > 0
51 | description += ''
52 | description += "
Description
"
53 | description += "
" + classInfo.args.descriptions.long + "
"
54 | description += "
"
55 |
56 | return description
57 |
58 | ###*
59 | * Gets the correct selector when a class or namespace is clicked.
60 | *
61 | * @param {jQuery.Event} event A jQuery event.
62 | *
63 | * @return {object|null} A selector to be used with jQuery.
64 | ###
65 | getSelectorFromEvent: (event) ->
66 | return @parser.getClassSelectorFromEvent(event)
67 |
68 | ###*
69 | * Gets the correct element to attach the popover to from the retrieved selector.
70 | * @param {jQuery.Event} event A jQuery event.
71 | * @return {object|null} A selector to be used with jQuery.
72 | ###
73 | getPopoverElementFromSelector: (selector) ->
74 | # getSelectorFromEvent can return multiple items because namespaces and class names are different HTML elements.
75 | # We have to select one to attach the popover to.
76 | array = @$(selector).toArray()
77 | return array[array.length - 1]
78 |
--------------------------------------------------------------------------------
/lib/tooltip/function-provider.coffee:
--------------------------------------------------------------------------------
1 | {Point} = require 'atom'
2 | {TextEditor} = require 'atom'
3 |
4 | AbstractProvider = require './abstract-provider'
5 |
6 | module.exports =
7 |
8 | class FunctionProvider extends AbstractProvider
9 | hoverEventSelectors: '.syntax--function-call'
10 |
11 | ###*
12 | * Retrieves a tooltip for the word given.
13 | * @param {TextEditor} editor TextEditor to search for namespace of term.
14 | * @param {string} term Term to search for.
15 | * @param {Point} bufferPosition The cursor location the term is at.
16 | ###
17 | getTooltipForWord: (editor, term, bufferPosition) ->
18 | value = @parser.getMemberContext(editor, term, bufferPosition)
19 |
20 | if not value
21 | return
22 |
23 | description = ""
24 |
25 | # Show the method's signature.
26 | accessModifier = ''
27 | returnType = ''
28 |
29 | if value.args.return?.type
30 | returnType = value.args.return.type
31 |
32 | if value.isPublic
33 | accessModifier = 'public'
34 |
35 | else if value.isProtected
36 | accessModifier = 'protected'
37 |
38 | else if not value.isFunction?
39 | accessModifier = 'private'
40 |
41 | description += ""
42 |
43 | if value.isFunction?
44 | description += returnType + ' ' + term + '' + '('
45 | else
46 | description += accessModifier + ' ' + returnType + ' ' + term + '' + '('
47 |
48 | if value.args.parameters?.length > 0
49 | description += value.args.parameters.join(', ');
50 |
51 | if value.args.optionals?.length > 0
52 | description += '['
53 |
54 | if value.args.parameters?.length > 0
55 | description += ', '
56 |
57 | description += value.args.optionals.join(', ')
58 | description += ']'
59 |
60 | description += ')'
61 | description += '
'
62 |
63 | # Show the summary (short description).
64 | description += ''
65 | description += (if value.args.descriptions.short then value.args.descriptions.short else '(No documentation available)')
66 | description += '
'
67 |
68 | # Show the (long) description.
69 | if value.args.descriptions.long?.length > 0
70 | description += ''
71 | description += "
Description
"
72 | description += "
" + value.args.descriptions.long + "
"
73 | description += "
"
74 |
75 | # Show the parameters the method has.
76 | parametersDescription = ""
77 |
78 | for param,info of value.args.docParameters
79 | parametersDescription += ""
80 |
81 | parametersDescription += "• "
82 |
83 | if param in value.args.optionals
84 | parametersDescription += "[" + param + "]"
85 |
86 | else
87 | parametersDescription += param
88 |
89 | parametersDescription += " | "
90 |
91 | parametersDescription += "" + (if info.type then info.type else ' ') + ' | '
92 | parametersDescription += "" + (if info.description then info.description else ' ') + ' | '
93 |
94 | parametersDescription += "
"
95 |
96 | if parametersDescription.length > 0
97 | description += ''
98 | description += "
Parameters
"
99 | description += "
" + parametersDescription + "
"
100 | description += "
"
101 |
102 | if value.args.return?.type
103 | returnValue = '' + value.args.return.type + ''
104 |
105 | if value.args.return.description
106 | returnValue += ' ' + value.args.return.description
107 |
108 | description += ''
109 | description += "
Returns
"
110 | description += "
" + returnValue + "
"
111 | description += "
"
112 |
113 | # Show an overview of the exceptions the method can throw.
114 | throwsDescription = ""
115 |
116 | for exceptionType,thrownWhenDescription of value.args.throws
117 | throwsDescription += ""
118 | throwsDescription += "• " + exceptionType + ""
119 |
120 | if thrownWhenDescription
121 | throwsDescription += ' ' + thrownWhenDescription
122 |
123 | throwsDescription += "
"
124 |
125 | if throwsDescription.length > 0
126 | description += ''
127 | description += "
Throws
"
128 | description += "
" + throwsDescription + "
"
129 | description += "
"
130 |
131 | return description
132 |
--------------------------------------------------------------------------------
/lib/tooltip/property-provider.coffee:
--------------------------------------------------------------------------------
1 | {TextEditor} = require 'atom'
2 |
3 | AbstractProvider = require './abstract-provider'
4 |
5 | module.exports =
6 |
7 | class PropertyProvider extends AbstractProvider
8 | hoverEventSelectors: '.syntax--property'
9 |
10 | ###*
11 | * Retrieves a tooltip for the word given.
12 | * @param {TextEditor} editor TextEditor to search for namespace of term.
13 | * @param {string} term Term to search for.
14 | * @param {Point} bufferPosition The cursor location the term is at.
15 | ###
16 | getTooltipForWord: (editor, term, bufferPosition) ->
17 | value = @parser.getMemberContext(editor, term, bufferPosition)
18 |
19 | if not value
20 | return
21 |
22 | accessModifier = ''
23 | returnType = if value.args.return?.type then value.args.return.type else 'mixed'
24 |
25 | if value.isPublic
26 | accessModifier = 'public'
27 |
28 | else if value.isProtected
29 | accessModifier = 'protected'
30 |
31 | else
32 | accessModifier = 'private'
33 |
34 | # Create a useful description to show in the tooltip.
35 | description = ''
36 |
37 | description += ""
38 | description += accessModifier + ' ' + returnType + '' + ' $' + term + ''
39 | description += '
'
40 |
41 | # Show the summary (short description).
42 | description += ''
43 | description += (if value.args.descriptions.short then value.args.descriptions.short else '(No documentation available)')
44 | description += '
'
45 |
46 | # Show the (long) description.
47 | if value.args.descriptions.long?.length > 0
48 | description += ''
49 | description += "
Description
"
50 | description += "
" + value.args.descriptions.long + "
"
51 | description += "
"
52 |
53 | if value.args.return?.type
54 | returnValue = '' + value.args.return.type + ''
55 |
56 | if value.args.return.description
57 | returnValue += ' ' + value.args.return.description
58 |
59 | description += ''
60 | description += "
Type
"
61 | description += "
" + returnValue + "
"
62 | description += "
"
63 |
64 | return description
65 |
--------------------------------------------------------------------------------
/lib/tooltip/tooltip-manager.coffee:
--------------------------------------------------------------------------------
1 | ClassProvider = require './class-provider.coffee'
2 | FunctionProvider = require './function-provider.coffee'
3 | PropertyProvider = require './property-provider.coffee'
4 |
5 | module.exports =
6 |
7 | class TooltipManager
8 | providers: []
9 |
10 | ###*
11 | * Initializes the tooltip providers.
12 | ###
13 | init: () ->
14 | @providers.push new ClassProvider()
15 | @providers.push new FunctionProvider()
16 | @providers.push new PropertyProvider()
17 |
18 | for provider in @providers
19 | provider.init(@)
20 |
21 | ###*
22 | * Deactivates the tooltip providers.
23 | ###
24 | deactivate: () ->
25 | for provider in @providers
26 | provider.deactivate()
27 |
--------------------------------------------------------------------------------
/lib/views/class-list-view.coffee:
--------------------------------------------------------------------------------
1 | {$$, SelectListView} = require 'atom-space-pen-views'
2 |
3 | module.exports =
4 |
5 | class ClassListView extends SelectListView
6 | initialize: (@suggestions, @onConfirm) ->
7 | super
8 | @show()
9 | @setItems @suggestions.map((suggestion) ->
10 | return {name: suggestion.text}
11 | )
12 | @focusFilterEditor()
13 | @currentPane = atom.workspace.getActivePane()
14 |
15 | getFilterKey: -> 'name'
16 |
17 | show: ->
18 | @panel ?= atom.workspace.addModalPanel(item: this)
19 | @panel.show()
20 | @storeFocusedElement()
21 |
22 | cancelled: -> @hide()
23 |
24 | hide: -> @panel?.destroy()
25 |
26 | viewForItem: ({name}) ->
27 | $$ ->
28 | @li name
29 |
30 | confirmed: (item) ->
31 | @onConfirm(item)
32 | @cancel()
33 | @currentPane.activate() if @currentPane?.isAlive()
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atom-autocomplete-php",
3 | "main": "./lib/peekmo-php-atom-autocomplete",
4 | "version": "0.25.6",
5 | "description": "Atom autocompletion plugin for PHP language",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/Peekmo/atom-autocomplete-php.git"
9 | },
10 | "license": "MIT",
11 | "engines": {
12 | "atom": ">0.50.0"
13 | },
14 | "providedServices": {
15 | "autocomplete.provider": {
16 | "versions": {
17 | "2.0.0": "getProvider"
18 | }
19 | },
20 | "php.autocomplete.tools": {
21 | "versions": {
22 | "0.16.0": "provideAutocompleteTools"
23 | }
24 | }
25 | },
26 | "dependencies": {
27 | "fuzzaldrin": "^2.1.0",
28 | "md5": "2.0.0",
29 | "sub-atom": "~1.0.0",
30 | "jquery": "~2.1.4",
31 | "atom-space-pen-views": "~2.0.5"
32 | },
33 | "consumedServices": {
34 | "status-bar": {
35 | "versions": {
36 | "^1.0.0": "consumeStatusBar"
37 | }
38 | },
39 | "autocomplete.php.plugin": {
40 | "versions": {
41 | "1.0.0": "consumePlugin"
42 | }
43 | }
44 | },
45 | "keywords": [
46 | "php",
47 | "composer",
48 | "autocomplete"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/php/Config.php:
--------------------------------------------------------------------------------
1 |
4 | *
5 | * Output PHP errors and exceptions in JSON.
6 | **/
7 |
8 | namespace Peekmo\AtomAutocompletePhp;
9 |
10 | class ErrorHandler
11 | {
12 | static private $reserve;
13 |
14 | /**
15 | * Set up error handling.
16 | */
17 | public static function register()
18 | {
19 | // Keep some room in case of memory exhaustion
20 | self::$reserve = str_repeat(' ', 4096);
21 |
22 | // Ensure nothing will be send to stdout
23 | ini_set('display_errors', false);
24 | ini_set('log_errors', false);
25 |
26 | // Register our handlers
27 | register_shutdown_function('Peekmo\AtomAutocompletePhp\ErrorHandler::onShutdown');
28 | set_error_handler('Peekmo\AtomAutocompletePhp\ErrorHandler::onError');
29 | set_exception_handler('Peekmo\AtomAutocompletePhp\ErrorHandler::onException');
30 | }
31 |
32 | /**
33 | * @throws ErrorException on any error.
34 | */
35 | public static function onError($code, $message, $file, $line, $context)
36 | {
37 | throw new \ErrorException($message, $code, 1, $file, $line);
38 | }
39 |
40 | /**
41 | * Display uncaught exception in JSON.
42 | *
43 | * @param \Exception $exception
44 | */
45 | public static function onException(\Exception $exception)
46 | {
47 | die(
48 | json_encode(
49 | array('error' =>
50 | array(
51 | 'message' => $exception->getMessage(),
52 | 'code' => $exception->getCode(),
53 | 'file' => $exception->getFile(),
54 | 'line' => $exception->getLine(),
55 | )
56 | )
57 | )
58 | );
59 | }
60 |
61 | /**
62 | * Display any fatal error in JSON.
63 | */
64 | public static function onShutdown()
65 | {
66 | // Do nothing
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/php/parser.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * This script returns all functions, classes & methods in the given directory.
11 | * Internals and user's one
12 | **/
13 |
14 | use Peekmo\AtomAutocompletePhp\ErrorHandler;
15 | use Peekmo\AtomAutocompletePhp\Config;
16 |
17 | require_once(__DIR__ . '/ErrorHandler.php');
18 | ErrorHandler::register();
19 |
20 | require_once(__DIR__ . '/tmp.php');
21 | require_once(__DIR__ . '/Config.php');
22 | require_once(__DIR__ . '/services/Tools.php');
23 | require_once(__DIR__ . '/services/DocParser.php');
24 | require_once(__DIR__ . '/services/FileParser.php');
25 | require_once(__DIR__ . '/providers/ProviderInterface.php');
26 | require_once(__DIR__ . '/providers/AutocompleteProvider.php');
27 | require_once(__DIR__ . '/providers/MethodsProvider.php');
28 | require_once(__DIR__ . '/providers/ClassProvider.php');
29 | require_once(__DIR__ . '/providers/ConstantsProvider.php');
30 | require_once(__DIR__ . '/providers/FunctionsProvider.php');
31 | require_once(__DIR__ . '/providers/ClassMapRefresh.php');
32 | require_once(__DIR__ . '/providers/DocParamProvider.php');
33 |
34 | $commands = array(
35 | '--class' => 'Peekmo\AtomAutocompletePhp\ClassProvider',
36 | '--methods' => 'Peekmo\AtomAutocompletePhp\MethodsProvider',
37 | '--functions' => 'Peekmo\AtomAutocompletePhp\FunctionsProvider',
38 | '--constants' => 'Peekmo\AtomAutocompletePhp\ConstantsProvider',
39 | '--refresh' => 'Peekmo\AtomAutocompletePhp\ClassMapRefresh',
40 | '--autocomplete' => 'Peekmo\AtomAutocompletePhp\AutocompleteProvider',
41 | '--doc-params' => 'Peekmo\AtomAutocompletePhp\DocParamProvider'
42 | );
43 |
44 | /**
45 | * Print an error
46 | * @param string $message
47 | */
48 | function show_error($message) {
49 | die(json_encode(array('error' => array('message' => $message))));
50 | }
51 |
52 | if (count($argv) < 3) {
53 | die('Usage : php parser.php ');
54 | }
55 |
56 | $project = $argv[1];
57 | $command = $argv[2];
58 |
59 | if (!isset($commands[$command])) {
60 | show_error(sprintf('Command %s not found', $command));
61 | }
62 |
63 | // Config
64 | Config::set('composer', $config['composer']);
65 | Config::set('php', $config['php']);
66 | Config::set('projectPath', $project);
67 |
68 | // To see if it fix #19
69 | chdir(Config::get('projectPath'));
70 | $indexDir = __DIR__ . '/../indexes/' . md5($project);
71 | if (!is_dir($indexDir)) {
72 | if (false === mkdir($indexDir, 0777, true)) {
73 | show_error('Unable to create directory ' . $indexDir);
74 | }
75 | }
76 |
77 | Config::set('indexClasses', $indexDir . '/index.classes.json');
78 |
79 | foreach ($config['autoload'] as $conf) {
80 | $path = sprintf('%s/%s', $project, trim($conf, '/'));
81 | if (file_exists($path)) {
82 | require_once($path);
83 | }
84 | }
85 |
86 | foreach ($config['classmap'] as $conf) {
87 | $path = sprintf('%s/%s', $project, trim($conf, '/'));
88 | if (file_exists($path)) {
89 | Config::set('classmap_file', $path);
90 | break;
91 | }
92 | }
93 |
94 | $new = new $commands[$command]();
95 |
96 | $args = array_slice($argv, 3);
97 | foreach ($args as &$arg) {
98 | $arg = str_replace('\\\\', '\\', $arg);
99 | }
100 |
101 | $data = $new->execute($args);
102 |
103 | if (false === $encoded = json_encode($data)) {
104 | echo json_encode(array());
105 | } else {
106 | echo $encoded;
107 | }
108 |
--------------------------------------------------------------------------------
/php/providers/AutocompleteProvider.php:
--------------------------------------------------------------------------------
1 | getClassMetadata($class);
32 |
33 | if (isset($data['values'][$name])) {
34 | $memberInfo = $data['values'][$name];
35 |
36 | if (!isset($data['values'][$name]['isMethod'])) {
37 | foreach ($data['values'][$name] as $value) {
38 | if ($value['isMethod'] && $isMethod) {
39 | $memberInfo = $value;
40 | } elseif (!$value['isMethod'] && !$isMethod) {
41 | $memberInfo = $value;
42 | }
43 | }
44 | }
45 |
46 | $returnValue = $memberInfo['args']['return']['type'];
47 |
48 | if ($returnValue == '$this' || $returnValue == 'static') {
49 | $relevantClass = $class;
50 | } elseif ($returnValue === 'self') {
51 | $relevantClass = $memberInfo['declaringClass']['name'];
52 | } else {
53 | $soleClassName = $this->getSoleClassName($returnValue);
54 |
55 | if ($soleClassName) {
56 | // At this point, this could either be a class name relative to the current namespace or a full
57 | // class name without a leading slash. For example, Foo\Bar could also be relative (e.g.
58 | // My\Foo\Bar), in which case its absolute path is determined by the namespace and use statements
59 | // of the file containing it.
60 | $relevantClass = $soleClassName;
61 |
62 | if (!empty($soleClassName) && $soleClassName[0] !== "\\") {
63 | $parser = new FileParser($memberInfo['declaringStructure']['filename']);
64 |
65 | $useStatementFound = false;
66 | $completedClassName = $parser->getFullClassName($soleClassName, $useStatementFound);
67 |
68 | if ($useStatementFound) {
69 | $relevantClass = $completedClassName;
70 | } else {
71 | // Try instantiating the class, e.g. My\Foo\Bar.
72 | try {
73 | // We don't care about the result.
74 | new \ReflectionClass($completedClassName);
75 |
76 | $relevantClass = $completedClassName;
77 | } catch (\Exception $e) {
78 | // The class, e.g. My\Foo\Bar, didn't exist. We can only assume its an absolute path,
79 | // using a namespace set up in composer.json, without a leading slash.
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 |
87 | // Minor optimization to avoid fetching the same data twice.
88 | return ($relevantClass === $class) ? $data : $this->getClassMetadata($relevantClass);
89 | }
90 |
91 | /**
92 | * Retrieves the sole class name from the specified return value statement.
93 | *
94 | * @example "null" returns null.
95 | * @example "FooClass" returns "FooClass".
96 | * @example "FooClass|null" returns "FooClass".
97 | * @example "FooClass|BarClass|null" returns null (there is no single type).
98 | *
99 | * @param string $returnValueStatement
100 | *
101 | * @return string|null
102 | */
103 | protected function getSoleClassName($returnValueStatement)
104 | {
105 | if ($returnValueStatement) {
106 | $types = explode(DocParser::TYPE_SPLITTER, $returnValueStatement);
107 |
108 | $classTypes = array();
109 |
110 | foreach ($types as $type) {
111 | if ($this->isClassType($type)) {
112 | $classTypes[] = $type;
113 | }
114 | }
115 |
116 | if (count($classTypes) === 1) {
117 | return $classTypes[0];
118 | }
119 | }
120 |
121 | return null;
122 | }
123 |
124 | /**
125 | * Returns a boolean indicating if the specified value is a class type or not.
126 | *
127 | * @param string $type
128 | *
129 | * @return bool
130 | */
131 | protected function isClassType($type)
132 | {
133 | return ucfirst($type) === $type;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/php/providers/ClassMapRefresh.php:
--------------------------------------------------------------------------------
1 | 0 && $file = $args[0]) {
18 | if (file_exists(Config::get('indexClasses'))) {
19 | $index = json_decode(file_get_contents(Config::get('indexClasses')), true);
20 |
21 | // Invalid json (#24)
22 | if (false !== $index) {
23 | $fileExists = true;
24 |
25 | $found = false;
26 | $fileParser = new FileParser($file);
27 | $class = $fileParser->getFullClassName(null, $found);
28 |
29 | // if (false !== $class = array_search($file, $classMap)) {
30 | if ($found) {
31 | if (isset($index['mapping'][$class])) {
32 | unset($index['mapping'][$class]);
33 | }
34 |
35 | if (false !== $key = array_search($class, $index['autocomplete'])) {
36 | unset($index['autocomplete'][$key]);
37 | $index['autocomplete'] = array_values($index['autocomplete']);
38 | }
39 |
40 | if ($value = $this->buildIndexClass($class)) {
41 | $index['mapping'][$class] = $value;
42 | $index['autocomplete'][] = $class;
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | // Otherwise, full index
50 | if (!$fileExists) {
51 | // Autoload classes
52 | foreach (array_keys($this->getClassMap(true)) as $class) {
53 | if ($value = $this->buildIndexClass($class)) {
54 | $index['mapping'][$class] = $value;
55 | $index['autocomplete'][] = $class;
56 | }
57 | }
58 |
59 | $this->includeOldDrupal();
60 |
61 | // Internal classes
62 | foreach (get_declared_classes() as $class) {
63 | $provider = new ClassProvider();
64 |
65 | if ($value = $provider->execute(array($class, true))) {
66 | if (!empty($value)) {
67 | $index['mapping'][$class] = $value;
68 | $index['autocomplete'][] = $class;
69 | }
70 | }
71 | }
72 | }
73 |
74 | file_put_contents(Config::get('indexClasses'), json_encode($index));
75 |
76 | return array();
77 | }
78 |
79 | protected function buildIndexClass($class)
80 | {
81 | $ret = exec(sprintf('%s %s %s --class %s',
82 | escapeshellarg(Config::get('php')),
83 | escapeshellarg(__DIR__ . '/../parser.php'),
84 | escapeshellarg(Config::get('projectPath')),
85 | escapeshellarg($class)
86 | ));
87 |
88 | if (false === $value = json_decode($ret, true)) {
89 | return null;
90 | }
91 |
92 | if (isset($value['error'])) {
93 | return null;
94 | }
95 |
96 | return $value;
97 | }
98 |
99 |
100 | /**
101 | * Check if the project is Drupal 6/7 and include the necessary files to get the maximum functions as possible
102 | */
103 | public function includeOldDrupal()
104 | {
105 | $project = Config::get('projectPath');
106 |
107 | if (file_exists($project . '/misc') && file_exists($project . '/modules') && file_exists($project . '/sites')) {
108 | define('DRUPAL_ROOT', $project);
109 | include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
110 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/php/providers/ClassProvider.php:
--------------------------------------------------------------------------------
1 | $e->getMessage());
21 | }
22 |
23 | if ($internal && !$reflection->isInternal()) {
24 | return array();
25 | }
26 |
27 | $ctor = $reflection->getConstructor();
28 |
29 | $args = array();
30 | if (!is_null($ctor)) {
31 | $args = $this->getMethodArguments($ctor);
32 | }
33 |
34 | return array(
35 | 'class' => $this->getClassArguments($reflection),
36 | 'methods' => array(
37 | 'constructor' => array(
38 | 'has' => !is_null($ctor),
39 | 'args' => $args
40 | )
41 | )
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/php/providers/ConstantsProvider.php:
--------------------------------------------------------------------------------
1 | array(),
19 | 'values' => array()
20 | );
21 |
22 | foreach (get_defined_constants(true) as $namespace => $constantList) {
23 | // We don't want constants from our own code showing up, but we don't select the internal namespace
24 | // explicitly as there might be installed extensions such as PCRE adding globals as well.
25 | if ($namespace === 'user') {
26 | continue;
27 | }
28 |
29 | // NOTE: Be very careful if you want to pass back the value, there are also escaped paths, newlines
30 | // (PHP_EOL), etc. in there.
31 | foreach (array_keys($constantList) as $constantName) {
32 | $constants['names'][] = $constantName;
33 | $constants['values'][$constantName] = array(
34 | array(
35 | // NOTE: No additional information is available at the moment, but keep the format consistent.
36 | )
37 | );
38 | }
39 | }
40 |
41 | return $constants;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/php/providers/DocParamProvider.php:
--------------------------------------------------------------------------------
1 | get($class, 'method', $name, array(DocParser::PARAM_TYPE));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/php/providers/FunctionsProvider.php:
--------------------------------------------------------------------------------
1 | includeOldDrupal();
15 |
16 | $functions = array(
17 | 'names' => array(),
18 | 'values' => array()
19 | );
20 |
21 | $allFunctions = get_defined_functions();
22 |
23 | foreach ($allFunctions as $type => $currentFunctions) {
24 | foreach ($currentFunctions as $functionName) {
25 | try {
26 | $function = new \ReflectionFunction($functionName);
27 | } catch (\Exception $e) {
28 | continue;
29 | }
30 |
31 | $functions['names'][] = $function->getName();
32 |
33 | $args = $this->getMethodArguments($function);
34 |
35 | $functions['values'][$function->getName()] = array(
36 | array(
37 | 'isInternal' => $type == 'internal',
38 | 'isMethod' => true,
39 | 'isFunction' => true,
40 | 'args' => $args,
41 | 'declaringStructure' => [
42 | 'filename' => $function->getFileName(),
43 | ],
44 | 'startLine' => $function->getStartLine()
45 | )
46 | );
47 | }
48 | }
49 |
50 | return $functions;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/php/providers/MethodsProvider.php:
--------------------------------------------------------------------------------
1 | getClassMetadata($class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/php/providers/ProviderInterface.php:
--------------------------------------------------------------------------------
1 | getDocComment();
51 | return $this->parse($comment, $filters, $name);
52 | }
53 |
54 | /**
55 | * Parse the comment string to get its elements
56 | *
57 | * @param string|false|null $docblock The docblock to parse. If null, the return array will be filled up with the
58 | * correct keys, but they will be empty.
59 | * @param array $filters Elements to search (see constants).
60 | * @param string $itemName The name of the item (method, class, ...) the docblock is for.
61 | *
62 | * @return array
63 | */
64 | public function parse($docblock, array $filters, $itemName)
65 | {
66 | if (empty($filters)) {
67 | return array();
68 | }
69 |
70 | $tags = array();
71 | $result = array();
72 | $matches = array();
73 |
74 | $docblock = is_string($docblock) ? $docblock : null;
75 |
76 | if ($docblock) {
77 | preg_match_all('/\*\s+(@[a-z-]+)([^@]*)\n/', $docblock, $matches, PREG_SET_ORDER);
78 |
79 | foreach ($matches as $match) {
80 | if (!isset($tags[$match[1]])) {
81 | $tags[$match[1]] = array();
82 | }
83 |
84 | $tagValue = $match[2];
85 | $tagValue = $this->normalizeNewlines($tagValue);
86 |
87 | // Remove the delimiters of the docblock itself at the start of each line, if any.
88 | $tagValue = preg_replace('/\n\s+\*\s*/', ' ', $tagValue);
89 |
90 | // Collapse multiple spaces, just like HTML does.
91 | $tagValue = preg_replace('/\s\s+/', ' ', $tagValue);
92 |
93 | $tags[$match[1]][] = trim($tagValue);
94 | }
95 | }
96 |
97 | $filterMethodMap = array(
98 | static::RETURN_VALUE => 'filterReturn',
99 | static::PARAM_TYPE => 'filterParams',
100 | static::VAR_TYPE => 'filterVar',
101 | static::PROPERTY => 'filterProperty',
102 | static::METHOD => 'filterMethod',
103 | static::DEPRECATED => 'filterDeprecated',
104 | static::THROWS => 'filterThrows',
105 | static::DESCRIPTION => 'filterDescription'
106 | );
107 |
108 | foreach ($filters as $filter) {
109 | if (!isset($filterMethodMap[$filter])) {
110 | throw new \UnexpectedValueException('Unknown filter passed!');
111 | }
112 |
113 | $result = array_merge(
114 | $result,
115 | $this->{$filterMethodMap[$filter]}($docblock, $itemName, $tags)
116 | );
117 | }
118 |
119 | return $result;
120 | }
121 |
122 | /**
123 | * Returns an array of three values, the first value will go up until the first space, the second value will go up
124 | * until the second space, and the third value will contain the rest of the string. Convenience method for tags that
125 | * consist of three parameters.
126 | *
127 | * @param string $value
128 | *
129 | * @return string[]
130 | */
131 | protected function filterThreeParameterTag($value)
132 | {
133 | $parts = explode(' ', $value);
134 |
135 | $firstPart = trim(array_shift($parts));
136 | $secondPart = trim(array_shift($parts));
137 |
138 | if (!empty($parts)) {
139 | $thirdPart = trim(implode(' ', $parts));
140 | } else {
141 | $thirdPart = null;
142 | }
143 |
144 | return array($firstPart ?: null, $secondPart ?: null, $thirdPart);
145 | }
146 |
147 | /**
148 | * Returns an array of two values, the first value will go up until the first space and the second value will
149 | * contain the rest of the string. Convenience method for tags that consist of two parameters.
150 | *
151 | * @param string $value
152 | *
153 | * @return string[]
154 | */
155 | protected function filterTwoParameterTag($value)
156 | {
157 | list($firstPart, $secondPart, $thirdPart) = $this->filterThreeParameterTag($value);
158 |
159 | return array($firstPart, trim($secondPart . ' ' . $thirdPart));
160 | }
161 |
162 | /**
163 | * Filters out information about the return value of the function or method.
164 | *
165 | * @param string $docblock
166 | * @param string $methodName
167 | * @param array $tags
168 | *
169 | * @return array
170 | */
171 | protected function filterReturn($docblock, $methodName, array $tags)
172 | {
173 | if (isset($tags[static::RETURN_VALUE])) {
174 | list($type, $description) = $this->filterTwoParameterTag($tags[static::RETURN_VALUE][0]);
175 | } else {
176 | // According to http://www.phpdoc.org/docs/latest/guides/docblocks.html, a method that does
177 | // have a docblock, but no explicit return type returns void. Constructors, however, must
178 | // return self. If there is no docblock at all, we can't assume either of these types.
179 | $type = ($methodName === '__construct') ? 'self' : 'void';
180 | $description = null;
181 | }
182 |
183 | return array(
184 | 'return' => array(
185 | 'type' => $type,
186 | 'description' => utf8_encode($description)
187 | )
188 | );
189 | }
190 |
191 | /**
192 | * Filters out information about the parameters of the function or method.
193 | *
194 | * @param string $docblock
195 | * @param string $methodName
196 | * @param array $tags
197 | *
198 | * @return array
199 | */
200 | protected function filterParams($docblock, $methodName, array $tags)
201 | {
202 | $params = array();
203 |
204 | if (isset($tags[static::PARAM_TYPE])) {
205 | foreach ($tags[static::PARAM_TYPE] as $tag) {
206 | list($type, $variableName, $description) = $this->filterThreeParameterTag($tag);
207 |
208 | $params[$variableName] = array(
209 | 'type' => $type,
210 | 'description' => utf8_encode($description)
211 | );
212 | }
213 | }
214 |
215 | return array(
216 | 'params' => $params
217 | );
218 | }
219 |
220 | /**
221 | * Filters out information about the variable.
222 | *
223 | * @param string $docblock
224 | * @param string $methodName
225 | * @param array $tags
226 | *
227 | * @return array
228 | */
229 | protected function filterVar($docblock, $methodName, array $tags)
230 | {
231 | if (isset($tags[static::VAR_TYPE])) {
232 | list($type, $description) = $this->filterTwoParameterTag($tags[static::VAR_TYPE][0]);
233 | } else {
234 | $type = null;
235 | $description = null;
236 | }
237 |
238 | return array(
239 | 'var' => array(
240 | 'type' => $type,
241 | 'description' => utf8_encode($description)
242 | )
243 | );
244 | }
245 |
246 | /**
247 | * Filters out information about the property.
248 | *
249 | * @param string $docblock
250 | * @param string $methodName
251 | * @param array $tags
252 | *
253 | * @return array
254 | */
255 | protected function filterProperty($docblock, $methodName, array $tags)
256 | {
257 | $properties = array();
258 |
259 | if (isset($tags[static::PROPERTY])) {
260 | foreach ($tags[static::PROPERTY] as $tag) {
261 | list($type, $variableName) = $this->filterTwoParameterTag($tag);
262 |
263 | $properties[$variableName] = array(
264 | 'type' => $type,
265 | 'description' => null
266 | );
267 | }
268 | }
269 |
270 | return array(
271 | 'properties' => $properties
272 | );
273 | }
274 |
275 | /**
276 | * Filters out information about the return value of the method.
277 | *
278 | * @param string $docblock
279 | * @param string $methodName
280 | * @param array $tags
281 | *
282 | * @return array
283 | */
284 | protected function filterMethod($docblock, $methodName, array $tags)
285 | {
286 | $methods = array();
287 |
288 | if (isset($tags[static::METHOD])) {
289 | foreach ($tags[static::METHOD] as $tag) {
290 | list($type, $description) = $this->filterTwoParameterTag($tag);
291 | list($methodName, $args) = explode('(', utf8_encode($description));
292 |
293 | $methods[$methodName] = array(
294 | 'type' => $type,
295 | 'description' => null
296 | );
297 | }
298 | }
299 |
300 | return array(
301 | 'methods' => $methods,
302 | );
303 | }
304 |
305 | /**
306 | * Filters out deprecation information.
307 | *
308 | * @param string $docblock
309 | * @param string $methodName
310 | * @param array $tags
311 | *
312 | * @return array
313 | */
314 | protected function filterDeprecated($docblock, $methodName, array $tags)
315 | {
316 | return array(
317 | 'deprecated' => isset($tags[static::DEPRECATED])
318 | );
319 | }
320 |
321 | /**
322 | * Filters out information about what exceptions the method can throw.
323 | *
324 | * @param string $docblock
325 | * @param string $methodName
326 | * @param array $tags
327 | *
328 | * @return array
329 | */
330 | protected function filterThrows($docblock, $methodName, array $tags)
331 | {
332 | $throws = array();
333 |
334 | if (isset($tags[static::THROWS])) {
335 | foreach ($tags[static::THROWS] as $tag) {
336 | list($type, $description) = $this->filterTwoParameterTag($tag);
337 |
338 | $throws[$type] = utf8_encode($description);
339 | }
340 | }
341 |
342 | return array(
343 | 'throws' => $throws
344 | );
345 | }
346 |
347 | /**
348 | * Filters out information about the description.
349 | *
350 | * @param string $docblock
351 | * @param string $methodName
352 | * @param array $tags
353 | *
354 | * @return array
355 | */
356 | protected function filterDescription($docblock, $methodName, array $tags)
357 | {
358 | $summary = '';
359 | $description = '';
360 |
361 | $lines = explode("\n", $docblock);
362 |
363 | $isReadingSummary = true;
364 |
365 | foreach ($lines as $line) {
366 | if (preg_match(self::TAG_START_REGEX, $line) === 1) {
367 | break; // Found the start of a tag, the summary and description are finished.
368 | }
369 |
370 | // Remove the opening and closing tags.
371 | $line = preg_replace('/^\s*(?:\/)?\*+(?:\/)?/', '', $line);
372 | $line = preg_replace('/\s*\*+\/$/', '', $line);
373 |
374 | $line = trim($line);
375 |
376 | if ($isReadingSummary && empty($line) && !empty($summary)) {
377 | $isReadingSummary = false;
378 | } elseif ($isReadingSummary) {
379 | $summary = empty($summary) ? $line : ($summary . "\n" . $line);
380 | } else {
381 | $description = empty($description) ? $line : ($description . "\n" . $line);
382 | }
383 | }
384 |
385 | return array(
386 | 'descriptions' => array(
387 | 'short' => trim(utf8_encode($summary)),
388 | 'long' => trim(utf8_encode($description))
389 | )
390 | );
391 | }
392 |
393 | /**
394 | * Retrieves the specified string with its line separators replaced with the specifed separator.
395 | *
396 | * @param string $string
397 | * @param string $replacement
398 | *
399 | * @return string
400 | */
401 | protected function replaceNewlines($string, $replacement)
402 | {
403 | return str_replace(array("\n", "\r\n", PHP_EOL), $replacement, $string);
404 | }
405 |
406 | /**
407 | * Normalizes all types of newlines to the "\n" separator.
408 | *
409 | * @param string $string
410 | *
411 | * @return string
412 | */
413 | protected function normalizeNewlines($string)
414 | {
415 | return $this->replaceNewlines($string, "\n");
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/php/services/FileParser.php:
--------------------------------------------------------------------------------
1 | file = fopen($filePath, 'r');
31 | }
32 |
33 | /**
34 | * Destructor.
35 | */
36 | public function __destruct()
37 | {
38 | fclose($this->file);
39 | }
40 |
41 | /**
42 | * Retrieves the full class name of the given class, based on the namespace and use statements in the current file.
43 | *
44 | * @param string|null $className The class to search for. If null, the full class name of the first
45 | * class/trait/interface definition will be returned.
46 | * @param bool $found Set to true if an explicit use statement was found. If false, the full class name
47 | * could, for example, have been built using the namespace of the current file.
48 | *
49 | * @return string
50 | */
51 | public function getFullClassName($className, &$found)
52 | {
53 | if (!empty($className) && $className[0] == "\\") {
54 | return substr($className, 1); // FQCN, not subject to any further context.
55 | }
56 |
57 | // Reserved keyword
58 | if (in_array($className, $this->keywords)) {
59 | return $className;
60 | }
61 |
62 | $line = '';
63 | $found = false;
64 | $matches = array();
65 | $fullClass = $className;
66 |
67 | while (!feof($this->file)) {
68 | $line = fgets($this->file);
69 |
70 | if (preg_match(self::NAMESPACE_PATTERN, $line, $matches) === 1) {
71 | // The class name is relative to the namespace of the class it is contained in, unless a use statement
72 | // says otherwise.
73 | $fullClass = $matches[1] . '\\' . $className;
74 | } elseif ($className && preg_match(self::USE_PATTERN, $line, $matches) === 1) {
75 | $classNameParts = explode('\\', $className);
76 | $importNameParts = explode('\\', $matches[1]);
77 |
78 | $isAliasedImport = isset($matches[2]);
79 |
80 | if (($isAliasedImport && $matches[2] === $classNameParts[0]) ||
81 | (!$isAliasedImport && $importNameParts[count($importNameParts) - 1] === $classNameParts[0])) {
82 | $found = true;
83 |
84 | $fullClass = $matches[1];
85 |
86 | array_shift($classNameParts);
87 |
88 | if (!empty($classNameParts)) {
89 | $fullClass .= '\\' . implode('\\', $classNameParts);
90 | }
91 |
92 | break;
93 | }
94 | }
95 |
96 | if (preg_match(self::DEFINITION_PATTERN, $line, $matches) === 1) {
97 | if (!$className) {
98 | $found = true;
99 | $fullClass .= $matches[1];
100 | }
101 |
102 | break;
103 | }
104 | }
105 |
106 | if ($fullClass && $fullClass[0] == '\\') {
107 | $fullClass = substr($fullClass, 1);
108 | }
109 |
110 | return $fullClass;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/php/services/Tools.php:
--------------------------------------------------------------------------------
1 | classMap) || $force) {
27 | if (Config::get('classmap_file') && !file_exists(Config::get('classmap_file')) || $force) {
28 | // Check if composer is executable or not
29 | if (is_executable(Config::get('composer'))) {
30 | exec(sprintf('%s dump-autoload --optimize --quiet --no-interaction --working-dir=%s 2>&1',
31 | escapeshellarg(Config::get('composer')),
32 | escapeshellarg(Config::get('projectPath'))
33 | ));
34 | } else {
35 | exec(sprintf('%s %s dump-autoload --optimize --quiet --no-interaction --working-dir=%s 2>&1',
36 | escapeshellarg(Config::get('php')),
37 | escapeshellarg(Config::get('composer')),
38 | escapeshellarg(Config::get('projectPath'))
39 | ));
40 | }
41 | }
42 |
43 | if (Config::get('classmap_file')) {
44 | $this->classMap = require(Config::get('classmap_file'));
45 | }
46 | }
47 |
48 | return $this->classMap;
49 | }
50 |
51 | /**
52 | * Fetches information about the specified class, trait, interface, ...
53 | *
54 | * @param ReflectionClass $class The class to analyze.
55 | *
56 | * @return array
57 | */
58 | protected function getClassArguments(ReflectionClass $class)
59 | {
60 | $parser = new DocParser();
61 | $docComment = $class->getDocComment() ?: '';
62 |
63 | $docParseResult = $parser->parse($docComment, array(
64 | DocParser::DEPRECATED,
65 | DocParser::DESCRIPTION
66 | ), $class->getShortName());
67 |
68 | return array(
69 | 'descriptions' => $docParseResult['descriptions'],
70 | 'deprecated' => $docParseResult['deprecated']
71 | );
72 | }
73 |
74 | /**
75 | * Fetches information about the specified method or function, such as its parameters, a description from the
76 | * docblock (if available), the return type, ...
77 | *
78 | * @param ReflectionFunctionAbstract $function The function or method to analyze.
79 | *
80 | * @return array
81 | */
82 | protected function getMethodArguments(ReflectionFunctionAbstract $function)
83 | {
84 | $args = $function->getParameters();
85 |
86 | $optionals = array();
87 | $parameters = array();
88 |
89 | foreach ($args as $argument) {
90 | $value = '$' . $argument->getName();
91 |
92 | if ($argument->isPassedByReference()) {
93 | $value = '&' . $value;
94 | }
95 |
96 | if ($argument->isOptional()) {
97 | $optionals[] = $value;
98 | } else {
99 | $parameters[] = $value;
100 | }
101 | }
102 |
103 | // For variadic methods, append three dots to the last argument (if any) to indicate this to the user. This
104 | // requires PHP >= 5.6.
105 | if (!empty($args) && method_exists($function, 'isVariadic') && $function->isVariadic()) {
106 | $lastArgument = $args[count($args) - 1];
107 |
108 | if ($lastArgument->isOptional()) {
109 | $optionals[count($optionals) - 1] .= '...';
110 | } else {
111 | $parameters[count($parameters) - 1] .= '...';
112 | }
113 | }
114 |
115 | $parser = new DocParser();
116 | $docComment = $function->getDocComment();
117 |
118 | $docParseResult = $parser->parse($docComment, array(
119 | DocParser::THROWS,
120 | DocParser::PARAM_TYPE,
121 | DocParser::DEPRECATED,
122 | DocParser::DESCRIPTION,
123 | DocParser::RETURN_VALUE
124 | ), $function->name);
125 |
126 | $docblockInheritsLongDescription = false;
127 |
128 | // Ticket #86 - Add support for inheriting the entire docblock from the parent if the current docblock contains
129 | // nothing but these tags. Note that, according to draft PSR-5 and phpDocumentor's implementation, this is
130 | // incorrect. However, some large frameworks (such as Symfony) use this and it thus makes life easier for many
131 | // developers, hence this workaround.
132 | if (in_array($docParseResult['descriptions']['short'], array('{@inheritdoc}', '{@inheritDoc}'))) {
133 | $docComment = false; // Pretend there is no docblock.
134 | }
135 |
136 | if (strpos($docParseResult['descriptions']['long'], DocParser::INHERITDOC) !== false) {
137 | // The parent docblock is embedded, which we'll need to parse. Note that according to phpDocumentor this
138 | // only works for the long description (not the so-called 'summary' or short description).
139 | $docblockInheritsLongDescription = true;
140 | }
141 |
142 | // No immediate docblock available or we need to scan the parent docblock?
143 | if ((!$docComment || $docblockInheritsLongDescription) && $function instanceof ReflectionMethod) {
144 | $classIterator = new ReflectionClass($function->class);
145 | $classIterator = $classIterator->getParentClass();
146 |
147 | // Check if this method is implementing an abstract method from a trait, in which case that docblock should
148 | // be used.
149 | if (!$docComment) {
150 | foreach ($function->getDeclaringClass()->getTraits() as $trait) {
151 | if ($trait->hasMethod($function->getName())) {
152 | $traitMethod = $trait->getMethod($function->getName());
153 |
154 | if ($traitMethod->isAbstract() && $traitMethod->getDocComment()) {
155 | return $this->getMethodArguments($traitMethod);
156 | }
157 | }
158 | }
159 | }
160 |
161 | // Check if this method is implementing an interface method, in which case that docblock should be used.
162 | // NOTE: If the parent class has an interface, getMethods() on the parent class will include the interface
163 | // methods, along with their docblocks, even if the parent doesn't actually implement the method. So we only
164 | // have to check the interfaces of the declaring class.
165 | if (!$docComment) {
166 | foreach ($function->getDeclaringClass()->getInterfaces() as $interface) {
167 | if ($interface->hasMethod($function->getName())) {
168 | $interfaceMethod = $interface->getMethod($function->getName());
169 |
170 | if ($interfaceMethod->getDocComment()) {
171 | return $this->getMethodArguments($interfaceMethod);
172 | }
173 | }
174 | }
175 | }
176 |
177 | // Walk up base classes to see if any of them have additional info about this method.
178 | while ($classIterator) {
179 | if ($classIterator->hasMethod($function->getName())) {
180 | $baseClassMethod = $classIterator->getMethod($function->getName());
181 |
182 | if ($baseClassMethod->getDocComment()) {
183 | $baseClassMethodArgs = $this->getMethodArguments($baseClassMethod);
184 |
185 | if (!$docComment) {
186 | return $baseClassMethodArgs; // Fall back to parent docblock.
187 | } elseif ($docblockInheritsLongDescription) {
188 | $docParseResult['descriptions']['long'] = str_replace(
189 | DocParser::INHERITDOC,
190 | $baseClassMethodArgs['descriptions']['long'],
191 | $docParseResult['descriptions']['long']
192 | );
193 | }
194 |
195 | break;
196 | }
197 | }
198 |
199 | $classIterator = $classIterator->getParentClass();
200 | }
201 | }
202 |
203 |
204 | $result = array(
205 | 'parameters' => $parameters,
206 | 'optionals' => $optionals,
207 | 'docParameters' => $docParseResult['params'],
208 | 'throws' => $docParseResult['throws'],
209 | 'return' => $docParseResult['return'],
210 | 'descriptions' => $docParseResult['descriptions'],
211 | 'deprecated' => $function->isDeprecated() || $docParseResult['deprecated']
212 | );
213 |
214 | $result['return']['type'] = method_exists($function, 'getReturnType') && $function->hasReturnType() // PHP7
215 | ? $function->getReturnType()->__toString()
216 | : $result['return']['type']
217 | ;
218 |
219 | return $result;
220 | }
221 |
222 | /**
223 | * Fetches information about the specified class property, such as its type, description, ...
224 | *
225 | * @param ReflectionProperty $property The property to analyze.
226 | *
227 | * @return array
228 | */
229 | protected function getPropertyArguments(ReflectionProperty $property)
230 | {
231 | $parser = new DocParser();
232 | $docComment = $property->getDocComment() ?: '';
233 |
234 | $docParseResult = $parser->parse($docComment, array(
235 | DocParser::VAR_TYPE,
236 | DocParser::DEPRECATED,
237 | DocParser::DESCRIPTION
238 | ), $property->name);
239 |
240 | if (!$docComment) {
241 | $classIterator = new ReflectionClass($property->class);
242 | $classIterator = $classIterator->getParentClass();
243 |
244 | // Walk up base classes to see if any of them have additional info about this property.
245 | while ($classIterator) {
246 | if ($classIterator->hasProperty($property->getName())) {
247 | $baseClassProperty = $classIterator->getProperty($property->getName());
248 |
249 | if ($baseClassProperty->getDocComment()) {
250 | $baseClassPropertyArgs = $this->getPropertyArguments($baseClassProperty);
251 |
252 | return $baseClassPropertyArgs; // Fall back to parent docblock.
253 | }
254 | }
255 |
256 | $classIterator = $classIterator->getParentClass();
257 | }
258 | }
259 |
260 | return array(
261 | 'return' => $docParseResult['var'],
262 | 'descriptions' => $docParseResult['descriptions'],
263 | 'deprecated' => $docParseResult['deprecated']
264 | );
265 | }
266 |
267 | /**
268 | * Retrieves the class that contains the specified reflection member.
269 | *
270 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember
271 | *
272 | * @return array
273 | */
274 | protected function getDeclaringClass($reflectionMember)
275 | {
276 | // This will point to the class that contains the member, which will resolve to the parent class if it's
277 | // inherited (and not overridden).
278 | $declaringClass = $reflectionMember->getDeclaringClass();
279 |
280 | return array(
281 | 'name' => $declaringClass->name,
282 | 'filename' => $declaringClass->getFilename()
283 | );
284 | }
285 |
286 | /**
287 | * Retrieves the structure (class, trait, interface, ...) that contains the specified reflection member.
288 | *
289 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember
290 | *
291 | * @return array
292 | */
293 | protected function getDeclaringStructure($reflectionMember)
294 | {
295 | // This will point to the class that contains the member, which will resolve to the parent class if it's
296 | // inherited (and not overridden).
297 | $declaringStructure = $reflectionMember->getDeclaringClass();
298 | $isMethod = ($reflectionMember instanceof ReflectionFunctionAbstract);
299 |
300 | // Members from traits are seen as part of the structure using the trait, but we still want the actual trait
301 | // name.
302 | foreach ($declaringStructure->getTraits() as $trait) {
303 | if (($isMethod && $trait->hasMethod($reflectionMember->name)) ||
304 | (!$isMethod && $trait->hasProperty($reflectionMember->name))) {
305 | $declaringStructure = $trait;
306 | break;
307 | }
308 | }
309 |
310 | return array(
311 | 'name' => $declaringStructure->name,
312 | 'filename' => $declaringStructure->getFilename()
313 | );
314 | }
315 |
316 | /**
317 | * Retrieves information about what the specified member is overriding, if anything.
318 | *
319 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember
320 | *
321 | * @return array|null
322 | */
323 | protected function getOverrideInfo($reflectionMember)
324 | {
325 | $overriddenMember = null;
326 | $memberName = $reflectionMember->getName();
327 |
328 | $baseClass = $reflectionMember->getDeclaringClass();
329 |
330 | $type = ($reflectionMember instanceof ReflectionProperty) ? 'Property' : 'Method';
331 |
332 | while ($baseClass = $baseClass->getParentClass()) {
333 | if ($baseClass->{'has' . $type}($memberName)) {
334 | $overriddenMember = $baseClass->{'get' . $type}($memberName);
335 | break;
336 | }
337 | }
338 |
339 | if (!$overriddenMember) {
340 | // This method is not an override of a parent method, see if it is an 'override' of an abstract method from
341 | // a trait the class it is in is using.
342 | if ($reflectionMember instanceof ReflectionFunctionAbstract) {
343 | foreach ($reflectionMember->getDeclaringClass()->getTraits() as $trait) {
344 | if ($trait->hasMethod($memberName)) {
345 | $traitMethod = $trait->getMethod($memberName);
346 |
347 | if ($traitMethod->isAbstract()) {
348 | $overriddenMember = $traitMethod;
349 | }
350 | }
351 | }
352 | }
353 |
354 | if (!$overriddenMember) {
355 | return null;
356 | }
357 | }
358 |
359 | $startLine = null;
360 |
361 | if ($overriddenMember instanceof ReflectionFunctionAbstract) {
362 | $startLine = $overriddenMember->getStartLine();
363 | }
364 |
365 | return array(
366 | 'declaringClass' => $this->getDeclaringClass($overriddenMember),
367 | 'declaringStructure' => $this->getDeclaringStructure($overriddenMember),
368 | 'startLine' => $startLine
369 | );
370 | }
371 |
372 | /**
373 | * Retrieves information about what interface the specified member method is implementind, if any.
374 | *
375 | * @param ReflectionFunctionAbstract $reflectionMember
376 | *
377 | * @return array|null
378 | */
379 | protected function getImplementationInfo(ReflectionFunctionAbstract $reflectionMember)
380 | {
381 | $implementedMember = null;
382 | $methodName = $reflectionMember->getName();
383 |
384 | foreach ($reflectionMember->getDeclaringClass()->getInterfaces() as $interface) {
385 | if ($interface->hasMethod($methodName)) {
386 | $implementedMember = $interface->getMethod($methodName);
387 | break;
388 | }
389 | }
390 |
391 | if (!$implementedMember) {
392 | return null;
393 | }
394 |
395 | return array(
396 | 'declaringClass' => $this->getDeclaringClass($implementedMember),
397 | 'declaringStructure' => $this->getDeclaringStructure($implementedMember),
398 | 'startLine' => $implementedMember->getStartLine()
399 | );
400 | }
401 |
402 | /**
403 | * Retrieves a list of parent classes of the specified class, ordered from the closest to the furthest ancestor.
404 | *
405 | * @param ReflectionClass $class
406 | *
407 | * @return string[]
408 | */
409 | protected function getParentClasses(ReflectionClass $class)
410 | {
411 | $parents = [];
412 |
413 | $parentClass = $class;
414 |
415 | while ($parentClass = $parentClass->getParentClass()) {
416 | $parents[] = $parentClass->getName();
417 | }
418 |
419 | return $parents;
420 | }
421 |
422 | /**
423 | * Returns methods and properties of the given className
424 | *
425 | * @param string $className Full namespace of the parsed class.
426 | *
427 | * @return array
428 | */
429 | protected function getClassMetadata($className)
430 | {
431 | $data = array(
432 | 'wasFound' => false,
433 | 'class' => $className,
434 | 'shortName' => null,
435 | 'filename' => null,
436 | 'isTrait' => null,
437 | 'isClass' => null,
438 | 'isAbstract' => null,
439 | 'isInterface' => null,
440 | 'parents' => array(),
441 | 'names' => array(),
442 | 'values' => array(),
443 | 'args' => array()
444 | );
445 |
446 | try {
447 | $reflection = new ReflectionClass($className);
448 | } catch (\Exception $e) {
449 | return $data;
450 | }
451 |
452 | $data = array_merge($data, array(
453 | 'wasFound' => true,
454 | 'shortName' => $reflection->getShortName(),
455 | 'filename' => $reflection->getFileName(),
456 | 'isTrait' => $reflection->isTrait(),
457 | 'isClass' => !($reflection->isTrait() || $reflection->isInterface()),
458 | 'isAbstract' => $reflection->isAbstract(),
459 | 'isInterface' => $reflection->isInterface(),
460 | 'parents' => $this->getParentClasses($reflection),
461 | 'args' => $this->getClassArguments($reflection)
462 | ));
463 |
464 | // Retrieve information about methods.
465 | foreach ($reflection->getMethods() as $method) {
466 | $data['names'][] = $method->getName();
467 |
468 | $data['values'][$method->getName()] = array(
469 | 'isMethod' => true,
470 | 'isProperty' => false,
471 | 'isPublic' => $method->isPublic(),
472 | 'isProtected' => $method->isProtected(),
473 | 'isPrivate' => $method->isPrivate(),
474 | 'isStatic' => $method->isStatic(),
475 |
476 | 'override' => $this->getOverrideInfo($method),
477 | 'implementation' => $this->getImplementationInfo($method),
478 |
479 | 'args' => $this->getMethodArguments($method),
480 | 'declaringClass' => $this->getDeclaringClass($method),
481 | 'declaringStructure' => $this->getDeclaringStructure($method),
482 | 'startLine' => $method->getStartLine()
483 | );
484 | }
485 |
486 | // Retrieves information about properties/attributes.
487 | foreach ($reflection->getProperties() as $attribute) {
488 | if (!in_array($attribute->getName(), $data['names'])) {
489 | $data['names'][] = $attribute->getName();
490 | $data['values'][$attribute->getName()] = null;
491 | }
492 |
493 | $attributesValues = array(
494 | 'isMethod' => false,
495 | 'isProperty' => true,
496 | 'isPublic' => $attribute->isPublic(),
497 | 'isProtected' => $attribute->isProtected(),
498 | 'isPrivate' => $attribute->isPrivate(),
499 | 'isStatic' => $attribute->isStatic(),
500 |
501 | 'override' => $this->getOverrideInfo($attribute),
502 |
503 | 'args' => $this->getPropertyArguments($attribute),
504 | 'declaringClass' => $this->getDeclaringClass($attribute),
505 | 'declaringStructure' => $this->getDeclaringStructure($attribute)
506 | );
507 |
508 | if (is_array($data['values'][$attribute->getName()])) {
509 | $attributesValues = array(
510 | $attributesValues,
511 | $data['values'][$attribute->getName()]
512 | );
513 | }
514 |
515 | $data['values'][$attribute->getName()] = $attributesValues;
516 | }
517 |
518 | // Retrieve information about constants.
519 | $constants = $reflection->getConstants();
520 |
521 | foreach (array_keys($constants) as $constant) {
522 | if (!in_array($constant, $data['names'])) {
523 | $data['names'][] = $constant;
524 | $data['values'][$constant] = null;
525 | }
526 |
527 | // TODO: There is no direct way to know where the constant originated from (the current class, a base class,
528 | // an interface of a base class, a trait, ...). This could be done by looping up the chain of base classes
529 | // to the last class that also has the same property and then checking if any of that class' traits or
530 | // interfaces define the constant.
531 | $data['values'][$constant][] = array(
532 | 'isMethod' => false,
533 | 'isProperty' => false,
534 | 'isPublic' => true,
535 | 'isProtected' => false,
536 | 'isPrivate' => false,
537 | 'isStatic' => true,
538 | 'declaringClass' => array(
539 | 'name' => $reflection->name,
540 | 'filename' => $reflection->getFileName()
541 | ),
542 |
543 | // TODO: It is not possible to directly fetch the docblock of the constant through reflection, manual
544 | // file parsing is required.
545 | 'args' => array(
546 | 'return' => null,
547 | 'descriptions' => array(),
548 | 'deprecated' => false
549 | )
550 | );
551 | }
552 |
553 | $data = $this->getClassMetadataFromDocBlock($reflection, $data);
554 |
555 | return $data;
556 | }
557 |
558 | /**
559 | * Returns methods and properties from docblock
560 | *
561 | * @param ReflectionClass $reflection
562 | * @param array $data generated result by getClassMetadata
563 | *
564 | * @return array
565 | */
566 | protected function getClassMetadataFromDocBlock(ReflectionClass $reflection, array $data)
567 | {
568 | $parser = new DocParser();
569 | $filters = array(
570 | DocParser::PROPERTY,
571 | DocParser::METHOD
572 | );
573 | $docParseResult = $parser->parse($reflection->getDocComment(), $filters, $reflection->getShortName());
574 |
575 | // Retrieve method information from docblock.
576 | foreach ($docParseResult['methods'] as $methodName => $method) {
577 | if (isset($data['values'][$methodName]['args']['return']['type'])) {
578 | $data['values'][$methodName]['args']['return']['type'] = $method['type'];
579 | }
580 | }
581 |
582 | // Retrieve property information from docblock.
583 | foreach ($docParseResult['properties'] as $propertyName => $property) {
584 | if ($propertyName[0] == '$') {
585 | $propertyName = substr($propertyName, 1);
586 | }
587 | if (!in_array($propertyName, $data['names'])) {
588 | $data['names'][] = $propertyName;
589 | $data['values'][$propertyName] = null;
590 | }
591 |
592 | try {
593 | $propertyReflection = new ReflectionClass($property['type']);
594 | $propertyDocParseResult = $parser->parse(
595 | $propertyReflection->getDocComment(),
596 | [DocParser::DESCRIPTION],
597 | $propertyReflection->getShortName()
598 | );
599 | $args = array(
600 | 'return' => array(
601 | 'description' => '',
602 | 'type' => $propertyReflection->name,
603 | ),
604 | 'descriptions' => $propertyDocParseResult['descriptions'],
605 | 'deprecated' => false
606 | );
607 | } catch (\ReflectionException $e) {
608 | $args = array(
609 | 'return' => array(
610 | 'description' => '',
611 | 'type' => $property['type'],
612 | ),
613 | 'descriptions' => array(),
614 | 'deprecated' => false
615 | );
616 | }
617 |
618 | $propertyClass = array(
619 | 'name' => $reflection->name,
620 | 'filename' => $reflection->getFileName(),
621 | );
622 | $attributesValues = array(
623 | 'isMethod' => false,
624 | 'isProperty' => true,
625 | 'isPublic' => true,
626 | 'isProtected' => false,
627 | 'isPrivate' => false,
628 | 'isStatic' => false,
629 |
630 | 'override' => null,
631 |
632 | 'args' => $args,
633 | 'declaringClass' => $propertyClass,
634 | 'declaringStructure' => $propertyClass
635 | );
636 |
637 | if (is_array($data['values'][$propertyName])) {
638 | $attributesValues = array(
639 | $attributesValues,
640 | $data['values'][$propertyName]
641 | );
642 | }
643 |
644 | $data['values'][$propertyName] = $attributesValues;
645 | }
646 |
647 | return $data;
648 | }
649 |
650 | /**
651 | * Check if the project is Drupal 6/7 and include the necessary files to get the maximum functions as possible
652 | */
653 | public function includeOldDrupal()
654 | {
655 | $project = Config::get('projectPath');
656 |
657 | if (file_exists($project . '/misc') && file_exists($project . '/modules') && file_exists($project . '/sites')) {
658 | define('DRUPAL_ROOT', $project);
659 | include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
660 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
661 | }
662 | }
663 | }
664 |
--------------------------------------------------------------------------------
/spec/peekmo-php-atom-autocomplete-spec.coffee:
--------------------------------------------------------------------------------
1 | {WorkspaceView} = require 'atom'
2 | PeekmoPhpAtomAutocomplete = require '../lib/peekmo-php-atom-autocomplete'
3 |
4 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
5 | #
6 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit`
7 | # or `fdescribe`). Remove the `f` to unfocus the block.
8 |
9 | describe "PeekmoPhpAtomAutocomplete", ->
10 | activationPromise = null
11 |
12 | beforeEach ->
13 | atom.workspaceView = new WorkspaceView
14 | activationPromise = atom.packages.activatePackage('peekmo-php-atom-autocomplete')
15 |
16 | describe "when the peekmo-php-atom-autocomplete:toggle event is triggered", ->
17 | it "attaches and then detaches the view", ->
18 | expect(atom.workspaceView.find('.peekmo-php-atom-autocomplete')).not.toExist()
19 |
20 | # This is an activation event, triggering it will cause the package to be
21 | # activated.
22 | atom.commands.dispatch atom.workspaceView.element, 'peekmo-php-atom-autocomplete:toggle'
23 |
24 | waitsForPromise ->
25 | activationPromise
26 |
27 | runs ->
28 |
29 |
--------------------------------------------------------------------------------
/styles/peekmo-php-atom-autocomplete.less:
--------------------------------------------------------------------------------
1 | // The ui-variables file is provided by base themes provided by Atom.
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 | .peekmo-php-atom-autocomplete {
9 | }
10 |
11 | .php-atom-autocomplete-strike, .php-atom-autocomplete-strike .word {
12 | text-decoration: line-through;
13 | }
14 |
15 | .php-atom-autocomplete-goto-overlay {
16 | width: 100%;
17 | margin-left: -50%;
18 | }
19 |
20 | .php-atom-autocomplete-popover {
21 | max-width: 45%;
22 |
23 | .php-atom-autocomplete-popover-wrapper {
24 | margin: 0em 0em 0em 0em;
25 | text-align: left;
26 |
27 | div {
28 | white-space: normal;
29 | }
30 |
31 | .section {
32 | > h4 {
33 | margin-top: 1em;
34 | font-size: larger;
35 | font-weight: bold;
36 | }
37 |
38 | > div {
39 | padding-left: 1em;
40 | }
41 |
42 | td {
43 | vertical-align: top;
44 | }
45 |
46 | td:not(:last-child) {
47 | padding-right: 1em;
48 | }
49 | }
50 | }
51 | }
52 |
53 | atom-text-editor {
54 | .comment-clickable .region {
55 | pointer-events: all;
56 | z-index: 9999;
57 | }
58 |
59 | .gutter {
60 | .line-number {
61 | &.override {
62 | .icon-right {
63 | opacity: 1.0;
64 | cursor: pointer;
65 | visibility: visible;
66 |
67 | &:before {
68 | content: @link;
69 | color: @ui-site-color-2;
70 | }
71 | }
72 | }
73 |
74 | &.implementation {
75 | .icon-right {
76 | opacity: 1.0;
77 | cursor: pointer;
78 | visibility: visible;
79 |
80 | &:before {
81 | content: @link;
82 | color: @ui-site-color-3;
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------