├── core └── .gitignore ├── indexes └── .gitignore ├── .gitignore ├── .editorconfig ├── LICENSE ├── lib ├── ConfigTester.coffee ├── AtomConfig.coffee ├── CoreManager.coffee ├── Widgets │ ├── Popover.coffee │ ├── AttachedPopover.coffee │ └── StatusBarManager.coffee ├── Config.coffee ├── ComposerService.coffee ├── IndexingMediator.coffee ├── CachingProxy.coffee ├── UseStatementHelper.coffee ├── ProjectManager.coffee ├── Service.coffee ├── Main.coffee └── Proxy.coffee ├── menus └── menu.cson ├── ISSUE_TEMPLATE.md ├── package.json ├── styles └── main.less ├── LICENSE-atom-autocomplete-php ├── README.md └── CHANGELOG.md /core/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /indexes/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /lib/ConfigTester.coffee: -------------------------------------------------------------------------------- 1 | child_process = require "child_process" 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Tests configurations to see if they are properly usable. 7 | ## 8 | class ConfigTester 9 | ###* 10 | * Constructor. 11 | * 12 | * @param {Config} config 13 | ### 14 | constructor: (@config) -> 15 | 16 | ###* 17 | * Tests the user's configuration. 18 | * 19 | * @return {boolean} 20 | ### 21 | test: () -> 22 | response = child_process.spawnSync(@config.get('phpCommand'), ["-v"]) 23 | 24 | if response.status = null or response.status != 0 25 | return false 26 | 27 | return true 28 | -------------------------------------------------------------------------------- /menus/menu.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | 'label': 'Packages' 4 | 'submenu': [ 5 | { 6 | 'label': 'PHP Integrator', 7 | 'submenu': [ 8 | {'label': 'Set Up Current Project', 'command': 'php-integrator-base:set-up-current-project'}, 9 | {'label': '(Re)index Project', 'command': 'php-integrator-base:index-project'}, 10 | {'label': 'Forcibly (Re)index Project', 'command': 'php-integrator-base:force-index-project'}, 11 | {'type' : 'separator'}, 12 | {'label': 'Sort Use Statements', 'command': 'php-integrator-base:sort-use-statements'}, 13 | ] 14 | } 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Whoa, there! Thanks for contributing by reporting issues, but before you do, check that you can answer all the following questions with a firm **Yes**: 2 | * Have I checked the existing open issues? 3 | * More importantly, have I checked the existing closed issues? 4 | * If I've just installed or updated... 5 | * ... have I checked the **CHANGELOG** and the **README**? 6 | * ... have I tried removing and reinstalling the package and then restarting Atom to ensure Atom didn't break it? 7 | 8 | If you've answered **Yes** to all these questions, please do tell us what the problem is (you can remove this text first). 9 | 10 | Also, it would help to check the Atom developer tools (`View` → `Developer` → `Toggle Developer Tools`) console tab to see if there is any interesting output and include that in your report. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-integrator-base-legacy-php56", 3 | "main": "./lib/Main", 4 | "version": "1.0.1", 5 | "description": "Indexes PHP code and exposes services to query it (required for other php-integrator-* packages).", 6 | "repository": "php-integrator/atom-base-legacy-php56", 7 | "license": "GPL-3.0", 8 | "engines": { 9 | "atom": ">=1.13.0 <2.0.0" 10 | }, 11 | "providedServices": { 12 | "php-integrator.service": { 13 | "versions": { 14 | "2.0.0": "getServiceInstance" 15 | } 16 | } 17 | }, 18 | "consumedServices": { 19 | "status-bar": { 20 | "versions": { 21 | "^1.0.0": "setStatusBarService" 22 | } 23 | }, 24 | "project-manager": { 25 | "versions": { 26 | "^3.1.0": "setProjectManagerService" 27 | } 28 | } 29 | }, 30 | "dependencies": { 31 | "event-kit": "~2.0.0", 32 | "jquery": "~3.1.0", 33 | "md5": "~2.0.0", 34 | "rimraf": "~2.5.4", 35 | "download": "~5.0.2" 36 | }, 37 | "keywords": [ 38 | "php", 39 | "service", 40 | "integrator", 41 | "integration", 42 | "php-integrator" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /styles/main.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 | .php-integrator-popover { 9 | max-width: 45%; 10 | pointer-events: none; 11 | 12 | .php-integrator-popover-wrapper { 13 | margin: 0em 0em 0em 0em; 14 | text-align: left; 15 | 16 | div { 17 | white-space: normal; 18 | } 19 | 20 | .section { 21 | > h4 { 22 | margin-top: 1em; 23 | font-size: larger; 24 | font-weight: bold; 25 | } 26 | 27 | > div { 28 | padding-left: 1em; 29 | } 30 | 31 | td { 32 | vertical-align: top; 33 | } 34 | 35 | td:not(:last-child) { 36 | padding-right: 1em; 37 | } 38 | } 39 | } 40 | } 41 | 42 | .php-integrator-progress-bar { 43 | display: inline-block; 44 | 45 | > span { 46 | margin-left: 0.5em; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE-atom-autocomplete-php: -------------------------------------------------------------------------------- 1 | This project was forked from atom-autocomplete-php, thus the original code base 2 | was licensed under the MIT license. It can still be found at [1]. The original 3 | license is located below. 4 | 5 | [1] https://github.com/Peekmo/atom-autocomplete-php 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2014-2015 Axel Anceau 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of 12 | this software and associated documentation files (the "Software"), to deal in 13 | the Software without restriction, including without limitation the rights to 14 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 15 | the Software, and to permit persons to whom the Software is furnished to do so, 16 | subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 23 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 24 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 25 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /lib/AtomConfig.coffee: -------------------------------------------------------------------------------- 1 | Config = require './Config' 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Config that retrieves its settings from Atom's config. 7 | ## 8 | class AtomConfig extends Config 9 | ###* 10 | * The name of the package to use when searching for settings. 11 | ### 12 | packageName: null 13 | 14 | ###* 15 | * @inheritdoc 16 | ### 17 | constructor: (@packageName) -> 18 | super() 19 | 20 | @attachListeners() 21 | 22 | ###* 23 | * @inheritdoc 24 | ### 25 | load: () -> 26 | @set('phpCommand', atom.config.get("#{@packageName}.phpCommand")) 27 | @set('additionalIndexingDelay', atom.config.get("#{@packageName}.additionalIndexingDelay")) 28 | @set('memoryLimit', atom.config.get("#{@packageName}.memoryLimit")) 29 | @set('insertNewlinesForUseStatements', atom.config.get("#{@packageName}.insertNewlinesForUseStatements")) 30 | @set('packagePath', atom.packages.resolvePackagePath("#{@packageName}")) 31 | 32 | ###* 33 | * Attaches listeners to listen to Atom configuration changes. 34 | ### 35 | attachListeners: () -> 36 | atom.config.onDidChange "#{@packageName}.phpCommand", () => 37 | @set('phpCommand', atom.config.get("#{@packageName}.phpCommand")) 38 | 39 | atom.config.onDidChange "#{@packageName}.additionalIndexingDelay", () => 40 | @set('additionalIndexingDelay', atom.config.get("#{@packageName}.additionalIndexingDelay")) 41 | 42 | atom.config.onDidChange "#{@packageName}.memoryLimit", () => 43 | @set('memoryLimit', atom.config.get("#{@packageName}.memoryLimit")) 44 | 45 | atom.config.onDidChange "#{@packageName}.insertNewlinesForUseStatements", () => 46 | @set('insertNewlinesForUseStatements', atom.config.get("#{@packageName}.insertNewlinesForUseStatements")) 47 | -------------------------------------------------------------------------------- /lib/CoreManager.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = 5 | 6 | ##* 7 | # Handles management of the (PHP) core that is needed to handle the server side. 8 | ## 9 | class CoreManager 10 | ###* 11 | * The commit to download from the Composer repository. 12 | * 13 | * Currently set to version 1.2.4. 14 | * 15 | * @see https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md 16 | * 17 | * @var {String} 18 | ### 19 | COMPOSER_COMMIT: 'd0310b646229c3dc57b71bfea2f14ed6c560a5bd' 20 | 21 | ###* 22 | * @var {String} 23 | ### 24 | COMPOSER_PACKAGE_NAME: 'php-integrator/core' 25 | 26 | ###* 27 | * @var {ComposerService} 28 | ### 29 | composerService: null 30 | 31 | ###* 32 | * @var {String} 33 | ### 34 | versionSpecification: null 35 | 36 | ###* 37 | * @var {String} 38 | ### 39 | folder: null 40 | 41 | ###* 42 | * @param {ComposerService} composerService 43 | * @param {String} versionSpecification 44 | * @param {String} folder 45 | ### 46 | constructor: (@composerService, @versionSpecification, @folder) -> 47 | 48 | ###* 49 | * @return {Promise} 50 | ### 51 | install: () -> 52 | return @composerService.run([ 53 | 'create-project', 54 | @COMPOSER_PACKAGE_NAME, 55 | @getCoreSourcePath(), 56 | @versionSpecification, 57 | '--prefer-dist', 58 | '--no-dev', 59 | '--no-progress' 60 | ], @folder) 61 | 62 | ###* 63 | * @return {Boolean} 64 | ### 65 | isInstalled: () -> 66 | return fs.existsSync(@getComposerLockFilePath()) 67 | 68 | ###* 69 | * @return {String} 70 | ### 71 | getComposerLockFilePath: () -> 72 | return path.join(@getCoreSourcePath(), 'composer.lock') 73 | 74 | ###* 75 | * @return {String} 76 | ### 77 | getCoreSourcePath: () -> 78 | return path.join(@folder, @versionSpecification) 79 | -------------------------------------------------------------------------------- /lib/Widgets/Popover.coffee: -------------------------------------------------------------------------------- 1 | {Disposable} = require 'atom' 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Widget that can be used to display information about a certain context. 7 | ## 8 | class Popover extends Disposable 9 | element: null 10 | 11 | ###* 12 | * Constructor. 13 | ### 14 | constructor: () -> 15 | @$ = require 'jquery' 16 | 17 | @element = document.createElement('div') 18 | @element.className = 'tooltip bottom fade php-integrator-popover' 19 | @element.innerHTML = "
" 20 | 21 | document.body.appendChild(@element) 22 | 23 | super @destructor 24 | 25 | ###* 26 | * Destructor. 27 | ### 28 | destructor: () -> 29 | @hide() 30 | document.body.removeChild(@element) 31 | 32 | ###* 33 | * Retrieves the HTML element containing the popover. 34 | * 35 | * @return {HTMLElement} 36 | ### 37 | getElement: () -> 38 | return @element 39 | 40 | ###* 41 | * sets the text to display. 42 | * 43 | * @param {String} text 44 | ### 45 | setText: (text) -> 46 | @$('.tooltip-inner', @element).html( 47 | '
' + text.replace(/\n\n/g, '

