├── .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 | ![GPLv3 Logo](http://gplv3.fsf.org/gplv3-127x51.png) 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 |