├── .editorconfig
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── LICENSE-atom-autocomplete-php
├── README.md
├── lib
├── AbstractProvider.coffee
├── CachingScopeDescriptorHelper.coffee
├── ClassConstantProvider.coffee
├── ClassProvider.coffee
├── ConstantProvider.coffee
├── FunctionProvider.coffee
├── HyperclickProviderDispatcher.coffee
├── Main.coffee
├── MethodProvider.coffee
├── PropertyProvider.coffee
└── ScopeDescriptorHelper.coffee
├── package.json
└── spec
├── ClassConstantProvider-spec.coffee
├── ClassProvider-spec.coffee
├── ConstantProvider-spec.coffee
├── FunctionProvider-spec.coffee
├── MethodProvider-spec.coffee
├── PropertyProvider-spec.coffee
└── ScopeDescriptorHelper-spec.coffee
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | indent_style = space
8 | indent_size = 4
9 | charset = utf-8
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.2.2
2 | * Require Atom <= 1.18
3 | * See also the README for an explanation.
4 |
5 | ## 1.2.1
6 | * Attempt to fix apm not picking new release.
7 |
8 | ## 1.2.0 (base 3.0.0)
9 | * Upgrade to base 3.0.0.
10 |
11 | ## 1.1.2
12 | * Fix hyperclick providers triggering in other languages than PHP. (https://github.com/php-integrator/atom-navigation/issues/37)
13 |
14 | ## 1.1.1
15 | * Fix deprecations.
16 |
17 | ## 1.1.0 (base 2.0.0)
18 | ### Features and enhancements
19 | * The dependency on SubAtom and jQuery has been removed.
20 | * Hyperclick is now used as back end, which allowed a lot of code to be replaced with a single, consistent, implementation.
21 | * You can now attach a shortcut to navigation (see also hyperclick's settings).
22 | * The default modifier key is now the control key, hyperclick fixes the issue where it created an additional cursor.
23 | * You can modify the modifier key via hyperlick's settings. Support for this was added in version 0.0.39.
24 | * The `ClassProvider` will no longer continuously scan the entire buffer, creating markers in the buffer to properly handle comment ranges. Instead, this scanning is performed only when trying to navigate to something inside a comment block.
25 | * This should improve editor responsiveness, during editing as well as when starting Atom.
26 |
27 | ### Bugs fixed
28 | * Fix navigation to unqualified global constants not working.
29 | * Fix navigation to unqualified global functions not working.
30 | * Fix navigation to qualified global constants with namespace prefix not working.
31 | * Fix navigation to qualified global functions with namespace prefix not working.
32 | * Fix navigation to global constants imported using use statements not working.
33 | * Fix navigation to global functions imported using use statements not working.
34 | * Fix not being able to navigate to the PHP documentation for built-in classes with longer FQCN's, such as classes from MongoDB.
35 | * Fix not being able to navigate to method names with leading slashes, such as `__toString`, because PHP's URL endpoints are terrifically consistent.
36 | * Fix built-in classes sometimes navigating to the wrong page, e.g. `DateTime` was navigating to the overview page instead of the class documentation page.
37 |
38 | ## 1.0.3
39 | * Rename the package and repository.
40 |
41 | ## 1.0.2
42 | * Fix not being able to navigate to annotation classes (e.g. Doctrine or Symfony annotations).
43 | * Fix not being able to navigate to types if they were suffixed with square brackets, i.e. `Foo[]`.
44 |
45 | ## 1.0.1
46 | * Fix the version specifier not being compatible with newer versions of the base service.
47 |
48 | ## 1.0.0 (base 1.0.0)
49 | * Update to use the most recent version of the base service.
50 |
51 | ## 0.7.1
52 | * It is now possible to navigate to the PHP documentation by clicking methods from built-in classes.
53 |
54 | ## 0.7.0 (base 0.9.0)
55 | * Navigation is now asynchronous (i.e. it uses the asynchronous method calls from the base service rather than synchronous calls).
56 |
57 | ## 0.6.2 (base 0.8.0)
58 | * Update to use the most recent version of the base service.
59 |
60 | ## 0.6.1
61 | * Fixed issues occurring when deactivating and reactivating the package.
62 |
63 | ## 0.6.0 (base 0.7.0)
64 | * Update to use the most recent version of the base service.
65 |
66 | ## 0.5.0 (base 0.6.0)
67 | * The dependency on fuzzaldrin was removed.
68 | * Fixed class constants being underlined as if no navigation was possible, while it was.
69 | * It is now possible to alt-click built-in functions and classes to navigate to the PHP documentation in your browser.
70 |
71 | ## 0.4.0 (base 0.5.0)
72 | * The modifier keys that are used in combination with a mouse click are now modifiable as settings.
73 | * Show a dashed line if an item is recognized, but navigation is not possible (i.e. because the item wasn't found).
74 |
75 | ## 0.3.0 (base 0.4.0)
76 | * Added navigation to the definition of global constants.
77 | * Fixed navigation not working in corner cases where a property and method existed with the same name.
78 |
79 | ## 0.2.4
80 | * Don't try to navigate to items that don't have a filename set. Fixes trying to alt-click internal classes such as 'DateTime' opening an empty file.
81 |
82 | ## 0.2.3
83 | * Fixed markers not always registering on startup because the language-php package was not yet ready.
84 |
85 | ## 0.2.2
86 | * Simplified class navigation and fixed it not working in some rare cases.
87 |
88 | ## 0.2.1
89 | * Stop using maintainHistory to be compatible with upcoming Atom 1.3.
90 |
91 | ## 0.2.0
92 | * Added navigation to the definition of class constants.
93 | * Added navigation to the definition of (user-defined) global functions.
94 |
95 | ## 0.1.0
96 | * Initial release.
97 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This program is free software: you can redistribute it and/or modify
2 | it under the terms of the GNU General Public License as published by
3 | the Free Software Foundation, either version 3 of the License, or
4 | (at your option) any later version.
5 |
6 | This program is distributed in the hope that it will be useful,
7 | but WITHOUT ANY WARRANTY; without even the implied warranty of
8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 | GNU General Public License for more details.
10 |
11 | You should have received a copy of the GNU General Public License
12 | along with this program. If not, see .
13 |
--------------------------------------------------------------------------------
/LICENSE-atom-autocomplete-php:
--------------------------------------------------------------------------------
1 | This project was forked from atom-autocomplete-php, thus the original code base
2 | was licensed under the MIT license. It can still be found at [1]. The original
3 | license is located below.
4 |
5 | [1] https://github.com/Peekmo/atom-autocomplete-php
6 |
7 | The MIT License (MIT)
8 |
9 | Copyright (c) 2014-2015 Axel Anceau
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy of
12 | this software and associated documentation files (the "Software"), to deal in
13 | the Software without restriction, including without limitation the rights to
14 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
15 | the Software, and to permit persons to whom the Software is furnished to do so,
16 | subject to the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included in all
19 | copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
23 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
24 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
25 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
26 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # php-integrator/atom-navigation
2 | ## Legacy
3 | This is a legacy version that requires PHP >= 7.1 and Atom <= 1.18. Users that are on more recent version of Atom can and should use the [the base package](https://github.com/php-integrator/atom-base) instead.
4 |
5 | This package depends on the syntax classes in Atom and they changed dramatically in Atom 1.19, effectively breaking most of this package. Instead, [the functionality of this package has been reimplemented in the core and base package](https://github.com/php-integrator/atom-navigation/issues/42#issuecomment-333316791).
6 |
7 | ## About
8 | This package provides code navigation for your PHP source code using [PHP Integrator](https://github.com/php-integrator/atom-base).
9 |
10 | **Note that the [php-integrator-base](https://github.com/php-integrator/atom-base) package is required and needs to be set up correctly for this package to function correctly.**
11 |
12 | **Note that the [hyperclick](https://github.com/facebooknuclide/hyperclick) package is also required.**
13 |
14 | What is included?
15 | * Navigate to the definition of your global PHP constants and functions.
16 | * Navigate to the PHP documentation of built-in classes and functions.
17 | * Navigate to the definition of classes, traits and interfaces.
18 | * Navigate to the definition of class, trait and interface members.
19 |
20 | Note: The exact modifier key (ctrl, alt, shift, meta, ...) to hold whilst clicking depends on your configuration of the hyperclick package.
21 |
22 | Tip: You can also navigate to the names of classes, interfaces and traits inside docblocks! If an item is *navigable*, it will be underlined when moving your mouse over it while holding the alt modifier key.
23 |
24 | 
25 |
--------------------------------------------------------------------------------
/lib/AbstractProvider.coffee:
--------------------------------------------------------------------------------
1 | {Point, Range} = require 'atom'
2 |
3 | module.exports =
4 |
5 | ##*
6 | # Base class for providers.
7 | ##
8 | class AbstractProvider
9 | ###*
10 | * @var {Object}
11 | ###
12 | service: null
13 |
14 | ###*
15 | * @var {Object}
16 | ###
17 | scopeDescriptorHelper: null
18 |
19 | ###*
20 | * @param {Object} scopeDescriptorHelper
21 | ###
22 | constructor: (@scopeDescriptorHelper) ->
23 |
24 | ###*
25 | * @param {Object} service
26 | ###
27 | setService: (service) ->
28 | @service = service
29 |
30 | ###*
31 | * @param {TextEditor} editor
32 | * @param {Point} bufferPosition
33 | *
34 | * @return {boolean}
35 | ###
36 | canProvideForBufferPosition: (editor, bufferPosition) ->
37 | throw new Error("This method is abstract and must be implemented!")
38 |
39 | ###*
40 | * @param {TextEditor} editor
41 | * @param {Range} range
42 | * @param {String} text
43 | ###
44 | handleNavigation: (editor, range, text) ->
45 | return if not @service
46 |
47 | @handleSpecificNavigation(editor, range, text)
48 |
49 | ###*
50 | * @param {TextEditor} editor
51 | * @param {Range} range
52 | * @param {String} text
53 | ###
54 | handleSpecificNavigation: (editor, range, text) ->
55 | throw new Error("This method is abstract and must be implemented!")
56 |
--------------------------------------------------------------------------------
/lib/CachingScopeDescriptorHelper.coffee:
--------------------------------------------------------------------------------
1 | ScopeDescriptorHelper = require './ScopeDescriptorHelper'
2 |
3 | module.exports =
4 |
5 | ##*
6 | # Caching extension of ScopeDescriptorHelper.
7 | ##
8 | class CachingScopeDescriptorHelper extends ScopeDescriptorHelper
9 | ###*
10 | * @var {Object}
11 | ###
12 | cache: null
13 |
14 | ###*
15 | * @inherited
16 | ###
17 | constructor: (@config) ->
18 | @cache = {}
19 |
20 | ###*
21 | * Clears the cache.
22 | ###
23 | clearCache: () ->
24 | @cache = {}
25 |
26 | ###*
27 | * Internal convenience method that wraps a call to a parent method.
28 | *
29 | * @param {String} cacheKey
30 | * @param {String} parentMethodName
31 | * @param {Array} parameters
32 | *
33 | * @return {Promise|Object}
34 | ###
35 | wrapCachedRequestToParent: (cacheKey, parentMethodName, parameters) ->
36 | if cacheKey of @cache
37 | return @cache[cacheKey]
38 |
39 | else
40 | @cache[cacheKey] = CachingScopeDescriptorHelper.__super__[parentMethodName].apply(this, parameters)
41 |
42 | return @cache[cacheKey]
43 |
44 | ###*
45 | * @inherited
46 | ###
47 | getClassListForBufferPosition: (editor, bufferPosition, climbCount = 1) ->
48 | return @wrapCachedRequestToParent(
49 | "getClassListForBufferPosition-#{editor.getPath()}-#{bufferPosition.row}-#{bufferPosition.column}-#{climbCount}",
50 | 'getClassListForBufferPosition',
51 | arguments
52 | )
53 |
54 | ###*
55 | * @inherited
56 | ###
57 | getClassListFollowingBufferPosition: (editor, bufferPosition, climbCountForPosition) ->
58 | return @wrapCachedRequestToParent(
59 | "getClassListFollowingBufferPosition-#{editor.getPath()}-#{bufferPosition.row}-#{bufferPosition.column}-#{climbCountForPosition}",
60 | 'getClassListFollowingBufferPosition',
61 | arguments
62 | )
63 |
64 | ###*
65 | * @inherited
66 | ###
67 | getBufferRangeForClassListAtPosition: (editor, classList, bufferPosition, climbCount) ->
68 | return @wrapCachedRequestToParent(
69 | "getBufferRangeForClassListAtPosition-#{editor.getPath()}-#{classList.join('_')}-#{bufferPosition.row}-#{bufferPosition.column}-#{climbCount}",
70 | 'getBufferRangeForClassListAtPosition',
71 | arguments
72 | )
73 |
--------------------------------------------------------------------------------
/lib/ClassConstantProvider.coffee:
--------------------------------------------------------------------------------
1 | AbstractProvider = require './AbstractProvider'
2 |
3 | module.exports =
4 |
5 | ##*
6 | # Provides code navigation for class constants.
7 | ##
8 | class ClassConstantProvider extends AbstractProvider
9 | ###*
10 | * @inheritdoc
11 | ###
12 | canProvideForBufferPosition: (editor, bufferPosition) ->
13 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
14 |
15 | return false if 'php' not in classList
16 | return true if 'other' in classList and 'class' in classList
17 |
18 | return false
19 |
20 | ###*
21 | * @param {TextEditor} editor
22 | * @param {Point} bufferPosition
23 | ###
24 | getRangeForBufferPosition: (editor, bufferPosition) ->
25 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
26 |
27 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition)
28 |
29 | return range
30 |
31 | ###*
32 | * @param {TextEditor} editor
33 | * @param {Point} bufferPosition
34 | * @param {String} text
35 | *
36 | * @return {Promise}
37 | ###
38 | getInfoFor: (editor, bufferPosition, text) ->
39 | successHandler = (members) =>
40 | return null unless members.length > 0
41 |
42 | member = members[0]
43 |
44 | return null unless member.declaringStructure.filename
45 |
46 | return member
47 |
48 | failureHandler = () ->
49 | # Do nothing.
50 |
51 | return @getClassConstantsAt(editor, bufferPosition, text).then(successHandler, failureHandler)
52 |
53 | ###*
54 | * Returns the class constants used at the specified location.
55 | *
56 | * @param {TextEditor} editor The text editor to use.
57 | * @param {Point} bufferPosition The cursor location of the member.
58 | * @param {String} name The name of the member to retrieve information about.
59 | *
60 | * @return {Promise}
61 | ###
62 | getClassConstantsAt: (editor, bufferPosition, name) ->
63 | successHandler = (types) =>
64 | promises = []
65 |
66 | for type in types
67 | promises.push @getClassConstant(type, name)
68 |
69 | return Promise.all(promises)
70 |
71 | failureHandler = () ->
72 | # Do nothing.
73 |
74 | return @service.getResultingTypesAt(editor, bufferPosition, true).then(successHandler, failureHandler)
75 |
76 | ###*
77 | * Retrieves information about the specified constant of the specified class.
78 | *
79 | * @param {String} className The full name of the class to examine.
80 | * @param {String} name The name of the constant to retrieve information about.
81 | *
82 | * @return {Promise}
83 | ###
84 | getClassConstant: (className, name) ->
85 | successHandler = (classInfo) =>
86 | if name of classInfo.constants
87 | return classInfo.constants[name]
88 |
89 | failureHandler = () ->
90 | # Do nothing.
91 |
92 | return @service.getClassInfo(className).then(successHandler, failureHandler)
93 |
94 | ###*
95 | * @inheritdoc
96 | ###
97 | handleSpecificNavigation: (editor, range, text) ->
98 | successHandler = (info) =>
99 | return if not info?
100 |
101 | atom.workspace.open(info.declaringStructure.filename, {
102 | initialLine : (info.declaringStructure.startLineMember - 1),
103 | searchAllPanes: true
104 | })
105 |
106 | failureHandler = () ->
107 | # Do nothing.
108 |
109 | @getInfoFor(editor, range.start, text).then(successHandler, failureHandler)
110 |
--------------------------------------------------------------------------------
/lib/ClassProvider.coffee:
--------------------------------------------------------------------------------
1 | shell = require 'shell'
2 |
3 | {Point, Range} = require 'atom'
4 |
5 | AbstractProvider = require './AbstractProvider'
6 |
7 | module.exports =
8 |
9 | ##*
10 | # Provides code navigation for classes (i.e. being able to click class, interface and trait names to navigate to them).
11 | ##
12 | class ClassProvider extends AbstractProvider
13 | ###*
14 | * A list of all markers that have been placed inside comments to allow code navigation there as well.
15 | *
16 | * @var {Object}
17 | ###
18 | markers: null
19 |
20 | ###*
21 | * @inheritdoc
22 | ###
23 | canProvideForBufferPosition: (editor, bufferPosition) ->
24 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
25 |
26 | return false if 'php' not in classList
27 |
28 | climbCount = 1
29 |
30 | if 'punctuation' in classList and 'inheritance' in classList
31 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition, 2)
32 |
33 | climbCount = 2
34 |
35 | return true if 'class' in classList and 'support' in classList
36 | return true if 'inherited-class' in classList
37 | return true if 'namespace' in classList and 'use' in classList
38 | return true if 'phpdoc' in classList
39 | return true if 'comment' in classList # See also https://github.com/atom/language-php/issues/135
40 |
41 | if 'namespace' in classList
42 | classListFollowingBufferPosition = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition, climbCount)
43 |
44 | return true if ('class' in classListFollowingBufferPosition and 'support' in classListFollowingBufferPosition) or 'inherited-class' in classListFollowingBufferPosition
45 |
46 | return false
47 |
48 | ###*
49 | * @param {TextEditor} editor
50 | * @param {Point} bufferPosition
51 | ###
52 | getRangeForBufferPosition: (editor, bufferPosition) ->
53 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
54 |
55 | climbCount = 1
56 |
57 | if 'punctuation' in classList and 'inheritance' in classList
58 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition, 2)
59 |
60 | climbCount = 2
61 |
62 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition, 0)
63 |
64 | # Atom's consistency regarding the namespace separator splitting a namespace prefix and an actual class name
65 | # leaves something to be desired: sometimes it's part of the namespace, other times it's in its own class,
66 | # in even other cases it has no class at all. For some reason fetching the range for the scope also returns
67 | # "undefined". This entire if-block exists only to handle this corner case.
68 | if not range?
69 | newBufferPosition = bufferPosition.copy()
70 | --newBufferPosition.column
71 |
72 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, newBufferPosition)
73 |
74 | if 'punctuation' in classList and 'inheritance' in classList
75 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, newBufferPosition, 2)
76 |
77 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, newBufferPosition)
78 |
79 | ++bufferPosition.column
80 |
81 | if ('class' in classList and 'support' in classList) or 'inherited-class' in classList
82 | prefixRange = new Range(
83 | new Point(range.start.row, range.start.column - 1),
84 | new Point(range.start.row, range.start.column - 0)
85 | )
86 |
87 | prefixText = editor.getTextInBufferRange(prefixRange)
88 |
89 | if prefixText == "\\"
90 | prefixClassList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, prefixRange.start, 2)
91 |
92 | if "namespace" in prefixClassList
93 | namespaceRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, prefixClassList, prefixRange.start, 0)
94 |
95 | else
96 | namespaceRange = range
97 | namespaceRange.start.column--
98 |
99 | if namespaceRange?
100 | range = namespaceRange.union(range)
101 |
102 | else if 'namespace' in classList
103 | suffixClassList = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition, climbCount)
104 |
105 | # Expand the range to include the constant name, if present.
106 | if ('class' in suffixClassList and 'support' in suffixClassList) or 'inherited-class' in suffixClassList
107 | classNameRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, suffixClassList, new Point(range.end.row, range.end.column + 1))
108 |
109 | if classNameRange?
110 | range = range.union(classNameRange)
111 |
112 | else if 'phpdoc' in classList or 'comment' in classList
113 | # Docblocks are seen as one entire region of text as they don't have syntax highlighting. Use regular
114 | # expressions instead to find interesting parts containing class names.
115 | lineText = editor.lineTextForBufferRow(bufferPosition.row)
116 |
117 | ranges = []
118 |
119 | if /@param|@var|@return|@throws|@see/g.test(lineText)
120 | ranges = @getRangesForDocblockLine(lineText.split(' '), parseInt(bufferPosition.row), editor, true, 0, 0, false)
121 |
122 | else if /@\\?([A-Za-z0-9_]+)\\?([A-Za-zA-Z_\\]*)?/g.test(lineText)
123 | ranges = @getRangesForDocblockLine(lineText.split(' '), parseInt(bufferPosition.row), editor, true, 0, 0, true)
124 |
125 | for range in ranges
126 | if range.containsPoint(bufferPosition)
127 | return range
128 |
129 | return null
130 |
131 | return range
132 |
133 | ###*
134 | * @param {Array} words The array of words to check.
135 | * @param {Number} rowIndex The current row the words are on within the editor.
136 | * @param {TextEditor} editor The editor the words are from.
137 | * @param {bool} shouldBreak Flag to say whether the search should break after finding 1 class.
138 | * @param {Number} currentIndex The current column index the search is on.
139 | * @param {Number} offset Any offset that should be applied when creating the marker.
140 | ###
141 | getRangesForDocblockLine: (words, rowIndex, editor, shouldBreak, currentIndex = 0, offset = 0, isAnnotation = false) ->
142 | if isAnnotation
143 | regex = /^@(\\?(?:[A-Za-z0-9_]+)\\?(?:[A-Za-zA-Z_\\]*)?)/g
144 |
145 | else
146 | regex = /^(\\?(?:[A-Za-z0-9_]+)\\?(?:[A-Za-zA-Z_\\]*)?)/g
147 |
148 | ranges = []
149 |
150 | for key,value of words
151 | continue if value.length == 0
152 |
153 | newValue = value.match(regex)
154 |
155 | if newValue? && @service.isBasicType(value) == false
156 | newValue = newValue[0]
157 |
158 | if value.includes('|')
159 | ranges = ranges.concat(@getRangesForDocblockLine(value.split('|'), rowIndex, editor, false, currentIndex, parseInt(key)))
160 |
161 | else
162 | if isAnnotation
163 | newValue = newValue.substr(1)
164 | currentIndex += 1
165 |
166 | range = new Range(
167 | new Point(rowIndex, currentIndex + parseInt(key) + offset),
168 | new Point(rowIndex, currentIndex + parseInt(key) + newValue.length + offset)
169 | )
170 |
171 | ranges.push(range)
172 |
173 | if shouldBreak == true
174 | break
175 |
176 | currentIndex += value.length;
177 |
178 | return ranges
179 |
180 | ###*
181 | * Convenience method that returns information for the specified term.
182 | *
183 | * @param {TextEditor} editor
184 | * @param {Point} bufferPosition
185 | * @param {String} term
186 | *
187 | * @return {Promise}
188 | ###
189 | getInfoFor: (editor, bufferPosition, term) ->
190 | if not term
191 | return new Promise (resolve, reject) ->
192 | resolve(null)
193 |
194 | failureHandler = () ->
195 | # Do nothing.
196 |
197 | scopeChain = editor.scopeDescriptorForBufferPosition(bufferPosition).getScopeChain()
198 |
199 | # Don't attempt to resolve class names in use statements.
200 | if scopeChain.indexOf('.support.other.namespace.use') != -1
201 | successHandler = (currentClassName) =>
202 | # Scope descriptors for trait use statements and actual "import" use statements are the same, so we
203 | # have no choice but to use class information for this.
204 | if not currentClassName?
205 | return false
206 |
207 | return true
208 |
209 | firstPromise = @service.determineCurrentClassName(editor, bufferPosition).then(successHandler, failureHandler)
210 |
211 | else
212 | firstPromise = new Promise (resolve, reject) ->
213 | resolve(true)
214 |
215 | successHandler = (doResolve) =>
216 | promise = null
217 | className = term
218 |
219 | if doResolve
220 | promise = @service.resolveTypeAt(editor, bufferPosition, className, 'classlike')
221 |
222 | else
223 | promise = new Promise (resolve, reject) ->
224 | resolve(className)
225 |
226 | nestedSuccessHandler = (className) =>
227 | return @service.getClassInfo(className)
228 |
229 | return promise.then(nestedSuccessHandler, failureHandler)
230 |
231 | return firstPromise.then(successHandler, failureHandler)
232 |
233 | ###*
234 | * @inheritdoc
235 | ###
236 | handleSpecificNavigation: (editor, range, text) ->
237 | successHandler = (info) =>
238 | return if not info?
239 |
240 | if info.filename?
241 | atom.workspace.open(info.filename, {
242 | initialLine : (info.startLine - 1),
243 | searchAllPanes : true
244 | })
245 |
246 | else
247 | shell.openExternal(@service.getDocumentationUrlForClass(info.name))
248 |
249 | failureHandler = () ->
250 | # Do nothing.
251 |
252 | @getInfoFor(editor, range.start, text).then(successHandler, failureHandler)
253 |
--------------------------------------------------------------------------------
/lib/ConstantProvider.coffee:
--------------------------------------------------------------------------------
1 | {Point, Range} = require 'atom'
2 |
3 | AbstractProvider = require './AbstractProvider'
4 |
5 | module.exports =
6 |
7 | ##*
8 | # Provides code navigation for global constants.
9 | ##
10 | class ConstantProvider extends AbstractProvider
11 | ###*
12 | * @inheritdoc
13 | ###
14 | canProvideForBufferPosition: (editor, bufferPosition) ->
15 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
16 |
17 | return false if 'php' not in classList
18 | return true if 'constant' in classList and 'class' not in classList
19 | return true if 'namespace' in classList and 'constant' in @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition)
20 |
21 | if 'punctuation' in classList
22 | originalClassList = classList
23 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition, 2)
24 |
25 | if 'namespace' in classList
26 | climbCount = 1
27 |
28 | if 'punctuation' in originalClassList
29 | climbCount = 2
30 |
31 | return true if 'constant' in @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition, climbCount)
32 |
33 | return false
34 |
35 | ###*
36 | * @inheritdoc
37 | ###
38 | getRangeForBufferPosition: (editor, bufferPosition) ->
39 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
40 |
41 | originalClassList = classList
42 |
43 | if 'punctuation' in classList
44 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition, 2)
45 |
46 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition, 0)
47 |
48 | if 'constant' in classList
49 | prefixRange = new Range(
50 | new Point(range.start.row, range.start.column - 2),
51 | new Point(range.start.row, range.start.column - 0)
52 | )
53 |
54 | # Expand the range to include the namespace prefix, if present. We use two positions before the constant as
55 | # the slash itself sometimes has a "punctuation" class instead of a "namespace" class or, if it is alone, no
56 | # class at all.
57 | prefixText = editor.getTextInBufferRange(prefixRange)
58 |
59 | if prefixText.endsWith("\\")
60 | prefixClassList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, prefixRange.start)
61 |
62 | if "namespace" in prefixClassList
63 | namespaceRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, prefixClassList, prefixRange.start, 0)
64 |
65 | else
66 | namespaceRange = range
67 | namespaceRange.start.column--
68 |
69 | range = namespaceRange.union(range)
70 |
71 | else if 'namespace' in classList
72 | climbCount = 1
73 |
74 | if 'punctuation' in originalClassList
75 | climbCount = 2
76 |
77 | suffixClassList = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition, climbCount)
78 |
79 | # Expand the range to include the constant name, if present.
80 | if 'constant' in suffixClassList
81 | constantRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, suffixClassList, new Point(range.end.row, range.end.column + 1))
82 |
83 | range = range.union(constantRange)
84 |
85 | else
86 | return null
87 |
88 | return range
89 |
90 | ###*
91 | * @param {String} text
92 | *
93 | * @return {Promise}
94 | ###
95 | getInfoFor: (text) ->
96 | successHandler = (constants) =>
97 | if text?[0] != '\\'
98 | text = '\\' + text
99 |
100 | return null unless constants and text of constants
101 | return null unless constants[text].filename
102 |
103 | return constants[text]
104 |
105 | failureHandler = () ->
106 | # Do nothing.
107 |
108 | return @service.getGlobalConstants().then(successHandler, failureHandler)
109 |
110 | ###*
111 | * @inheritdoc
112 | ###
113 | handleSpecificNavigation: (editor, range, text) ->
114 | failureHandler = () ->
115 | # Do nothing.
116 |
117 | resolveTypeHandler = (type) =>
118 | successHandler = (info) =>
119 | return if not info?
120 |
121 | atom.workspace.open(info.filename, {
122 | initialLine : (info.startLine - 1),
123 | searchAllPanes : true
124 | })
125 |
126 | return @getInfoFor(type).then(successHandler, failureHandler)
127 |
128 | @service.resolveType(editor.getPath(), range.start.row + 1, text, 'constant').then(
129 | resolveTypeHandler,
130 | failureHandler
131 | )
132 |
--------------------------------------------------------------------------------
/lib/FunctionProvider.coffee:
--------------------------------------------------------------------------------
1 | shell = require 'shell'
2 |
3 | {Point, Range} = require 'atom'
4 |
5 | AbstractProvider = require './AbstractProvider'
6 |
7 | module.exports =
8 |
9 | ##*
10 | # Provides code navigation for global functions.
11 | ##
12 | class FunctionProvider extends AbstractProvider
13 | ###*
14 | * @inheritdoc
15 | ###
16 | canProvideForBufferPosition: (editor, bufferPosition) ->
17 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, ['meta', 'function-call', 'php'], bufferPosition, 0)
18 |
19 | return true if range?
20 |
21 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
22 |
23 | return false if 'php' not in classList
24 |
25 | return true if 'support' in classList and 'function' in classList
26 |
27 | if 'punctuation' in classList
28 | classListFollowingBufferPosition = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition)
29 |
30 | return true if 'support' in classListFollowingBufferPosition and 'function' in classListFollowingBufferPosition
31 |
32 | return false
33 |
34 | ###*
35 | * @param {TextEditor} editor
36 | * @param {Point} bufferPosition
37 | ###
38 | getRangeForBufferPosition: (editor, bufferPosition) ->
39 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, ['meta', 'function-call', 'php'], bufferPosition, 0)
40 |
41 | if not range?
42 | # Built-in function.
43 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
44 |
45 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition)
46 |
47 | if 'punctuation' in classList
48 | # Include the function call after the leading slash.
49 | positionAfterBufferPosition = bufferPosition.copy()
50 | positionAfterBufferPosition.column++
51 |
52 | classList = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition)
53 |
54 | functionCallRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, positionAfterBufferPosition)
55 |
56 | range = range.union(functionCallRange)
57 |
58 | else # .support.function.*.php
59 | # Include a leading slash, if any.
60 | prefixRange = new Range(
61 | new Point(range.start.row, range.start.column - 1),
62 | new Point(range.start.row, range.start.column - 0)
63 | )
64 |
65 | prefixText = editor.getTextInBufferRange(prefixRange)
66 |
67 | if prefixText == '\\'
68 | range.start.column--
69 |
70 | return range
71 |
72 | ###*
73 | * @param {String} text
74 | *
75 | * @return {Promise}
76 | ###
77 | getInfoFor: (text) ->
78 | successHandler = (functions) =>
79 | if text?[0] != '\\'
80 | text = '\\' + text
81 |
82 | return null unless functions and text of functions
83 |
84 | return functions[text]
85 |
86 | failureHandler = () ->
87 | # Do nothing.
88 |
89 | return @service.getGlobalFunctions().then(successHandler, failureHandler)
90 |
91 | ###*
92 | * @inheritdoc
93 | ###
94 | handleSpecificNavigation: (editor, range, text) ->
95 | failureHandler = () ->
96 | # Do nothing.
97 |
98 | resolveTypeHandler = (type) =>
99 | successHandler = (info) =>
100 | return if not info?
101 |
102 | if info.filename?
103 | atom.workspace.open(info.filename, {
104 | initialLine : (info.startLine - 1),
105 | searchAllPanes : true
106 | })
107 |
108 | else
109 | shell.openExternal(@service.getDocumentationUrlForFunction(info.name))
110 |
111 | return @getInfoFor(type).then(successHandler, failureHandler)
112 |
113 | @service.resolveType(editor.getPath(), range.start.row + 1, text, 'function').then(
114 | resolveTypeHandler,
115 | failureHandler
116 | )
117 |
--------------------------------------------------------------------------------
/lib/HyperclickProviderDispatcher.coffee:
--------------------------------------------------------------------------------
1 | {Point, Range} = require 'atom'
2 |
3 | AbstractProvider = require './AbstractProvider'
4 |
5 | module.exports =
6 |
7 | ##*
8 | # Dispatches a hyperclick request to the correct provider.
9 | #
10 | # Hyperclick only supports a single provider per package, so we have to figure out dispatching the request to the
11 | # correct provider on our own.
12 | ##
13 | class HyperclickProviderDispatcher extends AbstractProvider
14 | ###*
15 | * @var {Array}
16 | ###
17 | providers: null
18 |
19 | ###*
20 | * @var {Object}
21 | ###
22 | service: null
23 |
24 | ###*
25 | * @var {Object}
26 | ###
27 | cachingScopeDescriptorHelper: null
28 |
29 | ###*
30 | * @var {WeakMap}
31 | ###
32 | editorChangeSubscriptions: null
33 |
34 | ###*
35 | * Constructor.
36 | *
37 | * @param {Object} cachingScopeDescriptorHelper
38 | ###
39 | constructor: (@cachingScopeDescriptorHelper) ->
40 | @providers = []
41 | @editorChangeSubscriptions = new WeakMap()
42 |
43 | ###*
44 | * @param {AbstractProvider} provider
45 | ###
46 | addProvider: (provider) ->
47 | @providers.push(provider)
48 |
49 | provider.setService(@service)
50 |
51 | ###*
52 | * @param {Object} service
53 | ###
54 | setService: (service) ->
55 | @service = service
56 |
57 | for provider in @providers
58 | provider.setService(service)
59 |
60 | ###*
61 | * @param {TextEditor} editor
62 | * @param {Point} bufferPosition
63 | ###
64 | getSuggestion: (editor, bufferPosition) ->
65 | rangeToHighlight = null
66 | interestedProviderInfoList = []
67 |
68 | @registerEditorListenersIfNeeded(editor)
69 |
70 | for provider in @providers
71 | if provider.canProvideForBufferPosition(editor, bufferPosition)
72 | range = provider.getRangeForBufferPosition(editor, bufferPosition)
73 |
74 | interestedProviderInfoList.push({
75 | range : range
76 | provider : provider
77 | })
78 |
79 | # TODO: Expand range to always be that of the widest (or shortest) provider if there are multiple?
80 | rangeToHighlight = range
81 |
82 | return null if not rangeToHighlight?
83 |
84 | return {
85 | range : rangeToHighlight
86 |
87 | callback : () =>
88 | for interestedProviderInfo in interestedProviderInfoList
89 | continue if not interestedProviderInfo.range?
90 |
91 | text = editor.getTextInBufferRange(interestedProviderInfo.range)
92 |
93 | interestedProviderInfo.provider.handleNavigation(editor, interestedProviderInfo.range, text)
94 | }
95 |
96 | ###*
97 | * @param {TextEditor} editor
98 | ###
99 | registerEditorListenersIfNeeded: (editor) ->
100 | if not @editorChangeSubscriptions.has(editor)
101 | @registerEditorListeners(editor)
102 |
103 | ###*
104 | * @param {TextEditor} editor
105 | ###
106 | registerEditorListeners: (editor) ->
107 | onChangeDisposable = editor.onDidStopChanging () =>
108 | @cachingScopeDescriptorHelper.clearCache()
109 |
110 | @editorChangeSubscriptions.set(editor, onChangeDisposable)
111 |
--------------------------------------------------------------------------------
/lib/Main.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | ###*
3 | * The name of the package.
4 | *
5 | * @var {String}
6 | ###
7 | packageName: 'php-integrator-navigation'
8 |
9 | ###*
10 | * @var {HyperclickProviderDispatcher}
11 | ###
12 | hyperclickProviderDispatcher: null
13 |
14 | ###*
15 | * Activates the package.
16 | ###
17 | activate: () ->
18 | require('atom-package-deps').install(@packageName).then () =>
19 | # We're done!
20 |
21 | ###*
22 | * Deactivates the package.
23 | ###
24 | deactivate: () ->
25 |
26 | ###*
27 | * Sets the php-integrator service.
28 | *
29 | * @param {mixed} service
30 | ###
31 | setService: (service) ->
32 | @getHyperclickProvider().setService(service)
33 |
34 | ###*
35 | * @return {HyperclickProviderDispatcher}
36 | ###
37 | getHyperclickProviderDispatcher: () ->
38 | if not @hyperclickProviderDispatcher
39 | CachingScopeDescriptorHelper = require './CachingScopeDescriptorHelper'
40 | HyperclickProviderDispatcher = require './HyperclickProviderDispatcher'
41 |
42 | cachingScopeDescriptorHelper = new CachingScopeDescriptorHelper()
43 |
44 | @hyperclickProviderDispatcher = new HyperclickProviderDispatcher(cachingScopeDescriptorHelper)
45 |
46 | ClassProvider = require './ClassProvider'
47 | MethodProvider = require './MethodProvider'
48 | PropertyProvider = require './PropertyProvider'
49 | FunctionProvider = require './FunctionProvider'
50 | ConstantProvider = require './ConstantProvider'
51 | ClassConstantProvider = require './ClassConstantProvider'
52 |
53 | @hyperclickProviderDispatcher.addProvider(new ClassProvider(cachingScopeDescriptorHelper))
54 | @hyperclickProviderDispatcher.addProvider(new MethodProvider(cachingScopeDescriptorHelper))
55 | @hyperclickProviderDispatcher.addProvider(new PropertyProvider(cachingScopeDescriptorHelper))
56 | @hyperclickProviderDispatcher.addProvider(new FunctionProvider(cachingScopeDescriptorHelper))
57 | @hyperclickProviderDispatcher.addProvider(new ClassConstantProvider(cachingScopeDescriptorHelper))
58 | @hyperclickProviderDispatcher.addProvider(new ConstantProvider(cachingScopeDescriptorHelper))
59 |
60 | return @hyperclickProviderDispatcher
61 |
62 | ###*
63 | * @return {HyperclickProviderDispatcher}
64 | ###
65 | getHyperclickProvider: () ->
66 | return @getHyperclickProviderDispatcher()
67 |
--------------------------------------------------------------------------------
/lib/MethodProvider.coffee:
--------------------------------------------------------------------------------
1 | shell = require 'shell'
2 |
3 | AbstractProvider = require './AbstractProvider'
4 |
5 | module.exports =
6 |
7 | ##*
8 | # Provides code navigation for member methods.
9 | ##
10 | class MethodProvider extends AbstractProvider
11 | ###*
12 | * @inheritdoc
13 | ###
14 | canProvideForBufferPosition: (editor, bufferPosition) ->
15 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
16 |
17 | return false if 'php' not in classList
18 | return true if 'function-call' in classList and ('object' in classList or 'static' in classList)
19 |
20 | return false
21 |
22 | ###*
23 | * @param {TextEditor} editor
24 | * @param {Point} bufferPosition
25 | ###
26 | getRangeForBufferPosition: (editor, bufferPosition) ->
27 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
28 |
29 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition)
30 |
31 | return range
32 |
33 | ###*
34 | * Convenience method that returns information for the specified term.
35 | *
36 | * @param {TextEditor} editor
37 | * @param {Point} bufferPosition
38 | * @param {String} term
39 | *
40 | * @return {Promise}
41 | ###
42 | getInfoFor: (editor, bufferPosition, term) ->
43 | successHandler = (members) =>
44 | return null unless members.length > 0
45 |
46 | member = members[0]
47 |
48 | return member
49 |
50 | failureHandler = () ->
51 | # Do nothing.
52 |
53 | return @getClassMethodsAt(editor, bufferPosition, term).then(successHandler, failureHandler)
54 |
55 | ###*
56 | * Returns the class methods used at the specified location.
57 | *
58 | * @param {TextEditor} editor The text editor to use.
59 | * @param {Point} bufferPosition The cursor location of the member.
60 | * @param {String} name The name of the member to retrieve information about.
61 | *
62 | * @return {Promise}
63 | ###
64 | getClassMethodsAt: (editor, bufferPosition, name) ->
65 | if not @isUsingMethod(editor, bufferPosition)
66 | return new Promise (resolve, reject) ->
67 | resolve(null)
68 |
69 | successHandler = (types) =>
70 | promises = []
71 |
72 | for type in types
73 | promises.push @getClassMethod(type, name)
74 |
75 | return Promise.all(promises)
76 |
77 | failureHandler = () ->
78 | # Do nothing.
79 |
80 | return @service.getResultingTypesAt(editor, bufferPosition, true).then(successHandler, failureHandler)
81 |
82 | ###*
83 | * Retrieves information about the specified method of the specified class.
84 | *
85 | * @param {String} className The full name of the class to examine.
86 | * @param {String} name The name of the method to retrieve information about.
87 | *
88 | * @return {Promise}
89 | ###
90 | getClassMethod: (className, name) ->
91 | successHandler = (classInfo) =>
92 | if name of classInfo.methods
93 | return classInfo.methods[name]
94 |
95 | failureHandler = () ->
96 | # Do nothing.
97 |
98 | return @service.getClassInfo(className).then(successHandler, failureHandler)
99 |
100 | ###*
101 | * @inheritdoc
102 | ###
103 | handleSpecificNavigation: (editor, range, text) ->
104 | successHandler = (info) =>
105 | return if not info?
106 |
107 | if info.declaringStructure.filename?
108 | atom.workspace.open(info.declaringStructure.filename, {
109 | initialLine : (info.declaringStructure.startLineMember - 1),
110 | searchAllPanes : true
111 | })
112 |
113 | else
114 | shell.openExternal(@service.getDocumentationUrlForClassMethod(info.declaringStructure.name, info.name))
115 |
116 | failureHandler = () ->
117 | # Do nothing.
118 |
119 | @getInfoFor(editor, range.start, text).then(successHandler, failureHandler)
120 |
121 | ###*
122 | * @example When querying "$this->test()", using a position inside 'test' will return true.
123 | *
124 | * @param {TextEditor} editor
125 | * @param {Point} bufferPosition
126 | *
127 | * @return {boolean}
128 | ###
129 | isUsingMethod: (editor, bufferPosition) ->
130 | scopeDescriptor = editor.scopeDescriptorForBufferPosition(bufferPosition).getScopeChain()
131 |
132 | return (scopeDescriptor.indexOf('.property') == -1)
133 |
--------------------------------------------------------------------------------
/lib/PropertyProvider.coffee:
--------------------------------------------------------------------------------
1 | {Point, Range} = require 'atom'
2 |
3 | AbstractProvider = require './AbstractProvider'
4 |
5 | module.exports =
6 |
7 | ##*
8 | # Provides code navigation for member properties.
9 | ##
10 | class PropertyProvider extends AbstractProvider
11 | ###*
12 | * @inheritdoc
13 | ###
14 | canProvideForBufferPosition: (editor, bufferPosition) ->
15 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
16 |
17 | return false if 'php' not in classList
18 | return true if 'property' in classList
19 |
20 | # Ensure the dollar sign is also seen as a match
21 | if 'punctuation' in classList and 'definition' in classList and 'variable' in classList
22 | classList = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition)
23 |
24 | return true if 'variable' in classList and 'other' in classList and 'class' in classList
25 |
26 | return false
27 |
28 | ###*
29 | * @param {TextEditor} editor
30 | * @param {Point} bufferPosition
31 | ###
32 | getRangeForBufferPosition: (editor, bufferPosition) ->
33 | classList = @scopeDescriptorHelper.getClassListForBufferPosition(editor, bufferPosition)
34 |
35 | range = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, bufferPosition)
36 |
37 | if 'punctuation' in classList and 'definition' in classList and 'variable' in classList
38 | positionAfterBufferPosition = bufferPosition.copy()
39 | positionAfterBufferPosition.column++
40 |
41 | classList = @scopeDescriptorHelper.getClassListFollowingBufferPosition(editor, bufferPosition)
42 |
43 | staticPropertyRange = @scopeDescriptorHelper.getBufferRangeForClassListAtPosition(editor, classList, positionAfterBufferPosition)
44 |
45 | range = range.union(staticPropertyRange)
46 |
47 | else # if it is a static property (but not its leading dollar sign)
48 | prefixRange = new Range(
49 | new Point(range.start.row, range.start.column - 1),
50 | new Point(range.start.row, range.start.column - 0)
51 | )
52 |
53 | prefixText = editor.getTextInBufferRange(prefixRange)
54 |
55 | if prefixText == '$'
56 | range.start.column--
57 |
58 | return range
59 |
60 | ###*
61 | * Convenience method that returns information for the specified term.
62 | *
63 | * @param {TextEditor} editor
64 | * @param {Point} bufferPosition
65 | * @param {String} term
66 | *
67 | * @return {Promise}
68 | ###
69 | getInfoFor: (editor, bufferPosition, term) ->
70 | successHandler = (members) =>
71 | return null unless members.length > 0
72 |
73 | member = members[0]
74 |
75 | return null unless member.declaringStructure.filename
76 |
77 | return member
78 |
79 | failureHandler = () ->
80 | # Do nothing.
81 |
82 | return @getClassPropertiesAt(editor, bufferPosition, term).then(successHandler, failureHandler)
83 |
84 | ###*
85 | * Returns the class properties used at the specified location.
86 | *
87 | * @param {TextEditor} editor The text editor to use.
88 | * @param {Point} bufferPosition The cursor location of the member.
89 | * @param {String} name The name of the member to retrieve information about.
90 | *
91 | * @return {Promise}
92 | ###
93 | getClassPropertiesAt: (editor, bufferPosition, name) ->
94 | if not @isUsingProperty(editor, bufferPosition)
95 | return new Promise (resolve, reject) ->
96 | resolve(null)
97 |
98 | successHandler = (types) =>
99 | promises = []
100 |
101 | for type in types
102 | promises.push @getClassProperty(type, name)
103 |
104 | return Promise.all(promises)
105 |
106 | failureHandler = () ->
107 | # Do nothing.
108 |
109 | return @service.getResultingTypesAt(editor, bufferPosition, true).then(successHandler, failureHandler)
110 |
111 | ###*
112 | * Retrieves information about the specified property of the specified class.
113 | *
114 | * @param {String} className The full name of the class to examine.
115 | * @param {String} name The name of the property to retrieve information about.
116 | *
117 | * @return {Promise}
118 | ###
119 | getClassProperty: (className, name) ->
120 | successHandler = (classInfo) =>
121 | if name of classInfo.properties
122 | return classInfo.properties[name]
123 |
124 | failureHandler = () ->
125 | # Do nothing.
126 |
127 | return @service.getClassInfo(className).then(successHandler, failureHandler)
128 |
129 | ###*
130 | * @inheritdoc
131 | ###
132 | handleSpecificNavigation: (editor, range, text) ->
133 | successHandler = (info) =>
134 | return if not info?
135 |
136 | atom.workspace.open(info.declaringStructure.filename, {
137 | initialLine : (info.declaringStructure.startLineMember - 1),
138 | searchAllPanes : true
139 | })
140 |
141 | failureHandler = () ->
142 | # Do nothing.
143 |
144 | @getInfoFor(editor, range.start, text).then(successHandler, failureHandler)
145 |
146 | ###*
147 | * @example When querying "$this->test", using a position inside 'test' will return true.
148 | *
149 | * @param {TextEditor} editor
150 | * @param {Point} bufferPosition
151 | *
152 | * @return {boolean}
153 | ###
154 | isUsingProperty: (editor, bufferPosition) ->
155 | scopeDescriptor = editor.scopeDescriptorForBufferPosition(bufferPosition).getScopeChain()
156 |
157 | return (scopeDescriptor.indexOf('.property') != -1)
158 |
--------------------------------------------------------------------------------
/lib/ScopeDescriptorHelper.coffee:
--------------------------------------------------------------------------------
1 | {Point, Range} = require 'atom'
2 |
3 | module.exports =
4 |
5 | ##*
6 | # Provides functionality to aid in dealing with scope descriptors.
7 | ##
8 | class ScopeDescriptorHelper
9 | ###*
10 | * @param {TextEditor} editor
11 | * @param {Point} bufferPosition
12 | * @param {Number} climbCount
13 | *
14 | * @return {Array}
15 | ###
16 | getClassListForBufferPosition: (editor, bufferPosition, climbCount = 1) ->
17 | scopesArray = editor.scopeDescriptorForBufferPosition(bufferPosition).getScopesArray()
18 |
19 | return [] if not scopesArray?
20 | return [] if climbCount > scopesArray.length
21 |
22 | classes = scopesArray[scopesArray.length - climbCount]
23 |
24 | return [] if not classes?
25 |
26 | return classes.split('.')
27 |
28 | ###*
29 | * Skips the scope descriptor at the specified location, returning the class list of the next one.
30 | *
31 | * @param {TextEditor} editor
32 | * @param {Point} bufferPosition
33 | * @param {Number} climbCountForPosition
34 | *
35 | * @return {Array}
36 | ###
37 | getClassListFollowingBufferPosition: (editor, bufferPosition, climbCountForPosition) ->
38 | classList = @getClassListForBufferPosition(editor, bufferPosition, climbCountForPosition)
39 |
40 | range = @getBufferRangeForClassListAtPosition(editor, classList, bufferPosition, 0)
41 |
42 | return [] if not range?
43 |
44 | ++range.end.column
45 |
46 | classList = @getClassListForBufferPosition(editor, range.end)
47 |
48 | return classList
49 |
50 | ###*
51 | * Retrieves the (inclusive) start buffer position of the specified class list.
52 | *
53 | * @param {TextEditor} editor
54 | * @param {Array} classList
55 | * @param {Point} bufferPosition
56 | * @param {Number} climbCount
57 | *
58 | * @return {Point|null}
59 | ###
60 | getStartOfClassListAtPosition: (editor, classList, bufferPosition, climbCount = 1) ->
61 | startPosition = null
62 | position = bufferPosition.copy()
63 |
64 | loop
65 | doLoop = false
66 | exitLoop = false
67 | currentClimbCount = climbCount
68 |
69 | if currentClimbCount == 0
70 | doLoop = true
71 | currentClimbCount = 1
72 |
73 | loop
74 | positionClassList = @getClassListForBufferPosition(editor, position, currentClimbCount)
75 |
76 | if positionClassList.length == 0
77 | exitLoop = true
78 | break
79 |
80 | break if @areArraysEqual(positionClassList, classList)
81 |
82 | if not doLoop
83 | exitLoop = true
84 | break
85 |
86 | currentClimbCount++
87 |
88 | break if exitLoop
89 |
90 | startPosition = editor.clipBufferPosition(position.copy())
91 |
92 | break if not @moveToPreviousValidBufferPosition(editor, position)
93 |
94 | return startPosition
95 |
96 | ###*
97 | * Retrieves the (exclusive) end buffer position of the specified class list.
98 | *
99 | * @param {TextEditor} editor
100 | * @param {Array} classList
101 | * @param {Point} bufferPosition
102 | * @param {Number} climbCount
103 | *
104 | * @return {Point|null}
105 | ###
106 | getEndOfClassListAtPosition: (editor, classList, bufferPosition, climbCount = 1) ->
107 | endPosition = null
108 | position = bufferPosition.copy()
109 |
110 | loop
111 | doLoop = false
112 | exitLoop = false
113 | currentClimbCount = climbCount
114 |
115 | if currentClimbCount == 0
116 | doLoop = true
117 | currentClimbCount = 1
118 |
119 | loop
120 | positionClassList = @getClassListForBufferPosition(editor, position, currentClimbCount)
121 |
122 | if positionClassList.length == 0
123 | exitLoop = true
124 | break
125 |
126 | break if @areArraysEqual(positionClassList, classList)
127 |
128 | if not doLoop
129 | exitLoop = true
130 | break
131 |
132 | currentClimbCount++
133 |
134 | break if exitLoop
135 |
136 | endPosition = editor.clipBufferPosition(position.copy())
137 |
138 | break if not @moveToNextValidBufferPosition(editor, position)
139 |
140 | # Make the end exclusive
141 | if endPosition?
142 | endPosition.column++
143 |
144 | return endPosition
145 |
146 | ###*
147 | * @param {TextEditor} editor
148 | * @param {Array} classList
149 | * @param {Point} bufferPosition
150 | * @param {Number} climbCount
151 | *
152 | * @return {Range|null}
153 | ###
154 | getBufferRangeForClassListAtPosition: (editor, classList, bufferPosition, climbCount = 1) ->
155 | start = @getStartOfClassListAtPosition(editor, classList, bufferPosition, climbCount)
156 | end = @getEndOfClassListAtPosition(editor, classList, bufferPosition, climbCount)
157 |
158 | return null if not start?
159 | return null if not end?
160 |
161 | range = new Range(start, end)
162 |
163 | return range
164 |
165 | ###*
166 | * @param {TextEditor} editor
167 | * @param {Point} bufferPosition
168 | *
169 | * @return {Boolean}
170 | ###
171 | moveToPreviousValidBufferPosition: (editor, bufferPosition) ->
172 | return false if bufferPosition.row == 0 and bufferPosition.column == 0
173 |
174 | if bufferPosition.column > 0
175 | bufferPosition.column--
176 |
177 | else
178 | bufferPosition.row--
179 |
180 | lineText = editor.lineTextForBufferRow(bufferPosition.row)
181 |
182 | if lineText?
183 | bufferPosition.column = Math.max(lineText.length - 1, 0)
184 |
185 | else
186 | bufferPosition.column = 0
187 |
188 | return true
189 |
190 | ###*
191 | * @param {TextEditor} editor
192 | * @param {Point} bufferPosition
193 | *
194 | * @return {Boolean}
195 | ###
196 | moveToNextValidBufferPosition: (editor, bufferPosition) ->
197 | lastBufferPosition = editor.clipBufferPosition([Infinity, Infinity])
198 |
199 | return false if bufferPosition.row == lastBufferPosition.row and bufferPosition.column == lastBufferPosition.column
200 |
201 | lineText = editor.lineTextForBufferRow(bufferPosition.row)
202 |
203 | if lineText?
204 | lineLength = lineText.length
205 |
206 | else
207 | lineLength = 0
208 |
209 | if bufferPosition.column < lineLength
210 | bufferPosition.column++
211 |
212 | else
213 | bufferPosition.row++
214 | bufferPosition.column = 0
215 |
216 | return true
217 |
218 | ###*
219 | * @param {Array} left
220 | * @param {Array} right
221 | *
222 | * @return {Boolean}
223 | ###
224 | areArraysEqual: (left, right) ->
225 | return false if left.length != right.length
226 |
227 | for i in [0 .. left.length - 1]
228 | if left[i] != right[i]
229 | return false
230 |
231 | return true
232 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-integrator-navigation",
3 | "main": "./lib/Main",
4 | "version": "1.2.2",
5 | "description": "Provides code navigation and go to functionality for your PHP source code.",
6 | "repository": "php-integrator/atom-navigation",
7 | "license": "GPL-3.0",
8 | "engines": {
9 | "atom": ">=1.13.0 <=1.18.0"
10 | },
11 | "consumedServices": {
12 | "php-integrator.service": {
13 | "versions": {
14 | "^3.0": "setService"
15 | }
16 | }
17 | },
18 | "providedServices": {
19 | "hyperclick.provider": {
20 | "versions": {
21 | "0.0.0": "getHyperclickProvider"
22 | }
23 | }
24 | },
25 | "dependencies": {
26 | "atom-package-deps": "^4.3.1"
27 | },
28 | "package-deps": [
29 | "hyperclick"
30 | ],
31 | "keywords": [
32 | "php",
33 | "goto",
34 | "navigation",
35 | "integrator",
36 | "integration",
37 | "php-integrator"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/spec/ClassConstantProvider-spec.coffee:
--------------------------------------------------------------------------------
1 | {Point} = require 'atom'
2 |
3 | ClassConstantProvider = require '../lib/ClassConstantProvider'
4 | ScopeDescriptorHelper = require '../lib/ScopeDescriptorHelper'
5 |
6 | describe "ClassConstantProvider", ->
7 | editor = null
8 | grammar = null
9 | provider = new ClassConstantProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results", ->
29 | source =
30 | '''
31 |
7 | editor = null
8 | grammar = null
9 | provider = new ClassProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results for namespaced class names", ->
29 | source =
30 | '''
31 |
60 | source =
61 | '''
62 |
91 | source =
92 | '''
93 |
125 | source =
126 | '''
127 |
159 | source =
160 | '''
161 |
190 | source =
191 | '''
192 |
207 | return false
208 | })
209 |
210 | for i in [startColumn .. endColumn]
211 | point = new Point(line, i)
212 |
213 | canProvide = provider.canProvideForBufferPosition(editor, point)
214 |
215 | expect(canProvide).toBeTruthy()
216 |
217 | range = provider.getRangeForBufferPosition(editor, point)
218 |
219 | expect(range).toBeTruthy()
220 |
221 | expect(range.start.row).toEqual(line)
222 | expect(range.start.column).toEqual(startColumn)
223 |
224 | expect(range.end.row).toEqual(line)
225 | expect(range.end.column).toEqual(endColumn + 1)
226 |
227 | it "returns the correct results in docblock @var statements", ->
228 | source =
229 | '''
230 |
245 | return false
246 | })
247 |
248 | for i in [startColumn .. endColumn]
249 | point = new Point(line, i)
250 |
251 | canProvide = provider.canProvideForBufferPosition(editor, point)
252 |
253 | expect(canProvide).toBeTruthy()
254 |
255 | range = provider.getRangeForBufferPosition(editor, point)
256 |
257 | expect(range).toBeTruthy()
258 |
259 | expect(range.start.row).toEqual(line)
260 | expect(range.start.column).toEqual(startColumn)
261 |
262 | expect(range.end.row).toEqual(line)
263 | expect(range.end.column).toEqual(endColumn + 1)
264 |
265 | it "returns the correct results in docblock @return statements", ->
266 | source =
267 | '''
268 |
283 | return false
284 | })
285 |
286 | for i in [startColumn .. endColumn]
287 | point = new Point(line, i)
288 |
289 | canProvide = provider.canProvideForBufferPosition(editor, point)
290 |
291 | expect(canProvide).toBeTruthy()
292 |
293 | range = provider.getRangeForBufferPosition(editor, point)
294 |
295 | expect(range).toBeTruthy()
296 |
297 | expect(range.start.row).toEqual(line)
298 | expect(range.start.column).toEqual(startColumn)
299 |
300 | expect(range.end.row).toEqual(line)
301 | expect(range.end.column).toEqual(endColumn + 1)
302 |
303 | it "returns the correct results in docblock @throws statements", ->
304 | source =
305 | '''
306 |
321 | return false
322 | })
323 |
324 | for i in [startColumn .. endColumn]
325 | point = new Point(line, i)
326 |
327 | canProvide = provider.canProvideForBufferPosition(editor, point)
328 |
329 | expect(canProvide).toBeTruthy()
330 |
331 | range = provider.getRangeForBufferPosition(editor, point)
332 |
333 | expect(range).toBeTruthy()
334 |
335 | expect(range.start.row).toEqual(line)
336 | expect(range.start.column).toEqual(startColumn)
337 |
338 | expect(range.end.row).toEqual(line)
339 | expect(range.end.column).toEqual(endColumn + 1)
340 |
341 | it "returns the correct results in docblock @see statements", ->
342 | source =
343 | '''
344 |
359 | return false
360 | })
361 |
362 | for i in [startColumn .. endColumn]
363 | point = new Point(line, i)
364 |
365 | canProvide = provider.canProvideForBufferPosition(editor, point)
366 |
367 | expect(canProvide).toBeTruthy()
368 |
369 | range = provider.getRangeForBufferPosition(editor, point)
370 |
371 | expect(range).toBeTruthy()
372 |
373 | expect(range.start.row).toEqual(line)
374 | expect(range.start.column).toEqual(startColumn)
375 |
376 | expect(range.end.row).toEqual(line)
377 | expect(range.end.column).toEqual(endColumn + 1)
378 |
379 | it "returns the correct results in singe-line docblock statements ", ->
380 | source =
381 | '''
382 |
395 | return false
396 | })
397 |
398 | for i in [startColumn .. endColumn]
399 | point = new Point(line, i)
400 |
401 | canProvide = provider.canProvideForBufferPosition(editor, point)
402 |
403 | expect(canProvide).toBeTruthy()
404 |
405 | range = provider.getRangeForBufferPosition(editor, point)
406 |
407 | expect(range).toBeTruthy()
408 |
409 | expect(range.start.row).toEqual(line)
410 | expect(range.start.column).toEqual(startColumn)
411 |
412 | expect(range.end.row).toEqual(line)
413 | expect(range.end.column).toEqual(endColumn + 1)
414 |
--------------------------------------------------------------------------------
/spec/ConstantProvider-spec.coffee:
--------------------------------------------------------------------------------
1 | {Point} = require 'atom'
2 |
3 | ConstantProvider = require '../lib/ConstantProvider'
4 | ScopeDescriptorHelper = require '../lib/ScopeDescriptorHelper'
5 |
6 | describe "ConstantProvider", ->
7 | editor = null
8 | grammar = null
9 | provider = new ConstantProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results", ->
29 | source =
30 | '''
31 |
7 | editor = null
8 | grammar = null
9 | provider = new FunctionProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results for user-defined namespaced functions", ->
29 | source =
30 | '''
31 |
60 | source =
61 | '''
62 |
7 | editor = null
8 | grammar = null
9 | provider = new MethodProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results for non-static method calls", ->
29 | source =
30 | '''
31 | foo();
34 | '''
35 |
36 | editor.setText(source)
37 |
38 | line = 2
39 | startColumn = 15
40 | endColumn = 17
41 |
42 | for i in [startColumn .. endColumn]
43 | point = new Point(line, i)
44 |
45 | canProvide = provider.canProvideForBufferPosition(editor, point)
46 |
47 | expect(canProvide).toBeTruthy()
48 |
49 | range = provider.getRangeForBufferPosition(editor, point)
50 |
51 | expect(range).toBeTruthy()
52 |
53 | expect(range.start.row).toEqual(line)
54 | expect(range.start.column).toEqual(startColumn)
55 |
56 | expect(range.end.row).toEqual(line)
57 | expect(range.end.column).toEqual(endColumn + 1)
58 |
59 | it "returns the correct results for static method calls", ->
60 | source =
61 | '''
62 |
7 | editor = null
8 | grammar = null
9 | provider = new PropertyProvider(new ScopeDescriptorHelper())
10 |
11 | beforeEach ->
12 | waitsForPromise ->
13 | atom.workspace.open().then (result) ->
14 | editor = result
15 |
16 | waitsForPromise ->
17 | atom.packages.activatePackage('language-php')
18 |
19 | runs ->
20 | grammar = atom.grammars.selectGrammar('.text.html.php')
21 |
22 | waitsFor ->
23 | grammar and editor
24 |
25 | runs ->
26 | editor.setGrammar(grammar)
27 |
28 | it "returns the correct results for non-static property access", ->
29 | source =
30 | '''
31 | foo;
34 | '''
35 |
36 | editor.setText(source)
37 |
38 | line = 2
39 | startColumn = 15
40 | endColumn = 17
41 |
42 | for i in [startColumn .. endColumn]
43 | point = new Point(line, i)
44 |
45 | canProvide = provider.canProvideForBufferPosition(editor, point)
46 |
47 | expect(canProvide).toBeTruthy()
48 |
49 | range = provider.getRangeForBufferPosition(editor, point)
50 |
51 | expect(range).toBeTruthy()
52 |
53 | expect(range.start.row).toEqual(line)
54 | expect(range.start.column).toEqual(startColumn)
55 |
56 | expect(range.end.row).toEqual(line)
57 | expect(range.end.column).toEqual(endColumn + 1)
58 |
59 | it "returns the correct results for static property access", ->
60 | source =
61 | '''
62 |
6 | editor = null
7 | grammar = null
8 | helper = new ScopeDescriptorHelper()
9 |
10 | beforeEach ->
11 | waitsForPromise ->
12 | atom.workspace.open().then (result) ->
13 | editor = result
14 |
15 | waitsForPromise ->
16 | atom.packages.activatePackage('language-php')
17 |
18 | runs ->
19 | grammar = atom.grammars.selectGrammar('.text.html.php')
20 |
21 | waitsFor ->
22 | grammar and editor
23 |
24 | runs ->
25 | editor.setGrammar(grammar)
26 |
27 | it "getStartOfClassListAtPosition returns the range of a class list", ->
28 | source =
29 | '''
30 |
59 | source =
60 | '''
61 |