') + '
' 48 | ) 49 | 50 | ###* 51 | * Shows a popover at the specified location with the specified text and fade in time. 52 | * 53 | * @param {Number} x The X coordinate to show the popover at (left). 54 | * @param {Number} y The Y coordinate to show the popover at (top). 55 | * @param {Number} fadeInTime The amount of time to take to fade in the tooltip. 56 | ### 57 | show: (x, y, fadeInTime = 100) -> 58 | @$(@element).css('left', x + 'px') 59 | @$(@element).css('top', y + 'px') 60 | 61 | @$(@element).addClass('in') 62 | @$(@element).css('opacity', 100) 63 | @$(@element).css('display', 'block') 64 | 65 | ###* 66 | * Hides the tooltip, if it is displayed. 67 | ### 68 | hide: () -> 69 | @$(@element).removeClass('in') 70 | @$(@element).css('opacity', 0) 71 | @$(@element).css('display', 'none') 72 | -------------------------------------------------------------------------------- /lib/Config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Abstract base class for managing configurations. 7 | ## 8 | class Config 9 | ###* 10 | * Raw configuration object. 11 | ### 12 | data: null 13 | 14 | ###* 15 | * Array of change listeners. 16 | ### 17 | listeners: null 18 | 19 | ###* 20 | * Constructor. 21 | ### 22 | constructor: () -> 23 | @listeners = {} 24 | 25 | @data = 26 | phpCommand : null 27 | packagePath : null 28 | additionalIndexingDelay : 200 29 | memoryLimit : 512 30 | insertNewlinesForUseStatements : false 31 | 32 | # See also http://www.phpdoc.org/docs/latest/index.html . 33 | phpdoc_base_url : { 34 | prefix: 'http://www.phpdoc.org/docs/latest/references/phpdoc/tags/' 35 | suffix: '.html' 36 | } 37 | 38 | # See also https://secure.php.net/urlhowto.php . 39 | php_documentation_base_urls : { 40 | root : 'https://secure.php.net/' 41 | classes : 'https://secure.php.net/class.' 42 | functions : 'https://secure.php.net/function.' 43 | } 44 | 45 | @load() 46 | 47 | ###* 48 | * Loads the configuration. 49 | ### 50 | load: () -> 51 | throw new Error("This method is abstract and must be implemented!") 52 | 53 | ###* 54 | * Registers a listener that is invoked when the specified property is changed. 55 | ### 56 | onDidChange: (name, callback) -> 57 | if name not of @listeners 58 | @listeners[name] = [] 59 | 60 | @listeners[name].push(callback) 61 | 62 | ###* 63 | * Retrieves the config setting with the specified name. 64 | * 65 | * @return {mixed} 66 | ### 67 | get: (name) -> 68 | return @data[name] 69 | 70 | ###* 71 | * Retrieves the config setting with the specified name. 72 | * 73 | * @param {String} name 74 | * @param {mixed} value 75 | ### 76 | set: (name, value) -> 77 | @data[name] = value 78 | 79 | if name of @listeners 80 | for listener in @listeners[name] 81 | listener(value, name) 82 | -------------------------------------------------------------------------------- /lib/Widgets/AttachedPopover.coffee: -------------------------------------------------------------------------------- 1 | Popover = require './Popover' 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Popover that is attached to an HTML element. 7 | # 8 | # NOTE: The reason we do not use Atom's native tooltip is because it is attached to an element, which caused strange 9 | # problems such as tickets #107 and #72. This implementation uses the same CSS classes and transitions but handles the 10 | # displaying manually as we don't want to attach/detach, we only want to temporarily display a popover on mouseover. 11 | ## 12 | class AttachedPopover extends Popover 13 | ###* 14 | * Timeout ID, used for setting a timeout before displaying the popover. 15 | ### 16 | timeoutId: null 17 | 18 | ###* 19 | * The element to attach the popover to. 20 | ### 21 | elementToAttachTo: null 22 | 23 | ###* 24 | * Constructor. 25 | * 26 | * @param {HTMLElement} elementToAttachTo The element to show the popover over. 27 | * @param {Number} delay How long the mouse has to hover over the elment before the popover shows 28 | * up (in miliiseconds). 29 | ### 30 | constructor: (@elementToAttachTo, delay = 500) -> 31 | super() 32 | 33 | ###* 34 | * Destructor. 35 | ### 36 | destructor: () -> 37 | if @timeoutId 38 | clearTimeout(@timeoutId) 39 | @timeoutId = null 40 | 41 | super() 42 | 43 | ###* 44 | * Shows the popover with the specified text. 45 | * 46 | * @param {Number} fadeInTime The amount of time to take to fade in the tooltip. 47 | ### 48 | show: (fadeInTime = 100) -> 49 | coordinates = @elementToAttachTo.getBoundingClientRect(); 50 | 51 | centerOffset = ((coordinates.right - coordinates.left) / 2) 52 | 53 | x = coordinates.left - (@$(@getElement()).width() / 2) + centerOffset 54 | y = coordinates.bottom 55 | 56 | x = 0 if x < 0 57 | y = 0 if y < 0 58 | 59 | super(x, y, fadeInTime) 60 | 61 | ###* 62 | * Shows the popover with the specified text after the specified delay (in miliiseconds). Calling this method 63 | * multiple times will cancel previous show requests and restart. 64 | * 65 | * @param {Number} delay The delay before the tooltip shows up (in milliseconds). 66 | * @param {Number} fadeInTime The amount of time to take to fade in the tooltip. 67 | ### 68 | showAfter: (delay, fadeInTime = 100) -> 69 | @timeoutId = setTimeout(() => 70 | @show(fadeInTime) 71 | , delay) 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-integrator/atom-base-legacy-php56 2 | ## Legacy 3 | This is a legacy version that requires PHP >= 5.6. Users that are on PHP 7.1 can and should use [the newer version](https://github.com/php-integrator/atom-base). 4 | 5 | This package only exists to cater towards users that are not in any position to upgrade their host PHP version. As a result, any issues that appear in this package will not be fixed, no new features will be added and no enhancements will be done. 6 | 7 | ## About 8 | This package provides Atom integration for [PHP Integrator](https://gitlab.com/php-integrator/core) and exposes a service that other packages can use to provide additional functionality, such as autocompletion, 9 | code navigation and tooltips. The user can then select his desired combination of functionalities from these other packages: 10 | * **[php-integrator-autocomplete-plus](https://github.com/php-integrator/atom-autocompletion-legacy-php56)** - Provides intelligent PHP autocompletion in combination with autocomplete-plus. 11 | * **[php-integrator-navigation](https://github.com/php-integrator/atom-navigation-legacy-php56)** - Provides code navigation and go to functionality. 12 | * **[php-integrator-tooltips](https://github.com/php-integrator/atom-tooltips-legacy-php56)** - Shows tooltips with documentation. 13 | * **[php-integrator-annotations](https://github.com/php-integrator/atom-annotations-legacy-php56)** - Shows annotations, such as for overridden methods and interface implementations. 14 | * **[php-integrator-call-tips](https://github.com/php-integrator/atom-call-tips-legacy-php56)** - Shows call tips containing parameters in your code. (Complements the autocompletion package.) 15 | * **[php-integrator-refactoring](https://github.com/php-integrator/atom-refactoring-legacy-php56)** - Provides basic refactoring capabilities. 16 | * **[php-integrator-linter](https://github.com/php-integrator/atom-linter-legacy-php56)** - Shows indexing errors and problems with your code. 17 | 18 | The following package also exists, but is currently looking for a new maintainer (see also its README): 19 | * **[php-integrator-symbol-viewer](https://github.com/tocjent/php-integrator-symbol-viewer)** - Provides a side panel listing class symbols with search and filter features. 20 | 21 | Note that the heavy lifting is performed by the [PHP core](https://gitlab.com/php-integrator/core), which is automatically installed as _payload_ for this package and kept up to date automatically. 22 | 23 | The source code was originally based on the php-autocomplete-plus code base, but has significantly diverged from it since then. 24 | 25 | ## What do I need to do to make it work? 26 | See [the website](https://php-integrator.github.io/#what-do-i-need) as well as [the wiki](https://github.com/php-integrator/atom-base/wiki). 27 | 28 | ![GPLv3 Logo](http://gplv3.fsf.org/gplv3-127x51.png) 29 | -------------------------------------------------------------------------------- /lib/Widgets/StatusBarManager.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | 3 | module.exports = 4 | 5 | ##* 6 | # Manages the items in the status bar. 7 | ## 8 | class StatusBarManager 9 | ###* 10 | * The label to show. 11 | ### 12 | label: null 13 | 14 | ###* 15 | * The label HTML element. 16 | ### 17 | labelElement: null 18 | 19 | ###* 20 | * The progress bar HTML element. 21 | ### 22 | progressBar: null 23 | 24 | ###* 25 | * The root HTML element of the progress bar. 26 | ### 27 | element: null 28 | 29 | ###* 30 | * The tile that is located in the status bar. 31 | ### 32 | tile: null 33 | 34 | ###* 35 | * Initializes the progress bar, setting up its DOM structure. 36 | * 37 | * @param {mixed} statusBarService 38 | ### 39 | initialize: (statusBarService) -> 40 | @labelElement = document.createElement("span") 41 | @labelElement.className = "" 42 | @labelElement.innerHTML = @label 43 | 44 | @progressBar = document.createElement("progress") 45 | 46 | @element = document.createElement("div") 47 | @element.className = "php-integrator-progress-bar" 48 | @element.appendChild(@progressBar) 49 | @element.appendChild(@labelElement) 50 | 51 | atom.tooltips.add(@element, { 52 | title: ''' 53 | Your project is being indexed. During this time, functionality such as autocompletion
54 | may not be available. Saved or unsaved changes made to files may also not be indexed
55 | until the next modification is made. 56 | ''' 57 | }) 58 | 59 | @tile = statusBarService.addRightTile(item: @element, priority: 999999) 60 | 61 | ###* 62 | * Cleans up and removes all elements. 63 | ### 64 | destroy: () -> 65 | @tile.destroy() 66 | 67 | ###* 68 | * Sets the text to show in the label. 69 | * 70 | * @param {String} label 71 | ### 72 | setLabel: (@label) -> 73 | @labelElement.innerHTML = @label 74 | @labelElement.className = '' 75 | 76 | ###* 77 | * Sets the progress value for the progress bar (between 0 and 100). 78 | * 79 | * @param {int|null} progress The progress (between 0 and 100) or null for an indeterminate status. 80 | ### 81 | setProgress: (progress) -> 82 | if progress != null 83 | @progressBar.value = Math.max(Math.min(progress, 100), 0) 84 | @progressBar.max = 100 85 | 86 | else 87 | @progressBar.removeAttribute('value') 88 | @progressBar.removeAttribute('max') 89 | 90 | ###* 91 | * Shows the element. 92 | ### 93 | show: -> 94 | $(@element).show() 95 | $(@progressBar).show() 96 | 97 | ###* 98 | * Hides the element. 99 | ### 100 | hide: -> 101 | $(@element).hide() 102 | 103 | ###* 104 | * Shows only the label. 105 | ### 106 | showLabelOnly: -> 107 | $(@element).show() 108 | $(@progressBar).hide() 109 | 110 | ###* 111 | * Shows the specified message in the status area. 112 | * 113 | * @param {String} label 114 | * @param {String} className 115 | ### 116 | showMessage: (label, className = '') -> 117 | @setLabel(label) 118 | @labelElement.className = className 119 | @showLabelOnly() 120 | 121 | ###* 122 | * Attaches to the specified element or using the specified object. 123 | * 124 | * @param {mixed} object 125 | ### 126 | attach: (object) -> 127 | throw new Error("This method is absract and must be implemented!") 128 | 129 | ###* 130 | * Detaches from the previously attached element. 131 | ### 132 | detach: -> 133 | throw new Error("This method is absract and must be implemented!") 134 | -------------------------------------------------------------------------------- /lib/ComposerService.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | download = require 'download' 4 | child_process = require 'child_process' 5 | 6 | module.exports = 7 | 8 | ##* 9 | # Handles usage of Composer (PHP package manager). 10 | ## 11 | class ComposerService 12 | ###* 13 | * The commit to download from the Composer repository. 14 | * 15 | * Currently set to version 1.2.4. 16 | * 17 | * @see https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md 18 | * 19 | * @var {String} 20 | ### 21 | COMPOSER_COMMIT: 'd0310b646229c3dc57b71bfea2f14ed6c560a5bd' 22 | 23 | ###* 24 | * @var {String} 25 | ### 26 | phpBinary: null 27 | 28 | ###* 29 | * @var {String} 30 | ### 31 | folder: null 32 | 33 | ###* 34 | * @param {String} phpBinary 35 | * @param {String} folder 36 | ### 37 | constructor: (@phpBinary, @folder) -> 38 | 39 | ###* 40 | * @param {Array} parameters 41 | * @param {String|null} workingDirectory 42 | * 43 | * @return {Promise} 44 | ### 45 | run: (parameters, workingDirectory = null) -> 46 | return @installIfNecessary().then () => 47 | options = {} 48 | 49 | if workingDirectory? 50 | options.cwd = workingDirectory 51 | 52 | return new Promise (resolve, reject) => 53 | process = child_process.spawn(@phpBinary, [@getPath()].concat(parameters), options) 54 | 55 | process.stdout.on 'data', (data) => 56 | console.debug('Composer has something to say:', data.toString()) 57 | 58 | process.stderr.on 'data', (data) => 59 | console.warn('Composer has errors to report:', data.toString()) 60 | 61 | process.on 'close', (code) => 62 | console.debug('Composer exited with status code:', code) 63 | 64 | if code != 0 65 | reject() 66 | 67 | else 68 | resolve() 69 | 70 | ###* 71 | * @return {Promise} 72 | ### 73 | installIfNecessary: () -> 74 | if @isInstalled() 75 | return new Promise (resolve, reject) -> 76 | resolve() 77 | 78 | return @install() 79 | 80 | ###* 81 | * @param {Boolean} 82 | ### 83 | isInstalled: () -> 84 | return true if fs.existsSync(@getPath()) 85 | 86 | ###* 87 | * @return {Promise} 88 | ### 89 | install: () -> 90 | @download().then () => 91 | parameters = [ 92 | @getInstallerFileFilePath(), 93 | '--install-dir=' + @folder + '', 94 | '--filename=' + @getFileName() 95 | ] 96 | 97 | return new Promise (resolve, reject) => 98 | process = child_process.spawn(@phpBinary, parameters) 99 | 100 | process.stdout.on 'data', (data) => 101 | console.debug('Composer installer has something to say:', data.toString()) 102 | 103 | process.stderr.on 'data', (data) => 104 | console.warn('Composer installer has errors to report:', data.toString()) 105 | 106 | process.on 'close', (code) => 107 | console.debug('Composer installer exited with status code:', code) 108 | 109 | if code != 0 110 | reject() 111 | 112 | else 113 | resolve() 114 | 115 | ###* 116 | * @return {Promise} 117 | ### 118 | download: () -> 119 | return download( 120 | 'https://raw.githubusercontent.com/composer/getcomposer.org/' + @COMPOSER_COMMIT + '/web/installer', 121 | @getInstallerFilePath() 122 | ) 123 | 124 | ###* 125 | * @return {String} 126 | ### 127 | getInstallerFilePath: () -> 128 | return @folder 129 | 130 | ###* 131 | * @return {String} 132 | ### 133 | getInstallerFileFileName: () -> 134 | return 'installer' 135 | 136 | ###* 137 | * @return {String} 138 | ### 139 | getInstallerFileFilePath: () -> 140 | return path.join(@getInstallerFilePath(), @getInstallerFileFileName()) 141 | 142 | ###* 143 | * @return {String} 144 | ### 145 | getPath: () -> 146 | return path.join(@folder, @getFileName()) 147 | 148 | ###* 149 | * @return {String} 150 | ### 151 | getFileName: () -> 152 | return 'composer.phar' 153 | -------------------------------------------------------------------------------- /lib/IndexingMediator.coffee: -------------------------------------------------------------------------------- 1 | Popover = require './Widgets/Popover' 2 | AttachedPopover = require './Widgets/AttachedPopover' 3 | 4 | module.exports = 5 | 6 | ##* 7 | # A mediator that mediates between classes that need to do indexing and keep updated about the results. 8 | ## 9 | class IndexingMediator 10 | ###* 11 | * The proxy to use to contact the PHP side. 12 | ### 13 | proxy: null 14 | 15 | ###* 16 | * The emitter to use to emit indexing events. 17 | ### 18 | indexingEventEmitter: null 19 | 20 | ###* 21 | * Constructor. 22 | * 23 | * @param {CachingProxy} proxy 24 | * @param {Emitter} indexingEventEmitter 25 | ### 26 | constructor: (@proxy, @indexingEventEmitter) -> 27 | 28 | ###* 29 | * Refreshes the specified file or folder. This method is asynchronous and will return immediately. 30 | * 31 | * @param {String|Array} path The full path to the file or folder to refresh. Alternatively, 32 | * this can be a list of items to index at the same time. 33 | * @param {String|null} source The source code of the file to index. May be null if a directory is 34 | * passed instead. 35 | * @param {Array} excludedPaths A list of paths to exclude from indexing. 36 | * @param {Array} fileExtensionsToIndex A list of file extensions (without leading dot) to index. 37 | * 38 | * @return {Promise} 39 | ### 40 | reindex: (path, source, excludedPaths, fileExtensionsToIndex) -> 41 | return new Promise (resolve, reject) => 42 | @indexingEventEmitter.emit('php-integrator-base:indexing-started', { 43 | path : path 44 | }) 45 | 46 | successHandler = (output) => 47 | @indexingEventEmitter.emit('php-integrator-base:indexing-finished', { 48 | output : output 49 | path : path 50 | }) 51 | 52 | resolve(output) 53 | 54 | failureHandler = (error) => 55 | @indexingEventEmitter.emit('php-integrator-base:indexing-failed', { 56 | error : error 57 | path : path 58 | }) 59 | 60 | reject(error) 61 | 62 | progressStreamCallback = (progress) => 63 | progress = parseFloat(progress) 64 | 65 | if not isNaN(progress) 66 | @indexingEventEmitter.emit('php-integrator-base:indexing-progress', { 67 | path : path 68 | percentage : progress 69 | }) 70 | 71 | return @proxy.reindex( 72 | path, 73 | source, 74 | progressStreamCallback, 75 | excludedPaths, 76 | fileExtensionsToIndex 77 | ).then(successHandler, failureHandler) 78 | 79 | ###* 80 | * Initializes the project. 81 | * 82 | * @return {Promise} 83 | ### 84 | initialize: () -> 85 | return @proxy.initialize() 86 | 87 | ###* 88 | * Vacuums the project. 89 | * 90 | * @return {Promise} 91 | ### 92 | vacuum: () -> 93 | return @proxy.vacuum() 94 | 95 | ###* 96 | * Attaches a callback to indexing started event. The returned disposable can be used to detach your event handler. 97 | * 98 | * @param {Callback} callback A callback that takes one parameter which contains a 'path' property. 99 | * 100 | * @return {Disposable} 101 | ### 102 | onDidStartIndexing: (callback) -> 103 | @indexingEventEmitter.on('php-integrator-base:indexing-started', callback) 104 | 105 | ###* 106 | * Attaches a callback to indexing progress event. The returned disposable can be used to detach your event handler. 107 | * 108 | * @param {Callback} callback A callback that takes one parameter which contains a 'path' and a 'percentage' property. 109 | * 110 | * @return {Disposable} 111 | ### 112 | onDidIndexingProgress: (callback) -> 113 | @indexingEventEmitter.on('php-integrator-base:indexing-progress', callback) 114 | 115 | ###* 116 | * Attaches a callback to indexing finished event. The returned disposable can be used to detach your event handler. 117 | * 118 | * @param {Callback} callback A callback that takes one parameter which contains an 'output' and a 'path' property. 119 | * 120 | * @return {Disposable} 121 | ### 122 | onDidFinishIndexing: (callback) -> 123 | @indexingEventEmitter.on('php-integrator-base:indexing-finished', callback) 124 | 125 | ###* 126 | * Attaches a callback to indexing failed event. The returned disposable can be used to detach your event handler. 127 | * 128 | * @param {Callback} callback A callback that takes one parameter which contains an 'error' and a 'path' property. 129 | * 130 | * @return {Disposable} 131 | ### 132 | onDidFailIndexing: (callback) -> 133 | @indexingEventEmitter.on('php-integrator-base:indexing-failed', callback) 134 | -------------------------------------------------------------------------------- /lib/CachingProxy.coffee: -------------------------------------------------------------------------------- 1 | md5 = require 'md5' 2 | 3 | Proxy = require './Proxy' 4 | 5 | module.exports = 6 | 7 | ##* 8 | # Proxy that applies caching on top of its functionality. 9 | ## 10 | class CachingProxy extends Proxy 11 | ###* 12 | * @var {Object} 13 | ### 14 | cache: null 15 | 16 | ###* 17 | * @var {Object} 18 | ### 19 | pendingPromises: null 20 | 21 | ###* 22 | * @inherited 23 | ### 24 | constructor: (@config) -> 25 | super(@config) 26 | 27 | @cache = {} 28 | @pendingPromises = {} 29 | 30 | ###* 31 | * Clears the cache. 32 | ### 33 | clearCache: () -> 34 | @cache = {} 35 | @pendingPromises = {} 36 | 37 | ###* 38 | * Internal convenience method that wraps a call to a parent method. 39 | * 40 | * @param {String} cacheKey 41 | * @param {String} parentMethodName 42 | * @param {Array} parameters 43 | * 44 | * @return {Promise|Object} 45 | ### 46 | wrapCachedRequestToParent: (cacheKey, parentMethodName, parameters) -> 47 | if cacheKey of @cache 48 | return new Promise (resolve, reject) => 49 | resolve(@cache[cacheKey]) 50 | 51 | else if cacheKey of @pendingPromises 52 | # If a query with the same parameters (promise) is still running, don't start another one but just await 53 | # the results of the existing promise. 54 | return @pendingPromises[cacheKey] 55 | 56 | else 57 | successHandler = (output) => 58 | delete @pendingPromises[cacheKey] 59 | 60 | @cache[cacheKey] = output 61 | 62 | return output 63 | 64 | @pendingPromises[cacheKey] = CachingProxy.__super__[parentMethodName].apply(this, parameters).then(successHandler) 65 | 66 | return @pendingPromises[cacheKey] 67 | 68 | ###* 69 | * @inherited 70 | ### 71 | getClassList: () -> 72 | return @wrapCachedRequestToParent("getClassList", 'getClassList', arguments) 73 | 74 | ###* 75 | * @inherited 76 | ### 77 | getClassListForFile: (file) -> 78 | return @wrapCachedRequestToParent("getClassListForFile-#{file}", 'getClassListForFile', arguments) 79 | 80 | ###* 81 | * @inherited 82 | ### 83 | getNamespaceList: () -> 84 | return @wrapCachedRequestToParent("getNamespaceList", 'getNamespaceList', arguments) 85 | 86 | ###* 87 | * @inherited 88 | ### 89 | getNamespaceListForFile: (file) -> 90 | return @wrapCachedRequestToParent("getNamespaceListForFile-#{file}", 'getNamespaceListForFile', arguments) 91 | 92 | ###* 93 | * @inherited 94 | ### 95 | getGlobalConstants: () -> 96 | return @wrapCachedRequestToParent("getGlobalConstants", 'getGlobalConstants', arguments) 97 | 98 | ###* 99 | * @inherited 100 | ### 101 | getGlobalFunctions: () -> 102 | return @wrapCachedRequestToParent("getGlobalFunctions", 'getGlobalFunctions', arguments) 103 | 104 | ###* 105 | * @inherited 106 | ### 107 | getClassInfo: (className) -> 108 | return @wrapCachedRequestToParent("getClassInfo-#{className}", 'getClassInfo', arguments) 109 | 110 | ###* 111 | * @inherited 112 | ### 113 | resolveType: (file, line, type, kind = 'classlike') -> 114 | return @wrapCachedRequestToParent("resolveType-#{file}-#{line}-#{type}-#{kind}", 'resolveType', arguments) 115 | 116 | ###* 117 | * @inherited 118 | ### 119 | localizeType: (file, line, type, kind = 'classlike') -> 120 | return @wrapCachedRequestToParent("localizeType-#{file}-#{line}-#{type}-#{kind}", 'localizeType', arguments) 121 | 122 | ###* 123 | * @inherited 124 | ### 125 | semanticLint: (file, source, options) -> 126 | # md5 may sound expensive, but it's not as expensive as spawning an extra process that parses PHP code. 127 | sourceKey = if source? then md5(source) else null 128 | 129 | optionsKey = JSON.stringify(options) 130 | 131 | return @wrapCachedRequestToParent("semanticLint-#{file}-#{sourceKey}-#{optionsKey}", 'semanticLint', arguments) 132 | 133 | ###* 134 | * @inherited 135 | ### 136 | getAvailableVariables: (file, source, offset) -> 137 | sourceKey = if source? then md5(source) else null 138 | 139 | return @wrapCachedRequestToParent("getAvailableVariables-#{file}-#{sourceKey}-#{offset}", 'getAvailableVariables', arguments) 140 | 141 | ###* 142 | * @inherited 143 | ### 144 | deduceTypes: (expression, file, source, offset, ignoreLastElement) -> 145 | sourceKey = if source? then md5(source) else null 146 | 147 | return @wrapCachedRequestToParent("deduceTypes-#{expression}-#{file}-#{sourceKey}-#{offset}-#{ignoreLastElement}", 'deduceTypes', arguments) 148 | 149 | ###* 150 | * @inherited 151 | ### 152 | getInvocationInfo: (file, source, offset) -> 153 | sourceKey = if source? then md5(source) else null 154 | 155 | return @wrapCachedRequestToParent("getInvocationInfo-#{file}-#{sourceKey}-#{offset}", 'getInvocationInfo', arguments) 156 | 157 | ###* 158 | * @inherited 159 | ### 160 | initialize: () -> 161 | return super().then (output) => 162 | @clearCache() 163 | 164 | return output 165 | 166 | ###* 167 | * @inherited 168 | ### 169 | vacuum: () -> 170 | return super().then (output) => 171 | @clearCache() 172 | 173 | return output 174 | 175 | ###* 176 | * @inherited 177 | ### 178 | test: () -> 179 | return super() 180 | 181 | ###* 182 | * @inherited 183 | ### 184 | reindex: (path, source, progressStreamCallback, excludedPaths, fileExtensionsToIndex) -> 185 | return super(path, source, progressStreamCallback, excludedPaths, fileExtensionsToIndex).then (output) => 186 | @clearCache() 187 | 188 | return output 189 | -------------------------------------------------------------------------------- /lib/UseStatementHelper.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | ##* 4 | # Contains convenience methods for dealing with use statements. 5 | ## 6 | class UseStatementHelper 7 | ###* 8 | * Regular expression that will search for a structure (class, interface, trait, ...). 9 | * 10 | * @var {RegExp} 11 | ### 12 | structureStartRegex : /(?:abstract class|class|trait|interface)\s+(\w+)/ 13 | 14 | ###* 15 | * Regular expression that will search for a use statement. 16 | * 17 | * @var {RegExp} 18 | ### 19 | useStatementRegex : /(?:use)(?:[^\w\\])([\w\\]+)(?![\w\\])(?:(?:[ ]+as[ ]+)(\w+))?(?:;)/ 20 | 21 | ###* 22 | * Whether to allow adding additional newlines to attempt to group use statements. 23 | * 24 | * @var {Boolean} 25 | ### 26 | allowAdditionalNewlines : false 27 | 28 | ###* 29 | * @param {Boolean} allowAdditionalNewlines 30 | ### 31 | constructor: (@allowAdditionalNewlines) -> 32 | 33 | ###* 34 | * @param {Boolean} allowAdditionalNewlines 35 | ### 36 | setAllowAdditionalNewlines: (@allowAdditionalNewlines) -> 37 | 38 | ###* 39 | * Add the use for the given class if not already added. 40 | * 41 | * @param {TextEditor} editor Atom text editor. 42 | * @param {String} className Name of the class to add. 43 | * 44 | * @return {Number} The amount of lines added (including newlines), so you can reliably and easily offset your rows. 45 | # This could be zero if a use statement was already present. 46 | ### 47 | addUseClass: (editor, className) -> 48 | bestUseRow = 0 49 | placeBelow = true 50 | doNewLine = true 51 | lineCount = editor.getLineCount() 52 | previousMatchThatSharedNamespacePrefixRow = null 53 | 54 | # First see if the use statement is already present. The next loop stops early (and can't do this). 55 | for i in [0 .. lineCount - 1] 56 | line = editor.lineTextForBufferRow(i).trim() 57 | 58 | continue if line.length == 0 59 | 60 | scopeDescriptor = editor.scopeDescriptorForBufferPosition([i, line.length]).getScopeChain() 61 | 62 | if scopeDescriptor.indexOf('.comment') >= 0 63 | continue 64 | 65 | break if line.match(@structureStartRegex) 66 | 67 | if (matches = @useStatementRegex.exec(line)) 68 | if matches[1] == className or (matches[1][0] == '\\' and matches[1].substr(1) == className) 69 | return 0 70 | 71 | # Determine an appropriate location to place the use statement. 72 | for i in [0 .. lineCount - 1] 73 | line = editor.lineTextForBufferRow(i).trim() 74 | 75 | continue if line.length == 0 76 | 77 | scopeDescriptor = editor.scopeDescriptorForBufferPosition([i, line.length]).getScopeChain() 78 | 79 | if scopeDescriptor.indexOf('.comment') >= 0 80 | continue 81 | 82 | break if line.match(@structureStartRegex) 83 | 84 | if line.indexOf('namespace ') >= 0 85 | bestUseRow = i 86 | 87 | if (matches = @useStatementRegex.exec(line)) 88 | bestUseRow = i 89 | 90 | placeBelow = true 91 | shareCommonNamespacePrefix = @doShareCommonNamespacePrefix(className, matches[1]) 92 | 93 | doNewLine = not shareCommonNamespacePrefix 94 | 95 | if @scoreClassName(className, matches[1]) <= 0 96 | placeBelow = false 97 | 98 | # Normally we keep going until the sorting indicates we should stop, and then place the use 99 | # statement above the 'incorrect' match, but if the previous use statement was a use statement 100 | # that has the same namespace, we want to ensure we stick close to it instead of creating additional 101 | # newlines (which the item from the same namespace already placed). 102 | if previousMatchThatSharedNamespacePrefixRow? 103 | placeBelow = true 104 | doNewLine = false 105 | bestUseRow = previousMatchThatSharedNamespacePrefixRow 106 | 107 | break 108 | 109 | previousMatchThatSharedNamespacePrefixRow = if shareCommonNamespacePrefix then i else null 110 | 111 | # Insert the use statement itself. 112 | lineEnding = editor.getBuffer().lineEndingForRow(0) 113 | 114 | if not @allowAdditionalNewlines 115 | doNewLine = false 116 | 117 | if not lineEnding 118 | lineEnding = "\n" 119 | 120 | textToInsert = '' 121 | 122 | if doNewLine and placeBelow 123 | textToInsert += lineEnding 124 | 125 | textToInsert += "use #{className};" + lineEnding 126 | 127 | if doNewLine and not placeBelow 128 | textToInsert += lineEnding 129 | 130 | lineToInsertAt = bestUseRow + (if placeBelow then 1 else 0) 131 | editor.setTextInBufferRange([[lineToInsertAt, 0], [lineToInsertAt, 0]], textToInsert) 132 | 133 | return (1 + (if doNewLine then 1 else 0)) 134 | 135 | ###* 136 | * Returns a boolean indicating if the specified class names share a common namespace prefix. 137 | * 138 | * @param {String} firstClassName 139 | * @param {String} secondClassName 140 | * 141 | * @return {Boolean} 142 | ### 143 | doShareCommonNamespacePrefix: (firstClassName, secondClassName) -> 144 | firstClassNameParts = firstClassName.split('\\') 145 | secondClassNameParts = secondClassName.split('\\') 146 | 147 | firstClassNameParts.pop() 148 | secondClassNameParts.pop() 149 | 150 | return if firstClassNameParts.join('\\') == secondClassNameParts.join('\\') then true else false 151 | 152 | ###* 153 | * Scores the first class name against the second, indicating how much they 'match' each other. This can be used 154 | * to e.g. find an appropriate location to place a class in an existing list of classes. 155 | * 156 | * @param {String} firstClassName 157 | * @param {String} secondClassName 158 | * 159 | * @return {Number} A floating point number that represents the score. 160 | ### 161 | scoreClassName: (firstClassName, secondClassName) -> 162 | maxLength = 0 163 | totalScore = 0 164 | 165 | firstClassNameParts = firstClassName.split('\\') 166 | secondClassNameParts = secondClassName.split('\\') 167 | 168 | maxLength = Math.min(firstClassNameParts.length, secondClassNameParts.length) 169 | 170 | if maxLength >= 2 171 | for i in [0 .. maxLength - 2] 172 | if firstClassNameParts[i] != secondClassNameParts[i] 173 | return (firstClassNameParts[i].localeCompare(secondClassNameParts[i])) 174 | 175 | # At this point, both FQSEN's share a common namespace, e.g. A\B and A\B\C\D, or XMLElement and XMLDocument. 176 | # The one with the most namespace parts ends up last. 177 | if firstClassNameParts.length > secondClassNameParts.length 178 | return 1 179 | 180 | else if firstClassNameParts.length < secondClassNameParts.length 181 | return -1 182 | 183 | # Both items have share the same namespace, sort from shortest to longest last word (class, interface, ...). 184 | return firstClassName.length > secondClassName.length ? 1 : -1 185 | 186 | ###* 187 | * Sorts the use statements in the specified file according to the same algorithm used by 'addUseClass'. 188 | * 189 | * @param {TextEditor} editor 190 | ### 191 | sortUseStatements: (editor) -> 192 | endLine = null 193 | startLine = null 194 | useStatements = [] 195 | 196 | for i in [0 .. editor.getLineCount()] 197 | lineText = editor.lineTextForBufferRow(i) 198 | 199 | endLine = i 200 | 201 | if not lineText or lineText.trim() == '' 202 | continue 203 | 204 | else if (matches = @useStatementRegex.exec(lineText)) 205 | if not startLine 206 | startLine = i 207 | 208 | text = matches[1] 209 | 210 | if matches[2]? 211 | text += ' as ' + matches[2] 212 | 213 | useStatements.push(text); 214 | 215 | # We still do the regex check here to prevent continuing when there are no use statements at all. 216 | else if startLine or @structureStartRegex.test(lineText) 217 | break 218 | 219 | return if useStatements.length == 0 220 | 221 | editor.transact () => 222 | editor.setTextInBufferRange([[startLine, 0], [endLine, 0]], '') 223 | 224 | for useStatement in useStatements 225 | # The leading slash is unnecessary, not recommended, and messes up sorting, take it out. 226 | if useStatement[0] == '\\' 227 | useStatement = useStatement.substr(1) 228 | 229 | @addUseClass(editor, useStatement, @allowAdditionalNewlines) 230 | -------------------------------------------------------------------------------- /lib/ProjectManager.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | ##* 4 | # Handles project management 5 | ## 6 | class ProjectManager 7 | ###* 8 | * @var {Object} 9 | ### 10 | proxy: null 11 | 12 | ###* 13 | * @var {Object} 14 | ### 15 | indexingMediator: null 16 | 17 | ###* 18 | * The service instance from the project-manager package. 19 | * 20 | * @var {Object|null} 21 | ### 22 | activeProject: null 23 | 24 | ###* 25 | * Whether project indexing is currently happening. 26 | * 27 | * @var {bool} 28 | ### 29 | isProjectIndexingFlag: false 30 | 31 | ###* 32 | * Keeps track of files that are being indexed. 33 | * 34 | * @var {Object} 35 | ### 36 | indexMap: null 37 | 38 | ###* 39 | * Default settings for projects. 40 | * 41 | * Note that this object will be shared across instances! 42 | * 43 | * @var {Object} 44 | ### 45 | defaultProjectSettings: 46 | enabled: true 47 | php_integrator: 48 | enabled: true 49 | phpVersion: 5.6 50 | excludedPaths: [] 51 | fileExtensions: ['php'] 52 | 53 | ###* 54 | * @param {Object} proxy 55 | * @param {Object} indexingMediator 56 | ### 57 | constructor: (@proxy, @indexingMediator) -> 58 | @indexMap = {} 59 | 60 | ###* 61 | * @return {Object|null} 62 | ### 63 | getActiveProject: () -> 64 | return @activeProject 65 | 66 | ###* 67 | * @return {bool} 68 | ### 69 | hasActiveProject: () -> 70 | if @getActiveProject()? 71 | return true 72 | 73 | return false 74 | 75 | ###* 76 | * @return {bool} 77 | ### 78 | isProjectIndexing: () -> 79 | return @isProjectIndexingFlag 80 | 81 | ###* 82 | * Sets up the specified project for usage with this package. 83 | * 84 | * Default settings will be stored inside the package, if they aren't already present. If they already exist, they 85 | * will not be overwritten. 86 | * 87 | * Note that this method does not explicitly request persisting settings from the external project manager service. 88 | * 89 | * @param {Object} project 90 | * 91 | * @return {Object} The new settings of the project (that could be persisted). 92 | ### 93 | setUpProject: (project) -> 94 | projectPhpSettings = if project.getProps().php? then project.getProps().php else {} 95 | 96 | if projectPhpSettings.php_integrator? 97 | throw new Error(''' 98 | The currently active project was already initialized. To prevent existing settings from getting lost, 99 | the request has been aborted. 100 | ''') 101 | 102 | if not projectPhpSettings.enabled 103 | projectPhpSettings.enabled = true 104 | 105 | if not projectPhpSettings.php_integrator? 106 | projectPhpSettings.php_integrator = @defaultProjectSettings.php_integrator 107 | 108 | existingProps = project.getProps() 109 | existingProps.php = projectPhpSettings 110 | 111 | return existingProps 112 | 113 | ###* 114 | * @param {Object} project 115 | ### 116 | load: (project) -> 117 | @activeProject = null 118 | 119 | return if project.getProps().php?.enabled != true 120 | 121 | projectSettings = @getProjectSettings(project) 122 | 123 | return if projectSettings?.enabled != true 124 | 125 | @validateProject(project) 126 | 127 | @activeProject = project 128 | 129 | @proxy.setIndexDatabaseName(@getIndexDatabaseName(project)) 130 | 131 | successHandler = (repository) => 132 | return if not repository? 133 | return if not repository.async? 134 | 135 | # Will trigger on things such as git checkout. 136 | repository.async.onDidChangeStatuses () => 137 | @attemptIndex(project) 138 | 139 | failureHandler = () => 140 | return 141 | 142 | {Directory} = require 'atom' 143 | 144 | for projectDirectory in @getProjectPaths(project) 145 | projectDirectoryObject = new Directory(projectDirectory) 146 | 147 | atom.project.repositoryForDirectory(projectDirectoryObject).then(successHandler, failureHandler) 148 | 149 | ###* 150 | * @param {Object} 151 | * 152 | * @return {String} 153 | ### 154 | getIndexDatabaseName: (project) -> 155 | return project.getProps().title 156 | 157 | ###* 158 | * Validates a project by validating its settings. 159 | * 160 | * Throws an Error if something is not right with the project. 161 | * 162 | * @param {Object} project 163 | ### 164 | validateProject: (project) -> 165 | projectSettings = @getProjectSettings(project) 166 | 167 | if not projectSettings? 168 | throw new Error( 169 | 'No project settings were found, a php.php_integrator node must be present in your project settings!' 170 | ) 171 | 172 | phpVersion = projectSettings.phpVersion 173 | 174 | if isNaN(parseFloat(phpVersion)) or not isFinite(phpVersion) 175 | throw new Error(''' 176 | The PHP version that is set in your project settings is not valid! It must be a number, for example: 5.6 177 | ''') 178 | 179 | ###* 180 | * Retrieves a list of file extensions to include in indexing. 181 | * 182 | * @param {Object} project 183 | * 184 | * @return {Array} 185 | ### 186 | getFileExtensionsToIndex: (project) -> 187 | projectPaths = @getProjectPaths(project) 188 | projectSettings = @getProjectSettings(project) 189 | 190 | fileExtensions = projectSettings?.fileExtensions 191 | 192 | if not fileExtensions? 193 | fileExtensions = [] 194 | 195 | return fileExtensions 196 | 197 | ###* 198 | * Retrieves a list of absolute paths to exclude from indexing. 199 | * 200 | * @param {Object} project 201 | * 202 | * @return {Array} 203 | ### 204 | getAbsoluteExcludedPaths: (project) -> 205 | projectPaths = @getProjectPaths(project) 206 | projectSettings = @getProjectSettings(project) 207 | 208 | excludedPaths = projectSettings?.excludedPaths 209 | 210 | if not excludedPaths? 211 | excludedPaths = [] 212 | 213 | path = require 'path' 214 | 215 | absoluteExcludedPaths = [] 216 | 217 | for excludedPath in excludedPaths 218 | if path.isAbsolute(excludedPath) 219 | absoluteExcludedPaths.push(excludedPath) 220 | 221 | else 222 | matches = excludedPath.match(/^\{(\d+)\}(\/.*)$/) 223 | 224 | if matches? 225 | index = matches[1] 226 | 227 | # Relative paths starting with {n} are relative to the project path at index {n}, e.g. "{0}/test". 228 | if index > projectPaths.length 229 | throw new Error("Requested project path index " + index + ", but the project does not have that many paths!") 230 | 231 | absoluteExcludedPaths.push(projectPaths[index] + matches[2]) 232 | 233 | else 234 | absoluteExcludedPaths.push(path.normalize(excludedPath)) 235 | 236 | return absoluteExcludedPaths 237 | 238 | ###* 239 | * Indexes the project asynchronously. 240 | * 241 | * @param {Object} project 242 | * 243 | * @return {Promise} 244 | ### 245 | performIndex: (project) -> 246 | successHandler = () => 247 | return @indexingMediator.reindex( 248 | @getProjectPaths(project), 249 | null, 250 | @getAbsoluteExcludedPaths(project), 251 | @getFileExtensionsToIndex(project) 252 | ) 253 | 254 | return @indexingMediator.vacuum().then(successHandler) 255 | 256 | ###* 257 | * Performs a project index, but only if one is not currently already happening. 258 | * 259 | * @param {Object} project 260 | * 261 | * @return {Promise|null} 262 | ### 263 | attemptIndex: (project) -> 264 | return null if @isProjectIndexing() 265 | 266 | @isProjectIndexingFlag = true 267 | 268 | handler = () => 269 | @isProjectIndexingFlag = false 270 | 271 | successHandler = handler 272 | failureHandler = handler 273 | 274 | return @performIndex(project).then(successHandler, failureHandler) 275 | 276 | ###* 277 | * Indexes the current project, but only if one is not currently already happening. 278 | * 279 | * @return {Promise} 280 | ### 281 | attemptCurrentProjectIndex: () -> 282 | return @attemptIndex(@getActiveProject()) 283 | 284 | ###* 285 | * Initializes the project. 286 | * 287 | * @return {Promise|null} 288 | ### 289 | initializeCurrentProject: () -> 290 | return @indexingMediator.initialize() 291 | 292 | ###* 293 | * Vacuums the project. 294 | * 295 | * @return {Promise|null} 296 | ### 297 | vacuumCurrentProject: () -> 298 | return @indexingMediator.vacuum() 299 | 300 | ###* 301 | * Indexes a file asynchronously. 302 | * 303 | * @param {Object} project 304 | * @param {String} fileName The file to index. 305 | * @param {String|null} source The source code of the file to index. 306 | * 307 | * @return {Promise} 308 | ### 309 | performFileIndex: (project, fileName, source = null) -> 310 | return @indexingMediator.reindex( 311 | fileName, 312 | source, 313 | @getAbsoluteExcludedPaths(project), 314 | @getFileExtensionsToIndex(project) 315 | ) 316 | 317 | ###* 318 | * Performs a file index, but only if the file is not currently already being indexed (otherwise silently returns). 319 | * 320 | * @param {Object} project 321 | * @param {String} fileName The file to index. 322 | * @param {String|null} source The source code of the file to index. 323 | * 324 | * @return {Promise|null} 325 | ### 326 | attemptFileIndex: (project, fileName, source = null) -> 327 | return null if @isProjectIndexing() 328 | 329 | if fileName not of @indexMap 330 | @indexMap[fileName] = { 331 | isBeingIndexed : true 332 | nextIndexSource : null 333 | } 334 | 335 | else if @indexMap[fileName].isBeingIndexed 336 | # This file is already being indexed, so keep track of the most recent changes so we can index any changes 337 | # after the current indexing process finishes. 338 | @indexMap[fileName].nextIndexSource = source 339 | return null 340 | 341 | @indexMap[fileName].isBeingIndexed = true 342 | 343 | handler = () => 344 | @indexMap[fileName].isBeingIndexed = false 345 | 346 | if @indexMap[fileName].nextIndexSource? 347 | nextIndexSource = @indexMap[fileName].nextIndexSource 348 | 349 | @indexMap[fileName].nextIndexSource = null 350 | 351 | @attemptFileIndex(project, fileName, nextIndexSource) 352 | 353 | successHandler = handler 354 | failureHandler = handler 355 | 356 | return @performFileIndex(project, fileName, source).then(successHandler, failureHandler) 357 | 358 | ###* 359 | * Indexes the current project asynchronously. 360 | * 361 | * @param {String} fileName The file to index. 362 | * @param {String|null} source The source code of the file to index. 363 | * 364 | * @return {Promise} 365 | ### 366 | attemptCurrentProjectFileIndex: (fileName, source = null) -> 367 | return @attemptFileIndex(@getActiveProject(), fileName, source) 368 | 369 | ###* 370 | * @return {Object|null} 371 | ### 372 | getProjectSettings: (project) -> 373 | if project.getProps().php?.php_integrator? 374 | return project.getProps().php.php_integrator 375 | 376 | return null 377 | 378 | ###* 379 | * @return {Object|null} 380 | ### 381 | getCurrentProjectSettings: () -> 382 | return @getProjectSettings(@getActiveProject()) 383 | 384 | ###* 385 | * @return {Array} 386 | ### 387 | getProjectPaths: (project) -> 388 | return project.getProps().paths 389 | 390 | ###* 391 | * Indicates if the specified file is part of the project. 392 | * 393 | * @param {Object} project 394 | * @param {String} fileName 395 | * 396 | * @return {bool} 397 | ### 398 | isFilePartOfProject: (project, fileName) -> 399 | {Directory} = require 'atom' 400 | 401 | for projectDirectory in @getProjectPaths(project) 402 | projectDirectoryObject = new Directory(projectDirectory) 403 | 404 | if projectDirectoryObject.contains(fileName) 405 | return true 406 | 407 | return false 408 | 409 | ###* 410 | * Indicates if the specified file is part of the current project. 411 | * 412 | * @param {String} fileName 413 | * 414 | * @return {bool} 415 | ### 416 | isFilePartOfCurrentProject: (fileName) -> 417 | return @isFilePartOfProject(@getActiveProject(), fileName) 418 | -------------------------------------------------------------------------------- /lib/Service.coffee: -------------------------------------------------------------------------------- 1 | Popover = require './Widgets/Popover' 2 | AttachedPopover = require './Widgets/AttachedPopover' 3 | 4 | module.exports = 5 | 6 | ##* 7 | # The service that is exposed to other packages. 8 | ## 9 | class Service 10 | ###* 11 | * @var {Object} 12 | ### 13 | config: null 14 | 15 | ###* 16 | * @var {Object} 17 | ### 18 | proxy: null 19 | 20 | ###* 21 | * @var {Object} 22 | ### 23 | projectManager: null 24 | 25 | ###* 26 | * @var {Object} 27 | ### 28 | indexingMediator: null 29 | 30 | ###* 31 | * @var {Object} 32 | ### 33 | useStatementHelper: null 34 | 35 | ###* 36 | * Constructor. 37 | * 38 | * @param {AtomConfig} config 39 | * @param {CachingProxy} proxy 40 | * @param {Object} projectManager 41 | * @param {Object} indexingMediator 42 | * @param {Object} useStatementHelper 43 | ### 44 | constructor: (@config, @proxy, @projectManager, @indexingMediator, @useStatementHelper) -> 45 | 46 | ###* 47 | * Retrieves the use statement helper, which contains utility methods for dealing with use statements. 48 | * 49 | * @return {Object} 50 | ### 51 | getUseStatementHelper: () -> 52 | return @useStatementHelper 53 | 54 | ###* 55 | * Retrieves the settings (that are specific to this package) for the currently active project. If there is no 56 | * active project or the project does not have any settings, null is returned. 57 | * 58 | * @return {Object|null} 59 | ### 60 | getCurrentProjectSettings: () -> 61 | return @projectManager.getCurrentProjectSettings() 62 | 63 | ###* 64 | * Clears the autocompletion cache. Most fetching operations such as fetching constants, autocompletion, fetching 65 | * members, etc. are cached when they are first retrieved. This clears the cache, forcing them to be retrieved 66 | # again. Clearing the cache is automatically performed, so this method is usually unnecessary. 67 | ### 68 | clearCache: () -> 69 | @proxy.clearCache() 70 | 71 | ###* 72 | * Retrieves a list of available classes. 73 | * 74 | * @return {Promise} 75 | ### 76 | getClassList: () -> 77 | return @proxy.getClassList() 78 | 79 | ###* 80 | * Retrieves a list of available classes in the specified file. 81 | * 82 | * @param {String} file 83 | * 84 | * @return {Promise} 85 | ### 86 | getClassListForFile: (file) -> 87 | return @proxy.getClassListForFile(file) 88 | 89 | ###* 90 | * Retrieves a list of namespaces. 91 | * 92 | * @return {Promise} 93 | ### 94 | getNamespaceList: () -> 95 | return @proxy.getNamespaceList() 96 | 97 | ###* 98 | * Retrieves a list of namespaces in the specified file. 99 | * 100 | * @param {String} file 101 | * 102 | * @return {Promise} 103 | ### 104 | getNamespaceListForFile: (file) -> 105 | return @proxy.getNamespaceListForFile(file) 106 | 107 | ###* 108 | * Retrieves a list of available global constants. 109 | * 110 | * @return {Promise} 111 | ### 112 | getGlobalConstants: () -> 113 | return @proxy.getGlobalConstants() 114 | 115 | ###* 116 | * Retrieves a list of available global functions. 117 | * 118 | * @return {Promise} 119 | ### 120 | getGlobalFunctions: () -> 121 | return @proxy.getGlobalFunctions() 122 | 123 | ###* 124 | * Retrieves a list of available members of the class (or interface, trait, ...) with the specified name. 125 | * 126 | * @param {String} className 127 | * 128 | * @return {Promise} 129 | ### 130 | getClassInfo: (className) -> 131 | return @proxy.getClassInfo(className) 132 | 133 | ###* 134 | * Resolves a local type in the specified file, based on use statements and the namespace. 135 | * 136 | * @param {String} file 137 | * @param {Number} line The line the type is located at. The first line is 1, not 0. 138 | * @param {String} type 139 | * @param {String} kind The kind of element. Either 'classlike', 'constant' or 'function'. 140 | * 141 | * @return {Promise} 142 | ### 143 | resolveType: (file, line, type, kind) -> 144 | return @proxy.resolveType(file, line, type, kind) 145 | 146 | ###* 147 | * Localizes a type to the specified file, making it relative to local use statements, if possible. If not possible, 148 | * null is returned. 149 | * 150 | * @param {String} file 151 | * @param {Number} line The line the type is located at. The first line is 1, not 0. 152 | * @param {String} type 153 | * @param {String} kind The kind of element. Either 'classlike', 'constant' or 'function'. 154 | * 155 | * @return {Promise} 156 | ### 157 | localizeType: (file, line, type, kind) -> 158 | return @proxy.localizeType(file, line, type, kind) 159 | 160 | ###* 161 | * Performs a semantic lint of the specified file. 162 | * 163 | * @param {String} file 164 | * @param {String|null} source The source code of the file to index. May be null if a directory is passed instead. 165 | * @param {Object} options Additional options to set. Boolean properties noUnknownClasses, noUnknownMembers, 166 | * noUnknownGlobalFunctions, noUnknownGlobalConstants, noDocblockCorrectness and 167 | * noUnusedUseStatements are supported. 168 | * 169 | * @return {Promise} 170 | ### 171 | semanticLint: (file, source, options = {}) -> 172 | return @proxy.semanticLint(file, source, options) 173 | 174 | ###* 175 | * Fetches all available variables at a specific location. 176 | * 177 | * @param {String|null} file The path to the file to examine. May be null if the source parameter is passed. 178 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 179 | * @param {Number} offset The character offset into the file to examine. 180 | * 181 | * @return {Promise} 182 | ### 183 | getAvailableVariablesByOffset: (file, source, offset) -> 184 | return @proxy.getAvailableVariables(file, source, offset) 185 | 186 | ###* 187 | * Deduces the resulting types of an expression. 188 | * 189 | * @param {String|null} expression The expression to deduce the type of, e.g. '$this->foo()'. If null, the 190 | * expression just before the specified offset will be used. 191 | * @param {String} file The path to the file to examine. 192 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 193 | * @param {Number} offset The character offset into the file to examine. 194 | * @param {bool} ignoreLastElement Whether to remove the last element or not, this is useful when the user 195 | * is still writing code, e.g. "$this->foo()->b" would normally return the 196 | * type (class) of 'b', as it is the last element, but as the user is still 197 | * writing code, you may instead be interested in the type of 'foo()' 198 | * instead. 199 | * 200 | * @return {Promise} 201 | ### 202 | deduceTypes: (expression, file, source, offset, ignoreLastElement) -> 203 | return @proxy.deduceTypes(expression, file, source, offset, ignoreLastElement) 204 | 205 | ###* 206 | * Retrieves the call stack of the function or method that is being invoked at the specified position. This can be 207 | * used to fetch information about the function or method call the cursor is in. 208 | * 209 | * @param {String|null} file The path to the file to examine. May be null if the source parameter is passed. 210 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 211 | * @param {Number} offset The character offset into the file to examine. 212 | * 213 | * @return {Promise} With elements 'callStack' (array) as well as 'argumentIndex' which denotes the argument in the 214 | * parameter list the position is located at. Returns 'null' if not in a method or function call. 215 | ### 216 | getInvocationInfo: (file, source, offset) -> 217 | return @proxy.getInvocationInfo(file, source, offset) 218 | 219 | ###* 220 | * Convenience alias for {@see deduceTypes}. 221 | * 222 | * @param {String} expression 223 | * @param {TextEditor} editor 224 | * @param {Range} bufferPosition 225 | * 226 | * @return {Promise} 227 | ### 228 | deduceTypesAt: (expression, editor, bufferPosition) -> 229 | offset = editor.getBuffer().characterIndexForPosition(bufferPosition) 230 | 231 | bufferText = editor.getBuffer().getText() 232 | 233 | return @deduceTypes(expression, editor.getPath(), bufferText, offset) 234 | 235 | ###* 236 | * Refreshes the specified file or folder. This method is asynchronous and will return immediately. 237 | * 238 | * @param {String|Array} path The full path to the file or folder to refresh. Alternatively, 239 | * this can be a list of items to index at the same time. 240 | * @param {String|null} source The source code of the file to index. May be null if a directory is 241 | * passed instead. 242 | * @param {Array} excludedPaths A list of paths to exclude from indexing. 243 | * @param {Array} fileExtensionsToIndex A list of file extensions (without leading dot) to index. 244 | * 245 | * @return {Promise} 246 | ### 247 | reindex: (path, source, excludedPaths, fileExtensionsToIndex) -> 248 | return @indexingMediator.reindex(path, source, excludedPaths, fileExtensionsToIndex) 249 | 250 | ###* 251 | * Initializes a project. 252 | * 253 | * @return {Promise} 254 | ### 255 | initialize: () -> 256 | return @indexingMediator.initialize() 257 | 258 | ###* 259 | * Vacuums a project, cleaning up the index database (e.g. pruning files that no longer exist). 260 | * 261 | * @return {Promise} 262 | ### 263 | vacuum: () -> 264 | return @indexingMediator.vacuum() 265 | 266 | ###* 267 | * Attaches a callback to indexing started event. The returned disposable can be used to detach your event handler. 268 | * 269 | * @param {Callback} callback A callback that takes one parameter which contains a 'path' property. 270 | * 271 | * @return {Disposable} 272 | ### 273 | onDidStartIndexing: (callback) -> 274 | return @indexingMediator.onDidStartIndexing(callback) 275 | 276 | ###* 277 | * Attaches a callback to indexing progress event. The returned disposable can be used to detach your event handler. 278 | * 279 | * @param {Callback} callback A callback that takes one parameter which contains a 'path' and a 'percentage' 280 | * property. 281 | * 282 | * @return {Disposable} 283 | ### 284 | onDidIndexingProgress: (callback) -> 285 | return @indexingMediator.onDidIndexingProgress(callback) 286 | 287 | ###* 288 | * Attaches a callback to indexing finished event. The returned disposable can be used to detach your event handler. 289 | * 290 | * @param {Callback} callback A callback that takes one parameter which contains an 'output' and a 'path' property. 291 | * 292 | * @return {Disposable} 293 | ### 294 | onDidFinishIndexing: (callback) -> 295 | return @indexingMediator.onDidFinishIndexing(callback) 296 | 297 | ###* 298 | * Attaches a callback to indexing failed event. The returned disposable can be used to detach your event handler. 299 | * 300 | * @param {Callback} callback A callback that takes one parameter which contains an 'error' and a 'path' property. 301 | * 302 | * @return {Disposable} 303 | ### 304 | onDidFailIndexing: (callback) -> 305 | return @indexingMediator.onDidFailIndexing(callback) 306 | 307 | ###* 308 | * Determines the current class' FQCN based on the specified buffer position. 309 | * 310 | * @param {TextEditor} editor The editor that contains the class (needed to resolve relative class names). 311 | * @param {Point} bufferPosition 312 | * 313 | * @return {Promise} 314 | ### 315 | determineCurrentClassName: (editor, bufferPosition) -> 316 | return new Promise (resolve, reject) => 317 | path = editor.getPath() 318 | 319 | if not path? 320 | reject() 321 | return 322 | 323 | successHandler = (classesInFile) => 324 | for name,classInfo of classesInFile 325 | if bufferPosition.row >= classInfo.startLine and bufferPosition.row <= classInfo.endLine 326 | resolve(name) 327 | 328 | resolve(null) 329 | 330 | failureHandler = () => 331 | reject() 332 | 333 | return @getClassListForFile(path).then(successHandler, failureHandler) 334 | 335 | ###* 336 | * Determines the current namespace on the specified buffer position. 337 | * 338 | * @param {TextEditor} editor The editor that contains the class (needed to resolve relative class names). 339 | * @param {Point} bufferPosition 340 | * 341 | * @return {Promise} 342 | ### 343 | determineCurrentNamespaceName: (editor, bufferPosition) -> 344 | return new Promise (resolve, reject) => 345 | path = editor.getPath() 346 | 347 | if not path? 348 | reject() 349 | return 350 | 351 | successHandler = (namespacesInFile) => 352 | for namespace in namespacesInFile 353 | if bufferPosition.row >= namespace.startLine and (bufferPosition.row <= namespace.endLine or not namespace.endLine?) 354 | resolve(namespace.name) 355 | 356 | resolve(null) 357 | 358 | failureHandler = () => 359 | reject() 360 | 361 | return @getNamespaceListForFile(path).then(successHandler, failureHandler) 362 | 363 | ###* 364 | * Convenience function that resolves types using {@see resolveType}, automatically determining the correct 365 | * parameters for the editor and buffer position. 366 | * 367 | * @param {TextEditor} editor The editor. 368 | * @param {Point} bufferPosition The location of the type. 369 | * @param {String} type The (local) type to resolve. 370 | * @param {String} kind The kind of element. Either 'classlike', 'constant' or 'function'. 371 | * 372 | * @return {Promise} 373 | * 374 | * @example In a file with namespace A\B, determining C could lead to A\B\C. 375 | ### 376 | resolveTypeAt: (editor, bufferPosition, type, kind) -> 377 | return @resolveType(editor.getPath(), bufferPosition.row + 1, type, kind) 378 | 379 | ###* 380 | * Retrieves all variables that are available at the specified buffer position. 381 | * 382 | * @param {TextEditor} editor 383 | * @param {Range} bufferPosition 384 | * 385 | * @return {Promise} 386 | ### 387 | getAvailableVariables: (editor, bufferPosition) -> 388 | offset = editor.getBuffer().characterIndexForPosition(bufferPosition) 389 | 390 | return @getAvailableVariablesByOffset(editor.getPath(), editor.getBuffer().getText(), offset) 391 | 392 | ###* 393 | * Retrieves the types that are being used (called) at the specified location in the buffer. Note that this does not 394 | * guarantee that the returned types actually exist. You can use {@see getClassInfo} on the returned class name 395 | * to check for this instead. 396 | * 397 | * @param {TextEditor} editor The text editor to use. 398 | * @param {Point} bufferPosition The cursor location of the item, such as the class member. Note that this 399 | * should always be at the end of the actual member (i.e. just after it). 400 | * If you want to ignore the element at the buffer position itself, see 401 | * 'ignoreLastElement'. 402 | * @param {boolean} ignoreLastElement Whether to remove the last element or not, this is useful when the user 403 | * is still writing code, e.g. "$this->foo()->b" would normally return the 404 | * type (class) of 'b', as it is the last element, but as the user is still 405 | * writing code, you may instead be interested in the type of 'foo()' instead. 406 | * 407 | * @return {Promise} 408 | * 409 | * @example Invoking it on MyMethod::foo()->bar() will ask what class 'bar' is invoked on, which will whatever types 410 | * foo returns. 411 | ### 412 | getResultingTypesAt: (editor, bufferPosition, ignoreLastElement) -> 413 | offset = editor.getBuffer().characterIndexForPosition(bufferPosition) 414 | 415 | bufferText = editor.getBuffer().getText() 416 | 417 | return @deduceTypes(null, editor.getPath(), bufferText, offset, true) 418 | 419 | ###* 420 | * Retrieves the call stack of the function or method that is being invoked at the specified position. This can be 421 | * used to fetch information about the function or method call the cursor is in. 422 | * 423 | * @param {TextEditor} editor 424 | * @param {Point} bufferPosition 425 | * 426 | * @return {Promise} With elements 'callStack' (array) as well as 'argumentIndex' which denotes the argument in the 427 | * parameter list the position is located at. Returns 'null' if not in a method or function call. 428 | * 429 | * @example "$this->test(1, function () {},| 2);" (where the vertical bar denotes the cursor position) will yield 430 | * ['$this', 'test']. 431 | ### 432 | getInvocationInfoAt: (editor, bufferPosition) -> 433 | offset = editor.getBuffer().characterIndexForPosition(bufferPosition) 434 | 435 | bufferText = editor.getBuffer().getText() 436 | 437 | return @getInvocationInfo(editor.getPath(), bufferText, offset) 438 | 439 | ###* 440 | * Creates a popover with the specified constructor arguments. 441 | ### 442 | createPopover: () -> 443 | return new Popover(arguments...) 444 | 445 | ###* 446 | * Creates an attached popover with the specified constructor arguments. 447 | ### 448 | createAttachedPopover: () -> 449 | return new AttachedPopover(arguments...) 450 | 451 | ###* 452 | * Indicates if the specified type is a basic type (e.g. int, array, object, etc.). 453 | * 454 | * @param {String} type 455 | * 456 | * @return {boolean} 457 | ### 458 | isBasicType: (type) -> 459 | return /^(string|int|bool|float|object|mixed|array|resource|void|null|callable|false|true|self|static|parent|\$this)$/i.test(type) 460 | 461 | ###* 462 | * Utility function to convert byte offsets returned by the service into character offsets. 463 | * 464 | * @param {Number} byteOffset 465 | * @param {String} string 466 | * 467 | * @return {Number} 468 | ### 469 | getCharacterOffsetFromByteOffset: (byteOffset, string) -> 470 | {Buffer} = require 'buffer' 471 | 472 | buffer = new Buffer(string) 473 | 474 | return buffer.slice(0, byteOffset).toString().length 475 | 476 | ###* 477 | * @param {String} fqcn 478 | * 479 | * @return {String} 480 | ### 481 | getDocumentationUrlForClass: (fqcn) -> 482 | return @config.get('php_documentation_base_urls').classes + @getNormalizedFqcnDocumentationUrl(fqcn) 483 | 484 | ###* 485 | * @param {String} fqcn 486 | * 487 | * @return {String} 488 | ### 489 | getDocumentationUrlForFunction: (fqcn) -> 490 | return @config.get('php_documentation_base_urls').functions + @getNormalizedFqcnDocumentationUrl(fqcn) 491 | 492 | ###* 493 | * @param {String} classlikeFqcn 494 | * @param {String} name 495 | * 496 | * @return {String} 497 | ### 498 | getDocumentationUrlForClassMethod: (classlikeFqcn, name) -> 499 | return @config.get('php_documentation_base_urls').root + @getNormalizedFqcnDocumentationUrl(classlikeFqcn) + '.' + @getNormalizeMethodDocumentationUrl(name) 500 | 501 | ###* 502 | * @param {String} name 503 | * 504 | * @return {String} 505 | ### 506 | getNormalizedFqcnDocumentationUrl: (name) -> 507 | if name.length > 0 and name[0] == '\\' 508 | name = name.substr(1) 509 | 510 | return name.replace(/\\/g, '-').toLowerCase() 511 | 512 | ###* 513 | * @param {String} name 514 | * 515 | * @return {String} 516 | ### 517 | getNormalizeMethodDocumentationUrl: (name) -> 518 | return name.replace(/^__/, '') 519 | -------------------------------------------------------------------------------- /lib/Main.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | ###* 3 | * Configuration settings. 4 | ### 5 | config: 6 | phpCommand: 7 | title : 'PHP command' 8 | description : 'The path to your PHP binary (e.g. /usr/bin/php, php, ...). Requires a restart. If you update 9 | to a new minor or major version, you may want to force reindex your project to index the new 10 | built-in structural elements.' 11 | type : 'string' 12 | default : 'php' 13 | order : 1 14 | 15 | additionalIndexingDelay: 16 | title : 'Additional delay before reindexing' 17 | description : 'File reindexing occurs as soon as its editor\'s contents stop changing. This is after a 18 | fixed time (about 300 ms at the time of writing) and is managed by Atom itself. If this is 19 | too fast for you, you can add an additional delay with this option. Fewer indexes means less 20 | load as tasks such as linting are invoked less often. It also means that it will take longer 21 | for changes to be reflected in various components, such as autocompletion.' 22 | type : 'integer' 23 | default : 0 24 | order : 2 25 | 26 | memoryLimit: 27 | title : 'Memory limit (in MB)' 28 | description : 'The memory limit to set to the PHP process. The PHP process uses the available memory for 29 | in-memory caching as well, so it should not be too low. On the other hand, it should\'t be 30 | growing very large, so setting it to -1 is probably a bad idea as an infinite loop bug 31 | might take down your system. The default is probably a good value, unless there is a 32 | specific reason you want to change it.' 33 | type : 'integer' 34 | default : 1024 35 | order : 3 36 | 37 | insertNewlinesForUseStatements: 38 | title : 'Insert newlines for use statements' 39 | description : 'When enabled, additional newlines are inserted before or after an automatically added 40 | use statement when they can\'t be nicely added to an existing \'group\'. This results in 41 | more cleanly separated use statements but will create additional vertical whitespace.' 42 | type : 'boolean' 43 | default : false 44 | order : 4 45 | 46 | ###* 47 | * The version of the core to download (version specification string). 48 | * 49 | * @var {String} 50 | ### 51 | coreVersionSpecification: "2.1.7" 52 | 53 | ###* 54 | * The name of the package. 55 | * 56 | * @var {String} 57 | ### 58 | packageName: 'php-integrator-base-legacy-php56' 59 | 60 | ###* 61 | * The configuration object. 62 | * 63 | * @var {Object} 64 | ### 65 | configuration: null 66 | 67 | ###* 68 | * The proxy object. 69 | * 70 | * @var {Object} 71 | ### 72 | proxy: null 73 | 74 | ###* 75 | * The exposed service. 76 | * 77 | * @var {Object} 78 | ### 79 | service: null 80 | 81 | ###* 82 | * The status bar manager. 83 | * 84 | * @var {Object} 85 | ### 86 | statusBarManager: null 87 | 88 | ###* 89 | * @var {IndexingMediator} 90 | ### 91 | indexingMediator: null 92 | 93 | ###* 94 | * A list of disposables to dispose when the package deactivates. 95 | * 96 | * @var {Object|null} 97 | ### 98 | disposables: null 99 | 100 | ###* 101 | * The currently active project, if any. 102 | * 103 | * @var {Object|null} 104 | ### 105 | activeProject: null 106 | 107 | ###* 108 | * @var {String|null} 109 | ### 110 | timerName: null 111 | 112 | ###* 113 | * @var {String|null} 114 | ### 115 | progressBarTimeout: null 116 | 117 | ###* 118 | * The service instance from the project-manager package. 119 | * 120 | * @var {Object|null} 121 | ### 122 | projectManagerService: null 123 | 124 | ###* 125 | * @var {Object|null} 126 | ### 127 | editorTimeoutMap: null 128 | 129 | ###* 130 | * Tests the user's configuration. 131 | * 132 | * @param {bool} testServices 133 | * 134 | * @return {bool} 135 | ### 136 | testConfig: (testServices = true) -> 137 | ConfigTester = require './ConfigTester' 138 | 139 | configTester = new ConfigTester(@getConfiguration()) 140 | 141 | result = configTester.test() 142 | 143 | if not result 144 | errorMessage = 145 | "PHP is not correctly set up and as a result PHP integrator will not work. Please visit the settings 146 | screen to correct this error. If you are not specifying an absolute path for PHP or Composer, make 147 | sure they are in your PATH." 148 | 149 | atom.notifications.addError('Incorrect setup!', {'detail': errorMessage}) 150 | 151 | return false 152 | 153 | if testServices and not @projectManagerService? 154 | errorMessage = 155 | "There is no project manager service available. Install the atom-project-manager package for project 156 | support to work in its full extent." 157 | 158 | atom.notifications.addError('Incorrect setup!', {'detail': errorMessage}) 159 | 160 | return false 161 | 162 | return true 163 | 164 | ###* 165 | * Registers any commands that are available to the user. 166 | ### 167 | registerCommands: () -> 168 | atom.commands.add 'atom-workspace', "php-integrator-base:set-up-current-project": => 169 | if not @projectManagerService? 170 | errorMessage = ''' 171 | The project manager service was not found. Did you perhaps forget to install the project-manager 172 | package or another package able to provide it? 173 | ''' 174 | 175 | atom.notifications.addError('Incorrect setup!', {'detail': errorMessage}) 176 | return 177 | 178 | if not @activeProject? 179 | errorMessage = ''' 180 | No project is currently active. Please set up and activate one before attempting to set it up. 181 | ''' 182 | 183 | atom.notifications.addError('Incorrect setup!', {'detail': errorMessage}) 184 | return 185 | 186 | project = @activeProject 187 | 188 | newProperties = null 189 | 190 | try 191 | newProperties = @projectManager.setUpProject(project) 192 | 193 | if not newProperties? 194 | throw new Error('No properties returned, this should never happen!') 195 | 196 | catch error 197 | atom.notifications.addError('Error!', { 198 | 'detail' : error.message 199 | }) 200 | 201 | return 202 | 203 | @projectManagerService.saveProject(newProperties) 204 | 205 | atom.notifications.addSuccess 'Success', { 206 | 'detail' : 'Your current project has been set up as PHP project. Indexing will now commence.' 207 | } 208 | 209 | @projectManager.load(project) 210 | 211 | @performInitialFullIndexForCurrentProject() 212 | 213 | atom.commands.add 'atom-workspace', "php-integrator-base:index-project": => 214 | return if not @projectManager.hasActiveProject() 215 | 216 | @projectManager.attemptCurrentProjectIndex() 217 | 218 | atom.commands.add 'atom-workspace', "php-integrator-base:force-index-project": => 219 | return if not @projectManager.hasActiveProject() 220 | 221 | @performInitialFullIndexForCurrentProject() 222 | 223 | atom.commands.add 'atom-workspace', "php-integrator-base:configuration": => 224 | return unless @testConfig() 225 | 226 | atom.notifications.addSuccess 'Success', { 227 | 'detail' : 'Your PHP integrator configuration is working correctly!' 228 | } 229 | 230 | atom.commands.add 'atom-workspace', "php-integrator-base:sort-use-statements": => 231 | activeTextEditor = atom.workspace.getActiveTextEditor() 232 | 233 | return if not activeTextEditor? 234 | 235 | @getUseStatementHelper().sortUseStatements(activeTextEditor) 236 | 237 | ###* 238 | * Performs the "initial" index for a new project by initializing it and then performing a project index. 239 | * 240 | * @return {Promise} 241 | ### 242 | performInitialFullIndexForCurrentProject: () -> 243 | successHandler = () => 244 | return @projectManager.attemptCurrentProjectIndex() 245 | 246 | failureHandler = (reason) => 247 | console.error(reason) 248 | 249 | atom.notifications.addError('Error!', { 250 | 'detail' : 'The project could not be properly initialized!' 251 | }) 252 | 253 | return @projectManager.initializeCurrentProject().then(successHandler, failureHandler) 254 | 255 | ###* 256 | * Registers listeners for configuration changes. 257 | ### 258 | registerConfigListeners: () -> 259 | config = @getConfiguration() 260 | 261 | config.onDidChange 'insertNewlinesForUseStatements', (value) => 262 | @getUseStatementHelper().setAllowAdditionalNewlines(value) 263 | 264 | ###* 265 | * Registers status bar listeners. 266 | ### 267 | registerStatusBarListeners: () -> 268 | service = @getService() 269 | 270 | service.onDidStartIndexing () => 271 | if @progressBarTimeout 272 | clearTimeout(@progressBarTimeout) 273 | 274 | # Indexing could be anything: the entire project or just a file. If indexing anything takes too long, show 275 | # the progress bar to indicate we're doing something. 276 | @progressBarTimeout = setTimeout ( => 277 | @progressBarTimeout = null 278 | 279 | @timerName = @packageName + " - Indexing" 280 | 281 | console.time(@timerName); 282 | 283 | if @statusBarManager? 284 | @statusBarManager.setLabel("Indexing...") 285 | @statusBarManager.setProgress(null) 286 | @statusBarManager.show() 287 | ), 1000 288 | 289 | service.onDidFinishIndexing () => 290 | if @progressBarTimeout 291 | clearTimeout(@progressBarTimeout) 292 | @progressBarTimeout = null 293 | 294 | else 295 | console.timeEnd(@timerName) 296 | 297 | if @statusBarManager? 298 | @statusBarManager.setLabel("Indexing completed!") 299 | @statusBarManager.hide() 300 | 301 | service.onDidFailIndexing () => 302 | if @progressBarTimeout 303 | clearTimeout(@progressBarTimeout) 304 | @progressBarTimeout = null 305 | 306 | else 307 | console.timeEnd(@timerName) 308 | 309 | if @statusBarManager? 310 | @statusBarManager.showMessage("Indexing failed!", "highlight-error") 311 | @statusBarManager.hide() 312 | 313 | service.onDidIndexingProgress (data) => 314 | if @statusBarManager? 315 | @statusBarManager.setProgress(data.percentage) 316 | @statusBarManager.setLabel("Indexing... (" + data.percentage.toFixed(2) + " %)") 317 | 318 | ###* 319 | * Attaches items to the status bar. 320 | * 321 | * @param {mixed} statusBarService 322 | ### 323 | attachStatusBarItems: (statusBarService) -> 324 | if not @statusBarManager 325 | StatusBarManager = require "./Widgets/StatusBarManager" 326 | 327 | @statusBarManager = new StatusBarManager() 328 | @statusBarManager.initialize(statusBarService) 329 | @statusBarManager.setLabel("Indexing...") 330 | 331 | ###* 332 | * Detaches existing items from the status bar. 333 | ### 334 | detachStatusBarItems: () -> 335 | if @statusBarManager 336 | @statusBarManager.destroy() 337 | @statusBarManager = null 338 | 339 | ###* 340 | * @return {Promise} 341 | ### 342 | updateCoreIfOutdated: () -> 343 | if @getCoreManager().isInstalled() 344 | return new Promise (resolve, reject) -> 345 | resolve() 346 | 347 | message = 348 | "The core isn't installed yet or is outdated. A new version is in the process of being downloaded." 349 | 350 | atom.notifications.addInfo('PHP Integrator - Downloading Core', {'detail': message}) 351 | 352 | successHandler = () -> 353 | atom.notifications.addSuccess('Core installation successful') 354 | 355 | failureHandler = () -> 356 | atom.notifications.addError('Core installation failed') 357 | 358 | return @getCoreManager().install().then(successHandler, failureHandler) 359 | 360 | ###* 361 | * Activates the package. 362 | ### 363 | activate: -> 364 | @testConfig(false) 365 | 366 | @updateCoreIfOutdated().then () => 367 | @registerCommands() 368 | @registerConfigListeners() 369 | @registerStatusBarListeners() 370 | 371 | @editorTimeoutMap = {} 372 | 373 | @registerAtomListeners() 374 | 375 | @getCachingProxy().setIsActive(true) 376 | 377 | ###* 378 | * Registers listeners for events from Atom's API. 379 | ### 380 | registerAtomListeners: () -> 381 | @getDisposables().add atom.workspace.observeTextEditors (editor) => 382 | @registerTextEditorListeners(editor) 383 | 384 | ###* 385 | * @param {TextEditor} editor 386 | ### 387 | registerTextEditorListeners: (editor) -> 388 | # The default onDidStopChanging timeout is 300 milliseconds. As this is notcurrently configurable (and would 389 | # also impact other packages), we install our own timeout on top of the existing one. This is useful for users 390 | # that don't type particularly fast or are on slower machines and will prevent constant indexing from happening. 391 | @getDisposables().add editor.onDidStopChanging () => 392 | path = editor.getPath() 393 | 394 | additionalIndexingDelay = @getConfiguration().get('additionalIndexingDelay') 395 | 396 | @editorTimeoutMap[path] = setTimeout ( => 397 | @onEditorDidStopChanging(editor) 398 | @editorTimeoutMap[path] = null 399 | ), additionalIndexingDelay 400 | 401 | @getDisposables().add editor.onDidChange () => 402 | path = editor.getPath() 403 | 404 | if @editorTimeoutMap[path]? 405 | clearTimeout(@editorTimeoutMap[path]) 406 | @editorTimeoutMap[path] = null 407 | 408 | ###* 409 | * Invoked when an editor stops changing. 410 | * 411 | * @param {TextEditor} editor 412 | ### 413 | onEditorDidStopChanging: (editor) -> 414 | return unless /text.html.php$/.test(editor.getGrammar().scopeName) 415 | 416 | fileName = editor.getPath() 417 | 418 | return if not fileName 419 | 420 | projectManager = @getProjectManager() 421 | 422 | if projectManager.hasActiveProject() and projectManager.isFilePartOfCurrentProject(fileName) 423 | projectManager.attemptCurrentProjectFileIndex(fileName, editor.getBuffer().getText()) 424 | 425 | ###* 426 | * Deactivates the package. 427 | ### 428 | deactivate: -> 429 | if @disposables 430 | @disposables.dispose() 431 | @disposables = null 432 | 433 | @getCachingProxy().stopPhpServer() 434 | 435 | ###* 436 | * Sets the status bar service, which is consumed by this package. 437 | * 438 | * @param {Object} service 439 | ### 440 | setStatusBarService: (service) -> 441 | @attachStatusBarItems(service) 442 | 443 | # This method is usually invoked after the indexing has already started, hence we can't unconditionally hide it 444 | # here or it will never be made visible again. 445 | if not @getProjectManager().isProjectIndexing() 446 | @statusBarManager.hide() 447 | 448 | {Disposable} = require 'atom' 449 | 450 | return new Disposable => @detachStatusBarItems() 451 | 452 | ###* 453 | * Sets the project manager service. 454 | * 455 | * @param {Object} service 456 | ### 457 | setProjectManagerService: (service) -> 458 | @projectManagerService = service 459 | 460 | # NOTE: This method is actually called whenever the project changes as well. 461 | service.getProject (project) => 462 | @onProjectChanged(project) 463 | 464 | ###* 465 | * @param {Object} project 466 | ### 467 | onProjectChanged: (project) -> 468 | @activeProject = project 469 | 470 | return if not project? 471 | 472 | @proxy.clearCache() 473 | 474 | projectManager = @getProjectManager() 475 | projectManager.load(project) 476 | 477 | return if not projectManager.hasActiveProject() 478 | 479 | successHandler = (isProjectInGoodShape) => 480 | # NOTE: If the index is manually deleted, testing will return false so the project is reinitialized. 481 | # This is needed to index built-in items as they are not automatically indexed by indexing the project. 482 | if not isProjectInGoodShape 483 | return @performInitialFullIndexForCurrentProject() 484 | 485 | else 486 | return @projectManager.attemptCurrentProjectIndex() 487 | 488 | failureHandler = () -> 489 | # Ignore 490 | 491 | @proxy.test().then(successHandler, failureHandler) 492 | 493 | ###* 494 | * Retrieves the base package service that can be used by other packages. 495 | * 496 | * @return {Service} 497 | ### 498 | getServiceInstance: () -> 499 | return @getService() 500 | 501 | ###* 502 | * @return {Service} 503 | ### 504 | getService: () -> 505 | if not @disposables? 506 | Service = require './Service' 507 | 508 | @service = new Service( 509 | @getConfiguration(), 510 | @getCachingProxy(), 511 | @getProjectManager(), 512 | @getIndexingMediator(), 513 | @getUseStatementHelper() 514 | ) 515 | 516 | return @service 517 | 518 | ###* 519 | * @return {Disposables} 520 | ### 521 | getDisposables: () -> 522 | if not @disposables? 523 | {CompositeDisposable} = require 'atom'; 524 | 525 | @disposables = new CompositeDisposable() 526 | 527 | return @disposables 528 | 529 | ###* 530 | * @return {Configuration} 531 | ### 532 | getConfiguration: () -> 533 | if not @configuration? 534 | AtomConfig = require './AtomConfig' 535 | 536 | @configuration = new AtomConfig(@packageName) 537 | 538 | return @configuration 539 | 540 | ###* 541 | * @return {CachingProxy} 542 | ### 543 | getCachingProxy: () -> 544 | if not @proxy? 545 | CachingProxy = require './CachingProxy' 546 | 547 | @proxy = new CachingProxy(@getConfiguration()) 548 | @proxy.setCorePath(@getCoreManager().getCoreSourcePath()) 549 | 550 | return @proxy 551 | 552 | ###* 553 | * @return {Emitter} 554 | ### 555 | getEmitter: () -> 556 | if not @emitter? 557 | {Emitter} = require 'event-kit'; 558 | 559 | @emitter = new Emitter() 560 | 561 | return @emitter 562 | 563 | ###* 564 | * @return {ComposerService} 565 | ### 566 | getComposerService: () -> 567 | if not @composerService? 568 | ComposerService = require './ComposerService'; 569 | 570 | @composerService = new ComposerService( 571 | @getConfiguration().get('phpCommand'), 572 | @getConfiguration().get('packagePath') + '/core/' 573 | ) 574 | 575 | return @composerService 576 | 577 | ###* 578 | * @return {CoreManager} 579 | ### 580 | getCoreManager: () -> 581 | if not @coreManager? 582 | CoreManager = require './CoreManager'; 583 | 584 | @coreManager = new CoreManager( 585 | @getComposerService(), 586 | @coreVersionSpecification, 587 | @getConfiguration().get('packagePath') + '/core/' 588 | ) 589 | 590 | return @coreManager 591 | 592 | ###* 593 | * @return {UseStatementHelper} 594 | ### 595 | getUseStatementHelper: () -> 596 | if not @useStatementHelper? 597 | UseStatementHelper = require './UseStatementHelper'; 598 | 599 | @useStatementHelper = new UseStatementHelper(@getConfiguration().get('insertNewlinesForUseStatements')) 600 | 601 | return @useStatementHelper 602 | 603 | ###* 604 | * @return {IndexingMediator} 605 | ### 606 | getIndexingMediator: () -> 607 | if not @indexingMediator? 608 | IndexingMediator = require './IndexingMediator' 609 | 610 | @indexingMediator = new IndexingMediator(@getCachingProxy(), @getEmitter()) 611 | 612 | return @indexingMediator 613 | 614 | ###* 615 | * @return {ProjectManager} 616 | ### 617 | getProjectManager: () -> 618 | if not @projectManager? 619 | ProjectManager = require './ProjectManager' 620 | 621 | @projectManager = new ProjectManager(@getCachingProxy(), @getIndexingMediator()) 622 | 623 | return @projectManager 624 | -------------------------------------------------------------------------------- /lib/Proxy.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | net = require 'net' 3 | stream = require 'stream' 4 | child_process = require 'child_process' 5 | 6 | module.exports = 7 | 8 | ##* 9 | # Proxy that handles communicating with the PHP side. 10 | ## 11 | class Proxy 12 | ###* 13 | * The config to use. 14 | * 15 | * @var {Object} 16 | ### 17 | config: null 18 | 19 | ###* 20 | * The name (without path or extension) of the database file to use. 21 | * 22 | * @var {Object} 23 | ### 24 | indexDatabaseName: null 25 | 26 | ###* 27 | * @var {Boolean} 28 | ### 29 | isActive: false 30 | 31 | ###* 32 | * @var {String} 33 | ### 34 | corePath: null 35 | 36 | ###* 37 | * @var {Object} 38 | ### 39 | phpServer: null 40 | 41 | ###* 42 | * @var {Promise} 43 | ### 44 | phpServerPromise: null 45 | 46 | ###* 47 | * @var {Object} 48 | ### 49 | client: null 50 | 51 | ###* 52 | * @var {Object} 53 | ### 54 | requestQueue: null 55 | 56 | ###* 57 | * @var {Number} 58 | ### 59 | nextRequestId: 1 60 | 61 | ###* 62 | * @var {Object} 63 | ### 64 | response: null 65 | 66 | ###* 67 | * @var {String} 68 | ### 69 | HEADER_DELIMITER: "\r\n" 70 | 71 | ###* 72 | * @var {Number} 73 | ### 74 | FATAL_SERVER_ERROR: -32000 75 | 76 | ###* 77 | * Constructor. 78 | * 79 | * @param {Config} config 80 | ### 81 | constructor: (@config) -> 82 | @requestQueue = {} 83 | @port = @getRandomServerPort() 84 | 85 | @resetResponseState() 86 | 87 | ###* 88 | * Spawns the PHP socket server process. 89 | * 90 | * @param {Number} port 91 | * 92 | * @return {Promise} 93 | ### 94 | spawnPhpServer: (port) -> 95 | php = @config.get('phpCommand') 96 | memoryLimit = @config.get('memoryLimit') 97 | 98 | parameters = [ 99 | '-d memory_limit=' + memoryLimit + 'M', 100 | @corePath + "/src/Main.php", 101 | '--port=' + port 102 | ] 103 | 104 | process = child_process.spawn(php, parameters) 105 | 106 | return new Promise (resolve, reject) => 107 | process.stdout.on 'data', (data) => 108 | message = data.toString() 109 | 110 | console.debug('The PHP server has something to say:', message) 111 | 112 | if message.startsWith('Starting socket server') 113 | # Assume the server has successfully spawned the moment it says its first words. 114 | resolve(process) 115 | 116 | process.stderr.on 'data', (data) => 117 | console.warn('The PHP server has errors to report:', data.toString()) 118 | 119 | process.on 'error', (error) => 120 | console.error('An error ocurred whilst invoking PHP', error) 121 | reject() 122 | 123 | process.on 'close', (code) => 124 | if code == 2 125 | console.error('Port ' + port + ' is already taken') 126 | return 127 | 128 | console.error('PHP socket server exited by itself, a fatal error must have occurred.') 129 | 130 | @closeServerConnection() 131 | 132 | @phpServer = null 133 | reject() 134 | 135 | ###* 136 | * @return {Number} 137 | ### 138 | getRandomServerPort: () -> 139 | minPort = 10000 140 | maxPort = 40000 141 | 142 | return Math.floor(Math.random() * (maxPort - minPort) + minPort) 143 | 144 | ###* 145 | * Spawns the PHP socket server process. 146 | * 147 | * @param {Number} port 148 | * 149 | * @return {Promise} 150 | ### 151 | spawnPhpServerIfNecessary: (port) -> 152 | if @phpServer 153 | @phpServerPromise = null 154 | 155 | return new Promise (resolve, reject) => 156 | resolve(@phpServer) 157 | 158 | else if @phpServerPromise 159 | return @phpServerPromise 160 | 161 | successHandler = (phpServer) => 162 | @phpServer = phpServer 163 | 164 | return phpServer 165 | 166 | failureHandler = () => 167 | @phpServerPromise = null 168 | 169 | @phpServerPromise = @spawnPhpServer(port).then(successHandler, failureHandler) 170 | 171 | return @phpServerPromise 172 | 173 | ###* 174 | * Sends the kill signal to the socket server. 175 | * 176 | * Note that this is a signal, the process may ignore it (but it usually will not, unless it's really persistent in 177 | * continuing whatever it's doing). 178 | * 179 | * @param {Array} parameters 180 | * 181 | * @return {Array} 182 | ### 183 | stopPhpServer: (port) -> 184 | @closeServerConnection() 185 | 186 | return if not @phpServer 187 | 188 | @phpServer.kill() 189 | @phpServer = null 190 | 191 | ###* 192 | * Closes the socket connection to the server. 193 | ### 194 | closeServerConnection: () -> 195 | @rejectAllOpenRequests() 196 | 197 | return if not @client 198 | 199 | @client.destroy() 200 | @client = null 201 | 202 | @resetResponseState() 203 | 204 | ###* 205 | * Rejects all currently open requests. 206 | ### 207 | rejectAllOpenRequests: () -> 208 | for id,request of @requestQueue 209 | request.promise.reject('The socket connection was closed and had to reconnect') 210 | 211 | @requestQueue = {} 212 | 213 | ###* 214 | * @return {Object} 215 | ### 216 | getSocketConnection: () -> 217 | return new Promise (resolve, reject) => 218 | @spawnPhpServerIfNecessary(@port).then () => 219 | if @client? 220 | resolve(@client) 221 | return 222 | 223 | @client = net.createConnection {port: @port}, () => 224 | resolve(@client) 225 | 226 | @client.setNoDelay(true) 227 | @client.on('error', @onSocketError.bind(this)) 228 | @client.on('data', @onDataReceived.bind(this)) 229 | @client.on('close', @onConnectionClosed.bind(this)) 230 | 231 | ###* 232 | * @param {String} data 233 | ### 234 | onDataReceived: (data) -> 235 | try 236 | @processDataBuffer(data) 237 | 238 | catch error 239 | console.warn('Encountered some invalid data, resetting state. Error: ', error) 240 | 241 | @resetResponseState() 242 | 243 | ###* 244 | * @param {Object} error 245 | ### 246 | onSocketError: (error) -> 247 | # Do nothing here, this should silence socket errors such as ECONNRESET. After this is called, the socket will 248 | # be closed and all handling is performed there. 249 | console.warn('The socket connection notified us of an error', error) 250 | 251 | ###* 252 | * @param {Boolean} hadError 253 | ### 254 | onConnectionClosed: (hadError) -> 255 | detail = 256 | "The socket connection to the PHP server was unexpectedly closed. Either something caused the " + 257 | "process to stop, it crashed, or the socket closed. In case of the first two, you should see " + 258 | "additional output indicating this is the case and you can report a bug. If there is no additional " + 259 | "output, the socket connection should automatically be reestablished and everything should continue " + 260 | "working." 261 | 262 | if hadError 263 | detail += ' It was also reported that an error was involved' 264 | 265 | console.warn(detail) 266 | 267 | @closeServerConnection() 268 | 269 | ###* 270 | * @param {Buffer} dataBuffer 271 | ### 272 | processDataBuffer: (dataBuffer) -> 273 | if not @response.length? 274 | contentLengthHeader = @readRawHeader(dataBuffer) 275 | @response.length = @getLengthFromContentLengthHeader(contentLengthHeader) 276 | 277 | bytesRead = contentLengthHeader.length + @HEADER_DELIMITER.length 278 | 279 | else if not @response.wasBoundaryFound 280 | header = @readRawHeader(dataBuffer) 281 | 282 | if header.length == 0 283 | @response.wasBoundaryFound = true 284 | 285 | bytesRead = header.length + @HEADER_DELIMITER.length 286 | 287 | else 288 | bytesRead = Math.min(dataBuffer.length, @response.length - @response.bytesRead) 289 | 290 | @response.content = Buffer.concat([@response.content, dataBuffer.slice(0, bytesRead)]) 291 | @response.bytesRead += bytesRead 292 | 293 | if @response.bytesRead == @response.length 294 | jsonRpcResponse = @getJsonRpcResponseFromResponseBuffer(@response.content) 295 | 296 | @processJsonRpcResponse(jsonRpcResponse) 297 | 298 | @resetResponseState() 299 | 300 | dataBuffer = dataBuffer.slice(bytesRead) 301 | 302 | if dataBuffer.length > 0 303 | @processDataBuffer(dataBuffer) 304 | 305 | ###* 306 | * @param {Object} jsonRpcResponse 307 | ### 308 | processJsonRpcResponse: (jsonRpcResponse) -> 309 | if jsonRpcResponse.id? 310 | jsonRpcRequest = @requestQueue[jsonRpcResponse.id] 311 | 312 | @processJsonRpcResponseForRequest(jsonRpcResponse, jsonRpcRequest) 313 | 314 | delete @requestQueue[jsonRpcResponse.id] 315 | 316 | else 317 | @processNotificationJsonRpcResponse(jsonRpcResponse) 318 | 319 | ###* 320 | * @param {Object} jsonRpcResponse 321 | * @param {Object} jsonRpcRequest 322 | ### 323 | processJsonRpcResponseForRequest: (jsonRpcResponse, jsonRpcRequest) -> 324 | if jsonRpcResponse.error? 325 | jsonRpcRequest.promise.reject({ 326 | request : jsonRpcRequest 327 | response : jsonRpcResponse 328 | error : jsonRpcResponse.error 329 | }) 330 | 331 | if jsonRpcResponse.error.code == @FATAL_SERVER_ERROR 332 | @showUnexpectedSocketResponseError(jsonRpcResponse.error.message) 333 | 334 | else 335 | jsonRpcRequest.promise.resolve(jsonRpcResponse.result) 336 | 337 | ###* 338 | * @param {Object} jsonRpcResponse 339 | * @param {Object} jsonRpcRequest 340 | ### 341 | processNotificationJsonRpcResponse: (jsonRpcResponse) -> 342 | if not jsonRpcResponse.result? 343 | console.warn('Received a server notification without a result', jsonRpcResponse) 344 | return 345 | 346 | if jsonRpcResponse.result.type == 'reindexProgressInformation' 347 | if not jsonRpcResponse.result.requestId? 348 | console.warn('Received progress information without a request ID to go with it', jsonRpcResponse) 349 | return 350 | 351 | relatedJsonRpcRequest = @requestQueue[jsonRpcResponse.result.requestId] 352 | 353 | if not relatedJsonRpcRequest.streamCallback? 354 | console.warn('Received progress information for a request that isn\'t interested in it') 355 | return 356 | 357 | relatedJsonRpcRequest.streamCallback(jsonRpcResponse.result.progress) 358 | 359 | else 360 | console.warn('Received a server notification with an unknown type', jsonRpcResponse) 361 | 362 | ###* 363 | * @param {Buffer} dataBuffer 364 | * 365 | * @return {Object} 366 | ### 367 | getJsonRpcResponseFromResponseBuffer: (dataBuffer) -> 368 | jsonRpcResponseString = dataBuffer.toString() 369 | 370 | return @getJsonRpcResponseFromResponseContent(jsonRpcResponseString) 371 | 372 | ###* 373 | * @param {String} content 374 | * 375 | * @return {Object} 376 | ### 377 | getJsonRpcResponseFromResponseContent: (content) -> 378 | return JSON.parse(content) 379 | 380 | ###* 381 | * @param {Buffer} dataBuffer 382 | * 383 | * @throws {Error} 384 | * 385 | * @return {String} 386 | ### 387 | readRawHeader: (dataBuffer) -> 388 | end = dataBuffer.indexOf(@HEADER_DELIMITER) 389 | 390 | if end == -1 391 | throw new Error('Header delimiter not found'); 392 | 393 | return dataBuffer.slice(0, end).toString() 394 | 395 | ###* 396 | * @param {String} rawHeader 397 | * 398 | * @throws {Error} 399 | * 400 | * @return {Number} 401 | ### 402 | getLengthFromContentLengthHeader: (rawHeader) -> 403 | parts = rawHeader.split(':') 404 | 405 | if parts.length != 2 406 | throw new Error('Unexpected amount of header parts found') 407 | 408 | contentLength = parseInt(parts[1]) 409 | 410 | if not contentLength? 411 | throw new Error('Content length header does not have an integer as value') 412 | 413 | return contentLength 414 | 415 | ###* 416 | * Resets the current response's state. 417 | ### 418 | resetResponseState: () -> 419 | @response = 420 | length : null 421 | wasBoundaryFound : false 422 | bytesRead : 0 423 | content : new Buffer([]) 424 | 425 | ###* 426 | * Performs an asynchronous request to the PHP side. 427 | * 428 | * @param {Number} id 429 | * @param {String} method 430 | * @param {Object} parameters 431 | * @param {Callback} streamCallback 432 | * 433 | * @return {Promise} 434 | ### 435 | performJsonRpcRequest: (id, method, parameters, streamCallback = null) -> 436 | return new Promise (resolve, reject) => 437 | if not @getIsActive() 438 | reject('The proxy is not yet active, the core may be in the process of being downloaded') 439 | return 440 | 441 | JsonRpcRequest = 442 | jsonrpc : 2.0 443 | id : id 444 | method : method 445 | params : parameters 446 | 447 | @requestQueue[id] = { 448 | id : id 449 | streamCallback : streamCallback 450 | request : JsonRpcRequest 451 | 452 | promise: { 453 | resolve : resolve 454 | reject : reject 455 | } 456 | } 457 | 458 | content = @getContentForJsonRpcRequest(JsonRpcRequest) 459 | 460 | @writeRawRequest(content) 461 | 462 | ###* 463 | * @param {Object} request 464 | * 465 | * @return {String} 466 | ### 467 | getContentForJsonRpcRequest: (request) -> 468 | return JSON.stringify(request) 469 | 470 | ###* 471 | * Writes a raw request to the connection. 472 | * 473 | * This may not happen immediately if the connection is not available yet. In that case, the request will be 474 | * dispatched as soon as the connection becomes available. 475 | * 476 | * @param {String} content The content (body) of the request. 477 | ### 478 | writeRawRequest: (content) -> 479 | @getSocketConnection().then (connection) => 480 | lengthInBytes = (new TextEncoder('utf-8').encode(content)).length 481 | 482 | connection.write("Content-Length: " + lengthInBytes + @HEADER_DELIMITER) 483 | connection.write(@HEADER_DELIMITER); 484 | connection.write(content) 485 | 486 | ###* 487 | * @param {String} rawOutput 488 | * @param {Array|null} parameters 489 | ### 490 | showUnexpectedSocketResponseError: (rawOutput, parameters = null) -> 491 | detail = 492 | "The socket server sent back something unexpected. This could be a bug, but it could also be a problem " + 493 | "with your setup. If you're sure it is a bug, feel free to report it on the bug tracker." 494 | 495 | if parameters? 496 | detail += "\n \nCommand\n → " + parameters.join(' ') 497 | 498 | detail += "\n \nOutput\n → " + rawOutput 499 | 500 | atom.notifications.addError('PHP Integrator - Oops, something went wrong!', { 501 | dismissable : true 502 | detail : detail 503 | }) 504 | 505 | ###* 506 | * @param {String} method 507 | * @param {Object} parameters 508 | * @param {Callback} streamCallback A method to invoke each time streaming data is received. 509 | * @param {String|null} stdinData The data to pass to STDIN. 510 | * 511 | * @return {Promise} 512 | ### 513 | performRequest: (method, parameters, streamCallback = null, stdinData = null) -> 514 | if stdinData? 515 | parameters.stdin = true 516 | parameters.stdinData = stdinData 517 | 518 | requestId = @nextRequestId++ 519 | 520 | return @performJsonRpcRequest(requestId, method, parameters, streamCallback) 521 | 522 | ###* 523 | * Retrieves a list of available classes. 524 | * 525 | * @return {Promise} 526 | ### 527 | getClassList: () -> 528 | if not @getIndexDatabasePath()? 529 | return new Promise (resolve, reject) -> 530 | reject('Request aborted as there is no project active (yet)') 531 | 532 | parameters = { 533 | database : @getIndexDatabasePath() 534 | } 535 | 536 | return @performRequest('classList', parameters) 537 | 538 | ###* 539 | * Retrieves a list of available classes in the specified file. 540 | * 541 | * @param {String} file 542 | * 543 | * @return {Promise} 544 | ### 545 | getClassListForFile: (file) -> 546 | if not file 547 | return new Promise (resolve, reject) -> 548 | reject('No file passed!') 549 | 550 | if not @getIndexDatabasePath()? 551 | return new Promise (resolve, reject) -> 552 | reject('Request aborted as there is no project active (yet)') 553 | 554 | parameters = { 555 | database : @getIndexDatabasePath() 556 | file : file 557 | } 558 | 559 | return @performRequest('classList', parameters) 560 | 561 | ###* 562 | * Retrieves a list of namespaces. 563 | * 564 | * @return {Promise} 565 | ### 566 | getNamespaceList: () -> 567 | if not @getIndexDatabasePath()? 568 | return new Promise (resolve, reject) -> 569 | reject('Request aborted as there is no project active (yet)') 570 | 571 | parameters = { 572 | database : @getIndexDatabasePath() 573 | } 574 | 575 | return @performRequest('namespaceList', parameters) 576 | 577 | ###* 578 | * Retrieves a list of namespaces in the specified file. 579 | * 580 | * @param {String} file 581 | * 582 | * @return {Promise} 583 | ### 584 | getNamespaceListForFile: (file) -> 585 | if not file 586 | return new Promise (resolve, reject) -> 587 | reject('No file passed!') 588 | 589 | if not @getIndexDatabasePath()? 590 | return new Promise (resolve, reject) -> 591 | reject('Request aborted as there is no project active (yet)') 592 | 593 | parameters = { 594 | database : @getIndexDatabasePath() 595 | file : file 596 | } 597 | 598 | return @performRequest('namespaceList', parameters) 599 | 600 | ###* 601 | * Retrieves a list of available global constants. 602 | * 603 | * @return {Promise} 604 | ### 605 | getGlobalConstants: () -> 606 | if not @getIndexDatabasePath()? 607 | return new Promise (resolve, reject) -> 608 | reject('Request aborted as there is no project active (yet)') 609 | 610 | parameters = { 611 | database : @getIndexDatabasePath() 612 | } 613 | 614 | return @performRequest('globalConstants', parameters) 615 | 616 | ###* 617 | * Retrieves a list of available global functions. 618 | * 619 | * @return {Promise} 620 | ### 621 | getGlobalFunctions: () -> 622 | if not @getIndexDatabasePath()? 623 | return new Promise (resolve, reject) -> 624 | reject('Request aborted as there is no project active (yet)') 625 | 626 | parameters = { 627 | database : @getIndexDatabasePath() 628 | } 629 | 630 | return @performRequest('globalFunctions', parameters) 631 | 632 | ###* 633 | * Retrieves a list of available members of the class (or interface, trait, ...) with the specified name. 634 | * 635 | * @param {String} className 636 | * 637 | * @return {Promise} 638 | ### 639 | getClassInfo: (className) -> 640 | if not className 641 | return new Promise (resolve, reject) -> 642 | reject('No class name passed!') 643 | 644 | if not @getIndexDatabasePath()? 645 | return new Promise (resolve, reject) -> 646 | reject('Request aborted as there is no project active (yet)') 647 | 648 | parameters = { 649 | database : @getIndexDatabasePath() 650 | name : className 651 | } 652 | 653 | return @performRequest('classInfo', parameters) 654 | 655 | ###* 656 | * Resolves a local type in the specified file, based on use statements and the namespace. 657 | * 658 | * @param {String} file 659 | * @param {Number} line The line the type is located at. The first line is 1, not 0. 660 | * @param {String} type 661 | * @param {String} kind The kind of element. Either 'classlike', 'constant' or 'function'. 662 | * 663 | * @return {Promise} 664 | ### 665 | resolveType: (file, line, type, kind = 'classlike') -> 666 | if not file 667 | return new Promise (resolve, reject) -> 668 | reject('No file passed!') 669 | 670 | if not line 671 | return new Promise (resolve, reject) -> 672 | reject('No line passed!') 673 | 674 | if not type 675 | return new Promise (resolve, reject) -> 676 | reject('No type passed!') 677 | 678 | if not kind 679 | return new Promise (resolve, reject) -> 680 | reject('No kind passed!') 681 | 682 | if not @getIndexDatabasePath()? 683 | return new Promise (resolve, reject) -> 684 | reject('Request aborted as there is no project active (yet)') 685 | 686 | parameters = { 687 | database : @getIndexDatabasePath() 688 | file : file 689 | line : line 690 | type : type 691 | kind : kind 692 | } 693 | 694 | return @performRequest('resolveType', parameters) 695 | 696 | ###* 697 | * Localizes a type to the specified file, making it relative to local use statements, if possible. If not possible, 698 | * null is returned. 699 | * 700 | * @param {String} file 701 | * @param {Number} line The line the type is located at. The first line is 1, not 0. 702 | * @param {String} type 703 | * @param {String} kind The kind of element. Either 'classlike', 'constant' or 'function'. 704 | * 705 | * @return {Promise} 706 | ### 707 | localizeType: (file, line, type, kind = 'classlike') -> 708 | if not file 709 | return new Promise (resolve, reject) -> 710 | reject('No file passed!') 711 | 712 | if not line 713 | return new Promise (resolve, reject) -> 714 | reject('No line passed!') 715 | 716 | if not type 717 | return new Promise (resolve, reject) -> 718 | reject('No type passed!') 719 | 720 | if not kind 721 | return new Promise (resolve, reject) -> 722 | reject('No kind passed!') 723 | 724 | if not @getIndexDatabasePath()? 725 | return new Promise (resolve, reject) -> 726 | reject('Request aborted as there is no project active (yet)') 727 | 728 | parameters = { 729 | database : @getIndexDatabasePath() 730 | file : file 731 | line : line 732 | type : type 733 | kind : kind 734 | } 735 | 736 | return @performRequest('localizeType', parameters) 737 | 738 | ###* 739 | * Performs a semantic lint of the specified file. 740 | * 741 | * @param {String} file 742 | * @param {String|null} source The source code of the file to index. May be null if a directory is passed instead. 743 | * @param {Object} options Additional options to set. Boolean properties noUnknownClasses, noUnknownMembers, 744 | * noUnknownGlobalFunctions, noUnknownGlobalConstants, noDocblockCorrectness and 745 | * noUnusedUseStatements are supported. 746 | * 747 | * @return {Promise} 748 | ### 749 | semanticLint: (file, source, options = {}) -> 750 | if not file 751 | return new Promise (resolve, reject) -> 752 | reject('No file passed!') 753 | 754 | if not @getIndexDatabasePath()? 755 | return new Promise (resolve, reject) -> 756 | reject('Request aborted as there is no project active (yet)') 757 | 758 | parameters = { 759 | database : @getIndexDatabasePath() 760 | file : file 761 | stdin : true 762 | } 763 | 764 | if options.noUnknownClasses == true 765 | parameters['no-unknown-classes'] = true 766 | 767 | if options.noUnknownMembers == true 768 | parameters['no-unknown-members'] = true 769 | 770 | if options.noUnknownGlobalFunctions == true 771 | parameters['no-unknown-global-functions'] = true 772 | 773 | if options.noUnknownGlobalConstants == true 774 | parameters['no-unknown-global-constants'] = true 775 | 776 | if options.noDocblockCorrectness == true 777 | parameters['no-docblock-correctness'] = true 778 | 779 | if options.noUnusedUseStatements == true 780 | parameters['no-unused-use-statements'] = true 781 | 782 | return @performRequest('semanticLint', parameters, null, source) 783 | 784 | ###* 785 | * Fetches all available variables at a specific location. 786 | * 787 | * @param {String|null} file The path to the file to examine. May be null if the source parameter is passed. 788 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 789 | * @param {Number} offset The character offset into the file to examine. 790 | * 791 | * @return {Promise} 792 | ### 793 | getAvailableVariables: (file, source, offset) -> 794 | if not file? and not source? 795 | return new Promise (resolve, reject) -> 796 | reject('Either a path to a file or source code must be passed!') 797 | 798 | if not @getIndexDatabasePath()? 799 | return new Promise (resolve, reject) -> 800 | reject('Request aborted as there is no project active (yet)') 801 | 802 | parameters = { 803 | database : @getIndexDatabasePath() 804 | offset : offset 805 | charoffset : true 806 | } 807 | 808 | if file? 809 | parameters.file = file 810 | 811 | return @performRequest('availableVariables', parameters, null, source) 812 | 813 | ###* 814 | * Deduces the resulting types of an expression. 815 | * 816 | * @param {String|null} expression The expression to deduce the type of, e.g. '$this->foo()'. If null, the 817 | * expression just before the specified offset will be used. 818 | * @param {String} file The path to the file to examine. 819 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 820 | * @param {Number} offset The character offset into the file to examine. 821 | * @param {bool} ignoreLastElement Whether to remove the last element or not, this is useful when the user 822 | * is still writing code, e.g. "$this->foo()->b" would normally return the 823 | * type (class) of 'b', as it is the last element, but as the user is still 824 | * writing code, you may instead be interested in the type of 'foo()' 825 | * instead. 826 | * 827 | * @return {Promise} 828 | ### 829 | deduceTypes: (expression, file, source, offset, ignoreLastElement) -> 830 | if not file? 831 | return new Promise (resolve, reject) -> 832 | reject('A path to a file must be passed!') 833 | 834 | if not @getIndexDatabasePath()? 835 | return new Promise (resolve, reject) -> 836 | reject('Request aborted as there is no project active (yet)') 837 | 838 | parameters = { 839 | database : @getIndexDatabasePath() 840 | offset : offset 841 | charoffset : true 842 | } 843 | 844 | if file? 845 | parameters.file = file 846 | 847 | if ignoreLastElement 848 | parameters['ignore-last-element'] = true 849 | 850 | if expression? 851 | parameters.expression = expression 852 | 853 | return @performRequest('deduceTypes', parameters, null, source) 854 | 855 | ###* 856 | * Fetches invocation information of a method or function call. 857 | * 858 | * @param {String|null} file The path to the file to examine. May be null if the source parameter is passed. 859 | * @param {String|null} source The source code to search. May be null if a file is passed instead. 860 | * @param {Number} offset The character offset into the file to examine. 861 | * 862 | * @return {Promise} 863 | ### 864 | getInvocationInfo: (file, source, offset) -> 865 | if not file? and not source? 866 | return new Promise (resolve, reject) -> 867 | reject('Either a path to a file or source code must be passed!') 868 | 869 | if not @getIndexDatabasePath()? 870 | return new Promise (resolve, reject) -> 871 | reject('Request aborted as there is no project active (yet)') 872 | 873 | parameters = { 874 | database : @getIndexDatabasePath() 875 | offset : offset 876 | charoffset : true 877 | } 878 | 879 | if file? 880 | parameters.file = file 881 | 882 | return @performRequest('invocationInfo', parameters, null, source) 883 | 884 | ###* 885 | * Initializes a project. 886 | * 887 | * @return {Promise} 888 | ### 889 | initialize: () -> 890 | if not @getIndexDatabasePath()? 891 | return new Promise (resolve, reject) -> 892 | reject('Request aborted as there is no project active (yet)') 893 | 894 | parameters = { 895 | database : @getIndexDatabasePath() 896 | } 897 | 898 | return @performRequest('initialize', parameters, null, null) 899 | 900 | ###* 901 | * Vacuums a project, cleaning up the index database (e.g. pruning files that no longer exist). 902 | * 903 | * @return {Promise} 904 | ### 905 | vacuum: () -> 906 | if not @getIndexDatabasePath()? 907 | return new Promise (resolve, reject) -> 908 | reject('Request aborted as there is no project active (yet)') 909 | 910 | parameters = { 911 | database : @getIndexDatabasePath() 912 | } 913 | 914 | return @performRequest('vacuum', parameters, null, null) 915 | 916 | ###* 917 | * Tests a project, to see if it is in a properly usable state. 918 | * 919 | * @return {Promise} 920 | ### 921 | test: () -> 922 | if not @getIndexDatabasePath()? 923 | return new Promise (resolve, reject) -> 924 | reject('Request aborted as there is no project active (yet)') 925 | 926 | parameters = { 927 | database : @getIndexDatabasePath() 928 | } 929 | 930 | return @performRequest('test', parameters, null, null) 931 | 932 | ###* 933 | * Refreshes the specified file or folder. This method is asynchronous and will return immediately. 934 | * 935 | * @param {String|Array} path The full path to the file or folder to refresh. Alternatively, 936 | * this can be a list of items to index at the same time. 937 | * @param {String|null} source The source code of the file to index. May be null if a directory is 938 | * passed instead. 939 | * @param {Callback|null} progressStreamCallback A method to invoke each time progress streaming data is received. 940 | * @param {Array} excludedPaths A list of paths to exclude from indexing. 941 | * @param {Array} fileExtensionsToIndex A list of file extensions (without leading dot) to index. 942 | * 943 | * @return {Promise} 944 | ### 945 | reindex: (path, source, progressStreamCallback, excludedPaths, fileExtensionsToIndex) -> 946 | if typeof path == "string" 947 | pathsToIndex = [] 948 | 949 | if path 950 | pathsToIndex.push(path) 951 | 952 | else 953 | pathsToIndex = path 954 | 955 | if path.length == 0 956 | return new Promise (resolve, reject) -> 957 | reject('No filename passed!') 958 | 959 | if not @getIndexDatabasePath()? 960 | return new Promise (resolve, reject) -> 961 | reject('Request aborted as there is no project active (yet)') 962 | 963 | progressStreamCallbackWrapper = null 964 | 965 | parameters = { 966 | database : @getIndexDatabasePath() 967 | } 968 | 969 | if progressStreamCallback? 970 | parameters['stream-progress'] = true 971 | 972 | progressStreamCallbackWrapper = progressStreamCallback 973 | 974 | parameters.source = pathsToIndex 975 | parameters.exclude = excludedPaths 976 | parameters.extension = fileExtensionsToIndex 977 | 978 | return @performRequest('reindex', parameters, progressStreamCallbackWrapper, source) 979 | 980 | ###* 981 | * Sets the name (without path or extension) of the database file to use. 982 | * 983 | * @param {String} name 984 | ### 985 | setIndexDatabaseName: (name) -> 986 | @indexDatabaseName = name 987 | 988 | ###* 989 | * Retrieves the full path to the database file to use. 990 | * 991 | * @return {String} 992 | ### 993 | getIndexDatabasePath: () -> 994 | return @config.get('packagePath') + '/indexes/' + @indexDatabaseName + '.sqlite' 995 | 996 | ###* 997 | * @param {String} corePath 998 | ### 999 | setCorePath: (corePath) -> 1000 | @corePath = corePath 1001 | 1002 | ###* 1003 | * @return {Boolean} 1004 | ### 1005 | getIsActive: (isActive) -> 1006 | return @isActive 1007 | 1008 | ###* 1009 | * @param {Boolean} isActive 1010 | ### 1011 | setIsActive: (isActive) -> 1012 | @isActive = isActive 1013 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Note 2 | Starting with version **2.0.0**, this repository only contains the CoffeeScript or _client_ side (for Atom) of the indexer. Most of the interesting changes are happening on the PHP or _server_ side. You can view its changelog [here](https://gitlab.com/php-integrator/core/blob/master/CHANGELOG.md) for the master branch or [here](https://gitlab.com/php-integrator/core/blob/development/CHANGELOG.md) for the development branch. 3 | 4 | ## 2.1.12 5 | * Update to core [2.1.6](https://gitlab.com/php-integrator/core/tags/2.1.7). 6 | 7 | ## 2.1.11 8 | * Update to core [2.1.6](https://gitlab.com/php-integrator/core/tags/2.1.6). 9 | 10 | ## 2.1.10 11 | * Update to core [2.1.5](https://gitlab.com/php-integrator/core/tags/2.1.5). 12 | 13 | ## 2.1.9 14 | * Nothing changed, `apm` just failed to publish 2.1.8 and decided to bump the version, even though I explicitly asked it to try and publish 2.1.8 again. 15 | 16 | ## 2.1.8 17 | * Update to core [2.1.4](https://gitlab.com/php-integrator/core/tags/2.1.4). 18 | 19 | ## 2.1.7 20 | * Update to core [2.1.3](https://gitlab.com/php-integrator/core/tags/2.1.3). 21 | 22 | ## 2.1.6 23 | * Update to core [2.1.2](https://gitlab.com/php-integrator/core/tags/2.1.2). 24 | * Socket closes and reconnections will no longer display errors to the user. See also 46fc09f9b072a601dd6f6b02ee753a9785bfa397. 25 | 26 | ## 2.1.5 27 | * When the socket connection closes and a reconnect happens, the state of response reading is now reset. 28 | * Previously, the base package kept accumulating response data in a buffer, never seeing the end of the one that was interrupted. 29 | 30 | ## 2.1.4 31 | * Update to core [2.1.1](https://gitlab.com/php-integrator/core/tags/2.1.1). 32 | * Composer output will no longer be silenced when installing the core. 33 | 34 | ## 2.1.3 35 | * Fix incorrect method call that only occurred on shutdown or when deactivating the package. 36 | * All open requests will now be rejected whenever the socket is unexpectedly closed. 37 | * This fixes the issue where some requests would hang up because the promise they received was never resolved or rejected. 38 | * This should fix the issue where indexing gets stuck (and blocks further indexing events) whenever the socket reconnects. 39 | 40 | ## 2.1.2 41 | * Sockets closing without an error code will now warn instead of throw an error. 42 | * Socket connections that _do_ have an error code will still show an error message. 43 | * An attempt to automatically reestablish the socket connections will always be made. 44 | * In the case that the server crashed, an error will still also be printed in the developer tools, to distinguish between seemingly "magic self-closing" and actual server crashes. 45 | 46 | ## 2.1.1 47 | * Fix another issue where the server wasn't always properly restarted after it unexpectedly stopped. 48 | * Explicitly close open socket connections when the server dies as a safety measure (as the port is reused when the server is spawned again). 49 | 50 | ## 2.1.0 51 | * Update to core [2.1.0](https://gitlab.com/php-integrator/core/tags/2.1.0). 52 | * The server wasn't always being restarted automatically when it died or the connection failed. This should be fixed now. 53 | * The core is now downloaded using Composer instead of apm, which is more robust and allows properly selecting the right version to download. 54 | 55 | ## 2.0.2 56 | * Update to core [2.0.2](https://gitlab.com/php-integrator/core/tags/2.0.2). 57 | 58 | ## 2.0.1 59 | * Update to core [2.0.1](https://gitlab.com/php-integrator/core/tags/2.0.1). 60 | 61 | ## 2.0.0 62 | ### Features and enhancements 63 | * The PHP side is no longer part of the base package. Instead, it has been separated into the [php-integrator/core](https://gitlab.com/php-integrator/core) repository so it can be more easily installed via Composer for use in other projects. 64 | * This change should not impact users as they upgrade, as this package will be automatically installed and upgraded along with this one if necessary (a notification will be shown whenever that happens). 65 | * Communication with the core now happens via sockets. This means only a single process is spawned on startup and kept active throughout the lifetime of Atom. 66 | * This should reduce latency across the board as process spawning is rather expensive in comparison. It will also ensure there is never more than one process active when multiple requests are fired simultaneously (they will simply be queued and handled one by one, which will not freeze up Atom as communication is completely asynchronous). 67 | * It is now also possible to specify an additional indexing delay via the settings screen. 68 | * It's currently set to `200 ms` by default. As Atom's default delay before invoking an event after an editor stopped changing is about `300 ms`, this results in indexing happening after `500 ms` by default. Increasing this will reduce the load of constant reindexing happening, but will also make results from autocompletion and linting less current. 69 | * Error messages will now be shown if setting up the current project fails because there is no active project or the project manager service is not available. 70 | * Caching has been improved, improving performance and responsiveness. 71 | * Previously, similar queries to the PHP side that were happening closely in succession did not hit the cache because the promise of the similar query had not resolved yet. In this case, two promises were resolved, fetching the same information. 72 | * A memory limit for the PHP process is now configurable in the package settings. 73 | 74 | ### Bugs fixed 75 | * The status bar was not showing progress when a project index happened through a repository status change. 76 | * Popovers will no longer go beyond the left or top part of the screen. They will move respectively right or down in that case. 77 | 78 | ### Changes for developers (see also the core) 79 | * `getVariableTypes` has been removed as it was deprecated and just an alias for `deduceTypes`. 80 | * The `truncate` call was removed as you can now simply call `initialize` again to reinitialize a project. 81 | * The `deduceTypes` call now expects the (optional) entire expression to be passed rather than just its parts. 82 | * The `reindex` call no longer automatically indexes built-in structural elements, nor will it automatically prune removed files from the database. 83 | * A new call, `vacuum`, can be used to vacuum a project and prune removed files from its index. 84 | * A new call, `initialize`, can be used to initialize a project and index built-in structural elements. 85 | * The `reindex` call no longer takes a `progressStreamCallback`, this was necessary because it was holding back refactoring and it really did not belong there. 86 | * It also wasn't really useful in its current form, as the only the one doing the indexing could register a callback. 87 | * As a better alternative, you can now register a callback with `onDidIndexingProgress` to listen to the progress, even if you did not spawn the reindex yourself. 88 | * A new call, `onDidStartIndexing`, has been added that allows you to listen to an indexing action starting. 89 | * A new service method, `getCurrentProjectSettings`, allows retrieving the settings (specific to this package) of a project. This includes things such as the PHP version, paths and excluded folders. 90 | * A new service method `getUseStatementHelper`, can be used to retrieve an object that handles adding use statements and sorting them. This has been imported from the autocompletion package so it can be reused in other packages as well (and bugfixes are centralized). 91 | * A couple of new service methods have been added to allow fetching namespace information: 92 | * `getNamespaceList` 93 | * `getNamespaceListForFile` 94 | * `determineCurrentNamespaceName` 95 | * A couple of new service methods have been added to allow fetching documentation URL's for built-in structural elements. These were imported from php-integrator-navigation so they can be reused by other packages as well: 96 | * `getDocumentationUrlForClass` 97 | * `getDocumentationUrlForFunction` 98 | * `getDocumentationUrlForClassMethod` 99 | * The `resolveType` and `localizeType` commands now have a `kind` parameter that indicates what type of element te resolve. This is required as duplicate use statements may exist in PHP as long as the 'kind' is different (i.e. a `use const A\FOO` may exist alongside a `use A\FOO`). 100 | * Built-in interfaces no longer have `isAbstract` set to true. They _are_ abstract in a certain sense, but this property is meant to indicate if a classlike has been defined using the abstract keyword. It was also not consistent with the behavior for non-built-in interfaces. 101 | * Proxy methods will no longer throw exceptions if some parameters are missing or invalid. Instead, a promise rejection will occur. 102 | 103 | ## 1.2.6 104 | ### Bugs fixed 105 | * Rename the package and repository. 106 | 107 | ## 1.2.5 108 | ### Bugs fixed 109 | * Thanks to a quick response by [danielbrodin](https://github.com/danielbrodin), all functionality related to project management should now once again be working. 110 | 111 | ## 1.2.4 112 | ### Bugs fixed 113 | * Add workarounds for project-manager 3.0.0. Unfortunately, some functionalities are currently broken because of the backwards-incompatible upgrade. See also [this ticket](https://github.com/danielbrodin/atom-project-manager/issues/252) for more information. 114 | * Setting up new projects is currently impossible via the package, you can however still manually set up projects by adding PHP settings to their root object via the `projects.cson` file: 115 | ``` 116 | php: 117 | enabled: true 118 | php_integrator: 119 | enabled: true 120 | phpVersion: 5.6 121 | excludedPaths: [] 122 | fileExtensions: [ 123 | "php" 124 | ] 125 | ``` 126 | >>>>>>> master 127 | 128 | ## 1.2.3 129 | ### Bugs fixed 130 | * Fix built-in classes with FQCN's with more than one part, such as from the MongoDB extension, not properly being indexed. 131 | 132 | ## 1.2.2 133 | ### Bugs fixed 134 | * Fixed an error related to `JSON_PRESERVE_ZERO_FRACTION`, which needs PHP >= 5.6. 135 | 136 | ## 1.2.1 137 | ### Bugs fixed 138 | * Fixed `exclude` warnings (thanks to [@UziTech](https://github.com/UziTech)). 139 | 140 | ## 1.2.0 141 | ### Features and enhancements 142 | * Project support has (finally) arrived in a basic form. Unfortunately this change **needs user intervention** and will possibly break the workflow of some users: 143 | 1. Install the [atom-project-manager](https://github.com/danielbrodin/atom-project-manager) package [1]. 144 | 2. Ensure that your projects are saved using `atom-project-manager`. 145 | 3. Click `Packages → PHP Integrator → Set Up Current Project` (or use the command palette). 146 | * Built-in functions and methods will now have (mostly) proper documentation and return types. 147 | * This is achieved by keeping an internal list of stubs of the online PHP documentation fetched using [php-doc-parser](https://github.com/martinsik/php-doc-parser). 148 | * Note that this will never be perfect as, even though the online documentation is much more complete than reflection, it is also not always entirely correct. For example, `DateTime::createFromFormat` can actually return `DateTime|false`, but accordiing to its online documentation signature it always returns a `DateTime`). 149 | * Regardless of the previous item, it will still remain a major improvement over the old reflection-only approach. Various built-in functions and methods will now properly show documentation and have proper return types, easing the pain of having to work with PHP's built-in functionality. 150 | * Project and folder indexing performance has improved. 151 | * `define()` statements will now be indexed much like global constants. 152 | * Specifying the file extensions to index is now supported via project settings. 153 | * Excluding directories from indexing is now supported via project settings, see also [the wiki](https://github.com/Gert-dev/php-integrator-base/wiki/Excluding-Folders-From-Indexing) for more information. 154 | * For those interested, the wiki now [has an article](https://github.com/Gert-dev/php-integrator-base/wiki/Proper-Documentation-And-Type-Hinting) with information about how analysis of your code happens regarding docblocks and type hinting. Reading it may help you improve your code as well as code assistance from this package. 155 | * The remaining parts of the analyzer that were implemented in CoffeeScript have been moved to PHP. This means the base package is now only reliant on PHP itself for processing. This may positively affect performance, but more importantly allows extracting and using the analyzer in its entirety outside Atom as well (i.e. for other editors or projects). 156 | * Previously, ternary expressions could only be properly analyzed if they had the same return type: 157 | 158 | ```php 159 | $a1 = new A(); 160 | $a2 = new A(); 161 | 162 | $a3 = some_condition() ? $a1 : $a2; 163 | 164 | // $a3 is now an 'A' because both conditions yield the same type. 165 | ``` 166 | 167 | This restriction has now been lifted. Using ternary operators with conditions resulting in different types will now simply yield multiple return types: 168 | 169 | ```php 170 | $a = new A(); 171 | $b = new B(); 172 | 173 | $c = some_condition() ? $a : $b; 174 | 175 | // $c is now of type A|B. 176 | ``` 177 | 178 | * The default value for classlike properties and constants is now used for type deduction. This will improve type guessing in several situations. For example, when generating a docblock for `$foo`: 179 | 180 | ```php 181 | // Before: 182 | 183 | /** 184 | * @var mixed 185 | */ 186 | protected $foo = 'test'; 187 | 188 | // After: 189 | 190 | /** 191 | * @var string 192 | */ 193 | protected $foo = 'test'; 194 | 195 | ``` 196 | 197 | ### Bugs fixed 198 | * The return type of global functions was being ignored if they had multiple return types. 199 | * In rare cases, caching would complain that it could not create the `accessing_shared_cache.lock` file. 200 | * When force indexing, the cache was not properly invalidated, sometimes leading to the wrong data being fetched. 201 | * Retrieving locally available variables wasn't always using the most up to date version of the editor's contents. 202 | * Fix an empty error message and notification sometimes being shown in Atom because the PHP side was incorrectly parsing docblocks containing Unicode characters. 203 | * When retrieving available variables or deducing types, character offsets were not correctly being translated to byte offsets, sometimes leading to incorrect results. 204 | 205 | ### Changes for developers 206 | * `getInvocationInfo` is now also available separately as PHP command. 207 | * Constants and properties will now also return their default values, if present. 208 | * `determineCurrentClassName` was not causing a promise rejection when fetching the class list failed internally. 209 | * A new command `truncate` now does what a force reindex does, but also ensures the cache is properly invalidated. 210 | * `getInvocationInfoAt` will now return an `offset` rather than a `bufferPosition` to be consistent with the other commands. 211 | * `getResultingTypesAt` is now simply a convenience call to `deduceTypes` as its underlying code has been completely moved to PHP. 212 | * `deduceTypes` gained a new parameter `ignoreLastElement`, which does the same as the identically-named parameter for `getResultingTypesAt` does. 213 | 214 | ### Notes 215 | [1] Why `atom-project-manager`? The truth is that I would have preferred not to select any specific third-party package for project support so users can use the package they prefer. Users familiar with the project discussions will remember that I have long postponed this change for this reason. Unfortunately, as nothing seems to change and project support is feeling more and more like a missing feature, I decided to go ahead and link to the most popular project management package currently out there. `atom-project-manager` also supports other generic project settings in its CSON file, which leaves room for other Atom packages to also save settings to the same file in harmony with this one. In the future, someone could even develop a GUI for managing project settings. 216 | 217 | ## 1.1.2 218 | ### Bugs fixed 219 | * Fixed grouped use statements never being seen as unused. 220 | * Fixed grouped use statements being marked as unknown classes. 221 | * Fixed grouped use statements being ignored for type resolution when indexing. 222 | * Files that do not have UTF-8 encoding will be converted before they are indexed. 223 | 224 | ## 1.1.1 225 | ### Bugs fixed 226 | * Fixed the various `codeCoverageIgnore` tags from PHPUnit being seen as invalid tags (thanks to [@hultberg](https://github.com/hultberg)). 227 | 228 | ## 1.1.0 229 | ### Features and enhancements 230 | * Caching performance has been improved. 231 | * Added simple support for multiple root folders in the tree view. 232 | * At least PHP 5.5 is now required to run the service. PHP 5.4 has been declared end of life for quite some time now and 5.5 will be declared end of life 10 July 2016. This does not affect the code you can actually write, the indexer still supports PHP 5.2 up to PHP 7.0, it is just the PHP interpreter running the indexer that had a required version bump. 233 | * Some commands delegate work to other commands, but each command that requires parsing performed its own parsing of the (same) source code, even though it only needs to happen once. This unnecessary overhead has been removed, resulting in performance improvements across the board. 234 | * The strictness on `instanceof` has been lifted. The variable type deducer is now able to parse somewhat more complex if statements: 235 | 236 | ```php 237 | if ((1 ^ 0) && true && $b instanceof B && ($test || false && true)) { 238 | // $b will now be recognized as an instance of B. 239 | } 240 | ``` 241 | 242 | * If-statements containing variable handling functions such as `is_string`, `is_bool` will now influence type deduction: 243 | 244 | ```php 245 | if (is_string($b) || is_array($b)) { 246 | // $b is now of type string|array. 247 | } 248 | ``` 249 | 250 | ### Bugs fixed 251 | * The PHP Mess Detector `@SuppressWarnings` docblock tag will no longer be linted as unknown tag. 252 | * Different projects weren't using different caches. This means that in some cases the wrong cache was being used. 253 | * In some cases, the internal cache wasn't cleared when a class was modified, which resulted in old data being displayed. 254 | * Text following the `@var` tag in docblocks for class constants will now serve as short descriptions (summaries), similar to class properties. 255 | * Some internal PHP classes, such as `COM` have inconsistent naming (i.e. `COM` is actually returned as being named `com`). These are now corrected during indexing so you can use the names from the documentation. (For PHP this isn't a problem as it is mostly case insensitive, but we are.) 256 | * *Caching has been reenabled on Windows*, a fix has been applied that should refrain errors from popping up. The cache will simply reset itself if it runs into the erroneous condition (the reason behind which, up this date, is still unknown to me). This way, users are still able to enjoy some caching (users that did not experience any problems at all previously will be able to enjoy full caching). 257 | 258 | ### Changes for developers 259 | * Builtin functions did not have a FQCN set. 260 | * Added `deduceTypesAt` as a convenience alias. 261 | * The global function and constant list will now return a mapping of FQCN's to data (instead of names to data). 262 | * `semanticLint` learned how to validate unknown class members, global functions and global constants, which can be used by linter packages. 263 | * The path passed to handlers registered using `onDidFinishIndexing` and `onDidFailIndexing` will now be an array for project indexes (but not file indexes) as they can contain multiple root folders. 264 | * `getVariableTypes` is now deprecated as it is just an alias for calling `deduceTypes` and `deduceTypes` with the variable name as the sole part. It will now also just proxy calls to deduceTypes internally. 265 | 266 | ## 1.0.10 267 | ### Bugs fixed 268 | * Cleaned up the reindexing process. The locks that were causing so much trouble have been removed for now. 269 | * It was originally added as multiple concurrent indexing processes locked the database to ensure the other processes wait their turn. However, testing this again without it seems to indicate SQLite (automatically) gracefully waits for the transaction to finish. Either they accidentally solved the original problem, or the original problem might only manifest in certain circumstances. If the problem reappears anyway, I will investigate alternative solutions. 270 | 271 | ## 1.0.9 272 | ### Bugs fixed 273 | * An error is now returned if a file is not in UTF-8 encoding. 274 | * The encoding is now explicitly set to UTF-8, in case the encoding in your php.ini is set to something else. 275 | 276 | ## 1.0.8 277 | ### Bugs fixed 278 | * Fixed the database file never getting unlocked if indexing failed. 279 | 280 | ## 1.0.7 281 | ### Bugs fixed 282 | * Fixed the `setCachePrefix` error on Windows. 283 | 284 | ## 1.0.6 285 | ### Bugs fixed 286 | * Fixed docblock correctness linting going haywire when there were unicode characters inside the docblock. 287 | * The caching fix from 1.0.5 was rolled back because it didn't work. Caching has been disabled on Windows until a solution can be found. 288 | * Fixed words after an `@` symbol in docblocks incorrectly being marked as unknown annotation class (e.g. `@author Someone `). 289 | 290 | ## 1.0.5 291 | ### Bugs fixed 292 | * Attempt to fix the cache permission denied issue on Windows by using a different caching directory. 293 | 294 | ## 1.0.4 295 | ### Bugs fixed 296 | * Byte and character offsets were being mixed up. The expected formats have been documented in the README: 297 | * Output offsets retrieved from the service (and PHP side) have always been byte offsets, and this will also remain the same. To deal with the conversion, a new service method, `getCharacterOffsetFromByteOffset`, has been added. 298 | * The PHP side expects byte offsets, but commands that need offsets also gained an option `charoffset` to switch to pass character offsets instead. The CoffeeScript side has always expected character offsets as input because Atom mainly works with these, and this will remain the same. 299 | * In short, nothing has changed for dependent packages, but packages that use byte offsets incorrectly as character offsets may want to use the new service method to perform the conversion. 300 | 301 | ## 1.0.3 302 | ### Bugs fixed 303 | * Fixed namespaces without a name causing an error when resolving types. 304 | 305 | ## 1.0.2 306 | ### Bugs fixed 307 | * Fixed a circular dependency exception being thrown if a classlike implemented the same interface twice via separate paths. 308 | * If you have the same FQCN twice in your code base, only one of them will be analyzed when examining classlikes for associations (parents, traits, interfaces, ...). Having two classes with the same FQCN is actually an error, but it can happen if you store files that aren't actually directly part of your code inside your project folder. A better solution for this is to exclude those folders from indexing, which will be possible as soon as project support is implemented. Until that happens, this should mitigate the circular dependency exception that ensued because two classlikes with the same name were examined. 309 | 310 | ## 1.0.1 311 | ### Bugs fixed 312 | * Fixed error regarding `$type` when not using PHP 7 (thanks to [@UziTech](https://github.com/UziTech)). 313 | 314 | ## 1.0.0 315 | ### Features and enhancements 316 | * Minor performance improvements when calculating the global class list. 317 | * Annotation tags in docblocks will now be subject to unknown class examination. 318 | * A caching layer was introduced that will drastically cut down on time needed to calculate information about structures. This information is used almost everywhere throughout the code so an increase in responsiveness should be noticable across the board. This will especially be the case in complex classes that have many dependencies. The cache files will be stored in your system's temporary folder so they will get recycled automatically when necessary. 319 | * Type deduction learned how to deal with ternary expressions where both operands have the same resulting type: 320 | 321 | ```php 322 | $a1 = new A(); 323 | $a2 = new A(); 324 | 325 | $a = true ? $a1 : $a2; 326 | 327 | $a-> // Did not work before. Will now autocomplete A, as the type is guaranteed. 328 | 329 | $b1 = new B(); 330 | $b2 = new \B(); 331 | 332 | $b = $b1 ?: $b2; 333 | 334 | $b-> // Did not work before. Will now autocomplete B, for the same reasons. 335 | ``` 336 | 337 | * Type deduction learned how to deal with ternary expressions containing instanceof: 338 | 339 | ```php 340 | $a = ($foo instanceof Foo) ? $foo-> // Will now autocomplete Foo. 341 | ``` 342 | 343 | ### Bugs fixed 344 | * The type of global built-in function parameters was not getting analyzed correctly. 345 | * Built-in functions that have invalid UTF-8 characters in their name are now ignored. 346 | * Fixed docblock tags containing an @ sign in their description being incorrectly parsed. 347 | * Docblock types did not always get precedence over type hints of function or method parameters. 348 | * Use statements are no longer marked as unused if they are used inside docblocks as annotation. 349 | * Parameters that have the type `self` (in docblock or type hint), `static` or `$this` (in docblock) will now correctly be examined. 350 | * Don't freeze up until the call stack overflows when classes try to implement themselves (as interface) or use themselves (as trait). 351 | * Foreach statements that contain a list() expression as value will no longer cause an error (e.g. `foreach ($array as list($a, $b)) { ... }`). 352 | * Type localization didn't work properly for classlikes that were in the same namespace as the active one. For example, in namespace `A`, type `\A\B` would be localized to `A\B` instead of just `B`. 353 | * Fixed a classlike suddenly no longer being found if you defined another classlike with the same FQCN in another file (i.e. after copying a file). This happened, most annoyingly, even if you then changed the FQCN in the copied file, and couldn't be fixed without reindexing the original file. 354 | * Static method calls where the class name had a leading slash were not being examined correctly: 355 | 356 | ```php 357 | $foo = \Foo\Bar::method(); 358 | $foo-> // Didn't work, should work now. 359 | ``` 360 | 361 | * Type overrides with multiple types were not analyzed properly: 362 | 363 | ```php 364 | /** @var Foo|null $foo */ 365 | $foo-> // Did not work, should work now. 366 | ``` 367 | 368 | * Fix call stacks not correctly being retrieved after the new keyword, for example: 369 | 370 | ```php 371 | $test = new $this-> // Was seen as "new $this" rather than just "$this". 372 | ``` 373 | 374 | * Fix overridden or implemented methods with different parameter lists losing information about their changed parameter list. Whenever such a method now specifies different parameters than the "parent" method, parameters are no longer inherited and a docblock should be specified to document them. 375 | 376 | ```php 377 | class A 378 | { 379 | /** 380 | * @param Foo $foo 381 | */ 382 | public function __construct($foo) { ... } 383 | } 384 | 385 | class B extends A 386 | { 387 | // Previously, these two parameters would get overwritten and the parameter list would be 388 | // just "$foo" as the parent documentation is inherited. 389 | public function __construct($bar, $test) { ... } 390 | } 391 | ``` 392 | 393 | * Conditionals with instanceof in them will now no longer be examined outside their scope: 394 | 395 | ```php 396 | if ($foo instanceof Foo) { 397 | $foo-> // Autocompletion for Foo as before. 398 | } 399 | 400 | $foo-> // Worked before, now no longer works. 401 | ``` 402 | 403 | * More elaborate inline docblocks with type annotations will now also be parsed: 404 | 405 | ```php 406 | /** 407 | * Some documentation. 408 | * 409 | * @var D $d A description. 410 | */ 411 | $d = 5; 412 | $d-> // Didn't work because a docblock spanning multiple lines was not examined, should work now. 413 | ``` 414 | 415 | * Compound class property statements now properly have their docblocks examined: 416 | 417 | ```php 418 | /** 419 | * This is the summary. 420 | * 421 | * This is a long description. 422 | * 423 | * @var Foo1 $testProperty1 A description of the first property. 424 | * @var Foo2 $testProperty2 A description of the second property. 425 | */ 426 | protected $testProperty1, $testProperty2; 427 | ``` 428 | 429 | * In property docblocks with ambiguous summary and `@var` descriptions, the more specific `@var` will now take precedence. This more neatly fits in with the compound statements described above: 430 | 431 | ```php 432 | /** 433 | * This is the docblock summary, it will not be used or available anywhere as it is overridden below. 434 | * 435 | * This is a long description. It will be used as long description for all of the properties described below. 436 | * 437 | * @var Foo1 $testProperty1 A description of the first property. This will be used as the summary of this property. 438 | */ 439 | protected $testProperty1; 440 | ``` 441 | 442 | ### Changes for developers 443 | * All structural elements that involve types will now return arrays of type objects instead of a single type object. The following methods have been renamed to reflect this change: 444 | * `deduceType` -> `deduceTypes`. 445 | * `getVariableType` -> `getVariableTypes`. 446 | * `getResultingTypeAt` -> `getResultingTypesAt`. 447 | * `getVariableTypeByOffset` -> `getVariableTypesByOffset`. 448 | * An `isFinal` property is now returned for classes and methods. 449 | * An `isNullable` property is now returned for function and method parameters. 450 | * A `defaultValue` property is now returned for function and method parameters with the default value in string format. 451 | * `resolveType` will now also return types with a leading slash, consistent with other commands. 452 | * Return types for functions and types for function parameters will now also include a `typeHint` property that is set to the actual type hint that was specified (the type and resolvedType fall back to the docblock if it is present). 453 | * `localizeType` will now return the FQCN instead of null if it couldn't localize a type based on use statements. The reasoning behind this is that you want a localized version of the type you pass, if there are no use statements to localize it, the FQCN is the only way to address the type locally. 454 | * The format the descriptions are returned in has changed; there is no more `descriptions` property that has a `short` and `long` property. Instead, there is now the `shortDescription` and `longDescription` property. Similarly, the description of the return value of functions and methods has moved to `returnDescription` and the description of the type of properties and constants to `typeDescription`. 455 | 456 | ## 0.9.5 457 | ### Bugs fixed 458 | * Fixed the database handle being opened in an incorrect way. 459 | 460 | ## 0.9.4 461 | ### Bugs fixed 462 | * Fixed the database handle never being closed after it was opened. 463 | 464 | ## 0.9.3 465 | ### Bugs fixed 466 | * Fixed variables nclosure use statements not having their type resolved properly in some corner cases. 467 | 468 | ## 0.9.2 469 | ### Bugs fixed 470 | * (Attempt to) fix indexing for built-in classes returning malformed method parameters. 471 | 472 | ## 0.9.1 473 | ### Bugs fixed 474 | * Fixed variables used in closure use statements not having their type resolved from the parent scope, for example: 475 | 476 | ```php 477 | $a = new B(); 478 | 479 | $closure = new function () use ($a) { 480 | $a-> // Now correctly resolves to 'B' instead of not working at all. 481 | }; 482 | ``` 483 | 484 | ## 0.9.0 485 | ### Features and enhancements 486 | * An error will now be shown if your SQLite version is out of date. 487 | * Unknown classes in docblocks will now actually be underlined instead of the structural element they were part of. 488 | * Indexing performance has been improved, especially the scanning phase (before the progress bar actually started filling) has been improved. 489 | * Indexing is now more fault-tolerant: in some cases, indexing will still be able to complete even if there are syntax errors in the file. 490 | * Docblock types now take precedence over type hints. The reason for this is that docblock types are optional and they can be more specific. Take for example, the fluent interface for setters, PHP does not allow specifying `static` or `$this` as a return type using scalar type hinting, but you may still want to automatically resolve to child classes when the setter is inherited: 491 | 492 | ```php 493 | /** 494 | * @return static 495 | */ 496 | public function setFoo(string $foo): self 497 | { 498 | 499 | } 500 | ``` 501 | 502 | ### Bugs fixed 503 | * The return type of PHP 7 methods was not properly used as fallback. 504 | * If you (incorrectly) declare or define the same member twice in a class, one of them will now no longer be picked up as an override. 505 | * The `@inheritDoc` syntax without curly braces wasn't always correctly being handled, resulting in docblocks containing only them still being treated as actual docblocks that weren't inherited. 506 | 507 | ### Changes for developers 508 | * Semantic linting can now lint docblock correctness. 509 | * Semantic linting will now also return syntax errors. 510 | * Support for calling most methods synchronously has been removed. 511 | * Classes didn't return information about whether they have a docblock or documentation. 512 | * When fetching class information, types were sometimes returned without their leading slash. 513 | * Because of semantic linting now supporting syntax errors, the reindex command will no longer return them. 514 | * Global constants and functions will now also return an FQCN so you can deduce in what namespace they are located. 515 | * When returning types such as `string[]`, the `fullType` was still trying to resolve the type as if it were a class type. 516 | * The reindex command did not return false when indexing failed and the promise was, by consequence, not rejected. 517 | * Added a new command `localizeType` to localize FQCN's based on use statements, turning them back into relative class names. 518 | * The `semanticLint` method now takes an `options` object that allows you to disable certain parts of the linting process. 519 | * Classes will now return information about whether they are usable as annotation or not (determined by an `@Annotation` tag in their docblock). This is non-standard, but is becoming more and more commonly used in the PHP world (e.g. Symfony and Doctrine). 520 | 521 | ## 0.8.2 522 | ### Bugs fixed 523 | * Circular dependencies should no longer freeze but show an error notification instead. 524 | * Fixed the argument index (used by the call tips package) not being correct for function calls containing SQL strings. 525 | 526 | ## 0.8.1 527 | ### Bugs fixed 528 | * Fixed infinite loop occurring when assigning variables to an expression containing themselves. 529 | 530 | ## 0.8.0 531 | ### Features and enhancements 532 | * Some internal logic has been rewritten to support working asynchronously. The existing list of packages have already been adjusted to make use of this change, which will improve apparant responsiveness across the board. 533 | * Memory-mapped I/O is now enabled on the SQLite back end on systems that support it. This can drastically improve performance and latency of some queries. (In a local test with a large codebase, this literally halved a large class information fetch from 250 milliseconds down to 125 milliseconds). 534 | * A project index will now be triggered when the repository (if present) changes statusses. This isn't as aggressive as a file monitor, but will at least remove the annoyance of having to manually rescan when checking out a different branch to avoid incorrect data being served. Events that will trigger this involve: 535 | * Doing a checkout to switch to a different branch. 536 | * Modifying a file from the project tree externally and then coming back to Atom. 537 | 538 | ### Bugs fixed 539 | * Fixed PHP 7 anonymous classes still being parsed. 540 | * Fixed a rare error relating to an "undefined progressStreamCallback". 541 | * Fixed arrays of class names (e.g. `Foo[]`) in docblocks not being semantically linted. 542 | * Fixed `getInvocationInfoAt` incorrectly walking past control keywords such as `elseif`. 543 | 544 | ### Changes for developers 545 | * Almost all service methods now have an async parameter. It is recommended that you always use this functionality as it will ensure the editor remains responsive for the end user. In a future release, support for synchronous calls **will _probably_ be removed**. 546 | * A new method `deduceType` has been added. 547 | * A new method `getVariableTypeByOffset` has been added. 548 | * `getVariableType`, `getResultingTypeAt`, `resolveTypeAt` and `getInvocationInfoAt` have received an additional parameter, `async`, that will make them (mostly) asynchronous. 549 | * `getVariableType` and `getResultingTypeAt` have been rewritten in PHP. Class types returned by these methods will now _always_ be absolute and _always_ include a leading slash. Previously the returned type was _sometimes_ relative to the current file and _sometimes_ absolute. To make things worse, absolute types _sometimes_ contained a leading slash, leading to confusion. (Scalar types will still not include a leading slash.) 550 | * A new property `hasDocumentation` is now returned for items already having the `hasDocblock` property. The latter will still return false if the docblock is inherited from the parent, but the former will return true in that case. 551 | * The following methods have been removed, they were barely used and just provided very thin wrappers over existing functionality: 552 | * `getClassMember` 553 | * `getClassMemberAt` 554 | * `getClassConstant` 555 | * `getClassConstantAt` 556 | * `getClassMethod` 557 | * `getClassMethodAt` 558 | * `getClassProperty` 559 | * `getClassPropertyAt` 560 | * `getClassSelectorFromEvent` (didn't really belong in the base package). 561 | 562 | ## 0.7.2 563 | ### Bugs fixed 564 | * Fixed minor error where the service version was bumped incorrectly. 565 | 566 | ## 0.7.1 567 | ### Bugs fixed 568 | * Fixed semantic linting not marking use statements in docblocks as used when their types were used inside a type union (e.g. `string|DateTime`). 569 | * Fixed semantic linting not checking if class names in docblocks existed when they occurred inside a type union (e.g. `string|DateTime`). 570 | * Fixed semantic linting not validating class names that were used inside function calls or in class constant fetching. 571 | * Fixed semantic linting marking use statements as unused whilst they were being used inside function calls or in class constant fetching. 572 | 573 | ### Changes for developers 574 | * Methods will now contain information about whether they are abstract or not. 575 | * Methods will now contain information about whether the method they override was abstract or not. 576 | 577 | ## 0.7.0 578 | ### Features and enhancements 579 | * The SQLite database will now use the WAL journal mode, which offers performance benefits. (See also [the SQLite documentation](https://www.sqlite.org/draft/wal.html) for those interested.) 580 | * Additional checks have been added to ensure the indexing database doesn't go into an invalid state. 581 | * The use of `{@inheritDoc}` to extend long descriptions will now also work for child classes and extending interfaces. 582 | * Indexing will now respond less aggressively to changes in the buffer. Instead of spawning an indexing process unconditionally each time a file stops changing (after a couple hundred milliseconds by default), an indexing process will now only be spawned as soon as the previous one finishes. In essence, if an indexing process is already running, the indexer holds on to the changes and issues a reindex as soon as the previous one finishes. If a file takes very long to index and at this point you make multiple changes to the same file, only the last version will be reindexed (i.e. reindexing actions are not actually queued and the indexer does not need to catch up on a possibly long list of changes). 583 | 584 | ### Bugs fixed 585 | * Fixed some parameters of magic methods not showing up at all. 586 | * Fixed event handlers not being disposed when the package was deactivated. 587 | * Fixed parameter names for magic methods containing a comma in some cases. 588 | * Fixed parameters for magic methods being incorrectly marked as being optional. 589 | * Return types for magic properties were not being fetched at all. 590 | * Fixed problems with paths containing spaces on anything non-Windows (Linux, Mac OS X, ...). 591 | * Fixed the indexing progress bar disappearing sometimes if you edited a file while it was busy. 592 | * Fixed the `Warning: is_dir(): Unable to find the wrapper "atom"` error showing up in rare cases. 593 | * Fixed call stacks of simple expressions interpolated inside strings not being correctly retrieved. 594 | * Return types for magic methods were not being resolved relative to use statements or the namespace. 595 | * Parameter names for magic methods no longer contain a dollar sign (consistent with everything else). 596 | * Always set the timezone to UTC. (I changed my mind about this, due to reasons listed in [PR #129](https://github.com/Gert-dev/php-integrator-base/pull/129).) (thanks to [@tillkruss](https://github.com/tillkruss)). 597 | * Fixed issues with the indexer not correctly dealing with class names starting with a lower case letter. 598 | * Some non-standard behavior has been removed that may or may not be noticable to users: 599 | * Constants in traits will no longer be picked up (PHP error). 600 | * Properties in traits will no longer be scanned for aliases (PHP error). 601 | * Properties inside interfaces will not be "inherited" anymore (PHP error). 602 | 603 | ### Changes for developers 604 | * Magic properties and methods will no longer return true for `hasDocblock`. 605 | * `getAvailableVariables` received an extra (optional) `async` parameter. 606 | * `determineCurrentClassName` received an extra (optional) `async` parameter. 607 | * Magic properties and methods now return the class start line as their end line. 608 | * A new method `semanticLint` is now available that can semantically lint files and return various issues with it. 609 | * A new method `getAvailableVariablesByOffset` has been added that allows fetching available variables via an offset in a file. This method is implemented in PHP, supports asynchronous operation and the existing `getAvailableVariables` has been rewritten to use this method. 610 | 611 | ## 0.6.10 612 | ### Bugs fixed 613 | * Fixed the type resolver (--resolve-type) returning types prefixed with a leading slash when a file had no namespace, which is unnecessary and confusing as this slash isn't prepended anywhere else. 614 | 615 | ## 0.6.9 616 | ### Bugs fixed 617 | * Fixed built-in functions not being marked as being built-in. 618 | * Fixed the "Oops, something went wrong!" message when opening a comment that encompassed the rest of the file. 619 | 620 | ## 0.6.8 621 | ### Bugs fixed 622 | * Fixed no internal namespace record being generated when there were no use statements nor a namespace in a file (resulting in notifications being shown relating to no namespace being found). 623 | 624 | ## 0.6.7 625 | ### Bugs fixed 626 | * Fixed incorrect array key being used (`Undefined index: end_line`). 627 | * Notices and warnings from the PHP side will now be shown in the error notification. 628 | * Error reporting is now enabled explicitly in case you have it disabled on a global scale so you can see what is going wrong in the Atom error notification (without having to manually debug it). 629 | 630 | ## 0.6.6 631 | ### Bugs fixed 632 | * Fixed compatibility with PHP < 5.6 by avoiding use of ARRAY_FILTER_USE_KEY. 633 | 634 | ## 0.6.5 635 | ### Bugs fixed 636 | * Reintroduced the xdebug max nesting level, but this time set it to a ridiculously large number for compatibility. 637 | 638 | ## 0.6.4 639 | ### Bugs fixed 640 | * Fixed a minor logic error. 641 | 642 | ## 0.6.3 643 | ### Bugs fixed 644 | * Don't treat invalid JSON output as a JavaScript error, as it's usually not a bug in our package. Just display an error to the user instead. 645 | 646 | ## 0.6.2 647 | ### Bugs fixed 648 | * The "Indexing failed!" message has disappeared. If you wish to know when the indexer is having problems, consider using the new linter package (see also the README). 649 | 650 | ## 0.6.1 651 | ### Bugs fixed 652 | * Removed the dependency on fuzzaldrin. 653 | * Fixed the type of new instances, wrapped in parentheses and spread over several lines, not being properly recognized: 654 | 655 | ```php 656 | // Works: 657 | (new \IteratorIterator)-> 658 | 659 | // Fails (fixed): 660 | (new \IteratorIterator( 661 | 662 | ))-> 663 | ``` 664 | 665 | ## 0.6.0 666 | ### Features and enhancements 667 | * xdebug will now be disabled in the indexer if it is present, it will only slow down the indexer. 668 | * Support for type inference when using the `clone` keyword, for example: 669 | 670 | ```php 671 | $a = new \DateTime(); 672 | $b = clone $a; 673 | $b-> // Autocompletion for DateTime. 674 | ``` 675 | 676 | * The draft PSR-5's `@inheritDoc` is now supported to indicate documentation was not forgotten. Also, note the difference with the non-standard ("incorrect"), yet commonly used, curly brace syntax used by Symfony 2 and other frameworks (which is also supported). 677 | 678 | ```php 679 | /** 680 | * @inheritDoc 681 | */ 682 | public function explicitInheritanceAccordingToDraftPsr5() 683 | { 684 | ... 685 | } 686 | 687 | /** 688 | * {@inheritDoc} 689 | */ 690 | public function incorrectInheritanceButCommonlyUsed() 691 | { 692 | ... 693 | } 694 | ``` 695 | 696 | ### Bugs fixed 697 | * Fixed autocompletion not working after the concatenation operator. 698 | * Fixed author tags that contained mail addresses (such as used in Symfony) being taken up in extended descriptions. 699 | * Fixed an incorrect parameter index being returned in cases where you had array access via keys in function or method invocations. 700 | * Fixed a database constraint error when indexing parameters, which can happen if you have certain PHP extensions enabled (I'm looking at you, ssh2). 701 | * Resolving types is now more correct, taking multiple namespaces per file into account as well as only utilizing use statements that actually apply for a specific line. 702 | 703 | ### Changes for developers 704 | * A new call `getClassListForFile` takes a file path to filter the class list by. 705 | * `getClassSelectorFromEvent` will no longer return null for built-in classes (you can still easily check this yourself). 706 | * Next to `startLine` information, `endLine` information will now also be returned. 707 | * Fetching class information will now also return information about direct and indirect implemented interfaces and used traits via the properties `parents`, `directParents`, `interfaces`, `directInterfaces`, `traits` and `directTraits`. 708 | * Fetching class information will now also return information about direct children, direct implementors (if it's an interface) and direct users (if it's a trait). 709 | * `determineFullClassName` was split up into two methods (separation of concerns): 710 | * `determineCurrentClassName` - Takes an editor and a buffer position and returns the FQCN of the class the location is in. The buffer position is now required as a file can contain multiple classes, which were not retrievable before. 711 | * `resolveType` (with accompanying convenience method `resolveTypeAt`) - Takes an editor, a line, and a type name and resolves that type to its FQCN based on local use statements and namespace rules. 712 | 713 | ## 0.5.4 714 | ### Features and enhancements 715 | * Better error reporting. Exceptions thrown by the PHP side will now be displayed in an Atom error. 716 | 717 | ### Bugs fixed 718 | * Fixed `getInvocationInfoAt` not giving correct results when inside an argument that was wrapped in parentheses. 719 | * Fixed `getInvocationInfoAt` not giving a correct argument index when inside an array parameter with square brackets. 720 | 721 | ## 0.5.3 722 | ### Bugs fixed 723 | * Fixed expressions inside ternary operators not correctly being fetched. 724 | 725 | ## 0.5.2 726 | ### Bugs fixed 727 | * Fixed an error being thrown when writing code in a PHP file that hadn't been saved yet. 728 | 729 | ## 0.5.1 730 | ### Bugs fixed 731 | * Fixed classes never being marked as being abstract. 732 | 733 | ## 0.5.0 734 | ### Features and enhancements 735 | * Minor performance improvements when resolving local class names from docblocks. 736 | * Indexing will now happen continuously (onDidStopChanging of the text buffer) instead of only on save. 737 | * Descriptions from base classes or base interfaces will now be inherited if no description is present for a child class. 738 | * Type inference has been added for arrays, e.g.: 739 | 740 | ```php 741 | /** @var Foo[] $list */ 742 | foreach ($list as $item) { 743 | $item-> // Autocompletion will be provided for 'Foo'. 744 | } 745 | ``` 746 | 747 | ### Bugs fixed 748 | * Fixed docblocks with an empty throws tag causing the indexer to fail (Filter/Encrypt/Mcrypt.php in Zend 1 has one of these, apparently). 749 | * Fixed types not being properly inferred with the new keyword in combination with keywords such as `self`, `static` and `parent`, e.g. `$foo = new static();`. 750 | * Fixed issues with retrieving types in call stacks including static access, such as `self::$property->foo`. 751 | * Fixed built-in methods and functions having class types as parameters not being properly indexed. Instead, a PHP object would be returned in the parameter list. (Only applies to PHP 7.) 752 | * Fixed paths with spaces not indexing properly on Windows. (thanks to [@dipyalov](https://github.com/dipyalov)) 753 | * Fixed variables being assigned to an expression on themselves sometimes not having their correct type deduced, for example: 754 | 755 | ```php 756 | $foo = new Foo(); 757 | $foo = $foo-> // The cursor is here. 758 | 759 | moreCode(); 760 | 761 | // The type would now be based on "$foo = $foo->moreCode()" instead of on "$foo = new Foo()". 762 | ``` 763 | 764 | ### Changes for developers 765 | * Popovers will, by default, no longer catch pointer events (making them click-through). 766 | * You can now listen to indexing succeeding or failing using `onDidFinishIndexing` and `onDidFailIndexing`. (thanks to [@tocjent](https://github.com/tocjent)) 767 | * The type for parameters of methods will now return the type as it was found in the file. If you wish to access the full resolved class type, you can use the new `fullType` property instead. 768 | * A new method `getInvocationInfoAt` is now available that allows fetching information about the function or method being invoked at the specified cursor position in an editor. 769 | 770 | ## 0.4.9 771 | ### Bugs fixed 772 | * Fixed the type of function or method parameters that were not documented incorrectly being set to the type of `$this`. 773 | 774 | ## 0.4.8 775 | ### Bugs fixed 776 | * The command names in the command palette will now be separated by spaces. 777 | * The indexer will no longer show warnings when `file_get_contents` fails. This will be caught and a warning will be displayed in verbose mode instead, ensuring that it will no longer cause indexing to fail in its entirety. 778 | * Fixed interfaces extending multiple other interfaces only having the first parent interface examined. This resulted in some class members not being listed if you implemented such an interface and could also result in files being scanned in the incorrect order (i.e. a child interface before a parent interface). This also resulted in a minor performance increase when fetching class information regarding inheritance as less queries are performed. 779 | 780 | ## 0.4.7 781 | ### Bugs fixed 782 | * Bump the database version. 783 | 784 | ## 0.4.6 785 | ### Bugs fixed 786 | * Fixed types sometimes showing up as [Object object] because the indexer was incorrectly saving an object instead of a string type. 787 | * Fixed built-in functions not having an empty array serialized to the throws_serialized field, resulting in warnings when iterating over them, which in turn caused problems on the CoffeeScript side. 788 | 789 | ## 0.4.5 790 | ### Bugs fixed 791 | * Fixed magic properties ending up in the index with a dollar sign in their name. 792 | * Fixed built-in class constants being indexed by value instead of name. This also resulted in a constraint error in the database. (thanks to [@tocjent](https://github.com/tocjent)) 793 | 794 | ## 0.4.4 795 | ### Bugs fixed 796 | * Do not try to index structural elements that do not have a namespaced name, which can happen for anonymous PHP 7 classes. 797 | 798 | ## 0.4.3 799 | ### Bugs fixed 800 | * Disable the xdebug nesting limit by setting it to -1, which was posing problems when resolving names. 801 | 802 | ## 0.4.2 803 | ### Bugs fixed 804 | * Fixed the `indexes` folder not automatically being created. 805 | 806 | ## 0.4.1 807 | ### Bugs fixed 808 | * Removed the 'old version' note from the README. 809 | 810 | ## 0.4.0 811 | ### Features and enhancements 812 | * The PHP side now no longer uses Reflection, but a php-parser-based incremental indexer. This has the following advantages: 813 | * Composer is no longer required. 814 | * Autoloading is no longer required. 815 | * Multiple structural elements (classes) in a file will correctly end up in the index. 816 | * Parsing is inherently more correct, no more misbehaving corner cases or regex-based parsing. 817 | * Incremental indexing, no more composer classmap dumping on startup and a long indexing process. 818 | * Paves the road for the future more detailed parsing, analyzing and better performance. (As a side-effect, the PHP side can also be used for other editors than Atom.) 819 | * The package still requires PHP >= 5.4, but the code you're writing can be anything that php-parser 2 supports, which is currently PHP >= 5.2 up to (and including) PHP 7. Note that the CoffeeScript side has not changed and also does some regex-based parsing, which may still lack some functionality (especially with relation to PHP 7). 820 | 821 | * Made it possible to make magic members static. This is not documented in phpDocumentor's documentation, but useful and also used by PHPStorm: 822 | 823 | ```php 824 | /** 825 | * @method static void foo() 826 | * @property static Foo $test 827 | */ 828 | class Foo 829 | { 830 | ... 831 | } 832 | ``` 833 | 834 | * Added menu items for some commands. 835 | * The progress bar will now actually show progress, which is useful for large projects. 836 | * There is now a command available in the command palette to reindex your project (thanks to [@wcatron](https://github.com/wcatron)). 837 | * Similarly, there is now also a command available to forcibly reindex the project (i.e. remove the entire database first). 838 | 839 | ### Bugs fixed 840 | * A progress bar will no longer be shown for windows opened without a project. 841 | * Methods returning `static` were not properly resolving to the current class if they were not overridden (they were resolving to the closest parent class that defined them). 842 | * Project and file paths that contain spaces should no longer pose a problem (thanks to [@wcatron](https://github.com/wcatron)). 843 | 844 | ### Changes for developers 845 | * Constants and class properties will now retrieve their start line as well. These items not being available was previously manually worked around in the php-integrator-navigation package. This manual workaround is now present in the base package as CoffeeScript should not have to bend itself backwards to get this information because PHP Reflection didn't offer it. 846 | * `declaringStructure` will now also contain a `startLineMember` property that indicates the start line of the member in the structural element. 847 | * Retrieving types for the list of local variables has been disabled for now as the performance impact is too large in longer functions. It may be reactivated in a future release. 848 | * Constructor information is no longer retrieved when fetching the class list as the performance hit is too large. In fact, this was also the case before the new indexer, but then a cache file was used, which was never properly updated with new classes and was the result of the very long indexing process at start-up. 849 | * `deprecated` was renamed to `isDeprecated` for consistency. 850 | * `wasFound` was removed when fetching class (structural element) information as a failure status is now returned instead. 851 | * `class` was removed when fetching class (structural element) information as this information is already available in the `name` property. 852 | * `isMethod` and `isProperty` were removed as they are no longer necessary since members are now in separate associative arrays. 853 | * Added the more specific convenience methods `getClassMethodAt`, `getClassPropertyAt` and `getClassConstantAt`. 854 | * `isTrait`, `isInterface` and `isClass` were replaced with a single string `type` property. 855 | * Functions and methods no longer return a separate `parameters` and `optionals` property. The two have now been merged and provide more information (such as whether the parameters are a reference, variadic, optional, ...). 856 | 857 | ## 0.3.1 858 | ### Bugs fixed 859 | * Fixed methods returning `static`, `self` or `$this` not properly having their full type deduced. 860 | * Fixed inline type override annotations not being able to contain descriptions (e.g. `/** @var Class $foo Some description. */`). 861 | 862 | ## 0.3.0 863 | ### Features and enhancements 864 | * Performance in general should be improved as the same parsing operations are now cached as often as possible. 865 | * Types of variables that are assigned to basic type expressions will now be deduced properly instead of just having the expression as type: 866 | 867 | ```php 868 | $var = 5; // $var = int 869 | $var = 5.0; // $var = float 870 | $var = true; // $var = bool 871 | $var = ""; // $var = string 872 | $var = []; // $var = array 873 | ``` 874 | 875 | * If statements that have (only) an 'instanceof' of a variable in them will now be used to deduce the type of a variable. (More complex if statements will, at least for now, not be picked up.) For example (when using php-integrator-autocomplete-plus): 876 | 877 | ```php 878 | if ($foo instanceof Foo) { 879 | $foo-> // Autocompletion for Foo will be shown. 880 | } 881 | ``` 882 | 883 | * Closures will now be detected, for example (when using php-integrator-autocomplete-plus): 884 | 885 | ```php 886 | $foo = function () { 887 | 888 | }; 889 | 890 | $foo-> // Autocompletion for Closure, listing bind and bindTo. 891 | ``` 892 | 893 | * Added support for parsing magic properties and methods for classes, which will now also be returned (property-read and property-write are also returned): 894 | 895 | ```php 896 | /** 897 | * @property Foo $someProperty A description. 898 | * @method magicFoo($param1, array $param2 = null) My magic method. 899 | */ 900 | class MyClass 901 | { 902 | 903 | } 904 | ``` 905 | 906 | ### Bugs fixed 907 | * The indexer will no longer try to index PHP files that don't belong to the project on save. 908 | * Docblock parameters weren't being analyzed for deducing the type of local variables when in a global function. 909 | * Types of variables that had their assigned values spread over multiple lines will now correctly have their type deduced. 910 | * In rare cases, types could not be properly deduced, such as in `$a = ['b' => (new Foo())->]` (`Foo` would incorrectly not be returned as type). 911 | * Only the relevant scopes will now be searched for the type of variables, previously all code was examined, even code outside the current scope. 912 | * Descriptions after the `@var` tag, i.e. `@var Foo $foo My description` , will now be used as fall back when there is no short description present. 913 | * The wrong type was sometimes shown for variables as their type was determined at the point they were found instead of the point at which they were requested. 914 | * Functions that had no docblock were wrongly assumed to return 'void' (or 'self' in the case of constructors). This only applies to functions that do have a docblock, but no `@return` tag in the docblock. 915 | * Support for the short annotation style, `/** @var FooClass */`, was dropped. The reason for this is that it's not supported by any IDE and is very specific to this package. It's also completely inflexible because it needs to be directly above the last assignment or other type deduction (such as a catch block) for it to be picked up incorrectly. The other annotation styles have none of these restrictions and also work in IDE's such as PHPStorm. 916 | 917 | ### Changes for developers 918 | * `determineFullClassName` will now return basic types as is, without prefixing them with the current namespace. 919 | * A new method `isBasicType` has been introduced, that will return true for basic types such as "int", "BOOL", "array", "string", ... 920 | * The `getDocParams` method has been removed. It was obsolete as the same information is already returned by `getClassInfo`. Also more caches can be reused by using `getClassInfo`. 921 | * The `autocomplete` method has been removed. It was confusing and also mostly obsolete as its functionality can already be mimicked through other methods (it was only internally used). 922 | * Data returned about methods, constants, functions and structures will no longer have an 'args' property containing information such as descriptions. Instead these were moved up one level (in other words you can just replace the `.args.property` with just `.property` everywhere). It wasn't clear what exactly belonged in `args` and what didn't, hence its entire removal. 923 | 924 | ## 0.2.0 925 | ### Features and enhancements 926 | * There was no convenient visual indicator of when indexing failed, a label is now shown in the status bar if that is indeed the case. 927 | * When the initial PHP process that indexes the entire project fails or is killed, it will now be picked up and displayed as an error. 928 | * The list of variables returned will now try to skip scopes that don't apply. For example, you will now only see variables that are relevant to your closure when inside one. 929 | * It is now possible to specify a list of additional scripts to load, which allows you to add things such as bootstrap scripts or scripts with global helper functions, which will then be made available to other packages (such as autocompletion). 930 | * The return type of your global functions will now be correctly analyzed, the following will now work: 931 | 932 | ```php 933 | /** 934 | * @return \DateTime 935 | */ 936 | function foo() 937 | { 938 | 939 | } 940 | 941 | foo()-> // Autocompletion for DateTime. 942 | ``` 943 | 944 | ### Bugs fixed 945 | * Fixed the 'className.split is not a function' error popping up sometimes. 946 | * Fixed type hints from function parameters not being correctly deduced in some cases. 947 | * Return values such as `\DateTime` (with a leading slash) were not always being found. 948 | * Numbers and underscores were not permitted in class names in several locations (they are now). 949 | * The PHP FileParser will no longer trip over class docblocks containing the pattern `class MyClass`. 950 | * Classes from this package are now no longer included in the class list and will no longer be indexed. 951 | * Fix several issues with autocompletion of `(new Foo())->` in corner cases such as inside arrays and function calls. 952 | * Global class names in combination with the 'new' operator such as `new \My\Class` were not properly having their type deduced (and e.g. getting no autocompletion as a result). 953 | * Fixed an issue where the package would attempt to index the project on shutdown. This could result in a message being displayed at shutdown about the Atom window not being responsive. 954 | 955 | ### Changes for developers 956 | * New service methods: 957 | * `getClassMethod` - Retrieves information about a specific class method. 958 | * `getClassProperty` - Retrieves information about a specific class property. 959 | * `getClassConstant` - Retrieves information about a specific class constant. 960 | * Previously, `getCalledClass` always ignored the last element in a call stack, causing `$this->foo->b` to return the type of `foo` instead of `b`. Because this behavior is unexpected and undocumented, this no longer happens. To maintain this 'feature', a new parameter `ignoreLastElement` has been added that can be set to true to restore this behavior (i.e. it will return the type of `foo`). Setting it to false will return the type of `b` instead. 961 | * `getGlobalFunctions` will now also return user-defined global functions (i.e. non-internal global functions). 962 | * `getGlobalConstants` will now also return information about if a constant is built-in or not (as opposed to user defined). 963 | * `getAvailableVariables` now returns an object with variable names mapped to information such as their type (if found). 964 | * `getClassMemberAt` will now return the correct member if a structure has a property and a method with the same name. 965 | * `getCalledClass` is now called `getResultingTypeAt` to better indicate what it does and that it needs a buffer position. 966 | * Class constants will now contain information about their declaring class and declaring structure, just like other members. 967 | * Several methods such as `getClassInfo` now take an additional parameter to make them execute asynchronously (a promise will be returned instead of the actual results). 968 | 969 | ## 0.1.0 970 | * Initial release. 971 | --------------------------------------------------------------------------------