├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── keymaps └── atom-autocomplete-php.cson ├── lib ├── annotation │ ├── abstract-provider.coffee │ ├── annotation-manager.coffee │ ├── method-provider.coffee │ └── property-provider.coffee ├── autocompletion │ ├── abstract-provider.coffee │ ├── autocompletion-manager.coffee │ ├── class-provider.coffee │ ├── constant-provider.coffee │ ├── function-provider.coffee │ ├── member-provider.coffee │ └── variable-provider.coffee ├── config.coffee ├── goto │ ├── abstract-provider.coffee │ ├── class-provider.coffee │ ├── function-provider.coffee │ ├── goto-manager.coffee │ └── property-provider.coffee ├── peekmo-php-atom-autocomplete.coffee ├── services │ ├── attached-popover.coffee │ ├── namespace.coffee │ ├── php-file-parser.coffee │ ├── php-proxy.coffee │ ├── plugin-manager.coffee │ ├── popover.coffee │ ├── status-error-autocomplete.coffee │ ├── status-in-progress.coffee │ └── use-statement.coffee ├── tooltip │ ├── abstract-provider.coffee │ ├── class-provider.coffee │ ├── function-provider.coffee │ ├── property-provider.coffee │ └── tooltip-manager.coffee └── views │ └── class-list-view.coffee ├── package.json ├── php ├── Config.php ├── ErrorHandler.php ├── parser.php ├── providers │ ├── AutocompleteProvider.php │ ├── ClassMapRefresh.php │ ├── ClassProvider.php │ ├── ConstantsProvider.php │ ├── DocParamProvider.php │ ├── FunctionsProvider.php │ ├── MethodsProvider.php │ └── ProviderInterface.php └── services │ ├── DocParser.php │ ├── FileParser.php │ └── Tools.php ├── spec └── peekmo-php-atom-autocomplete-spec.coffee └── styles └── peekmo-php-atom-autocomplete.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | /php/tmp.php 5 | /indexes/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.25.0 2 | * You can now reindex the project with the command "reindexing project" (the project is reindexed by default when you open a new window of your project) 3 | 4 | ## 0.24.0 5 | * Default keybinding for goto-backtrack changed to ctrl-shift-g (to not override windows/linux keybindings) 6 | * Ability to change the key to press with "click" in package settings 7 | 8 | ## 0.23.0 9 | * Multiple bug fixes 10 | * Import use of a selected class outside completion by @fuelingtheweb (see #338) (shortcut : ctrl-alt-u) 11 | 12 | ## 0.22.0 13 | * Support for `@property` and `@method` annotations (#318) 14 | 15 | ## 0.21.0 16 | * Support for atom 1.13 (fix deprecations) 17 | 18 | ## 0.20.0 19 | * Support completion and goto on drupal 6/7 functions 20 | 21 | ## 0.19.0 22 | * Multiple bug fixes 23 | * Function's return value type hint parsed (PHP7) 24 | 25 | ## 0.18.0 26 | * Plugin configuration is now more interactive. If there's an error, you'll know it without doing the command 27 | 28 | ## 0.17.0 29 | * [internal] String parameters are keep in autocomplete in order to be able to change the function return (needed for symfony2 support) 30 | 31 | ## 0.16.0 32 | * Support for external services (see atom-symfony2) package 33 | * Doc refactoring 34 | 35 | ## 0.15.0 36 | * Fail message when saving are now just in developer console by default (you can change this in settings) 37 | * A progress bar appears in status bar when there's a classes' indexing in progress 38 | * Uses are now ordered. You can even have new line between them (see plugin settings) 39 | * Annotations on class properties 40 | 41 | ## 0.14.0 42 | * Support configuration checking on Windows 43 | * Lot of code refactoring (many bug resolved, but perhaps some new :() 44 | * Tooltips on classes, interfaces 45 | * Constants provider 46 | * Autocomplete from type hints in closures 47 | 48 | ## 0.13.0 49 | * Custom tooltip management (does not rely on atom's one anymore) 50 | 51 | ## 0.12.0 52 | * Support for {@inheritdoc} && {@inheritDoc} as the only one comment (symfony2 style) 53 | * Bugfixes for non PHP projects 54 | * Bugfixes on Docblock parser 55 | 56 | ## 0.11.0 57 | * Bugfixes on Goto 58 | * Major refactor in the code of the plugin itself 59 | 60 | ## 0.10.0 61 | * Autocomplete in catch() {} #91 62 | * Comments "@var $var Class" now supported for completion 63 | 64 | ## 0.9.0 65 | * Many bugfixes and improvements for tooltips (from @hotoiledgoblinsack) 66 | * Basic autocomplete on "new" keyword (e.g : 67 | $x = new \DateTime(); 68 | $x->{autocomplete} 69 | ) 70 | 71 | ## 0.8.0 72 | * Tooltips on methods and attributes 73 | * Strikethrough style to deprecated methods 74 | 75 | ## 0.7.0 76 | * Goto class properties 77 | * Goto bugfixes 78 | 79 | ## 0.6.0 80 | * Goto command on first level of methods, and classes (#42 by @CWDN) 81 | * Fix namespace on the same line as PHP tag 82 | 83 | ## 0.5.0 84 | * Support for Windows 85 | 86 | ## 0.4.0 87 | * Completion on local variables 88 | * Bug fixes 89 | 90 | ## 0.3.0 91 | * Completion $this on multiline 92 | * Bug fixes 93 | 94 | ## 0.2.0 95 | * Completion on parent:: 96 | * Completion on self:: 97 | * Bug fixes 98 | 99 | ## 0.1.0 100 | * Completion on classNames 101 | * Completion on $this-> 102 | * Completion on static methods 103 | * Namespace management 104 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Here is a list of every people to thank, because without them, the package would not be what it is today. 2 | 3 | - Tom Gerrits 4 | - [Chris Normansell (@CWDN)](https://github.com/CWDN) 5 | - [Guillaume Perréal (@Adirelle)](https://github.com/Adirelle) 6 | - [Vincent Klaiber (@vinkla)](https://github.com/vinkla) 7 | - [@Benoth](https://github.com/Benoth) 8 | - [Axel Anceau (@Peekmo)](https://github.com/Peekmo) 9 | 10 | And **you** who are using the plugin and reporting issues ;) 11 | 12 | Thanks to everyone. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Axel Anceau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-autocomplete-php 2 | 3 | [![Join the chat at https://gitter.im/Peekmo/atom-autocomplete-php](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Peekmo/atom-autocomplete-php?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | atom-autocomplete-php provides autocompletion for the PHP language for projects that use Composer for dependency management. What cool things can you expect? 6 | * Autocompletion of class members, built-in constants, built-in PHP functions, ... 7 | * Autocompletion of class names and automatic adding of `use` statements where needed. 8 | * Alt-clicking class members, class names, etc. to navigate to their definition. 9 | * Annotations in the gutter for methods that are overrides or interface implementations. 10 | * Tooltips for methods, classes, etc. that display information about the item itself. 11 | * IntellJ-style variable annotations `/** @var MyType $var */` as well as `/** @var $var MyType */`. 12 | * Shortcut variable annotations (must appear right above the respective variable) `/** @var MyType */`. 13 | * Add use statement of class under cursor (ctrl-alt-u) 14 | * ... 15 | 16 | ## What do I need to do to make it work? 17 | Currently the following limitations or restrictions are present: 18 | * You must use [Composer](https://getcomposer.org/) for dependency management. 19 | * You must have PHP 5.5+ with xml extension installed. 20 | * You must follow the PSR standards (for the names of classes, methods, namespacing, etc.). 21 | * You must write proper docblocks for your methods. There currently is no standard around this, but we try to follow the draft PSR-5 standard (which, in turn, is mostly inspired by phpDocumentor's implementation). Minimum requirements for proper autocompletion: 22 | * `@return` statements for functions and methods. 23 | * `@param` statements for functions and methods. 24 | * `@var` statements for properties in classes. 25 | * (Type hints in functions and methods will also be checked.) 26 | 27 | Some features may or may not work outside these restrictions. Composer is primarily used for its classmap, to fetch a list of classes that are present in your codebase. Reflection is used to fetch information about classes. 28 | 29 | The package also requires a one time setup, To configure the plugin, click on "package" in your preferences, and select "settings" on atom-autocomplete-php plugin. 30 | 31 | - **Command to use composer** : it's highly recommended to write here the full path to your composer.phar bin. E.G on unix systems, it could be /usr/local/bin/composer. Using an alias is not recommended at all! 32 | - **Command php** : Command to execute PHP cli in your console. (php by default on unix systems). If it doesn't work, put here the full path to your PHP bin. 33 | - **Autoload file** : Write here, a coma separated list of all the different path to the autoload files. By default, it's "vendor/autoload.php" for composer projects ;) 34 | - **Classmap files** : All paths to PHP files that returns an array of "className" => "fullPath to the file where the class is located". The default one for composer is vendor/composer/autoload_classmap.php 35 | 36 | You can test your configuration by using a command (cmd - shift - p) : ```Atom Autocomplete Php : Configuration``` 37 | 38 | ### Linux 39 | ![Configuration](http://i.imgur.com/LYBcaHE.png) 40 |   41 | 42 | ### Windows (WAMP and ComposerSetup) 43 | ![Settings](http://i.imgur.com/hY5ypG2.png) 44 |   45 | 46 | ## CMS integration 47 | * Built-in support for Drupal 6/7 functions 48 | 49 | ## Framework integration 50 | * [Symfony2 plugin](https://github.com/Peekmo/atom-symfony2) 51 | 52 | ## What Does Not Work? 53 | * Most of the issue reports indicate things that are missing, but autocompletion should be working fairly well in general. 54 | 55 | ### Won't Fix (For Now) 56 | * "Go to definition" will take you to the incorrect location if a class is using a method with the exact same name as one in its own direct traits. You will be taken to the trait method instead of the class method (the latter should take precedence). See also issue #177. 57 | * `static` and `self` behave mostly like `$this` and can access non-static methods when used in non-static contexts. See also issue #101. 58 | 59 | ### Conflicts 60 | * This package has known conflicts with other PHP autocomplete packages. Disable this one or the other one to avoid some errors. 61 | 62 | ## What's Next & Contributing 63 | Keep in mind that this plugin is under active development. If you find a bug, please, open an issue with more information on how to reproduce. Feel free to contribute ;) 64 | 65 | ![A screenshot of your spankin' package](https://f.cloud.github.com/assets/69169/2290250/c35d867a-a017-11e3-86be-cd7c5bf3ff9b.gif) 66 | -------------------------------------------------------------------------------- /keymaps/atom-autocomplete-php.cson: -------------------------------------------------------------------------------- 1 | 'atom-text-editor': 2 | 'ctrl-alt-n': 'atom-autocomplete-php:namespace' 3 | 'ctrl-alt-g': 'atom-autocomplete-php:goto' 4 | 'ctrl-shift-g': 'atom-autocomplete-php:goto-backtrack' 5 | 'ctrl-alt-u': 'atom-autocomplete-php:import-use-statement' 6 | -------------------------------------------------------------------------------- /lib/annotation/abstract-provider.coffee: -------------------------------------------------------------------------------- 1 | {Range, Point, TextEditor} = require 'atom' 2 | 3 | SubAtom = require 'sub-atom' 4 | 5 | AttachedPopover = require '../services/attached-popover' 6 | 7 | module.exports = 8 | 9 | class AbstractProvider 10 | # The regular expression that a line must match in order for it to be checked if it requires an annotation. 11 | regex: null 12 | markers: [] 13 | subAtoms: [] 14 | 15 | ###* 16 | * Initializes this provider. 17 | ### 18 | init: () -> 19 | @$ = require 'jquery' 20 | @parser = require '../services/php-file-parser' 21 | 22 | atom.workspace.observeTextEditors (editor) => 23 | editor.onDidSave (event) => 24 | @rescan(editor) 25 | 26 | @registerAnnotations editor 27 | @registerEvents editor 28 | 29 | # When you go back to only have 1 pane the events are lost, so need to re-register. 30 | atom.workspace.onDidDestroyPane (pane) => 31 | panes = atom.workspace.getPanes() 32 | 33 | if panes.length == 1 34 | for paneItem in panes[0].items 35 | if paneItem instanceof TextEditor 36 | @registerEvents paneItem 37 | 38 | # Having to re-register events as when a new pane is created the old panes lose the events. 39 | atom.workspace.onDidAddPane (observedPane) => 40 | panes = atom.workspace.getPanes() 41 | 42 | for pane in panes 43 | if pane == observedPane 44 | continue 45 | 46 | for paneItem in pane.items 47 | if paneItem instanceof TextEditor 48 | @registerEvents paneItem 49 | 50 | ###* 51 | * Deactives the provider. 52 | ### 53 | deactivate: () -> 54 | @removeAnnotations() 55 | 56 | ###* 57 | * Registers event handlers. 58 | * 59 | * @param {TextEditor} editor TextEditor to register events to. 60 | ### 61 | registerEvents: (editor) -> 62 | if editor.getGrammar().scopeName.match /text.html.php$/ 63 | # Ticket #107 - Mouseout isn't generated until the mouse moves, even when scrolling (with the keyboard or 64 | # mouse). If the element goes out of the view in the meantime, its HTML element disappears, never removing 65 | # it. 66 | editor.onDidDestroy () => 67 | @removePopover() 68 | 69 | editor.onDidStopChanging () => 70 | @removePopover() 71 | 72 | textEditorElement = atom.views.getView(editor) 73 | 74 | @$(textEditorElement).find('.horizontal-scrollbar').on 'scroll', () => 75 | @removePopover() 76 | 77 | @$(textEditorElement).find('.vertical-scrollbar').on 'scroll', () => 78 | @removePopover() 79 | 80 | ###* 81 | * Registers the annotations. 82 | * 83 | * @param {TextEditor} editor The editor to search through. 84 | ### 85 | registerAnnotations: (editor) -> 86 | text = editor.getText() 87 | rows = text.split('\n') 88 | @subAtoms[editor.getLongTitle()] = new SubAtom 89 | 90 | for rowNum,row of rows 91 | while (match = @regex.exec(row)) 92 | @placeAnnotation(editor, rowNum, row, match) 93 | 94 | ###* 95 | * Places an annotation at the specified line and row text. 96 | * 97 | * @param {TextEditor} editor 98 | * @param {int} row 99 | * @param {String} rowText 100 | * @param {Array} match 101 | ### 102 | placeAnnotation: (editor, row, rowText, match) -> 103 | annotationInfo = @extractAnnotationInfo(editor, row, rowText, match) 104 | 105 | if not annotationInfo 106 | return 107 | 108 | range = new Range( 109 | new Point(parseInt(row), 0), 110 | new Point(parseInt(row), rowText.length) 111 | ) 112 | 113 | # For Atom 1.3 or greater, maintainHistory can only be applied to entire 114 | # marker layers. Layers don't exist in earlier versions, hence the 115 | # conditional logic. 116 | if typeof editor.addMarkerLayer is 'function' 117 | @markerLayers ?= new WeakMap 118 | unless markerLayer = @markerLayers.get(editor) 119 | markerLayer = editor.addMarkerLayer(maintainHistory: true) 120 | @markerLayers.set(editor, markerLayer) 121 | 122 | marker = (markerLayer ? editor).markBufferRange(range) 123 | 124 | decoration = editor.decorateMarker(marker, { 125 | type: 'line-number', 126 | class: annotationInfo.lineNumberClass 127 | }) 128 | 129 | longTitle = editor.getLongTitle() 130 | 131 | if @markers[longTitle] == undefined 132 | @markers[longTitle] = [] 133 | 134 | @markers[longTitle].push(marker) 135 | 136 | @registerAnnotationEventHandlers(editor, row, annotationInfo) 137 | 138 | ###* 139 | * Exracts information about the annotation match. 140 | * 141 | * @param {TextEditor} editor 142 | * @param {int} row 143 | * @param {String} rowText 144 | * @param {Array} match 145 | ### 146 | extractAnnotationInfo: (editor, row, rowText, match) -> 147 | 148 | ###* 149 | * Registers annotation event handlers for the specified row. 150 | * 151 | * @param {TextEditor} editor 152 | * @param {int} row 153 | * @param {Object} annotationInfo 154 | ### 155 | registerAnnotationEventHandlers: (editor, row, annotationInfo) -> 156 | textEditorElement = atom.views.getView(editor) 157 | gutterContainerElement = @$(textEditorElement).find('.gutter-container') 158 | 159 | do (editor, gutterContainerElement, annotationInfo) => 160 | longTitle = editor.getLongTitle() 161 | selector = '.line-number' + '.' + annotationInfo.lineNumberClass + '[data-buffer-row=' + row + '] .icon-right' 162 | 163 | @subAtoms[longTitle].add gutterContainerElement, 'mouseover', selector, (event) => 164 | @handleMouseOver(event, editor, annotationInfo) 165 | 166 | @subAtoms[longTitle].add gutterContainerElement, 'mouseout', selector, (event) => 167 | @handleMouseOut(event, editor, annotationInfo) 168 | 169 | @subAtoms[longTitle].add gutterContainerElement, 'click', selector, (event) => 170 | @handleMouseClick(event, editor, annotationInfo) 171 | 172 | ###* 173 | * Handles the mouse over event on an annotation. 174 | * 175 | * @param {jQuery.Event} event 176 | * @param {TextEditor} editor 177 | * @param {Object} annotationInfo 178 | ### 179 | handleMouseOver: (event, editor, annotationInfo) -> 180 | if annotationInfo.tooltipText 181 | @removePopover() 182 | 183 | @attachedPopover = new AttachedPopover(event.target) 184 | @attachedPopover.setText(annotationInfo.tooltipText) 185 | @attachedPopover.show() 186 | 187 | ###* 188 | * Handles the mouse out event on an annotation. 189 | * 190 | * @param {jQuery.Event} event 191 | * @param {TextEditor} editor 192 | * @param {Object} annotationInfo 193 | ### 194 | handleMouseOut: (event, editor, annotationInfo) -> 195 | @removePopover() 196 | 197 | ###* 198 | * Handles the mouse click event on an annotation. 199 | * 200 | * @param {jQuery.Event} event 201 | * @param {TextEditor} editor 202 | * @param {Object} annotationInfo 203 | ### 204 | handleMouseClick: (event, editor, annotationInfo) -> 205 | 206 | ###* 207 | * Removes the existing popover, if any. 208 | ### 209 | removePopover: () -> 210 | if @attachedPopover 211 | @attachedPopover.dispose() 212 | @attachedPopover = null 213 | 214 | ###* 215 | * Removes any annotations that were created. 216 | * 217 | * @param {TextEditor} editor The editor to search through. 218 | ### 219 | removeAnnotations: (editor) -> 220 | if editor? 221 | for i,marker of @markers[editor.getLongTitle()] 222 | marker.destroy() 223 | @markers[editor.getLongTitle()] = [] 224 | @subAtoms[editor.getLongTitle()]?.dispose() 225 | else 226 | for i,name of @markers 227 | for i,marker of @markers[name] 228 | marker.destroy() 229 | @markers = [] 230 | for i, subAtom of @subAtoms 231 | subAtom.dispose() 232 | @subAtoms = [] 233 | 234 | ###* 235 | * Rescans the editor, updating all annotations. 236 | * 237 | * @param {TextEditor} editor The editor to search through. 238 | ### 239 | rescan: (editor) -> 240 | @removeAnnotations(editor) 241 | @registerAnnotations(editor) 242 | -------------------------------------------------------------------------------- /lib/annotation/annotation-manager.coffee: -------------------------------------------------------------------------------- 1 | MethodProvider = require './method-provider.coffee' 2 | PropertyProvider = require './property-provider.coffee' 3 | 4 | module.exports = 5 | 6 | class AnnotationManager 7 | providers: [] 8 | 9 | ###* 10 | * Initializes the tooltip providers. 11 | ### 12 | init: () -> 13 | @providers.push new MethodProvider() 14 | @providers.push new PropertyProvider() 15 | 16 | for provider in @providers 17 | provider.init(@) 18 | 19 | ###* 20 | * Deactivates the tooltip providers. 21 | ### 22 | deactivate: () -> 23 | for provider in @providers 24 | provider.deactivate() 25 | -------------------------------------------------------------------------------- /lib/annotation/method-provider.coffee: -------------------------------------------------------------------------------- 1 | AbstractProvider = require './abstract-provider' 2 | 3 | module.exports = 4 | 5 | # Provides annotations for overriding methods and implementations of interface methods. 6 | class FunctionProvider extends AbstractProvider 7 | regex: /(\s*(?:public|protected|private)\s+function\s+)(\w+)\s*\(/g 8 | 9 | ###* 10 | * @inheritdoc 11 | ### 12 | extractAnnotationInfo: (editor, row, rowText, match) -> 13 | currentClass = @parser.getFullClassName(editor) 14 | 15 | propertyName = match[2] 16 | 17 | context = @parser.getMemberContext(editor, propertyName, null, currentClass) 18 | 19 | if not context or (not context.override and not context.implementation) 20 | return null 21 | 22 | extraData = null 23 | tooltipText = '' 24 | lineNumberClass = '' 25 | 26 | # NOTE: We deliberately show the declaring class here, not the structure (which could be a trait). 27 | if context.override 28 | extraData = context.override 29 | lineNumberClass = 'override' 30 | tooltipText = 'Overrides method from ' + extraData.declaringClass.name 31 | 32 | else 33 | extraData = context.implementation 34 | lineNumberClass = 'implementation' 35 | tooltipText = 'Implements method for ' + extraData.declaringClass.name 36 | 37 | return { 38 | lineNumberClass : lineNumberClass 39 | tooltipText : tooltipText 40 | extraData : extraData 41 | } 42 | 43 | ###* 44 | * @inheritdoc 45 | ### 46 | handleMouseClick: (event, editor, annotationInfo) -> 47 | atom.workspace.open(annotationInfo.extraData.declaringStructure.filename, { 48 | initialLine : annotationInfo.extraData.startLine - 1, 49 | searchAllPanes : true 50 | }) 51 | 52 | ###* 53 | * @inheritdoc 54 | ### 55 | removePopover: () -> 56 | if @attachedPopover 57 | @attachedPopover.dispose() 58 | @attachedPopover = null 59 | -------------------------------------------------------------------------------- /lib/annotation/property-provider.coffee: -------------------------------------------------------------------------------- 1 | AbstractProvider = require './abstract-provider' 2 | 3 | module.exports = 4 | 5 | # Provides annotations for overriding property. 6 | class FunctionProvider extends AbstractProvider 7 | regex: /(\s*(?:public|protected|private)\s+\$)(\w+)\s+/g 8 | 9 | ###* 10 | * @inheritdoc 11 | ### 12 | extractAnnotationInfo: (editor, row, rowText, match) -> 13 | currentClass = @parser.getFullClassName(editor) 14 | 15 | propertyName = match[2] 16 | 17 | context = @parser.getMemberContext(editor, propertyName, null, currentClass) 18 | 19 | if not context or not context.override 20 | return null 21 | 22 | # NOTE: We deliberately show the declaring class here, not the structure (which could be a trait). 23 | return { 24 | lineNumberClass : 'override' 25 | tooltipText : 'Overrides property from ' + context.override.declaringClass.name 26 | extraData : context.override 27 | } 28 | 29 | ###* 30 | * @inheritdoc 31 | ### 32 | handleMouseClick: (event, editor, annotationInfo) -> 33 | atom.workspace.open(annotationInfo.extraData.declaringStructure.filename, { 34 | # initialLine : annotationInfo.startLine - 1, 35 | searchAllPanes : true 36 | }) 37 | -------------------------------------------------------------------------------- /lib/autocompletion/abstract-provider.coffee: -------------------------------------------------------------------------------- 1 | parser = require "../services/php-file-parser.coffee" 2 | 3 | module.exports = 4 | 5 | # Abstract base class for autocompletion providers. 6 | class AbstractProvider 7 | regex: '' 8 | selector: '.source.php' 9 | 10 | inclusionPriority: 10 11 | 12 | disableForSelector: '.source.php .comment, .source.php .string' 13 | 14 | ###* 15 | * Initializes this provider. 16 | ### 17 | init: () -> 18 | 19 | ###* 20 | * Deactives the provider. 21 | ### 22 | deactivate: () -> 23 | 24 | ###* 25 | * Entry point of all request from autocomplete-plus 26 | * Calls @fetchSuggestion in the provider if allowed 27 | * @return array Suggestions 28 | ### 29 | getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 30 | new Promise (resolve) => 31 | resolve(@fetchSuggestions({editor, bufferPosition, scopeDescriptor, prefix})) 32 | 33 | ###* 34 | * Builds a snippet for a PHP function 35 | * @param {string} word Function name 36 | * @param {array} elements All arguments for the snippet (parameters, optionals) 37 | * @return string The snippet 38 | ### 39 | getFunctionSnippet: (word, elements) -> 40 | body = word + "(" 41 | lastIndex = 0 42 | 43 | # Non optional elements 44 | for arg, index in elements.parameters 45 | body += ", " if index != 0 46 | body += "${" + (index+1) + ":" + arg + "}" 47 | lastIndex = index+1 48 | 49 | # Optional elements. One big same snippet 50 | if elements.optionals.length > 0 51 | body += " ${" + (lastIndex + 1) + ":[" 52 | body += ", " if lastIndex != 0 53 | 54 | lastIndex += 1 55 | 56 | for arg, index in elements.optionals 57 | body += ", " if index != 0 58 | body += arg 59 | body += "]}" 60 | 61 | body += ")" 62 | 63 | # Ensure the user ends up after the inserted text when he's done cycling through the parameters with tab. 64 | body += "$0" 65 | 66 | return body 67 | 68 | ###* 69 | * Builds the signature for a PHP function 70 | * @param {string} word Function name 71 | * @param {array} elements All arguments for the signature (parameters, optionals) 72 | * @return string The signature 73 | ### 74 | getFunctionSignature: (word, element) -> 75 | snippet = @getFunctionSnippet(word, element) 76 | 77 | # Just strip out the placeholders. 78 | signature = snippet.replace(/\$\{\d+:([^\}]+)\}/g, '$1') 79 | 80 | return signature[0 .. -3] 81 | 82 | ###* 83 | * Get prefix from bufferPosition and @regex 84 | * @return string 85 | ### 86 | getPrefix: (editor, bufferPosition) -> 87 | # Get the text for the line up to the triggered buffer position 88 | line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]) 89 | 90 | # Match the regex to the line, and return the match 91 | matches = line.match(@regex) 92 | 93 | # Looking for the correct match 94 | if matches? 95 | for match in matches 96 | start = bufferPosition.column - match.length 97 | if start >= 0 98 | word = editor.getTextInBufferRange([[bufferPosition.row, bufferPosition.column - match.length], bufferPosition]) 99 | if word == match 100 | # Not really nice hack.. But non matching groups take the first word before. So I remove it. 101 | # Necessary to have completion juste next to a ( or [ or { 102 | if match[0] == '{' or match[0] == '(' or match[0] == '[' 103 | match = match.substring(1) 104 | 105 | return match 106 | 107 | return '' 108 | -------------------------------------------------------------------------------- /lib/autocompletion/autocompletion-manager.coffee: -------------------------------------------------------------------------------- 1 | ClassProvider = require './class-provider.coffee' 2 | MemberProvider = require './member-provider.coffee' 3 | ConstantProvider = require './constant-provider.coffee' 4 | VariableProvider = require './variable-provider.coffee' 5 | FunctionProvider = require './function-provider.coffee' 6 | 7 | module.exports = 8 | 9 | class AutocompletionManager 10 | providers: [] 11 | 12 | ###* 13 | * Initializes the autocompletion providers. 14 | ### 15 | init: () -> 16 | @providers.push new ConstantProvider() 17 | @providers.push new VariableProvider() 18 | @providers.push new FunctionProvider() 19 | @providers.push new ClassProvider() 20 | @providers.push new MemberProvider() 21 | 22 | for provider in @providers 23 | provider.init(@) 24 | 25 | ###* 26 | * Deactivates the autocompletion providers. 27 | ### 28 | deactivate: () -> 29 | for provider in @providers 30 | provider.deactivate() 31 | 32 | ###* 33 | * Deactivates the autocompletion providers. 34 | ### 35 | getProviders: () -> 36 | @providers 37 | -------------------------------------------------------------------------------- /lib/autocompletion/class-provider.coffee: -------------------------------------------------------------------------------- 1 | fuzzaldrin = require 'fuzzaldrin' 2 | exec = require "child_process" 3 | 4 | config = require "../config.coffee" 5 | proxy = require "../services/php-proxy.coffee" 6 | parser = require "../services/php-file-parser.coffee" 7 | AbstractProvider = require "./abstract-provider" 8 | 9 | module.exports = 10 | 11 | # Autocompletion for class names (e.g. after the new or use keyword). 12 | class ClassProvider extends AbstractProvider 13 | classes = [] 14 | disableForSelector: '.source.php .string' 15 | 16 | ###* 17 | * Get suggestions from the provider (@see provider-api) 18 | * @return array 19 | ### 20 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 21 | # "new" keyword or word starting with capital letter 22 | @regex = /((?:new|use)?(?:[^a-z0-9_])\\?(?:[A-Z][a-zA-Z_\\]*)+)/g 23 | 24 | prefix = @getPrefix(editor, bufferPosition) 25 | return unless prefix.length 26 | 27 | @classes = proxy.classes() 28 | return unless @classes?.autocomplete? 29 | 30 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]]) 31 | insertParameterList = if characterAfterPrefix == '(' then false else true 32 | 33 | suggestions = @findSuggestionsForPrefix(prefix.trim(), insertParameterList) 34 | return unless suggestions.length 35 | return suggestions 36 | 37 | ###* 38 | * Get suggestions from the provider for a single word (@see provider-api) 39 | * @return array 40 | ### 41 | fetchSuggestionsFromWord: (word) -> 42 | @classes = proxy.classes() 43 | return unless @classes?.autocomplete? 44 | 45 | suggestions = @findSuggestionsForPrefix(word) 46 | return unless suggestions.length 47 | return suggestions 48 | 49 | ###* 50 | * Returns suggestions available matching the given prefix 51 | * @param {string} prefix Prefix to match. 52 | * @param {bool} insertParameterList Whether to insert a list of parameters for methods. 53 | * @return array 54 | ### 55 | findSuggestionsForPrefix: (prefix, insertParameterList = true) -> 56 | # Get rid of the leading "new" or "use" keyword 57 | instantiation = false 58 | use = false 59 | 60 | if prefix.indexOf("new \\") != -1 61 | instantiation = true 62 | prefix = prefix.replace /new \\/, '' 63 | else if prefix.indexOf("new ") != -1 64 | instantiation = true 65 | prefix = prefix.replace /new /, '' 66 | else if prefix.indexOf("use ") != -1 67 | use = true 68 | prefix = prefix.replace /use /, '' 69 | 70 | if prefix.indexOf("\\") == 0 71 | prefix = prefix.substring(1, prefix.length) 72 | 73 | # Filter the words using fuzzaldrin 74 | words = fuzzaldrin.filter @classes.autocomplete, prefix 75 | 76 | # Builds suggestions for the words 77 | suggestions = [] 78 | 79 | for word in words when word isnt prefix 80 | classInfo = @classes.mapping[word] 81 | 82 | # Just print classes with constructors with "new" 83 | if instantiation and @classes.mapping[word].methods.constructor.has 84 | args = classInfo.methods.constructor.args 85 | 86 | suggestions.push 87 | text: word, 88 | type: 'class', 89 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else '' 90 | snippet: if insertParameterList then @getFunctionSnippet(word, args) else null 91 | displayText: @getFunctionSignature(word, args) 92 | data: 93 | kind: 'instantiation', 94 | prefix: prefix, 95 | replacementPrefix: prefix 96 | 97 | else if use 98 | suggestions.push 99 | text: word, 100 | type: 'class', 101 | prefix: prefix, 102 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else '' 103 | replacementPrefix: prefix, 104 | data: 105 | kind: 'use' 106 | 107 | # Not instantiation => not printing constructor params 108 | else 109 | suggestions.push 110 | text: word, 111 | type: 'class', 112 | className: if classInfo.class.deprecated then 'php-atom-autocomplete-strike' else '' 113 | data: 114 | kind: 'static', 115 | prefix: prefix, 116 | replacementPrefix: prefix 117 | 118 | return suggestions 119 | 120 | ###* 121 | * Adds the missing use if needed 122 | * @param {TextEditor} editor 123 | * @param {Position} triggerPosition 124 | * @param {object} suggestion 125 | ### 126 | onDidInsertSuggestion: ({editor, triggerPosition, suggestion}) -> 127 | return unless suggestion.data?.kind 128 | 129 | if suggestion.data.kind == 'instantiation' or suggestion.data.kind == 'static' 130 | editor.transact () => 131 | linesAdded = parser.addUseClass(editor, suggestion.text, config.config.insertNewlinesForUseStatements, config.config.ensureNewLineAfterNamespace) 132 | 133 | # Removes namespace from classname 134 | if linesAdded != null 135 | name = suggestion.text 136 | splits = name.split('\\') 137 | 138 | nameLength = splits[splits.length-1].length 139 | startColumn = triggerPosition.column - suggestion.data.prefix.length 140 | row = triggerPosition.row + linesAdded 141 | 142 | if suggestion.data.kind == 'instantiation' 143 | endColumn = startColumn + name.length - nameLength - splits.length + 1 144 | 145 | else 146 | endColumn = startColumn + name.length - nameLength 147 | 148 | editor.setTextInBufferRange([ 149 | [row, startColumn], 150 | [row, endColumn] # Because when selected there's not \ (why?) 151 | ], "") 152 | 153 | ###* 154 | * Adds the missing use if needed without removing text from editor 155 | * @param {TextEditor} editor 156 | * @param {object} suggestion 157 | ### 158 | onSelectedClassSuggestion: ({editor, suggestion}) -> 159 | return unless suggestion.data?.kind 160 | 161 | if suggestion.data.kind == 'instantiation' or suggestion.data.kind == 'static' 162 | editor.transact () => 163 | linesAdded = parser.addUseClass(editor, suggestion.text, config.config.insertNewlinesForUseStatements, config.config.ensureNewLineAfterNamespace) 164 | -------------------------------------------------------------------------------- /lib/autocompletion/constant-provider.coffee: -------------------------------------------------------------------------------- 1 | fuzzaldrin = require 'fuzzaldrin' 2 | 3 | proxy = require "../services/php-proxy.coffee" 4 | parser = require "../services/php-file-parser.coffee" 5 | AbstractProvider = require "./abstract-provider" 6 | 7 | config = require "../config.coffee" 8 | 9 | module.exports = 10 | 11 | # Autocompletion for internal PHP constants. 12 | class ConstantProvider extends AbstractProvider 13 | constants: [] 14 | 15 | ###* 16 | * Get suggestions from the provider (@see provider-api) 17 | * @return array 18 | ### 19 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 20 | # not preceded by a > (arrow operator), a $ (variable start), ... 21 | @regex = /(?:(?:^|[^\w\$_\>]))([A-Z_]+)(?![\w\$_\>])/g 22 | 23 | prefix = @getPrefix(editor, bufferPosition) 24 | return unless prefix.length 25 | 26 | @constants = proxy.constants() 27 | return unless @constants?.names? 28 | 29 | suggestions = @findSuggestionsForPrefix(prefix.trim()) 30 | return unless suggestions.length 31 | return suggestions 32 | 33 | ###* 34 | * Returns suggestions available matching the given prefix 35 | * @param {string} prefix Prefix to match 36 | * @return array 37 | ### 38 | findSuggestionsForPrefix: (prefix) -> 39 | # Filter the words using fuzzaldrin 40 | words = fuzzaldrin.filter @constants.names, prefix 41 | 42 | # Builds suggestions for the words 43 | suggestions = [] 44 | for word in words 45 | for element in @constants.values[word] 46 | suggestions.push 47 | text: word, 48 | type: 'constant', 49 | description: 'Built-in PHP constant.' 50 | 51 | return suggestions 52 | -------------------------------------------------------------------------------- /lib/autocompletion/function-provider.coffee: -------------------------------------------------------------------------------- 1 | fuzzaldrin = require 'fuzzaldrin' 2 | 3 | proxy = require "../services/php-proxy.coffee" 4 | parser = require "../services/php-file-parser.coffee" 5 | AbstractProvider = require "./abstract-provider" 6 | 7 | config = require "../config.coffee" 8 | 9 | module.exports = 10 | 11 | # Autocompletion for internal PHP functions. 12 | class FunctionProvider extends AbstractProvider 13 | functions: [] 14 | 15 | ###* 16 | * Get suggestions from the provider (@see provider-api) 17 | * @return array 18 | ### 19 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 20 | # not preceded by a > (arrow operator), a $ (variable start), ... 21 | @regex = /(?:(?:^|[^\w\$_\>]))([a-zA-Z_]+)(?![\w\$_\>])/g 22 | 23 | prefix = @getPrefix(editor, bufferPosition) 24 | return unless prefix.length 25 | 26 | @functions = proxy.functions() 27 | return unless @functions?.names? 28 | 29 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]]) 30 | insertParameterList = if characterAfterPrefix == '(' then false else true 31 | 32 | suggestions = @findSuggestionsForPrefix(prefix.trim(), insertParameterList) 33 | return unless suggestions.length 34 | return suggestions 35 | 36 | ###* 37 | * Returns suggestions available matching the given prefix. 38 | * 39 | * @param {string} prefix Prefix to match. 40 | * @param {bool} insertParameterList Whether to insert a list of parameters. 41 | * 42 | * @return {Array} 43 | ### 44 | findSuggestionsForPrefix: (prefix, insertParameterList = true) -> 45 | # Filter the words using fuzzaldrin 46 | words = fuzzaldrin.filter @functions.names, prefix 47 | 48 | # Builds suggestions for the words 49 | suggestions = [] 50 | for word in words 51 | for element in @functions.values[word] 52 | returnValueParts = if element.args.return?.type then element.args.return.type.split('\\') else [] 53 | returnValue = returnValueParts[returnValueParts.length - 1] 54 | 55 | suggestion = 56 | text: word, 57 | type: 'function', 58 | description: if element.isInternal then 'Built-in PHP function.' else (if element.args.descriptions.short? then element.args.descriptions.short else '') 59 | className: if element.args.deprecated then 'php-atom-autocomplete-strike' else '' 60 | snippet: if insertParameterList then @getFunctionSnippet(word, element.args) else null 61 | displayText: @getFunctionSignature(word, element.args) 62 | replacementPrefix: prefix 63 | leftLabel: returnValue 64 | 65 | if element.isInternal 66 | suggestion.descriptionMoreURL = config.config.php_documentation_base_url.functions + word 67 | 68 | suggestions.push suggestion 69 | 70 | 71 | return suggestions 72 | -------------------------------------------------------------------------------- /lib/autocompletion/member-provider.coffee: -------------------------------------------------------------------------------- 1 | fuzzaldrin = require 'fuzzaldrin' 2 | exec = require "child_process" 3 | 4 | proxy = require "../services/php-proxy.coffee" 5 | parser = require "../services/php-file-parser.coffee" 6 | AbstractProvider = require "./abstract-provider" 7 | 8 | module.exports = 9 | 10 | # Autocompletion for members of variables such as after ->, ::. 11 | class MemberProvider extends AbstractProvider 12 | methods: [] 13 | 14 | ###* 15 | * Get suggestions from the provider (@see provider-api) 16 | * @return array 17 | ### 18 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 19 | # Autocompletion for class members, i.e. after a ::, ->, ... 20 | @regex = /(?:(?:[a-zA-Z0-9_]*)\s*(?:\(.*\))?\s*(?:->|::)\s*)+([a-zA-Z0-9_]*)/g 21 | 22 | prefix = @getPrefix(editor, bufferPosition) 23 | return unless prefix.length 24 | 25 | elements = parser.getStackClasses(editor, bufferPosition) 26 | return unless elements? 27 | 28 | className = parser.parseElements(editor, bufferPosition, elements) 29 | return unless className? 30 | 31 | elements = prefix.split(/(->|::)/) 32 | 33 | # We only autocomplete after splitters, so there must be at least one word, one splitter, and another word 34 | # (the latter which could be empty). 35 | return unless elements.length > 2 36 | 37 | currentClass = parser.getFullClassName(editor) 38 | currentClassParents = [] 39 | 40 | if currentClass 41 | classInfo = proxy.methods(currentClass) 42 | currentClassParents = if classInfo?.parents then classInfo?.parents else [] 43 | 44 | mustBeStatic = false 45 | 46 | if elements[elements.length - 2] == '::' and elements[elements.length - 3].trim() != 'parent' 47 | mustBeStatic = true 48 | 49 | characterAfterPrefix = editor.getTextInRange([bufferPosition, [bufferPosition.row, bufferPosition.column + 1]]) 50 | insertParameterList = if characterAfterPrefix == '(' then false else true 51 | 52 | suggestions = @findSuggestionsForPrefix(className, elements[elements.length-1].trim(), (element) => 53 | # See also ticket #127. 54 | return false if mustBeStatic and not element.isStatic 55 | return false if element.isPrivate and element.declaringClass.name != currentClass 56 | return false if element.isProtected and element.declaringClass.name != currentClass and element.declaringClass.name not in currentClassParents 57 | 58 | # Constants are only available when statically accessed. 59 | return false if not element.isMethod and not element.isProperty and not mustBeStatic 60 | 61 | return true 62 | , insertParameterList) 63 | 64 | return unless suggestions.length 65 | return suggestions 66 | 67 | ###* 68 | * Returns suggestions available matching the given prefix 69 | * @param {string} className The name of the class to show members of. 70 | * @param {string} prefix Prefix to match (may be left empty to list all members). 71 | * @param {callback} filterCallback A callback that should return true if the item should be added to the 72 | * suggestions list. 73 | * @param {bool} insertParameterList Whether to insert a list of parameters for methods. 74 | * @return array 75 | ### 76 | findSuggestionsForPrefix: (className, prefix, filterCallback, insertParameterList = true) -> 77 | methods = proxy.methods(className) 78 | 79 | if not methods?.names 80 | return [] 81 | 82 | # Filter the words using fuzzaldrin 83 | words = fuzzaldrin.filter(methods.names, prefix) 84 | 85 | # Builds suggestions for the words 86 | suggestions = [] 87 | 88 | for word in words 89 | element = methods.values[word] 90 | 91 | if element not instanceof Array 92 | element = [element] 93 | 94 | for ele in element 95 | if filterCallback and not filterCallback(ele) 96 | continue 97 | 98 | # Ensure we don't get very long return types by just showing the last part. 99 | snippet = null 100 | displayText = word 101 | returnValueParts = if ele.args.return?.type then ele.args.return.type.split('\\') else [] 102 | returnValue = returnValueParts[returnValueParts.length - 1] 103 | 104 | if ele.isMethod 105 | type = 'method' 106 | snippet = if insertParameterList then @getFunctionSnippet(word, ele.args) else null 107 | displayText = @getFunctionSignature(word, ele.args) 108 | 109 | else if ele.isProperty 110 | type = 'property' 111 | 112 | else 113 | type = 'constant' 114 | 115 | suggestions.push 116 | text : word, 117 | type : type 118 | snippet : snippet 119 | displayText : displayText 120 | leftLabel : returnValue 121 | description : if ele.args.descriptions.short? then ele.args.descriptions.short else '' 122 | className : if ele.args.deprecated then 'php-atom-autocomplete-strike' else '' 123 | 124 | return suggestions 125 | -------------------------------------------------------------------------------- /lib/autocompletion/variable-provider.coffee: -------------------------------------------------------------------------------- 1 | fuzzaldrin = require 'fuzzaldrin' 2 | 3 | parser = require "../services/php-file-parser.coffee" 4 | AbstractProvider = require "./abstract-provider" 5 | 6 | module.exports = 7 | 8 | # Autocomplete for local variable names. 9 | class VariableProvider extends AbstractProvider 10 | variables: [] 11 | 12 | ###* 13 | * Get suggestions from the provider (@see provider-api) 14 | * @return array 15 | ### 16 | fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) -> 17 | # "new" keyword or word starting with capital letter 18 | @regex = /(\$[a-zA-Z_]*)/g 19 | 20 | prefix = @getPrefix(editor, bufferPosition) 21 | return unless prefix.length 22 | 23 | @variables = parser.getAllVariablesInFunction(editor, bufferPosition) 24 | return unless @variables.length 25 | 26 | suggestions = @findSuggestionsForPrefix(prefix.trim()) 27 | return unless suggestions.length 28 | return suggestions 29 | 30 | ###* 31 | * Returns suggestions available matching the given prefix 32 | * @param {string} prefix Prefix to match 33 | * @return array 34 | ### 35 | findSuggestionsForPrefix: (prefix) -> 36 | # Filter the words using fuzzaldrin 37 | words = fuzzaldrin.filter @variables, prefix 38 | 39 | # Builds suggestions for the words 40 | suggestions = [] 41 | for word in words 42 | suggestions.push 43 | text: word, 44 | type: 'variable', 45 | replacementPrefix: prefix 46 | 47 | return suggestions 48 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | namespace = require './services/namespace.coffee' 3 | useStatement = require './services/use-statement.coffee' 4 | StatusInProgress = require "./services/status-in-progress.coffee" 5 | StatusErrorAutocomplete = require "./services/status-error-autocomplete.coffee" 6 | 7 | module.exports = 8 | 9 | config: {} 10 | statusInProgress: null 11 | statusErrorAutocomplete: null 12 | 13 | ###* 14 | * Get plugin configuration 15 | ### 16 | getConfig: () -> 17 | # See also https://secure.php.net/urlhowto.php . 18 | @config['php_documentation_base_url'] = { 19 | functions: 'https://secure.php.net/function.' 20 | } 21 | 22 | @config['composer'] = atom.config.get('atom-autocomplete-php.binComposer') 23 | @config['php'] = atom.config.get('atom-autocomplete-php.binPhp') 24 | @config['autoload'] = atom.config.get('atom-autocomplete-php.autoloadPaths') 25 | @config['gotoKey'] = atom.config.get('atom-autocomplete-php.gotoKey') 26 | @config['classmap'] = atom.config.get('atom-autocomplete-php.classMapFiles') 27 | @config['packagePath'] = atom.packages.resolvePackagePath('atom-autocomplete-php') 28 | @config['verboseErrors'] = atom.config.get('atom-autocomplete-php.verboseErrors') 29 | @config['insertNewlinesForUseStatements'] = atom.config.get('atom-autocomplete-php.insertNewlinesForUseStatements') 30 | @config['ensureNewLineAfterNamespace'] = atom.config.get('atom-autocomplete-php.ensureNewLineAfterNamespace') 31 | 32 | ###* 33 | * Writes configuration in "php lib" folder 34 | ### 35 | writeConfig: () -> 36 | @getConfig() 37 | 38 | files = "" 39 | for file in @config.autoload 40 | files += "'#{file}'," 41 | 42 | classmaps = "" 43 | for classmap in @config.classmap 44 | classmaps += "'#{classmap}'," 45 | 46 | text = " '#{@config.composer}', 49 | 'php' => '#{@config.php}', 50 | 'autoload' => array(#{files}), 51 | 'classmap' => array(#{classmaps}) 52 | ); 53 | " 54 | 55 | fs.writeFileSync(@config.packagePath + '/php/tmp.php', text) 56 | 57 | ###* 58 | * Tests the user's PHP and Composer configuration. 59 | * @return {bool} 60 | ### 61 | testConfig: (interactive) -> 62 | @getConfig() 63 | 64 | exec = require "child_process" 65 | testResult = exec.spawnSync(@config.php, ["-v"]) 66 | 67 | errorTitle = 'atom-autocomplete-php - Incorrect setup!' 68 | errorMessage = 'Either PHP or Composer is not correctly set up and as a result PHP autocompletion will not work. ' + 69 | 'Please visit the settings screen to correct this error. If you are not specifying an absolute path for PHP or ' + 70 | 'Composer, make sure they are in your PATH. 71 | Feel free to look package\'s README for configuration examples' 72 | 73 | if testResult.status = null or testResult.status != 0 74 | atom.notifications.addError(errorTitle, {'detail': errorMessage}) 75 | return false 76 | 77 | # Test Composer. 78 | testResult = exec.spawnSync(@config.php, [@config.composer, "--version"]) 79 | 80 | if testResult.status = null or testResult.status != 0 81 | testResult = exec.spawnSync(@config.composer, ["--version"]) 82 | 83 | # Try executing Composer directly. 84 | if testResult.status = null or testResult.status != 0 85 | atom.notifications.addError(errorTitle, {'detail': errorMessage}) 86 | return false 87 | 88 | if interactive 89 | atom.notifications.addSuccess('atom-autocomplete-php - Success', {'detail': 'Configuration OK !'}) 90 | 91 | return true 92 | 93 | ###* 94 | * Init function called on package activation 95 | * Register config events and write the first config 96 | ### 97 | init: () -> 98 | @statusInProgress = new StatusInProgress 99 | @statusInProgress.hide() 100 | 101 | @statusErrorAutocomplete = new StatusErrorAutocomplete 102 | @statusErrorAutocomplete.hide() 103 | 104 | # Command for namespaces 105 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:namespace': => 106 | namespace.createNamespace(atom.workspace.getActivePaneItem()) 107 | 108 | # Command for importing use statement 109 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:import-use-statement': => 110 | useStatement.importUseStatement(atom.workspace.getActivePaneItem()) 111 | 112 | # Command to reindex the current project 113 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:reindex-project': -> 114 | proxy = require './services/php-proxy.coffee' 115 | proxy.refresh() 116 | 117 | # Command to test configuration 118 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:configuration': => 119 | @testConfig(true) 120 | 121 | @writeConfig() 122 | 123 | atom.config.onDidChange 'atom-autocomplete-php.binPhp', () => 124 | @writeConfig() 125 | @testConfig(true) 126 | 127 | atom.config.onDidChange 'atom-autocomplete-php.binComposer', () => 128 | @writeConfig() 129 | @testConfig(true) 130 | 131 | atom.config.onDidChange 'atom-autocomplete-php.autoloadPaths', () => 132 | @writeConfig() 133 | 134 | atom.config.onDidChange 'atom-autocomplete-php.gotoKey', () => 135 | @writeConfig() 136 | 137 | atom.config.onDidChange 'atom-autocomplete-php.classMapFiles', () => 138 | @writeConfig() 139 | 140 | atom.config.onDidChange 'atom-autocomplete-php.verboseErrors', () => 141 | @writeConfig() 142 | 143 | atom.config.onDidChange 'atom-autocomplete-php.insertNewlinesForUseStatements', () => 144 | @writeConfig() 145 | 146 | atom.config.onDidChange 'atom-autocomplete-php.ensureNewLineAfterNamespace', () => 147 | @writeConfig() 148 | -------------------------------------------------------------------------------- /lib/goto/abstract-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | SubAtom = require 'sub-atom' 4 | config = require '../config.coffee' 5 | 6 | module.exports = 7 | 8 | class AbstractProvider 9 | allMarkers: [] 10 | hoverEventSelectors: '' 11 | clickEventSelectors: '' 12 | manager: {} 13 | gotoRegex: '' 14 | jumpWord: '' 15 | 16 | ###* 17 | * Initialisation of Gotos 18 | * 19 | * @param {GotoManager} manager The manager that stores this goto. Used mainly for backtrack registering. 20 | ### 21 | init: (manager) -> 22 | @subAtom = new SubAtom 23 | 24 | @$ = require 'jquery' 25 | @parser = require '../services/php-file-parser' 26 | @fuzzaldrin = require 'fuzzaldrin' 27 | 28 | @manager = manager 29 | 30 | atom.workspace.observeTextEditors (editor) => 31 | editor.onDidSave (event) => 32 | @rescanMarkers(editor) 33 | 34 | @registerMarkers editor 35 | @registerEvents editor 36 | 37 | atom.workspace.onDidChangeActivePaneItem (paneItem) => 38 | if paneItem instanceof TextEditor && @jumpWord != '' && @jumpWord != undefined 39 | @jumpTo(paneItem, @jumpWord) 40 | @jumpWord = '' 41 | 42 | # When you go back to only have 1 pane the events are lost, so need to re-register. 43 | atom.workspace.onDidDestroyPane (pane) => 44 | panes = atom.workspace.getPanes() 45 | 46 | if panes.length == 1 47 | for paneItem in panes[0].items 48 | if paneItem instanceof TextEditor 49 | @registerEvents paneItem 50 | 51 | # Having to re-register events as when a new pane is created the old panes lose the events. 52 | atom.workspace.onDidAddPane (observedPane) => 53 | panes = atom.workspace.getPanes() 54 | 55 | for pane in panes 56 | if pane == observedPane 57 | continue 58 | 59 | for paneItem in pane.items 60 | if paneItem instanceof TextEditor 61 | @registerEvents paneItem 62 | 63 | ###* 64 | * Deactives the goto feature. 65 | ### 66 | deactivate: () -> 67 | @subAtom.dispose() 68 | allMarkers = [] 69 | 70 | ###* 71 | * Goto from the current cursor position in the editor. 72 | * 73 | * @param {TextEditor} editor TextEditor to pull term from. 74 | ### 75 | gotoFromEditor: (editor) -> 76 | if editor.getGrammar().scopeName.match /text.html.php$/ 77 | position = editor.getCursorBufferPosition() 78 | term = @parser.getFullWordFromBufferPosition(editor, position) 79 | 80 | termParts = term.split(/(?:\-\>|::)/) 81 | term = termParts.pop().replace('(', '') 82 | 83 | @gotoFromWord(editor, term) 84 | 85 | ###* 86 | * Goto from the term given. 87 | * 88 | * @param {TextEditor} editor TextEditor to search for namespace of term. 89 | * @param {string} term Term to search for. 90 | ### 91 | gotoFromWord: (editor, term) -> 92 | 93 | ###* 94 | * Registers the mouse events for alt-click. 95 | * 96 | * @param {TextEditor} editor TextEditor to register events to. 97 | ### 98 | registerEvents: (editor) -> 99 | if editor.getGrammar().scopeName.match /text.html.php$/ 100 | textEditorElement = atom.views.getView(editor) 101 | scrollViewElement = @$(textEditorElement).find('.scroll-view') 102 | 103 | @subAtom.add scrollViewElement, 'mousemove', @hoverEventSelectors, (event) => 104 | return unless @isGotoKeyPressed(event) 105 | 106 | selector = @getSelectorFromEvent(event) 107 | 108 | return unless selector 109 | 110 | @$(selector).css('border-bottom', '1px solid ' + @$(selector).css('color')) 111 | @$(selector).css('cursor', 'pointer') 112 | 113 | @isHovering = true 114 | 115 | @subAtom.add scrollViewElement, 'mouseout', @hoverEventSelectors, (event) => 116 | return unless @isHovering 117 | 118 | selector = @getSelectorFromEvent(event) 119 | 120 | return unless selector 121 | 122 | @$(selector).css('border-bottom', '') 123 | @$(selector).css('cursor', '') 124 | 125 | @isHovering = false 126 | 127 | @subAtom.add scrollViewElement, 'click', @clickEventSelectors, (event) => 128 | selector = @getSelectorFromEvent(event) 129 | 130 | if selector == null || @isGotoKeyPressed(event) == false 131 | return 132 | 133 | if event.handled != true 134 | @gotoFromWord(editor, @$(selector).text()) 135 | event.handled = true 136 | 137 | # This is needed to be able to alt-click class names inside comments (docblocks). 138 | editor.onDidChangeCursorPosition (event) => 139 | return unless @isHovering 140 | 141 | markerProperties = 142 | containsBufferPosition: event.newBufferPosition 143 | 144 | markers = event.cursor.editor.findMarkers markerProperties 145 | 146 | for key,marker of markers 147 | for allKey,allMarker of @allMarkers[editor.getLongTitle()] 148 | if marker.id == allMarker.id 149 | @gotoFromWord(event.cursor.editor, marker.getProperties().term) 150 | break 151 | 152 | ###* 153 | * Check if the key binded to the goto with click is pressed or not (according to the settings) 154 | * 155 | * @param {Object} event JS event 156 | * 157 | * @return {Boolean} 158 | ### 159 | isGotoKeyPressed: (event) -> 160 | switch config.config.gotoKey 161 | when 'ctrl'then return event.ctrlKey 162 | when 'alt' then return event.altKey 163 | when 'cmd' then return event.metaKey 164 | else return false 165 | 166 | ###* 167 | * Register any markers that you need. 168 | * 169 | * @param {TextEditor} editor The editor to search through. 170 | ### 171 | registerMarkers: (editor) -> 172 | 173 | ###* 174 | * Removes any markers previously created by registerMarkers. 175 | * 176 | * @param {TextEditor} editor The editor to search through. 177 | ### 178 | cleanMarkers: (editor) -> 179 | 180 | ###* 181 | * Rescans the editor, updating all markers. 182 | * 183 | * @param {TextEditor} editor The editor to search through. 184 | ### 185 | rescanMarkers: (editor) -> 186 | @cleanMarkers(editor) 187 | @registerMarkers(editor) 188 | 189 | ###* 190 | * Gets the correct selector when a selector is clicked. 191 | * 192 | * @param {jQuery.Event} event A jQuery event. 193 | * 194 | * @return {object|null} A selector to be used with jQuery. 195 | ### 196 | getSelectorFromEvent: (event) -> 197 | return event.currentTarget 198 | 199 | ###* 200 | * Returns whether this goto is able to jump using the term. 201 | * 202 | * @param {string} term Term to check. 203 | * 204 | * @return {boolean} Whether a jump is possible. 205 | ### 206 | canGoto: (term) -> 207 | return term.match(@gotoRegex)?.length > 0 208 | 209 | ###* 210 | * Gets the regex used when looking for a word within the editor. 211 | * 212 | * @param {string} term Term being search. 213 | * 214 | * @return {regex} Regex to be used. 215 | ### 216 | getJumpToRegex: (term) -> 217 | 218 | ###* 219 | * Jumps to a word within the editor 220 | * @param {TextEditor} editor The editor that has the function in. 221 | * @param {string} word The word to find and then jump to. 222 | * @return {boolean} Whether the finding was successful. 223 | ### 224 | jumpTo: (editor, word) -> 225 | bufferPosition = @parser.findBufferPositionOfWord(editor, word, @getJumpToRegex(word)) 226 | 227 | if bufferPosition == null 228 | return false 229 | 230 | # Small delay to wait for when a editor is being created. 231 | setTimeout(() -> 232 | editor.setCursorBufferPosition(bufferPosition, { 233 | autoscroll: false 234 | }) 235 | 236 | # Separated these as the autoscroll on setCursorBufferPosition didn't work as well. 237 | editor.scrollToScreenPosition(editor.screenPositionForBufferPosition(bufferPosition), { 238 | center: true 239 | }) 240 | , 100) 241 | -------------------------------------------------------------------------------- /lib/goto/class-provider.coffee: -------------------------------------------------------------------------------- 1 | AbstractProvider = require './abstract-provider' 2 | 3 | module.exports = 4 | 5 | class ClassProvider extends AbstractProvider 6 | hoverEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class, .syntax--comment-clickable .syntax--region' 7 | clickEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class' 8 | gotoRegex: /^\\?[A-Z][A-za-z0-9_]*(\\[A-Z][A-Za-z0-9_])*$/ 9 | 10 | ###* 11 | * Goto the class from the term given. 12 | * 13 | * @param {TextEditor} editor TextEditor to search for namespace of term. 14 | * @param {string} term Term to search for. 15 | ### 16 | gotoFromWord: (editor, term) -> 17 | if term == undefined || term.indexOf('$') == 0 18 | return 19 | 20 | term = @parser.getFullClassName(editor, term) 21 | 22 | proxy = require '../services/php-proxy.coffee' 23 | classesResponse = proxy.classes() 24 | 25 | return unless classesResponse.autocomplete 26 | 27 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition()) 28 | 29 | # See what matches we have for this class name. 30 | matches = @fuzzaldrin.filter(classesResponse.autocomplete, term) 31 | 32 | if matches[0] == term 33 | regexMatches = /(?:\\)(\w+)$/i.exec(matches[0]) 34 | 35 | if regexMatches == null || regexMatches.length == 0 36 | @jumpWord = matches[0] 37 | 38 | else 39 | @jumpWord = regexMatches[1] 40 | 41 | classInfo = proxy.methods(matches[0]) 42 | 43 | atom.workspace.open(classInfo.filename, { 44 | searchAllPanes: true 45 | }) 46 | 47 | ###* 48 | * Gets the correct selector when a class or namespace is clicked. 49 | * 50 | * @param {jQuery.Event} event A jQuery event. 51 | * 52 | * @return {object|null} A selector to be used with jQuery. 53 | ### 54 | getSelectorFromEvent: (event) -> 55 | return @parser.getClassSelectorFromEvent(event) 56 | 57 | ###* 58 | * Goes through all the lines within the editor looking for classes within comments. More specifically if they have 59 | * @var, @param or @return prefixed. 60 | * 61 | * @param {TextEditor} editor The editor to search through. 62 | ### 63 | registerMarkers: (editor) -> 64 | text = editor.getText() 65 | rows = text.split('\n') 66 | 67 | for key,row of rows 68 | regex = /@param|@var|@return|@throws|@see/gi 69 | 70 | if regex.test(row) 71 | @addMarkerToCommentLine row.split(' '), parseInt(key), editor, true 72 | 73 | ###* 74 | * Removes any markers previously created by registerMarkers. 75 | * 76 | * @param {TextEditor} editor The editor to search through 77 | ### 78 | cleanMarkers: (editor) -> 79 | for i,marker of @allMarkers[editor.getLongTitle()] 80 | marker.destroy() 81 | 82 | @allMarkers = [] 83 | 84 | ###* 85 | * Analyses the words array given for any classes and then creates a marker for them. 86 | * 87 | * @param {array} words The array of words to check. 88 | * @param {int} rowIndex The current row the words are on within the editor. 89 | * @param {TextEditor} editor The editor the words are from. 90 | * @param {bool} shouldBreak Flag to say whether the search should break after finding 1 class. 91 | * @param {int} currentIndex = 0 The current column index the search is on. 92 | * @param {int} offset = 0 Any offset that should be applied when creating the marker. 93 | ### 94 | addMarkerToCommentLine: (words, rowIndex, editor, shouldBreak, currentIndex = 0, offset = 0) -> 95 | for key,value of words 96 | regex = /^\\?([A-Za-z0-9_]+)\\?([A-Za-zA-Z_\\]*)?/g 97 | keywordRegex = /^(array|object|bool|string|static|null|boolean|void|int|integer|mixed|callable)$/gi 98 | 99 | if value && regex.test(value) && keywordRegex.test(value) == false 100 | if value.includes('|') 101 | @addMarkerToCommentLine value.split('|'), rowIndex, editor, false, currentIndex, parseInt(key) 102 | 103 | else 104 | range = [[rowIndex, currentIndex + parseInt(key) + offset], [rowIndex, currentIndex + parseInt(key) + value.length + offset]]; 105 | 106 | marker = editor.markBufferRange(range) 107 | 108 | markerProperties = 109 | term: value 110 | 111 | marker.setProperties markerProperties 112 | 113 | options = 114 | type: 'highlight' 115 | class: 'comment-clickable comment' 116 | 117 | if !marker.isDestroyed() 118 | editor.decorateMarker marker, options 119 | 120 | if @allMarkers[editor.getLongTitle()] == undefined 121 | @allMarkers[editor.getLongTitle()] = [] 122 | 123 | @allMarkers[editor.getLongTitle()].push(marker) 124 | 125 | if shouldBreak == true 126 | break 127 | 128 | currentIndex += value.length; 129 | 130 | ###* 131 | * Gets the regex used when looking for a word within the editor 132 | * 133 | * @param {string} term Term being search. 134 | * 135 | * @return {regex} Regex to be used. 136 | ### 137 | getJumpToRegex: (term) -> 138 | return ///^(class|interface|abstract class|trait)\ +#{term}///i 139 | -------------------------------------------------------------------------------- /lib/goto/function-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | AbstractProvider = require './abstract-provider' 4 | 5 | module.exports = 6 | 7 | class FunctionProvider extends AbstractProvider 8 | hoverEventSelectors: '.syntax--function-call' 9 | clickEventSelectors: '.syntax--function-call' 10 | gotoRegex: /(?:(?:[a-zA-Z0-9_]*)\s*(?:\(.*\))?\s*(?:->|::)\s*)+([a-zA-Z0-9_]*)/ 11 | 12 | ###* 13 | * Goto the class from the term given. 14 | * 15 | * @param {TextEditor} editor TextEditor to search for namespace of term. 16 | * @param {string} term Term to search for. 17 | ### 18 | gotoFromWord: (editor, term) -> 19 | bufferPosition = editor.getCursorBufferPosition() 20 | 21 | calledClass = @parser.getCalledClass(editor, term, bufferPosition) 22 | 23 | if not calledClass 24 | return 25 | 26 | currentClass = @parser.getFullClassName(editor) 27 | 28 | if currentClass == calledClass && @jumpTo(editor, term) 29 | @manager.addBackTrack(editor.getPath(), bufferPosition) 30 | return 31 | 32 | value = @parser.getMemberContext(editor, term, bufferPosition, calledClass) 33 | 34 | if not value 35 | return 36 | 37 | atom.workspace.open(value.declaringStructure.filename, { 38 | initialLine : (value.startLine - 1), 39 | searchAllPanes : true 40 | }) 41 | 42 | @manager.addBackTrack(editor.getPath(), bufferPosition) 43 | 44 | ###* 45 | * Gets the regex used when looking for a word within the editor 46 | * 47 | * @param {string} term Term being search. 48 | * 49 | * @return {regex} Regex to be used. 50 | ### 51 | getJumpToRegex: (term) -> 52 | return ///function\ +#{term}(\ +|\()///i 53 | -------------------------------------------------------------------------------- /lib/goto/goto-manager.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | ClassProvider = require './class-provider.coffee' 4 | FunctionProvider = require './function-provider.coffee' 5 | PropertyProvider = require './property-provider.coffee' 6 | 7 | parser = require '../services/php-file-parser.coffee' 8 | 9 | module.exports = 10 | 11 | class GotoManager 12 | providers: [] 13 | trace: [] 14 | 15 | ###* 16 | * Initialisation of all the providers and commands for goto 17 | ### 18 | init: () -> 19 | @providers.push new ClassProvider() 20 | @providers.push new FunctionProvider() 21 | @providers.push new PropertyProvider() 22 | 23 | for provider in @providers 24 | provider.init(@) 25 | 26 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:goto-backtrack': => 27 | @backTrack(atom.workspace.getActivePaneItem()) 28 | 29 | atom.commands.add 'atom-workspace', 'atom-autocomplete-php:goto': => 30 | @goto(atom.workspace.getActivePaneItem()) 31 | 32 | ###* 33 | * Deactivates the goto functionaility 34 | ### 35 | deactivate: () -> 36 | for provider in @providers 37 | provider.deactivate() 38 | 39 | ###* 40 | * Adds a backtrack step to the stack. 41 | * 42 | * @param {string} fileName The file where the jump took place. 43 | * @param {BufferPosition} bufferPosition The buffer position the cursor was last on. 44 | ### 45 | addBackTrack: (fileName, bufferPosition) -> 46 | @trace.push({ 47 | file: fileName, 48 | position: bufferPosition 49 | }) 50 | 51 | ###* 52 | * Pops one of the stored back tracks and jump the user to its position. 53 | * 54 | * @param {TextEditor} editor The current editor. 55 | ### 56 | backTrack: (editor) -> 57 | if @trace.length == 0 58 | return 59 | 60 | lastTrace = @trace.pop() 61 | 62 | if editor instanceof TextEditor && editor.getPath() == lastTrace.file 63 | editor.setCursorBufferPosition(lastTrace.position, { 64 | autoscroll: false 65 | }) 66 | 67 | # Separated these as the autoscroll on setCursorBufferPosition 68 | # didn't work as well. 69 | editor.scrollToScreenPosition(editor.screenPositionForBufferPosition(lastTrace.position), { 70 | center: true 71 | }) 72 | 73 | else 74 | atom.workspace.open(lastTrace.file, { 75 | searchAllPanes: true, 76 | initialLine: lastTrace.position[0] 77 | initialColumn: lastTrace.position[1] 78 | }) 79 | 80 | ###* 81 | * Takes the editor and jumps using one of the providers. 82 | * 83 | * @param {TextEditor} editor Current active editor 84 | ### 85 | goto: (editor) -> 86 | fullTerm = parser.getFullWordFromBufferPosition(editor, editor.getCursorBufferPosition()) 87 | 88 | for provider in @providers 89 | if provider.canGoto(fullTerm) 90 | provider.gotoFromEditor(editor) 91 | break 92 | -------------------------------------------------------------------------------- /lib/goto/property-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | AbstractProvider = require './abstract-provider' 4 | 5 | module.exports = 6 | 7 | class PropertyProvider extends AbstractProvider 8 | hoverEventSelectors: '.syntax--property' 9 | clickEventSelectors: '.syntax--property' 10 | gotoRegex: /^(\$\w+)?((->|::)\w+)+/ 11 | 12 | ###* 13 | * Goto the property from the term given. 14 | * 15 | * @param {TextEditor} editor TextEditor to search for namespace of term. 16 | * @param {string} term Term to search for. 17 | ### 18 | gotoFromWord: (editor, term) -> 19 | bufferPosition = editor.getCursorBufferPosition() 20 | 21 | calledClass = @parser.getCalledClass(editor, term, bufferPosition) 22 | 23 | if not calledClass 24 | return 25 | 26 | currentClass = @parser.getFullClassName(editor) 27 | 28 | if currentClass == calledClass && @jumpTo(editor, term) 29 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition()) 30 | return 31 | 32 | value = @parser.getMemberContext(editor, term, bufferPosition, calledClass) 33 | 34 | if not value 35 | return 36 | 37 | atom.workspace.open(value.declaringStructure.filename, { 38 | searchAllPanes: true 39 | }) 40 | 41 | @manager.addBackTrack(editor.getPath(), editor.getCursorBufferPosition()) 42 | @jumpWord = term 43 | 44 | ###* 45 | * Gets the regex used when looking for a word within the editor 46 | * 47 | * @param {string} term Term being search. 48 | * 49 | * @return {regex} Regex to be used. 50 | ### 51 | getJumpToRegex: (term) -> 52 | return ///(protected|public|private|static)\ +\$#{term}///i 53 | -------------------------------------------------------------------------------- /lib/peekmo-php-atom-autocomplete.coffee: -------------------------------------------------------------------------------- 1 | GotoManager = require "./goto/goto-manager.coffee" 2 | TooltipManager = require "./tooltip/tooltip-manager.coffee" 3 | AnnotationManager = require "./annotation/annotation-manager.coffee" 4 | AutocompletionManager = require "./autocompletion/autocompletion-manager.coffee" 5 | StatusInProgress = require "./services/status-in-progress.coffee" 6 | config = require './config.coffee' 7 | proxy = require './services/php-proxy.coffee' 8 | parser = require './services/php-file-parser.coffee' 9 | plugins = require './services/plugin-manager.coffee' 10 | 11 | module.exports = 12 | config: 13 | binComposer: 14 | title: 'Command to use composer' 15 | description: 'This plugin depends on composer in order to work. Specify the path 16 | to your composer bin (e.g : bin/composer, composer.phar, composer)' 17 | type: 'string' 18 | default: '/usr/local/bin/composer' 19 | order: 1 20 | 21 | binPhp: 22 | title: 'Command php' 23 | description: 'This plugin use php CLI in order to work. Please specify your php 24 | command ("php" on UNIX systems)' 25 | type: 'string' 26 | default: 'php' 27 | order: 2 28 | 29 | autoloadPaths: 30 | title: 'Autoloader file' 31 | description: 'Relative path to the files of autoload.php from composer (or an other one). You can specify multiple 32 | paths (comma separated) if you have different paths for some projects.' 33 | type: 'array' 34 | default: ['vendor/autoload.php', 'autoload.php'] 35 | order: 3 36 | 37 | gotoKey: 38 | title: 'Goto key' 39 | description: 'Key to use with "click" to use goto. By default "alt" (because on macOS, ctrl + click is like right click)' 40 | type: 'string' 41 | default: 'alt' 42 | enum: ['alt', 'ctrl', 'cmd'] 43 | order: 4 44 | 45 | classMapFiles: 46 | title: 'Classmap files' 47 | description: 'Relative path to the files that contains a classmap (array with "className" => "fileName"). By default 48 | on composer it\'s vendor/composer/autoload_classmap.php' 49 | type: 'array' 50 | default: ['vendor/composer/autoload_classmap.php', 'autoload/ezp_kernel.php'] 51 | order: 5 52 | 53 | insertNewlinesForUseStatements: 54 | title: 'Insert newlines for use statements.' 55 | description: 'When enabled, the plugin will add additional newlines before or after an automatically added 56 | use statement when it can\'t add them nicely to an existing group. This results in more cleanly 57 | separated use statements but will create additional vertical whitespace.' 58 | type: 'boolean' 59 | default: false 60 | order: 6 61 | 62 | ensureNewLineAfterNamespace: 63 | title: 'Ensure new line after namespace.' 64 | description: 'When enabled, the plugin will add a new line before the use statement when it comes directly after the namespace.' 65 | type: 'boolean' 66 | default: false 67 | order: 6 68 | 69 | verboseErrors: 70 | title: 'Errors on file saving showed' 71 | description: 'When enabled, you\'ll have a notification once an error occured on autocomplete. Otherwise, the message will just be logged in developer console' 72 | type: 'boolean' 73 | default: false 74 | order: 7 75 | 76 | activate: -> 77 | config.testConfig() 78 | config.init() 79 | 80 | @autocompletionManager = new AutocompletionManager() 81 | @autocompletionManager.init() 82 | 83 | @gotoManager = new GotoManager() 84 | @gotoManager.init() 85 | 86 | @tooltipManager = new TooltipManager() 87 | @tooltipManager.init() 88 | 89 | @annotationManager = new AnnotationManager() 90 | @annotationManager.init() 91 | 92 | proxy.init() 93 | 94 | deactivate: -> 95 | @gotoManager.deactivate() 96 | @tooltipManager.deactivate() 97 | @annotationManager.deactivate() 98 | @autocompletionManager.deactivate() 99 | proxy.deactivate() 100 | 101 | consumeStatusBar: (statusBar) -> 102 | config.statusInProgress.initialize(statusBar) 103 | config.statusInProgress.attach() 104 | 105 | config.statusErrorAutocomplete.initialize(statusBar) 106 | config.statusErrorAutocomplete.attach() 107 | 108 | consumePlugin: (plugin) -> 109 | plugins.plugins.push(plugin) 110 | 111 | provideAutocompleteTools: -> 112 | @services = 113 | proxy: proxy 114 | parser: parser 115 | 116 | return @services 117 | 118 | getProvider: -> 119 | return @autocompletionManager.getProviders() 120 | -------------------------------------------------------------------------------- /lib/services/attached-popover.coffee: -------------------------------------------------------------------------------- 1 | Popover = require './popover' 2 | 3 | module.exports = 4 | 5 | class AttachedPopover extends Popover 6 | ### 7 | NOTE: The reason we do not use Atom's native tooltip is because it is attached to an element, which caused 8 | strange problems such as tickets #107 and #72. This implementation uses the same CSS classes and transitions but 9 | handles the displaying manually as we don't want to attach/detach, we only want to temporarily display a popover 10 | on mouseover. 11 | ### 12 | timeoutId: null 13 | elementToAttachTo: null 14 | 15 | ###* 16 | * Constructor. 17 | * 18 | * @param {HTMLElement} elementToAttachTo The element to show the popover over. 19 | * @param {int} delay How long the mouse has to hover over the elment before the popover shows 20 | * up (in miliiseconds). 21 | ### 22 | constructor: (@elementToAttachTo, delay = 500) -> 23 | super() 24 | 25 | ###* 26 | * Destructor. 27 | * 28 | ### 29 | destructor: () -> 30 | if @timeoutId 31 | clearTimeout(@timeoutId) 32 | @timeoutId = null 33 | 34 | super() 35 | 36 | ###* 37 | * Shows the popover with the specified text. 38 | * 39 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip. 40 | ### 41 | show: (fadeInTime = 100) -> 42 | coordinates = @elementToAttachTo.getBoundingClientRect(); 43 | 44 | centerOffset = ((coordinates.right - coordinates.left) / 2) 45 | 46 | x = coordinates.left - (@$(@getElement()).width() / 2) + centerOffset 47 | y = coordinates.bottom 48 | 49 | super(x, y, fadeInTime) 50 | 51 | ###* 52 | * Shows the popover with the specified text after the specified delay (in miliiseconds). Calling this method 53 | * multiple times will cancel previous show requests and restart. 54 | * 55 | * @param {int} delay The delay before the tooltip shows up (in milliseconds). 56 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip. 57 | ### 58 | showAfter: (delay, fadeInTime = 100) -> 59 | @timeoutId = setTimeout(() => 60 | @show(fadeInTime) 61 | , delay) 62 | -------------------------------------------------------------------------------- /lib/services/namespace.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | * PHP files namespace management 3 | ### 4 | 5 | module.exports = 6 | 7 | ###* 8 | * Add the good namespace to the given file 9 | * @param {TextEditor} editor 10 | ### 11 | createNamespace: (editor) -> 12 | proxy = require './php-proxy.coffee' 13 | 14 | composer = proxy.composer() 15 | autoloaders = [] 16 | 17 | if not composer 18 | return 19 | 20 | # Get elements from composer.json 21 | for psr, autoload of composer.autoload 22 | for namespace, src of autoload 23 | if namespace.endsWith("\\") 24 | namespace = namespace.substr(0, namespace.length-1) 25 | 26 | autoloaders[src] = namespace 27 | 28 | if composer["autoload-dev"] 29 | for psr, autoload of composer["autoload-dev"] 30 | for namespace, src of autoload 31 | if namespace.endsWith("\\") 32 | namespace = namespace.substr(0, namespace.length-1) 33 | 34 | autoloaders[src] = namespace 35 | 36 | # Get the current path of the file 37 | path = editor.getPath() 38 | for directory in atom.project.getDirectories() 39 | if path.indexOf(directory.path) == 0 40 | path = path.substr(directory.path.length+1) 41 | break 42 | 43 | # Path with \ replaced by / to be ok with composer.json 44 | path = path.replace(/\\/g, '/') 45 | 46 | # Get the root namespace 47 | namespace = null 48 | for src, name of autoloaders 49 | if path.indexOf(src) == 0 50 | path = path.substr(src.length) 51 | namespace = name 52 | break 53 | 54 | # No namespace found ? Let's leave 55 | if namespace == null 56 | return 57 | 58 | # If the path starts with "/", we remove it 59 | if path.indexOf("/") == 0 60 | path = path.substr(1) 61 | 62 | elements = path.split('/') 63 | 64 | # Build the namespace 65 | index = 1 66 | for element in elements 67 | if element == "" or index == elements.length 68 | continue 69 | 70 | namespace = if namespace == "" then element else namespace + "\\" + element 71 | index++ 72 | 73 | text = editor.getText() 74 | index = 0 75 | 76 | # Search for the good place to write the namespace 77 | lines = text.split('\n') 78 | for line in lines 79 | line = line.trim() 80 | 81 | # If we found class keyword, we are not in namespace space, so return 82 | if line.indexOf('namespace ') == 0 83 | editor.setTextInBufferRange([[index,0], [index+1, 0]], "namespace #{namespace};\n") 84 | return 85 | else if line.trim() != "" and line.trim().indexOf(" 24 | options = {} if not options 25 | processKey = command.join("_") 26 | 27 | for directory in atom.project.getDirectories() 28 | for c in command 29 | c.replace(/\\/g, '\\\\') 30 | 31 | if not async 32 | try 33 | # avoid multiple processes of the same command 34 | if not @currentProcesses[processKey]? 35 | @currentProcesses[processKey] = true 36 | 37 | args = [__dirname + "/../../php/parser.php", directory.path].concat(command) 38 | if noparser 39 | args = command 40 | stdout = exec.spawnSync(config.config.php, args, options).output[1].toString('ascii') 41 | 42 | delete @currentProcesses[processKey] 43 | 44 | if noparser 45 | res = 46 | result: stdout 47 | else 48 | res = JSON.parse(stdout) 49 | catch err 50 | console.log err 51 | res = 52 | error: err 53 | 54 | if !res 55 | return [] 56 | 57 | if res.error? 58 | @printError(res.error) 59 | 60 | return res 61 | else 62 | if not @currentProcesses[processKey]? 63 | config.statusErrorAutocomplete.update("Autocomplete failure", false) 64 | 65 | if processKey.indexOf("--refresh") != -1 66 | config.statusInProgress.update("Indexing...", true) 67 | 68 | args = [__dirname + "/../../php/parser.php", directory.path].concat(command) 69 | if noparser 70 | args = command 71 | 72 | @currentProcesses[processKey] = exec.spawn(config.config.php, args, options) 73 | @currentProcesses[processKey].on("exit", (exitCode) => 74 | delete @currentProcesses[processKey] 75 | ) 76 | 77 | commandData = '' 78 | @currentProcesses[processKey].stdout.on("data", (data) => 79 | commandData += data.toString() 80 | ) 81 | 82 | @currentProcesses[processKey].on("close", () => 83 | if processKey.indexOf("--functions") != -1 84 | try 85 | @data.functions = JSON.parse(commandData) 86 | catch err 87 | config.statusErrorAutocomplete.update("Autocomplete failure", true) 88 | 89 | if processKey.indexOf("--refresh") != -1 90 | config.statusInProgress.update("Indexing...", false) 91 | ) 92 | 93 | ###* 94 | * Reads an index by its name (file in indexes/index.[name].json) 95 | * @param {string} name Name of the index to read 96 | ### 97 | readIndex: (name) -> 98 | for directory in atom.project.getDirectories() 99 | crypt = md5(directory.path) 100 | path = __dirname + "/../../indexes/" + crypt + "/index." + name + ".json" 101 | try 102 | fs.accessSync(path, fs.F_OK | fs.R_OK) 103 | catch err 104 | return [] 105 | 106 | options = 107 | encoding: 'UTF-8' 108 | return JSON.parse(fs.readFileSync(path, options)) 109 | 110 | break 111 | 112 | ###* 113 | * Open and read the composer.json file in the current folder 114 | ### 115 | readComposer: () -> 116 | for directory in atom.project.getDirectories() 117 | path = "#{directory.path}/composer.json" 118 | 119 | try 120 | fs.accessSync(path, fs.F_OK | fs.R_OK) 121 | catch err 122 | continue 123 | 124 | options = 125 | encoding: 'UTF-8' 126 | @data.composer = JSON.parse(fs.readFileSync(path, options)) 127 | return @data.composer 128 | 129 | console.log 'Unable to find composer.json file or to open it. The plugin will not work as expected. It only works on composer project' 130 | throw "Error" 131 | 132 | ###* 133 | * Throw a formatted error 134 | * @param {object} error Error to show 135 | ### 136 | printError:(error) -> 137 | @data.error = true 138 | message = error.message 139 | 140 | #if error.file? and error.line? 141 | #message = message + ' [from file ' + error.file + ' - Line ' + error.line + ']' 142 | 143 | #throw new Error(message) 144 | 145 | ###* 146 | * Clear all cache of the plugin 147 | ### 148 | clearCache: () -> 149 | @data = 150 | error: false, 151 | autocomplete: [], 152 | methods: [], 153 | composer: null 154 | 155 | # Fill the functions array because it can take times 156 | @functions() 157 | 158 | ###* 159 | * Autocomplete for classes name 160 | * @return {array} 161 | ### 162 | classes: () -> 163 | return @readIndex('classes') 164 | 165 | ###* 166 | * Returns composer.json file 167 | * @return {Object} 168 | ### 169 | composer: () -> 170 | return @readComposer() 171 | 172 | ###* 173 | * Autocomplete for internal PHP constants 174 | * @return {array} 175 | ### 176 | constants: () -> 177 | if not @data.constants? 178 | res = @execute(["--constants"], false) 179 | @data.constants = res 180 | 181 | return @data.constants 182 | 183 | ###* 184 | * Autocomplete for internal PHP functions 185 | * 186 | * @return {array} 187 | ### 188 | functions: () -> 189 | if not @data.functions? 190 | @execute(["--functions"], true) 191 | 192 | return @data.functions 193 | 194 | ###* 195 | * Autocomplete for methods & properties of a class 196 | * @param {string} className Class complete name (with namespace) 197 | * @return {array} 198 | ### 199 | methods: (className) -> 200 | if not @data.methods[className]? 201 | res = @execute(["--methods","#{className}"], false) 202 | @data.methods[className] = res 203 | 204 | return @data.methods[className] 205 | 206 | ###* 207 | * Autocomplete for methods & properties of a class 208 | * @param {string} className Class complete name (with namespace) 209 | * @return {array} 210 | ### 211 | autocomplete: (className, name) -> 212 | cacheKey = className + "." + name 213 | 214 | if not @data.autocomplete[cacheKey]? 215 | res = @execute(["--autocomplete", className, name], false) 216 | @data.autocomplete[cacheKey] = res 217 | 218 | return @data.autocomplete[cacheKey] 219 | 220 | ###* 221 | * Returns params from the documentation of the given function 222 | * 223 | * @param {string} className 224 | * @param {string} functionName 225 | ### 226 | docParams: (className, functionName) -> 227 | res = @execute(["--doc-params", "#{className}", "#{functionName}"], false) 228 | return res 229 | 230 | ###* 231 | * Refresh the full index or only for the given classPath 232 | * @param {string} classPath Full path (dir) of the class to refresh 233 | ### 234 | refresh: (classPath) -> 235 | if not classPath? 236 | @execute(["--refresh"], true) 237 | else 238 | @execute(["--refresh", "#{classPath}"], true) 239 | 240 | ###* 241 | * Method called on plugin activation 242 | ### 243 | init: () -> 244 | @refresh() 245 | atom.workspace.observeTextEditors (editor) => 246 | editor.onDidSave((event) => 247 | # Only .php file 248 | if editor.getGrammar().scopeName.match /text.html.php$/ 249 | @clearCache() 250 | 251 | # For Windows - Replace \ in class namespace to / because 252 | # composer use / instead of \ 253 | path = event.path 254 | for directory in atom.project.getDirectories() 255 | if path.indexOf(directory.path) == 0 256 | classPath = path.substr(0, directory.path.length+1) 257 | path = path.substr(directory.path.length+1) 258 | break 259 | 260 | @refresh(classPath + path.replace(/\\/g, '/')) 261 | ) 262 | 263 | atom.config.onDidChange 'atom-autocomplete-php.binPhp', () => 264 | @clearCache() 265 | 266 | atom.config.onDidChange 'atom-autocomplete-php.binComposer', () => 267 | @clearCache() 268 | 269 | atom.config.onDidChange 'atom-autocomplete-php.autoloadPaths', () => 270 | @clearCache() 271 | 272 | ###* 273 | * Function called when plugin is deactivate 274 | * Cleanup every request in progress (#330) 275 | ### 276 | deactivate: () -> 277 | for key, process of @currentProcesses 278 | process.kill() 279 | -------------------------------------------------------------------------------- /lib/services/plugin-manager.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | plugins: [] 3 | -------------------------------------------------------------------------------- /lib/services/popover.coffee: -------------------------------------------------------------------------------- 1 | {Disposable} = require 'atom' 2 | 3 | module.exports = 4 | 5 | class Popover extends Disposable 6 | element: null 7 | 8 | ###* 9 | * Constructor. 10 | ### 11 | constructor: () -> 12 | @$ = require 'jquery' 13 | 14 | @element = document.createElement('div') 15 | @element.className = 'tooltip bottom fade php-atom-autocomplete-popover' 16 | @element.innerHTML = "
" 17 | 18 | document.body.appendChild(@element) 19 | 20 | super @destructor 21 | 22 | ###* 23 | * Destructor. 24 | ### 25 | destructor: () -> 26 | @hide() 27 | document.body.removeChild(@element) 28 | 29 | ###* 30 | * Retrieves the HTML element containing the popover. 31 | * 32 | * @return {HTMLElement} 33 | ### 34 | getElement: () -> 35 | return @element 36 | 37 | ###* 38 | * sets the text to display. 39 | * 40 | * @param {string} text 41 | ### 42 | setText: (text) -> 43 | @$('.tooltip-inner', @element).html( 44 | '
' + text.replace(/\n\n/g, '

') + '
' 45 | ) 46 | 47 | ###* 48 | * Shows a popover at the specified location with the specified text and fade in time. 49 | * 50 | * @param {int} x The X coordinate to show the popover at (left). 51 | * @param {int} y The Y coordinate to show the popover at (top). 52 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip. 53 | ### 54 | show: (x, y, fadeInTime = 100) -> 55 | @$(@element).css('left', x + 'px') 56 | @$(@element).css('top', y + 'px') 57 | 58 | @$(@element).addClass('in') 59 | @$(@element).css('opacity', 100) 60 | @$(@element).css('display', 'block') 61 | 62 | ###* 63 | * Hides the tooltip, if it is displayed. 64 | ### 65 | hide: () -> 66 | @$(@element).removeClass('in') 67 | @$(@element).css('opacity', 0) 68 | @$(@element).css('display', 'none') 69 | -------------------------------------------------------------------------------- /lib/services/status-error-autocomplete.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | ##* 4 | # Progress bar in the status bar 5 | ## 6 | class StatusErrorAutocomplete 7 | actions: [] 8 | 9 | constructor: -> 10 | @span = document.createElement("span") 11 | @span.className = "inline-block text-subtle" 12 | @span.innerHTML = "Autocomplete failure" 13 | 14 | @container = document.createElement("div") 15 | @container.className = "inline-block" 16 | 17 | @subcontainer = document.createElement("div") 18 | @subcontainer.className = "block" 19 | @container.appendChild(@subcontainer) 20 | 21 | @subcontainer.appendChild(@span) 22 | 23 | initialize: (@statusBar) -> 24 | 25 | update: (text, show) -> 26 | if show 27 | @container.className = "inline-block" 28 | @span.innerHTML = text 29 | @actions.push(text) 30 | else 31 | @actions.forEach((value, index) -> 32 | if value == text 33 | @actions.splice(index, 1) 34 | , @) 35 | 36 | if @actions.length == 0 37 | @hide() 38 | else 39 | @span.innerHTML = @actions[0] 40 | 41 | hide: -> 42 | @container.className = 'hidden' 43 | 44 | attach: -> 45 | @tile = @statusBar.addRightTile(item: @container, priority: 20) 46 | 47 | detach: -> 48 | @tile.destroy() 49 | -------------------------------------------------------------------------------- /lib/services/status-in-progress.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | ##* 4 | # Progress bar in the status bar 5 | ## 6 | class StatusInProgress 7 | actions: [] 8 | 9 | constructor: -> 10 | @span = document.createElement("span") 11 | @span.className = "inline-block text-subtle" 12 | @span.innerHTML = "Indexing.." 13 | 14 | @progress = document.createElement("progress") 15 | 16 | @container = document.createElement("div") 17 | @container.className = "inline-block" 18 | 19 | @subcontainer = document.createElement("div") 20 | @subcontainer.className = "block" 21 | @container.appendChild(@subcontainer) 22 | 23 | @subcontainer.appendChild(@progress) 24 | @subcontainer.appendChild(@span) 25 | 26 | initialize: (@statusBar) -> 27 | 28 | update: (text, show) -> 29 | if show 30 | @container.className = "inline-block" 31 | @span.innerHTML = text 32 | @actions.push(text) 33 | else 34 | @actions.forEach((value, index) -> 35 | if value == text 36 | @actions.splice(index, 1) 37 | , @) 38 | 39 | if @actions.length == 0 40 | @hide() 41 | else 42 | @span.innerHTML = @actions[0] 43 | 44 | hide: -> 45 | @container.className = 'hidden' 46 | 47 | attach: -> 48 | @tile = @statusBar.addRightTile(item: @container, priority: 19) 49 | 50 | detach: -> 51 | @tile.destroy() 52 | -------------------------------------------------------------------------------- /lib/services/use-statement.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | * PHP import use statement 3 | ### 4 | 5 | module.exports = 6 | 7 | ###* 8 | * Import use statement for class under cursor 9 | * @param {TextEditor} editor 10 | ### 11 | importUseStatement: (editor) -> 12 | ClassProvider = require '../autocompletion/class-provider.coffee' 13 | provider = new ClassProvider() 14 | word = editor.getWordUnderCursor() 15 | regex = new RegExp('\\\\' + word + '$'); 16 | 17 | suggestions = provider.fetchSuggestionsFromWord(word) 18 | return unless suggestions 19 | 20 | suggestions = suggestions.filter((suggestion) -> 21 | return suggestion.text == word || regex.test(suggestion.text) 22 | ) 23 | 24 | return unless suggestions.length 25 | 26 | if suggestions.length < 2 27 | return provider.onSelectedClassSuggestion {editor, suggestion: suggestions.shift()} 28 | 29 | ClassListView = require '../views/class-list-view' 30 | 31 | return new ClassListView(suggestions, ({name}) -> 32 | suggestion = suggestions.filter((suggestion) -> 33 | return suggestion.text == name 34 | ).shift() 35 | provider.onSelectedClassSuggestion {editor, suggestion} 36 | ) 37 | -------------------------------------------------------------------------------- /lib/tooltip/abstract-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | SubAtom = require 'sub-atom' 4 | AttachedPopover = require '../services/attached-popover' 5 | 6 | module.exports = 7 | 8 | class AbstractProvider 9 | hoverEventSelectors: '' 10 | 11 | ###* 12 | * Initializes this provider. 13 | ### 14 | init: () -> 15 | @$ = require 'jquery' 16 | @parser = require '../services/php-file-parser' 17 | 18 | @subAtom = new SubAtom 19 | 20 | atom.workspace.observeTextEditors (editor) => 21 | @registerEvents editor 22 | 23 | # When you go back to only have one pane the events are lost, so need to re-register. 24 | atom.workspace.onDidDestroyPane (pane) => 25 | panes = atom.workspace.getPanes() 26 | 27 | if panes.length == 1 28 | for paneItem in panes[0].items 29 | if paneItem instanceof TextEditor 30 | @registerEvents paneItem 31 | 32 | # Having to re-register events as when a new pane is created the old panes lose the events. 33 | atom.workspace.onDidAddPane (observedPane) => 34 | panes = atom.workspace.getPanes() 35 | 36 | for pane in panes 37 | if pane == observedPane 38 | continue 39 | 40 | for paneItem in pane.items 41 | if paneItem instanceof TextEditor 42 | @registerEvents paneItem 43 | 44 | ###* 45 | * Deactives the provider. 46 | ### 47 | deactivate: () -> 48 | @subAtom.dispose() 49 | @removePopover() 50 | 51 | ###* 52 | * Registers the necessary event handlers. 53 | * 54 | * @param {TextEditor} editor TextEditor to register events to. 55 | ### 56 | registerEvents: (editor) -> 57 | if editor.getGrammar().scopeName.match /text.html.php$/ 58 | textEditorElement = atom.views.getView(editor) 59 | scrollViewElement = @$(textEditorElement).find('.scroll-view') 60 | 61 | @subAtom.add scrollViewElement, 'mouseover', @hoverEventSelectors, (event) => 62 | if @timeout 63 | clearTimeout(@timeout) 64 | 65 | selector = @getSelectorFromEvent(event) 66 | 67 | if selector == null 68 | return 69 | 70 | editorViewComponent = atom.views.getView(editor).component 71 | 72 | # Ticket #140 - In rare cases the component is null. 73 | if editorViewComponent 74 | cursorPosition = editorViewComponent.screenPositionForMouseEvent(event) 75 | 76 | @removePopover() 77 | @showPopoverFor(editor, selector, cursorPosition) 78 | 79 | @subAtom.add scrollViewElement, 'mouseout', @hoverEventSelectors, (event) => 80 | @removePopover() 81 | 82 | # Ticket #107 - Mouseout isn't generated until the mouse moves, even when scrolling (with the keyboard or 83 | # mouse). If the element goes out of the view in the meantime, its HTML element disappears, never removing 84 | # it. 85 | editor.onDidDestroy () => 86 | @removePopover() 87 | 88 | editor.onDidStopChanging () => 89 | @removePopover() 90 | 91 | @$(textEditorElement).find('.horizontal-scrollbar').on 'scroll', () => 92 | @removePopover() 93 | 94 | @$(textEditorElement).find('.vertical-scrollbar').on 'scroll', () => 95 | @removePopover() 96 | 97 | ###* 98 | * Shows a popover containing the documentation of the specified element located at the specified location. 99 | * 100 | * @param {TextEditor} editor TextEditor containing the elemment. 101 | * @param {string} element The element to search for. 102 | * @param {Point} bufferPosition The cursor location the element is at. 103 | * @param {int} delay How long to wait before the popover shows up. 104 | * @param {int} fadeInTime The amount of time to take to fade in the tooltip. 105 | ### 106 | showPopoverFor: (editor, element, bufferPosition, delay = 500, fadeInTime = 100) -> 107 | term = @$(element).text() 108 | tooltipText = @getTooltipForWord(editor, term, bufferPosition) 109 | 110 | if tooltipText?.length > 0 111 | popoverElement = @getPopoverElementFromSelector(element) 112 | 113 | @attachedPopover = new AttachedPopover(popoverElement) 114 | @attachedPopover.setText('
' + tooltipText + '
') 115 | @attachedPopover.showAfter(delay, fadeInTime) 116 | 117 | ###* 118 | * Removes the popover, if it is displayed. 119 | ### 120 | removePopover: () -> 121 | if @attachedPopover 122 | @attachedPopover.dispose() 123 | @attachedPopover = null 124 | 125 | ###* 126 | * Retrieves a tooltip for the word given. 127 | * 128 | * @param {TextEditor} editor TextEditor to search for namespace of term. 129 | * @param {string} term Term to search for. 130 | * @param {Point} bufferPosition The cursor location the term is at. 131 | ### 132 | getTooltipForWord: (editor, term, bufferPosition) -> 133 | 134 | ###* 135 | * Gets the correct selector when a selector is clicked. 136 | * @param {jQuery.Event} event A jQuery event. 137 | * @return {object|null} A selector to be used with jQuery. 138 | ### 139 | getSelectorFromEvent: (event) -> 140 | return event.currentTarget 141 | 142 | ###* 143 | * Gets the correct element to attach the popover to from the retrieved selector. 144 | * @param {jQuery.Event} event A jQuery event. 145 | * @return {object|null} A selector to be used with jQuery. 146 | ### 147 | getPopoverElementFromSelector: (selector) -> 148 | return selector 149 | -------------------------------------------------------------------------------- /lib/tooltip/class-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | proxy = require './abstract-provider' 4 | AbstractProvider = require './abstract-provider' 5 | 6 | module.exports = 7 | 8 | class ClassProvider extends AbstractProvider 9 | hoverEventSelectors: '.syntax--entity.syntax--inherited-class, .syntax--support.syntax--namespace, .syntax--support.syntax--class, .syntax--comment-clickable .syntax--region' 10 | 11 | ###* 12 | * Retrieves a tooltip for the word given. 13 | * @param {TextEditor} editor TextEditor to search for namespace of term. 14 | * @param {string} term Term to search for. 15 | * @param {Point} bufferPosition The cursor location the term is at. 16 | ### 17 | getTooltipForWord: (editor, term, bufferPosition) -> 18 | fullClassName = @parser.getFullClassName(editor, term) 19 | 20 | proxy = require '../services/php-proxy.coffee' 21 | classInfo = proxy.methods(fullClassName) 22 | 23 | if not classInfo or not classInfo.wasFound 24 | return 25 | 26 | type = '' 27 | 28 | if classInfo.isClass 29 | type = (if classInfo.isAbstract then 'abstract ' else '') + 'class' 30 | 31 | else if classInfo.isTrait 32 | type = 'trait' 33 | 34 | else if classInfo.isInterface 35 | type = 'interface' 36 | 37 | # Create a useful description to show in the tooltip. 38 | description = '' 39 | 40 | description += "

" 41 | description += type + ' ' + '' + classInfo.shortName + ' — ' + classInfo.class 42 | description += '

' 43 | 44 | # Show the summary (short description). 45 | description += '
' 46 | description += (if classInfo.args.descriptions.short then classInfo.args.descriptions.short else '(No documentation available)') 47 | description += '
' 48 | 49 | # Show the (long) description. 50 | if classInfo.args.descriptions.long?.length > 0 51 | description += '
' 52 | description += "

Description

" 53 | description += "
" + classInfo.args.descriptions.long + "
" 54 | description += "
" 55 | 56 | return description 57 | 58 | ###* 59 | * Gets the correct selector when a class or namespace is clicked. 60 | * 61 | * @param {jQuery.Event} event A jQuery event. 62 | * 63 | * @return {object|null} A selector to be used with jQuery. 64 | ### 65 | getSelectorFromEvent: (event) -> 66 | return @parser.getClassSelectorFromEvent(event) 67 | 68 | ###* 69 | * Gets the correct element to attach the popover to from the retrieved selector. 70 | * @param {jQuery.Event} event A jQuery event. 71 | * @return {object|null} A selector to be used with jQuery. 72 | ### 73 | getPopoverElementFromSelector: (selector) -> 74 | # getSelectorFromEvent can return multiple items because namespaces and class names are different HTML elements. 75 | # We have to select one to attach the popover to. 76 | array = @$(selector).toArray() 77 | return array[array.length - 1] 78 | -------------------------------------------------------------------------------- /lib/tooltip/function-provider.coffee: -------------------------------------------------------------------------------- 1 | {Point} = require 'atom' 2 | {TextEditor} = require 'atom' 3 | 4 | AbstractProvider = require './abstract-provider' 5 | 6 | module.exports = 7 | 8 | class FunctionProvider extends AbstractProvider 9 | hoverEventSelectors: '.syntax--function-call' 10 | 11 | ###* 12 | * Retrieves a tooltip for the word given. 13 | * @param {TextEditor} editor TextEditor to search for namespace of term. 14 | * @param {string} term Term to search for. 15 | * @param {Point} bufferPosition The cursor location the term is at. 16 | ### 17 | getTooltipForWord: (editor, term, bufferPosition) -> 18 | value = @parser.getMemberContext(editor, term, bufferPosition) 19 | 20 | if not value 21 | return 22 | 23 | description = "" 24 | 25 | # Show the method's signature. 26 | accessModifier = '' 27 | returnType = '' 28 | 29 | if value.args.return?.type 30 | returnType = value.args.return.type 31 | 32 | if value.isPublic 33 | accessModifier = 'public' 34 | 35 | else if value.isProtected 36 | accessModifier = 'protected' 37 | 38 | else if not value.isFunction? 39 | accessModifier = 'private' 40 | 41 | description += "

" 42 | 43 | if value.isFunction? 44 | description += returnType + ' ' + term + '' + '(' 45 | else 46 | description += accessModifier + ' ' + returnType + ' ' + term + '' + '(' 47 | 48 | if value.args.parameters?.length > 0 49 | description += value.args.parameters.join(', '); 50 | 51 | if value.args.optionals?.length > 0 52 | description += '[' 53 | 54 | if value.args.parameters?.length > 0 55 | description += ', ' 56 | 57 | description += value.args.optionals.join(', ') 58 | description += ']' 59 | 60 | description += ')' 61 | description += '

' 62 | 63 | # Show the summary (short description). 64 | description += '
' 65 | description += (if value.args.descriptions.short then value.args.descriptions.short else '(No documentation available)') 66 | description += '
' 67 | 68 | # Show the (long) description. 69 | if value.args.descriptions.long?.length > 0 70 | description += '
' 71 | description += "

Description

" 72 | description += "
" + value.args.descriptions.long + "
" 73 | description += "
" 74 | 75 | # Show the parameters the method has. 76 | parametersDescription = "" 77 | 78 | for param,info of value.args.docParameters 79 | parametersDescription += "" 80 | 81 | parametersDescription += "• " 82 | 83 | if param in value.args.optionals 84 | parametersDescription += "[" + param + "]" 85 | 86 | else 87 | parametersDescription += param 88 | 89 | parametersDescription += "" 90 | 91 | parametersDescription += "" + (if info.type then info.type else ' ') + '' 92 | parametersDescription += "" + (if info.description then info.description else ' ') + '' 93 | 94 | parametersDescription += "" 95 | 96 | if parametersDescription.length > 0 97 | description += '
' 98 | description += "

Parameters

" 99 | description += "
" + parametersDescription + "
" 100 | description += "
" 101 | 102 | if value.args.return?.type 103 | returnValue = '' + value.args.return.type + '' 104 | 105 | if value.args.return.description 106 | returnValue += ' ' + value.args.return.description 107 | 108 | description += '
' 109 | description += "

Returns

" 110 | description += "
" + returnValue + "
" 111 | description += "
" 112 | 113 | # Show an overview of the exceptions the method can throw. 114 | throwsDescription = "" 115 | 116 | for exceptionType,thrownWhenDescription of value.args.throws 117 | throwsDescription += "
" 118 | throwsDescription += "• " + exceptionType + "" 119 | 120 | if thrownWhenDescription 121 | throwsDescription += ' ' + thrownWhenDescription 122 | 123 | throwsDescription += "
" 124 | 125 | if throwsDescription.length > 0 126 | description += '
' 127 | description += "

Throws

" 128 | description += "
" + throwsDescription + "
" 129 | description += "
" 130 | 131 | return description 132 | -------------------------------------------------------------------------------- /lib/tooltip/property-provider.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | AbstractProvider = require './abstract-provider' 4 | 5 | module.exports = 6 | 7 | class PropertyProvider extends AbstractProvider 8 | hoverEventSelectors: '.syntax--property' 9 | 10 | ###* 11 | * Retrieves a tooltip for the word given. 12 | * @param {TextEditor} editor TextEditor to search for namespace of term. 13 | * @param {string} term Term to search for. 14 | * @param {Point} bufferPosition The cursor location the term is at. 15 | ### 16 | getTooltipForWord: (editor, term, bufferPosition) -> 17 | value = @parser.getMemberContext(editor, term, bufferPosition) 18 | 19 | if not value 20 | return 21 | 22 | accessModifier = '' 23 | returnType = if value.args.return?.type then value.args.return.type else 'mixed' 24 | 25 | if value.isPublic 26 | accessModifier = 'public' 27 | 28 | else if value.isProtected 29 | accessModifier = 'protected' 30 | 31 | else 32 | accessModifier = 'private' 33 | 34 | # Create a useful description to show in the tooltip. 35 | description = '' 36 | 37 | description += "

" 38 | description += accessModifier + ' ' + returnType + '' + ' $' + term + '' 39 | description += '

' 40 | 41 | # Show the summary (short description). 42 | description += '
' 43 | description += (if value.args.descriptions.short then value.args.descriptions.short else '(No documentation available)') 44 | description += '
' 45 | 46 | # Show the (long) description. 47 | if value.args.descriptions.long?.length > 0 48 | description += '
' 49 | description += "

Description

" 50 | description += "
" + value.args.descriptions.long + "
" 51 | description += "
" 52 | 53 | if value.args.return?.type 54 | returnValue = '' + value.args.return.type + '' 55 | 56 | if value.args.return.description 57 | returnValue += ' ' + value.args.return.description 58 | 59 | description += '
' 60 | description += "

Type

" 61 | description += "
" + returnValue + "
" 62 | description += "
" 63 | 64 | return description 65 | -------------------------------------------------------------------------------- /lib/tooltip/tooltip-manager.coffee: -------------------------------------------------------------------------------- 1 | ClassProvider = require './class-provider.coffee' 2 | FunctionProvider = require './function-provider.coffee' 3 | PropertyProvider = require './property-provider.coffee' 4 | 5 | module.exports = 6 | 7 | class TooltipManager 8 | providers: [] 9 | 10 | ###* 11 | * Initializes the tooltip providers. 12 | ### 13 | init: () -> 14 | @providers.push new ClassProvider() 15 | @providers.push new FunctionProvider() 16 | @providers.push new PropertyProvider() 17 | 18 | for provider in @providers 19 | provider.init(@) 20 | 21 | ###* 22 | * Deactivates the tooltip providers. 23 | ### 24 | deactivate: () -> 25 | for provider in @providers 26 | provider.deactivate() 27 | -------------------------------------------------------------------------------- /lib/views/class-list-view.coffee: -------------------------------------------------------------------------------- 1 | {$$, SelectListView} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | 5 | class ClassListView extends SelectListView 6 | initialize: (@suggestions, @onConfirm) -> 7 | super 8 | @show() 9 | @setItems @suggestions.map((suggestion) -> 10 | return {name: suggestion.text} 11 | ) 12 | @focusFilterEditor() 13 | @currentPane = atom.workspace.getActivePane() 14 | 15 | getFilterKey: -> 'name' 16 | 17 | show: -> 18 | @panel ?= atom.workspace.addModalPanel(item: this) 19 | @panel.show() 20 | @storeFocusedElement() 21 | 22 | cancelled: -> @hide() 23 | 24 | hide: -> @panel?.destroy() 25 | 26 | viewForItem: ({name}) -> 27 | $$ -> 28 | @li name 29 | 30 | confirmed: (item) -> 31 | @onConfirm(item) 32 | @cancel() 33 | @currentPane.activate() if @currentPane?.isAlive() 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-autocomplete-php", 3 | "main": "./lib/peekmo-php-atom-autocomplete", 4 | "version": "0.25.6", 5 | "description": "Atom autocompletion plugin for PHP language", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Peekmo/atom-autocomplete-php.git" 9 | }, 10 | "license": "MIT", 11 | "engines": { 12 | "atom": ">0.50.0" 13 | }, 14 | "providedServices": { 15 | "autocomplete.provider": { 16 | "versions": { 17 | "2.0.0": "getProvider" 18 | } 19 | }, 20 | "php.autocomplete.tools": { 21 | "versions": { 22 | "0.16.0": "provideAutocompleteTools" 23 | } 24 | } 25 | }, 26 | "dependencies": { 27 | "fuzzaldrin": "^2.1.0", 28 | "md5": "2.0.0", 29 | "sub-atom": "~1.0.0", 30 | "jquery": "~2.1.4", 31 | "atom-space-pen-views": "~2.0.5" 32 | }, 33 | "consumedServices": { 34 | "status-bar": { 35 | "versions": { 36 | "^1.0.0": "consumeStatusBar" 37 | } 38 | }, 39 | "autocomplete.php.plugin": { 40 | "versions": { 41 | "1.0.0": "consumePlugin" 42 | } 43 | } 44 | }, 45 | "keywords": [ 46 | "php", 47 | "composer", 48 | "autocomplete" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /php/Config.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * Output PHP errors and exceptions in JSON. 6 | **/ 7 | 8 | namespace Peekmo\AtomAutocompletePhp; 9 | 10 | class ErrorHandler 11 | { 12 | static private $reserve; 13 | 14 | /** 15 | * Set up error handling. 16 | */ 17 | public static function register() 18 | { 19 | // Keep some room in case of memory exhaustion 20 | self::$reserve = str_repeat(' ', 4096); 21 | 22 | // Ensure nothing will be send to stdout 23 | ini_set('display_errors', false); 24 | ini_set('log_errors', false); 25 | 26 | // Register our handlers 27 | register_shutdown_function('Peekmo\AtomAutocompletePhp\ErrorHandler::onShutdown'); 28 | set_error_handler('Peekmo\AtomAutocompletePhp\ErrorHandler::onError'); 29 | set_exception_handler('Peekmo\AtomAutocompletePhp\ErrorHandler::onException'); 30 | } 31 | 32 | /** 33 | * @throws ErrorException on any error. 34 | */ 35 | public static function onError($code, $message, $file, $line, $context) 36 | { 37 | throw new \ErrorException($message, $code, 1, $file, $line); 38 | } 39 | 40 | /** 41 | * Display uncaught exception in JSON. 42 | * 43 | * @param \Exception $exception 44 | */ 45 | public static function onException(\Exception $exception) 46 | { 47 | die( 48 | json_encode( 49 | array('error' => 50 | array( 51 | 'message' => $exception->getMessage(), 52 | 'code' => $exception->getCode(), 53 | 'file' => $exception->getFile(), 54 | 'line' => $exception->getLine(), 55 | ) 56 | ) 57 | ) 58 | ); 59 | } 60 | 61 | /** 62 | * Display any fatal error in JSON. 63 | */ 64 | public static function onShutdown() 65 | { 66 | // Do nothing 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /php/parser.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * This script returns all functions, classes & methods in the given directory. 11 | * Internals and user's one 12 | **/ 13 | 14 | use Peekmo\AtomAutocompletePhp\ErrorHandler; 15 | use Peekmo\AtomAutocompletePhp\Config; 16 | 17 | require_once(__DIR__ . '/ErrorHandler.php'); 18 | ErrorHandler::register(); 19 | 20 | require_once(__DIR__ . '/tmp.php'); 21 | require_once(__DIR__ . '/Config.php'); 22 | require_once(__DIR__ . '/services/Tools.php'); 23 | require_once(__DIR__ . '/services/DocParser.php'); 24 | require_once(__DIR__ . '/services/FileParser.php'); 25 | require_once(__DIR__ . '/providers/ProviderInterface.php'); 26 | require_once(__DIR__ . '/providers/AutocompleteProvider.php'); 27 | require_once(__DIR__ . '/providers/MethodsProvider.php'); 28 | require_once(__DIR__ . '/providers/ClassProvider.php'); 29 | require_once(__DIR__ . '/providers/ConstantsProvider.php'); 30 | require_once(__DIR__ . '/providers/FunctionsProvider.php'); 31 | require_once(__DIR__ . '/providers/ClassMapRefresh.php'); 32 | require_once(__DIR__ . '/providers/DocParamProvider.php'); 33 | 34 | $commands = array( 35 | '--class' => 'Peekmo\AtomAutocompletePhp\ClassProvider', 36 | '--methods' => 'Peekmo\AtomAutocompletePhp\MethodsProvider', 37 | '--functions' => 'Peekmo\AtomAutocompletePhp\FunctionsProvider', 38 | '--constants' => 'Peekmo\AtomAutocompletePhp\ConstantsProvider', 39 | '--refresh' => 'Peekmo\AtomAutocompletePhp\ClassMapRefresh', 40 | '--autocomplete' => 'Peekmo\AtomAutocompletePhp\AutocompleteProvider', 41 | '--doc-params' => 'Peekmo\AtomAutocompletePhp\DocParamProvider' 42 | ); 43 | 44 | /** 45 | * Print an error 46 | * @param string $message 47 | */ 48 | function show_error($message) { 49 | die(json_encode(array('error' => array('message' => $message)))); 50 | } 51 | 52 | if (count($argv) < 3) { 53 | die('Usage : php parser.php '); 54 | } 55 | 56 | $project = $argv[1]; 57 | $command = $argv[2]; 58 | 59 | if (!isset($commands[$command])) { 60 | show_error(sprintf('Command %s not found', $command)); 61 | } 62 | 63 | // Config 64 | Config::set('composer', $config['composer']); 65 | Config::set('php', $config['php']); 66 | Config::set('projectPath', $project); 67 | 68 | // To see if it fix #19 69 | chdir(Config::get('projectPath')); 70 | $indexDir = __DIR__ . '/../indexes/' . md5($project); 71 | if (!is_dir($indexDir)) { 72 | if (false === mkdir($indexDir, 0777, true)) { 73 | show_error('Unable to create directory ' . $indexDir); 74 | } 75 | } 76 | 77 | Config::set('indexClasses', $indexDir . '/index.classes.json'); 78 | 79 | foreach ($config['autoload'] as $conf) { 80 | $path = sprintf('%s/%s', $project, trim($conf, '/')); 81 | if (file_exists($path)) { 82 | require_once($path); 83 | } 84 | } 85 | 86 | foreach ($config['classmap'] as $conf) { 87 | $path = sprintf('%s/%s', $project, trim($conf, '/')); 88 | if (file_exists($path)) { 89 | Config::set('classmap_file', $path); 90 | break; 91 | } 92 | } 93 | 94 | $new = new $commands[$command](); 95 | 96 | $args = array_slice($argv, 3); 97 | foreach ($args as &$arg) { 98 | $arg = str_replace('\\\\', '\\', $arg); 99 | } 100 | 101 | $data = $new->execute($args); 102 | 103 | if (false === $encoded = json_encode($data)) { 104 | echo json_encode(array()); 105 | } else { 106 | echo $encoded; 107 | } 108 | -------------------------------------------------------------------------------- /php/providers/AutocompleteProvider.php: -------------------------------------------------------------------------------- 1 | getClassMetadata($class); 32 | 33 | if (isset($data['values'][$name])) { 34 | $memberInfo = $data['values'][$name]; 35 | 36 | if (!isset($data['values'][$name]['isMethod'])) { 37 | foreach ($data['values'][$name] as $value) { 38 | if ($value['isMethod'] && $isMethod) { 39 | $memberInfo = $value; 40 | } elseif (!$value['isMethod'] && !$isMethod) { 41 | $memberInfo = $value; 42 | } 43 | } 44 | } 45 | 46 | $returnValue = $memberInfo['args']['return']['type']; 47 | 48 | if ($returnValue == '$this' || $returnValue == 'static') { 49 | $relevantClass = $class; 50 | } elseif ($returnValue === 'self') { 51 | $relevantClass = $memberInfo['declaringClass']['name']; 52 | } else { 53 | $soleClassName = $this->getSoleClassName($returnValue); 54 | 55 | if ($soleClassName) { 56 | // At this point, this could either be a class name relative to the current namespace or a full 57 | // class name without a leading slash. For example, Foo\Bar could also be relative (e.g. 58 | // My\Foo\Bar), in which case its absolute path is determined by the namespace and use statements 59 | // of the file containing it. 60 | $relevantClass = $soleClassName; 61 | 62 | if (!empty($soleClassName) && $soleClassName[0] !== "\\") { 63 | $parser = new FileParser($memberInfo['declaringStructure']['filename']); 64 | 65 | $useStatementFound = false; 66 | $completedClassName = $parser->getFullClassName($soleClassName, $useStatementFound); 67 | 68 | if ($useStatementFound) { 69 | $relevantClass = $completedClassName; 70 | } else { 71 | // Try instantiating the class, e.g. My\Foo\Bar. 72 | try { 73 | // We don't care about the result. 74 | new \ReflectionClass($completedClassName); 75 | 76 | $relevantClass = $completedClassName; 77 | } catch (\Exception $e) { 78 | // The class, e.g. My\Foo\Bar, didn't exist. We can only assume its an absolute path, 79 | // using a namespace set up in composer.json, without a leading slash. 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | // Minor optimization to avoid fetching the same data twice. 88 | return ($relevantClass === $class) ? $data : $this->getClassMetadata($relevantClass); 89 | } 90 | 91 | /** 92 | * Retrieves the sole class name from the specified return value statement. 93 | * 94 | * @example "null" returns null. 95 | * @example "FooClass" returns "FooClass". 96 | * @example "FooClass|null" returns "FooClass". 97 | * @example "FooClass|BarClass|null" returns null (there is no single type). 98 | * 99 | * @param string $returnValueStatement 100 | * 101 | * @return string|null 102 | */ 103 | protected function getSoleClassName($returnValueStatement) 104 | { 105 | if ($returnValueStatement) { 106 | $types = explode(DocParser::TYPE_SPLITTER, $returnValueStatement); 107 | 108 | $classTypes = array(); 109 | 110 | foreach ($types as $type) { 111 | if ($this->isClassType($type)) { 112 | $classTypes[] = $type; 113 | } 114 | } 115 | 116 | if (count($classTypes) === 1) { 117 | return $classTypes[0]; 118 | } 119 | } 120 | 121 | return null; 122 | } 123 | 124 | /** 125 | * Returns a boolean indicating if the specified value is a class type or not. 126 | * 127 | * @param string $type 128 | * 129 | * @return bool 130 | */ 131 | protected function isClassType($type) 132 | { 133 | return ucfirst($type) === $type; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /php/providers/ClassMapRefresh.php: -------------------------------------------------------------------------------- 1 | 0 && $file = $args[0]) { 18 | if (file_exists(Config::get('indexClasses'))) { 19 | $index = json_decode(file_get_contents(Config::get('indexClasses')), true); 20 | 21 | // Invalid json (#24) 22 | if (false !== $index) { 23 | $fileExists = true; 24 | 25 | $found = false; 26 | $fileParser = new FileParser($file); 27 | $class = $fileParser->getFullClassName(null, $found); 28 | 29 | // if (false !== $class = array_search($file, $classMap)) { 30 | if ($found) { 31 | if (isset($index['mapping'][$class])) { 32 | unset($index['mapping'][$class]); 33 | } 34 | 35 | if (false !== $key = array_search($class, $index['autocomplete'])) { 36 | unset($index['autocomplete'][$key]); 37 | $index['autocomplete'] = array_values($index['autocomplete']); 38 | } 39 | 40 | if ($value = $this->buildIndexClass($class)) { 41 | $index['mapping'][$class] = $value; 42 | $index['autocomplete'][] = $class; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | // Otherwise, full index 50 | if (!$fileExists) { 51 | // Autoload classes 52 | foreach (array_keys($this->getClassMap(true)) as $class) { 53 | if ($value = $this->buildIndexClass($class)) { 54 | $index['mapping'][$class] = $value; 55 | $index['autocomplete'][] = $class; 56 | } 57 | } 58 | 59 | $this->includeOldDrupal(); 60 | 61 | // Internal classes 62 | foreach (get_declared_classes() as $class) { 63 | $provider = new ClassProvider(); 64 | 65 | if ($value = $provider->execute(array($class, true))) { 66 | if (!empty($value)) { 67 | $index['mapping'][$class] = $value; 68 | $index['autocomplete'][] = $class; 69 | } 70 | } 71 | } 72 | } 73 | 74 | file_put_contents(Config::get('indexClasses'), json_encode($index)); 75 | 76 | return array(); 77 | } 78 | 79 | protected function buildIndexClass($class) 80 | { 81 | $ret = exec(sprintf('%s %s %s --class %s', 82 | escapeshellarg(Config::get('php')), 83 | escapeshellarg(__DIR__ . '/../parser.php'), 84 | escapeshellarg(Config::get('projectPath')), 85 | escapeshellarg($class) 86 | )); 87 | 88 | if (false === $value = json_decode($ret, true)) { 89 | return null; 90 | } 91 | 92 | if (isset($value['error'])) { 93 | return null; 94 | } 95 | 96 | return $value; 97 | } 98 | 99 | 100 | /** 101 | * Check if the project is Drupal 6/7 and include the necessary files to get the maximum functions as possible 102 | */ 103 | public function includeOldDrupal() 104 | { 105 | $project = Config::get('projectPath'); 106 | 107 | if (file_exists($project . '/misc') && file_exists($project . '/modules') && file_exists($project . '/sites')) { 108 | define('DRUPAL_ROOT', $project); 109 | include_once DRUPAL_ROOT . '/includes/bootstrap.inc'; 110 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /php/providers/ClassProvider.php: -------------------------------------------------------------------------------- 1 | $e->getMessage()); 21 | } 22 | 23 | if ($internal && !$reflection->isInternal()) { 24 | return array(); 25 | } 26 | 27 | $ctor = $reflection->getConstructor(); 28 | 29 | $args = array(); 30 | if (!is_null($ctor)) { 31 | $args = $this->getMethodArguments($ctor); 32 | } 33 | 34 | return array( 35 | 'class' => $this->getClassArguments($reflection), 36 | 'methods' => array( 37 | 'constructor' => array( 38 | 'has' => !is_null($ctor), 39 | 'args' => $args 40 | ) 41 | ) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /php/providers/ConstantsProvider.php: -------------------------------------------------------------------------------- 1 | array(), 19 | 'values' => array() 20 | ); 21 | 22 | foreach (get_defined_constants(true) as $namespace => $constantList) { 23 | // We don't want constants from our own code showing up, but we don't select the internal namespace 24 | // explicitly as there might be installed extensions such as PCRE adding globals as well. 25 | if ($namespace === 'user') { 26 | continue; 27 | } 28 | 29 | // NOTE: Be very careful if you want to pass back the value, there are also escaped paths, newlines 30 | // (PHP_EOL), etc. in there. 31 | foreach (array_keys($constantList) as $constantName) { 32 | $constants['names'][] = $constantName; 33 | $constants['values'][$constantName] = array( 34 | array( 35 | // NOTE: No additional information is available at the moment, but keep the format consistent. 36 | ) 37 | ); 38 | } 39 | } 40 | 41 | return $constants; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /php/providers/DocParamProvider.php: -------------------------------------------------------------------------------- 1 | get($class, 'method', $name, array(DocParser::PARAM_TYPE)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /php/providers/FunctionsProvider.php: -------------------------------------------------------------------------------- 1 | includeOldDrupal(); 15 | 16 | $functions = array( 17 | 'names' => array(), 18 | 'values' => array() 19 | ); 20 | 21 | $allFunctions = get_defined_functions(); 22 | 23 | foreach ($allFunctions as $type => $currentFunctions) { 24 | foreach ($currentFunctions as $functionName) { 25 | try { 26 | $function = new \ReflectionFunction($functionName); 27 | } catch (\Exception $e) { 28 | continue; 29 | } 30 | 31 | $functions['names'][] = $function->getName(); 32 | 33 | $args = $this->getMethodArguments($function); 34 | 35 | $functions['values'][$function->getName()] = array( 36 | array( 37 | 'isInternal' => $type == 'internal', 38 | 'isMethod' => true, 39 | 'isFunction' => true, 40 | 'args' => $args, 41 | 'declaringStructure' => [ 42 | 'filename' => $function->getFileName(), 43 | ], 44 | 'startLine' => $function->getStartLine() 45 | ) 46 | ); 47 | } 48 | } 49 | 50 | return $functions; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /php/providers/MethodsProvider.php: -------------------------------------------------------------------------------- 1 | getClassMetadata($class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /php/providers/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | getDocComment(); 51 | return $this->parse($comment, $filters, $name); 52 | } 53 | 54 | /** 55 | * Parse the comment string to get its elements 56 | * 57 | * @param string|false|null $docblock The docblock to parse. If null, the return array will be filled up with the 58 | * correct keys, but they will be empty. 59 | * @param array $filters Elements to search (see constants). 60 | * @param string $itemName The name of the item (method, class, ...) the docblock is for. 61 | * 62 | * @return array 63 | */ 64 | public function parse($docblock, array $filters, $itemName) 65 | { 66 | if (empty($filters)) { 67 | return array(); 68 | } 69 | 70 | $tags = array(); 71 | $result = array(); 72 | $matches = array(); 73 | 74 | $docblock = is_string($docblock) ? $docblock : null; 75 | 76 | if ($docblock) { 77 | preg_match_all('/\*\s+(@[a-z-]+)([^@]*)\n/', $docblock, $matches, PREG_SET_ORDER); 78 | 79 | foreach ($matches as $match) { 80 | if (!isset($tags[$match[1]])) { 81 | $tags[$match[1]] = array(); 82 | } 83 | 84 | $tagValue = $match[2]; 85 | $tagValue = $this->normalizeNewlines($tagValue); 86 | 87 | // Remove the delimiters of the docblock itself at the start of each line, if any. 88 | $tagValue = preg_replace('/\n\s+\*\s*/', ' ', $tagValue); 89 | 90 | // Collapse multiple spaces, just like HTML does. 91 | $tagValue = preg_replace('/\s\s+/', ' ', $tagValue); 92 | 93 | $tags[$match[1]][] = trim($tagValue); 94 | } 95 | } 96 | 97 | $filterMethodMap = array( 98 | static::RETURN_VALUE => 'filterReturn', 99 | static::PARAM_TYPE => 'filterParams', 100 | static::VAR_TYPE => 'filterVar', 101 | static::PROPERTY => 'filterProperty', 102 | static::METHOD => 'filterMethod', 103 | static::DEPRECATED => 'filterDeprecated', 104 | static::THROWS => 'filterThrows', 105 | static::DESCRIPTION => 'filterDescription' 106 | ); 107 | 108 | foreach ($filters as $filter) { 109 | if (!isset($filterMethodMap[$filter])) { 110 | throw new \UnexpectedValueException('Unknown filter passed!'); 111 | } 112 | 113 | $result = array_merge( 114 | $result, 115 | $this->{$filterMethodMap[$filter]}($docblock, $itemName, $tags) 116 | ); 117 | } 118 | 119 | return $result; 120 | } 121 | 122 | /** 123 | * Returns an array of three values, the first value will go up until the first space, the second value will go up 124 | * until the second space, and the third value will contain the rest of the string. Convenience method for tags that 125 | * consist of three parameters. 126 | * 127 | * @param string $value 128 | * 129 | * @return string[] 130 | */ 131 | protected function filterThreeParameterTag($value) 132 | { 133 | $parts = explode(' ', $value); 134 | 135 | $firstPart = trim(array_shift($parts)); 136 | $secondPart = trim(array_shift($parts)); 137 | 138 | if (!empty($parts)) { 139 | $thirdPart = trim(implode(' ', $parts)); 140 | } else { 141 | $thirdPart = null; 142 | } 143 | 144 | return array($firstPart ?: null, $secondPart ?: null, $thirdPart); 145 | } 146 | 147 | /** 148 | * Returns an array of two values, the first value will go up until the first space and the second value will 149 | * contain the rest of the string. Convenience method for tags that consist of two parameters. 150 | * 151 | * @param string $value 152 | * 153 | * @return string[] 154 | */ 155 | protected function filterTwoParameterTag($value) 156 | { 157 | list($firstPart, $secondPart, $thirdPart) = $this->filterThreeParameterTag($value); 158 | 159 | return array($firstPart, trim($secondPart . ' ' . $thirdPart)); 160 | } 161 | 162 | /** 163 | * Filters out information about the return value of the function or method. 164 | * 165 | * @param string $docblock 166 | * @param string $methodName 167 | * @param array $tags 168 | * 169 | * @return array 170 | */ 171 | protected function filterReturn($docblock, $methodName, array $tags) 172 | { 173 | if (isset($tags[static::RETURN_VALUE])) { 174 | list($type, $description) = $this->filterTwoParameterTag($tags[static::RETURN_VALUE][0]); 175 | } else { 176 | // According to http://www.phpdoc.org/docs/latest/guides/docblocks.html, a method that does 177 | // have a docblock, but no explicit return type returns void. Constructors, however, must 178 | // return self. If there is no docblock at all, we can't assume either of these types. 179 | $type = ($methodName === '__construct') ? 'self' : 'void'; 180 | $description = null; 181 | } 182 | 183 | return array( 184 | 'return' => array( 185 | 'type' => $type, 186 | 'description' => utf8_encode($description) 187 | ) 188 | ); 189 | } 190 | 191 | /** 192 | * Filters out information about the parameters of the function or method. 193 | * 194 | * @param string $docblock 195 | * @param string $methodName 196 | * @param array $tags 197 | * 198 | * @return array 199 | */ 200 | protected function filterParams($docblock, $methodName, array $tags) 201 | { 202 | $params = array(); 203 | 204 | if (isset($tags[static::PARAM_TYPE])) { 205 | foreach ($tags[static::PARAM_TYPE] as $tag) { 206 | list($type, $variableName, $description) = $this->filterThreeParameterTag($tag); 207 | 208 | $params[$variableName] = array( 209 | 'type' => $type, 210 | 'description' => utf8_encode($description) 211 | ); 212 | } 213 | } 214 | 215 | return array( 216 | 'params' => $params 217 | ); 218 | } 219 | 220 | /** 221 | * Filters out information about the variable. 222 | * 223 | * @param string $docblock 224 | * @param string $methodName 225 | * @param array $tags 226 | * 227 | * @return array 228 | */ 229 | protected function filterVar($docblock, $methodName, array $tags) 230 | { 231 | if (isset($tags[static::VAR_TYPE])) { 232 | list($type, $description) = $this->filterTwoParameterTag($tags[static::VAR_TYPE][0]); 233 | } else { 234 | $type = null; 235 | $description = null; 236 | } 237 | 238 | return array( 239 | 'var' => array( 240 | 'type' => $type, 241 | 'description' => utf8_encode($description) 242 | ) 243 | ); 244 | } 245 | 246 | /** 247 | * Filters out information about the property. 248 | * 249 | * @param string $docblock 250 | * @param string $methodName 251 | * @param array $tags 252 | * 253 | * @return array 254 | */ 255 | protected function filterProperty($docblock, $methodName, array $tags) 256 | { 257 | $properties = array(); 258 | 259 | if (isset($tags[static::PROPERTY])) { 260 | foreach ($tags[static::PROPERTY] as $tag) { 261 | list($type, $variableName) = $this->filterTwoParameterTag($tag); 262 | 263 | $properties[$variableName] = array( 264 | 'type' => $type, 265 | 'description' => null 266 | ); 267 | } 268 | } 269 | 270 | return array( 271 | 'properties' => $properties 272 | ); 273 | } 274 | 275 | /** 276 | * Filters out information about the return value of the method. 277 | * 278 | * @param string $docblock 279 | * @param string $methodName 280 | * @param array $tags 281 | * 282 | * @return array 283 | */ 284 | protected function filterMethod($docblock, $methodName, array $tags) 285 | { 286 | $methods = array(); 287 | 288 | if (isset($tags[static::METHOD])) { 289 | foreach ($tags[static::METHOD] as $tag) { 290 | list($type, $description) = $this->filterTwoParameterTag($tag); 291 | list($methodName, $args) = explode('(', utf8_encode($description)); 292 | 293 | $methods[$methodName] = array( 294 | 'type' => $type, 295 | 'description' => null 296 | ); 297 | } 298 | } 299 | 300 | return array( 301 | 'methods' => $methods, 302 | ); 303 | } 304 | 305 | /** 306 | * Filters out deprecation information. 307 | * 308 | * @param string $docblock 309 | * @param string $methodName 310 | * @param array $tags 311 | * 312 | * @return array 313 | */ 314 | protected function filterDeprecated($docblock, $methodName, array $tags) 315 | { 316 | return array( 317 | 'deprecated' => isset($tags[static::DEPRECATED]) 318 | ); 319 | } 320 | 321 | /** 322 | * Filters out information about what exceptions the method can throw. 323 | * 324 | * @param string $docblock 325 | * @param string $methodName 326 | * @param array $tags 327 | * 328 | * @return array 329 | */ 330 | protected function filterThrows($docblock, $methodName, array $tags) 331 | { 332 | $throws = array(); 333 | 334 | if (isset($tags[static::THROWS])) { 335 | foreach ($tags[static::THROWS] as $tag) { 336 | list($type, $description) = $this->filterTwoParameterTag($tag); 337 | 338 | $throws[$type] = utf8_encode($description); 339 | } 340 | } 341 | 342 | return array( 343 | 'throws' => $throws 344 | ); 345 | } 346 | 347 | /** 348 | * Filters out information about the description. 349 | * 350 | * @param string $docblock 351 | * @param string $methodName 352 | * @param array $tags 353 | * 354 | * @return array 355 | */ 356 | protected function filterDescription($docblock, $methodName, array $tags) 357 | { 358 | $summary = ''; 359 | $description = ''; 360 | 361 | $lines = explode("\n", $docblock); 362 | 363 | $isReadingSummary = true; 364 | 365 | foreach ($lines as $line) { 366 | if (preg_match(self::TAG_START_REGEX, $line) === 1) { 367 | break; // Found the start of a tag, the summary and description are finished. 368 | } 369 | 370 | // Remove the opening and closing tags. 371 | $line = preg_replace('/^\s*(?:\/)?\*+(?:\/)?/', '', $line); 372 | $line = preg_replace('/\s*\*+\/$/', '', $line); 373 | 374 | $line = trim($line); 375 | 376 | if ($isReadingSummary && empty($line) && !empty($summary)) { 377 | $isReadingSummary = false; 378 | } elseif ($isReadingSummary) { 379 | $summary = empty($summary) ? $line : ($summary . "\n" . $line); 380 | } else { 381 | $description = empty($description) ? $line : ($description . "\n" . $line); 382 | } 383 | } 384 | 385 | return array( 386 | 'descriptions' => array( 387 | 'short' => trim(utf8_encode($summary)), 388 | 'long' => trim(utf8_encode($description)) 389 | ) 390 | ); 391 | } 392 | 393 | /** 394 | * Retrieves the specified string with its line separators replaced with the specifed separator. 395 | * 396 | * @param string $string 397 | * @param string $replacement 398 | * 399 | * @return string 400 | */ 401 | protected function replaceNewlines($string, $replacement) 402 | { 403 | return str_replace(array("\n", "\r\n", PHP_EOL), $replacement, $string); 404 | } 405 | 406 | /** 407 | * Normalizes all types of newlines to the "\n" separator. 408 | * 409 | * @param string $string 410 | * 411 | * @return string 412 | */ 413 | protected function normalizeNewlines($string) 414 | { 415 | return $this->replaceNewlines($string, "\n"); 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /php/services/FileParser.php: -------------------------------------------------------------------------------- 1 | file = fopen($filePath, 'r'); 31 | } 32 | 33 | /** 34 | * Destructor. 35 | */ 36 | public function __destruct() 37 | { 38 | fclose($this->file); 39 | } 40 | 41 | /** 42 | * Retrieves the full class name of the given class, based on the namespace and use statements in the current file. 43 | * 44 | * @param string|null $className The class to search for. If null, the full class name of the first 45 | * class/trait/interface definition will be returned. 46 | * @param bool $found Set to true if an explicit use statement was found. If false, the full class name 47 | * could, for example, have been built using the namespace of the current file. 48 | * 49 | * @return string 50 | */ 51 | public function getFullClassName($className, &$found) 52 | { 53 | if (!empty($className) && $className[0] == "\\") { 54 | return substr($className, 1); // FQCN, not subject to any further context. 55 | } 56 | 57 | // Reserved keyword 58 | if (in_array($className, $this->keywords)) { 59 | return $className; 60 | } 61 | 62 | $line = ''; 63 | $found = false; 64 | $matches = array(); 65 | $fullClass = $className; 66 | 67 | while (!feof($this->file)) { 68 | $line = fgets($this->file); 69 | 70 | if (preg_match(self::NAMESPACE_PATTERN, $line, $matches) === 1) { 71 | // The class name is relative to the namespace of the class it is contained in, unless a use statement 72 | // says otherwise. 73 | $fullClass = $matches[1] . '\\' . $className; 74 | } elseif ($className && preg_match(self::USE_PATTERN, $line, $matches) === 1) { 75 | $classNameParts = explode('\\', $className); 76 | $importNameParts = explode('\\', $matches[1]); 77 | 78 | $isAliasedImport = isset($matches[2]); 79 | 80 | if (($isAliasedImport && $matches[2] === $classNameParts[0]) || 81 | (!$isAliasedImport && $importNameParts[count($importNameParts) - 1] === $classNameParts[0])) { 82 | $found = true; 83 | 84 | $fullClass = $matches[1]; 85 | 86 | array_shift($classNameParts); 87 | 88 | if (!empty($classNameParts)) { 89 | $fullClass .= '\\' . implode('\\', $classNameParts); 90 | } 91 | 92 | break; 93 | } 94 | } 95 | 96 | if (preg_match(self::DEFINITION_PATTERN, $line, $matches) === 1) { 97 | if (!$className) { 98 | $found = true; 99 | $fullClass .= $matches[1]; 100 | } 101 | 102 | break; 103 | } 104 | } 105 | 106 | if ($fullClass && $fullClass[0] == '\\') { 107 | $fullClass = substr($fullClass, 1); 108 | } 109 | 110 | return $fullClass; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /php/services/Tools.php: -------------------------------------------------------------------------------- 1 | classMap) || $force) { 27 | if (Config::get('classmap_file') && !file_exists(Config::get('classmap_file')) || $force) { 28 | // Check if composer is executable or not 29 | if (is_executable(Config::get('composer'))) { 30 | exec(sprintf('%s dump-autoload --optimize --quiet --no-interaction --working-dir=%s 2>&1', 31 | escapeshellarg(Config::get('composer')), 32 | escapeshellarg(Config::get('projectPath')) 33 | )); 34 | } else { 35 | exec(sprintf('%s %s dump-autoload --optimize --quiet --no-interaction --working-dir=%s 2>&1', 36 | escapeshellarg(Config::get('php')), 37 | escapeshellarg(Config::get('composer')), 38 | escapeshellarg(Config::get('projectPath')) 39 | )); 40 | } 41 | } 42 | 43 | if (Config::get('classmap_file')) { 44 | $this->classMap = require(Config::get('classmap_file')); 45 | } 46 | } 47 | 48 | return $this->classMap; 49 | } 50 | 51 | /** 52 | * Fetches information about the specified class, trait, interface, ... 53 | * 54 | * @param ReflectionClass $class The class to analyze. 55 | * 56 | * @return array 57 | */ 58 | protected function getClassArguments(ReflectionClass $class) 59 | { 60 | $parser = new DocParser(); 61 | $docComment = $class->getDocComment() ?: ''; 62 | 63 | $docParseResult = $parser->parse($docComment, array( 64 | DocParser::DEPRECATED, 65 | DocParser::DESCRIPTION 66 | ), $class->getShortName()); 67 | 68 | return array( 69 | 'descriptions' => $docParseResult['descriptions'], 70 | 'deprecated' => $docParseResult['deprecated'] 71 | ); 72 | } 73 | 74 | /** 75 | * Fetches information about the specified method or function, such as its parameters, a description from the 76 | * docblock (if available), the return type, ... 77 | * 78 | * @param ReflectionFunctionAbstract $function The function or method to analyze. 79 | * 80 | * @return array 81 | */ 82 | protected function getMethodArguments(ReflectionFunctionAbstract $function) 83 | { 84 | $args = $function->getParameters(); 85 | 86 | $optionals = array(); 87 | $parameters = array(); 88 | 89 | foreach ($args as $argument) { 90 | $value = '$' . $argument->getName(); 91 | 92 | if ($argument->isPassedByReference()) { 93 | $value = '&' . $value; 94 | } 95 | 96 | if ($argument->isOptional()) { 97 | $optionals[] = $value; 98 | } else { 99 | $parameters[] = $value; 100 | } 101 | } 102 | 103 | // For variadic methods, append three dots to the last argument (if any) to indicate this to the user. This 104 | // requires PHP >= 5.6. 105 | if (!empty($args) && method_exists($function, 'isVariadic') && $function->isVariadic()) { 106 | $lastArgument = $args[count($args) - 1]; 107 | 108 | if ($lastArgument->isOptional()) { 109 | $optionals[count($optionals) - 1] .= '...'; 110 | } else { 111 | $parameters[count($parameters) - 1] .= '...'; 112 | } 113 | } 114 | 115 | $parser = new DocParser(); 116 | $docComment = $function->getDocComment(); 117 | 118 | $docParseResult = $parser->parse($docComment, array( 119 | DocParser::THROWS, 120 | DocParser::PARAM_TYPE, 121 | DocParser::DEPRECATED, 122 | DocParser::DESCRIPTION, 123 | DocParser::RETURN_VALUE 124 | ), $function->name); 125 | 126 | $docblockInheritsLongDescription = false; 127 | 128 | // Ticket #86 - Add support for inheriting the entire docblock from the parent if the current docblock contains 129 | // nothing but these tags. Note that, according to draft PSR-5 and phpDocumentor's implementation, this is 130 | // incorrect. However, some large frameworks (such as Symfony) use this and it thus makes life easier for many 131 | // developers, hence this workaround. 132 | if (in_array($docParseResult['descriptions']['short'], array('{@inheritdoc}', '{@inheritDoc}'))) { 133 | $docComment = false; // Pretend there is no docblock. 134 | } 135 | 136 | if (strpos($docParseResult['descriptions']['long'], DocParser::INHERITDOC) !== false) { 137 | // The parent docblock is embedded, which we'll need to parse. Note that according to phpDocumentor this 138 | // only works for the long description (not the so-called 'summary' or short description). 139 | $docblockInheritsLongDescription = true; 140 | } 141 | 142 | // No immediate docblock available or we need to scan the parent docblock? 143 | if ((!$docComment || $docblockInheritsLongDescription) && $function instanceof ReflectionMethod) { 144 | $classIterator = new ReflectionClass($function->class); 145 | $classIterator = $classIterator->getParentClass(); 146 | 147 | // Check if this method is implementing an abstract method from a trait, in which case that docblock should 148 | // be used. 149 | if (!$docComment) { 150 | foreach ($function->getDeclaringClass()->getTraits() as $trait) { 151 | if ($trait->hasMethod($function->getName())) { 152 | $traitMethod = $trait->getMethod($function->getName()); 153 | 154 | if ($traitMethod->isAbstract() && $traitMethod->getDocComment()) { 155 | return $this->getMethodArguments($traitMethod); 156 | } 157 | } 158 | } 159 | } 160 | 161 | // Check if this method is implementing an interface method, in which case that docblock should be used. 162 | // NOTE: If the parent class has an interface, getMethods() on the parent class will include the interface 163 | // methods, along with their docblocks, even if the parent doesn't actually implement the method. So we only 164 | // have to check the interfaces of the declaring class. 165 | if (!$docComment) { 166 | foreach ($function->getDeclaringClass()->getInterfaces() as $interface) { 167 | if ($interface->hasMethod($function->getName())) { 168 | $interfaceMethod = $interface->getMethod($function->getName()); 169 | 170 | if ($interfaceMethod->getDocComment()) { 171 | return $this->getMethodArguments($interfaceMethod); 172 | } 173 | } 174 | } 175 | } 176 | 177 | // Walk up base classes to see if any of them have additional info about this method. 178 | while ($classIterator) { 179 | if ($classIterator->hasMethod($function->getName())) { 180 | $baseClassMethod = $classIterator->getMethod($function->getName()); 181 | 182 | if ($baseClassMethod->getDocComment()) { 183 | $baseClassMethodArgs = $this->getMethodArguments($baseClassMethod); 184 | 185 | if (!$docComment) { 186 | return $baseClassMethodArgs; // Fall back to parent docblock. 187 | } elseif ($docblockInheritsLongDescription) { 188 | $docParseResult['descriptions']['long'] = str_replace( 189 | DocParser::INHERITDOC, 190 | $baseClassMethodArgs['descriptions']['long'], 191 | $docParseResult['descriptions']['long'] 192 | ); 193 | } 194 | 195 | break; 196 | } 197 | } 198 | 199 | $classIterator = $classIterator->getParentClass(); 200 | } 201 | } 202 | 203 | 204 | $result = array( 205 | 'parameters' => $parameters, 206 | 'optionals' => $optionals, 207 | 'docParameters' => $docParseResult['params'], 208 | 'throws' => $docParseResult['throws'], 209 | 'return' => $docParseResult['return'], 210 | 'descriptions' => $docParseResult['descriptions'], 211 | 'deprecated' => $function->isDeprecated() || $docParseResult['deprecated'] 212 | ); 213 | 214 | $result['return']['type'] = method_exists($function, 'getReturnType') && $function->hasReturnType() // PHP7 215 | ? $function->getReturnType()->__toString() 216 | : $result['return']['type'] 217 | ; 218 | 219 | return $result; 220 | } 221 | 222 | /** 223 | * Fetches information about the specified class property, such as its type, description, ... 224 | * 225 | * @param ReflectionProperty $property The property to analyze. 226 | * 227 | * @return array 228 | */ 229 | protected function getPropertyArguments(ReflectionProperty $property) 230 | { 231 | $parser = new DocParser(); 232 | $docComment = $property->getDocComment() ?: ''; 233 | 234 | $docParseResult = $parser->parse($docComment, array( 235 | DocParser::VAR_TYPE, 236 | DocParser::DEPRECATED, 237 | DocParser::DESCRIPTION 238 | ), $property->name); 239 | 240 | if (!$docComment) { 241 | $classIterator = new ReflectionClass($property->class); 242 | $classIterator = $classIterator->getParentClass(); 243 | 244 | // Walk up base classes to see if any of them have additional info about this property. 245 | while ($classIterator) { 246 | if ($classIterator->hasProperty($property->getName())) { 247 | $baseClassProperty = $classIterator->getProperty($property->getName()); 248 | 249 | if ($baseClassProperty->getDocComment()) { 250 | $baseClassPropertyArgs = $this->getPropertyArguments($baseClassProperty); 251 | 252 | return $baseClassPropertyArgs; // Fall back to parent docblock. 253 | } 254 | } 255 | 256 | $classIterator = $classIterator->getParentClass(); 257 | } 258 | } 259 | 260 | return array( 261 | 'return' => $docParseResult['var'], 262 | 'descriptions' => $docParseResult['descriptions'], 263 | 'deprecated' => $docParseResult['deprecated'] 264 | ); 265 | } 266 | 267 | /** 268 | * Retrieves the class that contains the specified reflection member. 269 | * 270 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember 271 | * 272 | * @return array 273 | */ 274 | protected function getDeclaringClass($reflectionMember) 275 | { 276 | // This will point to the class that contains the member, which will resolve to the parent class if it's 277 | // inherited (and not overridden). 278 | $declaringClass = $reflectionMember->getDeclaringClass(); 279 | 280 | return array( 281 | 'name' => $declaringClass->name, 282 | 'filename' => $declaringClass->getFilename() 283 | ); 284 | } 285 | 286 | /** 287 | * Retrieves the structure (class, trait, interface, ...) that contains the specified reflection member. 288 | * 289 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember 290 | * 291 | * @return array 292 | */ 293 | protected function getDeclaringStructure($reflectionMember) 294 | { 295 | // This will point to the class that contains the member, which will resolve to the parent class if it's 296 | // inherited (and not overridden). 297 | $declaringStructure = $reflectionMember->getDeclaringClass(); 298 | $isMethod = ($reflectionMember instanceof ReflectionFunctionAbstract); 299 | 300 | // Members from traits are seen as part of the structure using the trait, but we still want the actual trait 301 | // name. 302 | foreach ($declaringStructure->getTraits() as $trait) { 303 | if (($isMethod && $trait->hasMethod($reflectionMember->name)) || 304 | (!$isMethod && $trait->hasProperty($reflectionMember->name))) { 305 | $declaringStructure = $trait; 306 | break; 307 | } 308 | } 309 | 310 | return array( 311 | 'name' => $declaringStructure->name, 312 | 'filename' => $declaringStructure->getFilename() 313 | ); 314 | } 315 | 316 | /** 317 | * Retrieves information about what the specified member is overriding, if anything. 318 | * 319 | * @param ReflectionFunctionAbstract|ReflectionProperty $reflectionMember 320 | * 321 | * @return array|null 322 | */ 323 | protected function getOverrideInfo($reflectionMember) 324 | { 325 | $overriddenMember = null; 326 | $memberName = $reflectionMember->getName(); 327 | 328 | $baseClass = $reflectionMember->getDeclaringClass(); 329 | 330 | $type = ($reflectionMember instanceof ReflectionProperty) ? 'Property' : 'Method'; 331 | 332 | while ($baseClass = $baseClass->getParentClass()) { 333 | if ($baseClass->{'has' . $type}($memberName)) { 334 | $overriddenMember = $baseClass->{'get' . $type}($memberName); 335 | break; 336 | } 337 | } 338 | 339 | if (!$overriddenMember) { 340 | // This method is not an override of a parent method, see if it is an 'override' of an abstract method from 341 | // a trait the class it is in is using. 342 | if ($reflectionMember instanceof ReflectionFunctionAbstract) { 343 | foreach ($reflectionMember->getDeclaringClass()->getTraits() as $trait) { 344 | if ($trait->hasMethod($memberName)) { 345 | $traitMethod = $trait->getMethod($memberName); 346 | 347 | if ($traitMethod->isAbstract()) { 348 | $overriddenMember = $traitMethod; 349 | } 350 | } 351 | } 352 | } 353 | 354 | if (!$overriddenMember) { 355 | return null; 356 | } 357 | } 358 | 359 | $startLine = null; 360 | 361 | if ($overriddenMember instanceof ReflectionFunctionAbstract) { 362 | $startLine = $overriddenMember->getStartLine(); 363 | } 364 | 365 | return array( 366 | 'declaringClass' => $this->getDeclaringClass($overriddenMember), 367 | 'declaringStructure' => $this->getDeclaringStructure($overriddenMember), 368 | 'startLine' => $startLine 369 | ); 370 | } 371 | 372 | /** 373 | * Retrieves information about what interface the specified member method is implementind, if any. 374 | * 375 | * @param ReflectionFunctionAbstract $reflectionMember 376 | * 377 | * @return array|null 378 | */ 379 | protected function getImplementationInfo(ReflectionFunctionAbstract $reflectionMember) 380 | { 381 | $implementedMember = null; 382 | $methodName = $reflectionMember->getName(); 383 | 384 | foreach ($reflectionMember->getDeclaringClass()->getInterfaces() as $interface) { 385 | if ($interface->hasMethod($methodName)) { 386 | $implementedMember = $interface->getMethod($methodName); 387 | break; 388 | } 389 | } 390 | 391 | if (!$implementedMember) { 392 | return null; 393 | } 394 | 395 | return array( 396 | 'declaringClass' => $this->getDeclaringClass($implementedMember), 397 | 'declaringStructure' => $this->getDeclaringStructure($implementedMember), 398 | 'startLine' => $implementedMember->getStartLine() 399 | ); 400 | } 401 | 402 | /** 403 | * Retrieves a list of parent classes of the specified class, ordered from the closest to the furthest ancestor. 404 | * 405 | * @param ReflectionClass $class 406 | * 407 | * @return string[] 408 | */ 409 | protected function getParentClasses(ReflectionClass $class) 410 | { 411 | $parents = []; 412 | 413 | $parentClass = $class; 414 | 415 | while ($parentClass = $parentClass->getParentClass()) { 416 | $parents[] = $parentClass->getName(); 417 | } 418 | 419 | return $parents; 420 | } 421 | 422 | /** 423 | * Returns methods and properties of the given className 424 | * 425 | * @param string $className Full namespace of the parsed class. 426 | * 427 | * @return array 428 | */ 429 | protected function getClassMetadata($className) 430 | { 431 | $data = array( 432 | 'wasFound' => false, 433 | 'class' => $className, 434 | 'shortName' => null, 435 | 'filename' => null, 436 | 'isTrait' => null, 437 | 'isClass' => null, 438 | 'isAbstract' => null, 439 | 'isInterface' => null, 440 | 'parents' => array(), 441 | 'names' => array(), 442 | 'values' => array(), 443 | 'args' => array() 444 | ); 445 | 446 | try { 447 | $reflection = new ReflectionClass($className); 448 | } catch (\Exception $e) { 449 | return $data; 450 | } 451 | 452 | $data = array_merge($data, array( 453 | 'wasFound' => true, 454 | 'shortName' => $reflection->getShortName(), 455 | 'filename' => $reflection->getFileName(), 456 | 'isTrait' => $reflection->isTrait(), 457 | 'isClass' => !($reflection->isTrait() || $reflection->isInterface()), 458 | 'isAbstract' => $reflection->isAbstract(), 459 | 'isInterface' => $reflection->isInterface(), 460 | 'parents' => $this->getParentClasses($reflection), 461 | 'args' => $this->getClassArguments($reflection) 462 | )); 463 | 464 | // Retrieve information about methods. 465 | foreach ($reflection->getMethods() as $method) { 466 | $data['names'][] = $method->getName(); 467 | 468 | $data['values'][$method->getName()] = array( 469 | 'isMethod' => true, 470 | 'isProperty' => false, 471 | 'isPublic' => $method->isPublic(), 472 | 'isProtected' => $method->isProtected(), 473 | 'isPrivate' => $method->isPrivate(), 474 | 'isStatic' => $method->isStatic(), 475 | 476 | 'override' => $this->getOverrideInfo($method), 477 | 'implementation' => $this->getImplementationInfo($method), 478 | 479 | 'args' => $this->getMethodArguments($method), 480 | 'declaringClass' => $this->getDeclaringClass($method), 481 | 'declaringStructure' => $this->getDeclaringStructure($method), 482 | 'startLine' => $method->getStartLine() 483 | ); 484 | } 485 | 486 | // Retrieves information about properties/attributes. 487 | foreach ($reflection->getProperties() as $attribute) { 488 | if (!in_array($attribute->getName(), $data['names'])) { 489 | $data['names'][] = $attribute->getName(); 490 | $data['values'][$attribute->getName()] = null; 491 | } 492 | 493 | $attributesValues = array( 494 | 'isMethod' => false, 495 | 'isProperty' => true, 496 | 'isPublic' => $attribute->isPublic(), 497 | 'isProtected' => $attribute->isProtected(), 498 | 'isPrivate' => $attribute->isPrivate(), 499 | 'isStatic' => $attribute->isStatic(), 500 | 501 | 'override' => $this->getOverrideInfo($attribute), 502 | 503 | 'args' => $this->getPropertyArguments($attribute), 504 | 'declaringClass' => $this->getDeclaringClass($attribute), 505 | 'declaringStructure' => $this->getDeclaringStructure($attribute) 506 | ); 507 | 508 | if (is_array($data['values'][$attribute->getName()])) { 509 | $attributesValues = array( 510 | $attributesValues, 511 | $data['values'][$attribute->getName()] 512 | ); 513 | } 514 | 515 | $data['values'][$attribute->getName()] = $attributesValues; 516 | } 517 | 518 | // Retrieve information about constants. 519 | $constants = $reflection->getConstants(); 520 | 521 | foreach (array_keys($constants) as $constant) { 522 | if (!in_array($constant, $data['names'])) { 523 | $data['names'][] = $constant; 524 | $data['values'][$constant] = null; 525 | } 526 | 527 | // TODO: There is no direct way to know where the constant originated from (the current class, a base class, 528 | // an interface of a base class, a trait, ...). This could be done by looping up the chain of base classes 529 | // to the last class that also has the same property and then checking if any of that class' traits or 530 | // interfaces define the constant. 531 | $data['values'][$constant][] = array( 532 | 'isMethod' => false, 533 | 'isProperty' => false, 534 | 'isPublic' => true, 535 | 'isProtected' => false, 536 | 'isPrivate' => false, 537 | 'isStatic' => true, 538 | 'declaringClass' => array( 539 | 'name' => $reflection->name, 540 | 'filename' => $reflection->getFileName() 541 | ), 542 | 543 | // TODO: It is not possible to directly fetch the docblock of the constant through reflection, manual 544 | // file parsing is required. 545 | 'args' => array( 546 | 'return' => null, 547 | 'descriptions' => array(), 548 | 'deprecated' => false 549 | ) 550 | ); 551 | } 552 | 553 | $data = $this->getClassMetadataFromDocBlock($reflection, $data); 554 | 555 | return $data; 556 | } 557 | 558 | /** 559 | * Returns methods and properties from docblock 560 | * 561 | * @param ReflectionClass $reflection 562 | * @param array $data generated result by getClassMetadata 563 | * 564 | * @return array 565 | */ 566 | protected function getClassMetadataFromDocBlock(ReflectionClass $reflection, array $data) 567 | { 568 | $parser = new DocParser(); 569 | $filters = array( 570 | DocParser::PROPERTY, 571 | DocParser::METHOD 572 | ); 573 | $docParseResult = $parser->parse($reflection->getDocComment(), $filters, $reflection->getShortName()); 574 | 575 | // Retrieve method information from docblock. 576 | foreach ($docParseResult['methods'] as $methodName => $method) { 577 | if (isset($data['values'][$methodName]['args']['return']['type'])) { 578 | $data['values'][$methodName]['args']['return']['type'] = $method['type']; 579 | } 580 | } 581 | 582 | // Retrieve property information from docblock. 583 | foreach ($docParseResult['properties'] as $propertyName => $property) { 584 | if ($propertyName[0] == '$') { 585 | $propertyName = substr($propertyName, 1); 586 | } 587 | if (!in_array($propertyName, $data['names'])) { 588 | $data['names'][] = $propertyName; 589 | $data['values'][$propertyName] = null; 590 | } 591 | 592 | try { 593 | $propertyReflection = new ReflectionClass($property['type']); 594 | $propertyDocParseResult = $parser->parse( 595 | $propertyReflection->getDocComment(), 596 | [DocParser::DESCRIPTION], 597 | $propertyReflection->getShortName() 598 | ); 599 | $args = array( 600 | 'return' => array( 601 | 'description' => '', 602 | 'type' => $propertyReflection->name, 603 | ), 604 | 'descriptions' => $propertyDocParseResult['descriptions'], 605 | 'deprecated' => false 606 | ); 607 | } catch (\ReflectionException $e) { 608 | $args = array( 609 | 'return' => array( 610 | 'description' => '', 611 | 'type' => $property['type'], 612 | ), 613 | 'descriptions' => array(), 614 | 'deprecated' => false 615 | ); 616 | } 617 | 618 | $propertyClass = array( 619 | 'name' => $reflection->name, 620 | 'filename' => $reflection->getFileName(), 621 | ); 622 | $attributesValues = array( 623 | 'isMethod' => false, 624 | 'isProperty' => true, 625 | 'isPublic' => true, 626 | 'isProtected' => false, 627 | 'isPrivate' => false, 628 | 'isStatic' => false, 629 | 630 | 'override' => null, 631 | 632 | 'args' => $args, 633 | 'declaringClass' => $propertyClass, 634 | 'declaringStructure' => $propertyClass 635 | ); 636 | 637 | if (is_array($data['values'][$propertyName])) { 638 | $attributesValues = array( 639 | $attributesValues, 640 | $data['values'][$propertyName] 641 | ); 642 | } 643 | 644 | $data['values'][$propertyName] = $attributesValues; 645 | } 646 | 647 | return $data; 648 | } 649 | 650 | /** 651 | * Check if the project is Drupal 6/7 and include the necessary files to get the maximum functions as possible 652 | */ 653 | public function includeOldDrupal() 654 | { 655 | $project = Config::get('projectPath'); 656 | 657 | if (file_exists($project . '/misc') && file_exists($project . '/modules') && file_exists($project . '/sites')) { 658 | define('DRUPAL_ROOT', $project); 659 | include_once DRUPAL_ROOT . '/includes/bootstrap.inc'; 660 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 661 | } 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /spec/peekmo-php-atom-autocomplete-spec.coffee: -------------------------------------------------------------------------------- 1 | {WorkspaceView} = require 'atom' 2 | PeekmoPhpAtomAutocomplete = require '../lib/peekmo-php-atom-autocomplete' 3 | 4 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 5 | # 6 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 7 | # or `fdescribe`). Remove the `f` to unfocus the block. 8 | 9 | describe "PeekmoPhpAtomAutocomplete", -> 10 | activationPromise = null 11 | 12 | beforeEach -> 13 | atom.workspaceView = new WorkspaceView 14 | activationPromise = atom.packages.activatePackage('peekmo-php-atom-autocomplete') 15 | 16 | describe "when the peekmo-php-atom-autocomplete:toggle event is triggered", -> 17 | it "attaches and then detaches the view", -> 18 | expect(atom.workspaceView.find('.peekmo-php-atom-autocomplete')).not.toExist() 19 | 20 | # This is an activation event, triggering it will cause the package to be 21 | # activated. 22 | atom.commands.dispatch atom.workspaceView.element, 'peekmo-php-atom-autocomplete:toggle' 23 | 24 | waitsForPromise -> 25 | activationPromise 26 | 27 | runs -> 28 | 29 | -------------------------------------------------------------------------------- /styles/peekmo-php-atom-autocomplete.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | @import "octicon-utf-codes"; 7 | 8 | .peekmo-php-atom-autocomplete { 9 | } 10 | 11 | .php-atom-autocomplete-strike, .php-atom-autocomplete-strike .word { 12 | text-decoration: line-through; 13 | } 14 | 15 | .php-atom-autocomplete-goto-overlay { 16 | width: 100%; 17 | margin-left: -50%; 18 | } 19 | 20 | .php-atom-autocomplete-popover { 21 | max-width: 45%; 22 | 23 | .php-atom-autocomplete-popover-wrapper { 24 | margin: 0em 0em 0em 0em; 25 | text-align: left; 26 | 27 | div { 28 | white-space: normal; 29 | } 30 | 31 | .section { 32 | > h4 { 33 | margin-top: 1em; 34 | font-size: larger; 35 | font-weight: bold; 36 | } 37 | 38 | > div { 39 | padding-left: 1em; 40 | } 41 | 42 | td { 43 | vertical-align: top; 44 | } 45 | 46 | td:not(:last-child) { 47 | padding-right: 1em; 48 | } 49 | } 50 | } 51 | } 52 | 53 | atom-text-editor { 54 | .comment-clickable .region { 55 | pointer-events: all; 56 | z-index: 9999; 57 | } 58 | 59 | .gutter { 60 | .line-number { 61 | &.override { 62 | .icon-right { 63 | opacity: 1.0; 64 | cursor: pointer; 65 | visibility: visible; 66 | 67 | &:before { 68 | content: @link; 69 | color: @ui-site-color-2; 70 | } 71 | } 72 | } 73 | 74 | &.implementation { 75 | .icon-right { 76 | opacity: 1.0; 77 | cursor: pointer; 78 | visibility: visible; 79 | 80 | &:before { 81 | content: @link; 82 | color: @ui-site-color-3; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | --------------------------------------------------------------------------------