├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── extras └── example.php ├── iconsets ├── materialdesignicons-webfont.woff └── materialdesignicons.css ├── keymaps └── php-debug.cson ├── lib ├── breakpoints │ ├── default-breakpoint-item-view.js │ └── default-breakpoints-view.js ├── engines │ └── dbgp │ │ ├── dbgp-instance.js │ │ ├── debugging-context.js │ │ ├── engine.js │ │ └── server.js ├── helpers.js ├── models │ └── debug-engine.js ├── pathmaps │ └── pathmaps-view.js ├── php-debug.js ├── services │ └── decorator.js ├── status │ ├── console-view.js │ └── debug-view.js └── ui │ ├── component.js │ ├── double-filter-list-view.js │ └── editor.js ├── menus └── php-debugx.cson ├── package.json ├── screenshot.gif ├── spec ├── php-debug-spec.coffee └── php-debug-view-spec.coffee └── styles ├── php-debug.atom-text-editor.less └── php-debug.less /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.5 2 | * Fix issue where turning off multiple sessions wasn't working 3 | * Fix several issues where pathmaps weren't functioning correctly 4 | * Add ability to ignore pathmap files by setting the local path to "?" 5 | * Misc bug Fixes 6 | * Add configuration to have pathmap searching ignore certain directories 7 | 8 | ## 0.3.4 9 | * Even more parser fixes 10 | * Feature: Ability to ignore paths/files via pathmap setting where the local side is set to "!" 11 | * Feature: New configuration option "Allow for multiple debug sessions at once" to disable/enable multi session support 12 | * Feature: New configuration option "Automatically scan projects in Atom to try and find path maps" 13 | * Feature: New configuration option "Continue to listen for debug sessions even if the debugger windows are all closed" 14 | * Breakpoints now print info to the PHP-Debug console 15 | * Fix missing code for atom commands 16 | * Update to require version 1.0.3 of atom-debug-ui 17 | 18 | ## 0.3.3 19 | * Additional parser fixes 20 | * Additional debugging functionality to the parser 21 | 22 | ## 0.3.2 23 | * Fix typo in fix for 0.3.1 parser 24 | * Fix messages in config [thanks PHLAK] 25 | 26 | ## 0.3.1 27 | * Fix multiple issues with the parser 28 | * Fix typo for notifications 29 | * Fix check for notification installation prompt 30 | 31 | ## 0.3.0 32 | * Almost a complete rewrite 33 | * Utilize atom-debug-ui package 34 | * UTF-8 Support for member names and data 35 | * Support for multiple debug sessions/instances 36 | * New pathmaps functionality, old style is replaced 37 | * Better status messages 38 | * More options for UI tweeks 39 | 40 | * Via atom-debug-ui: A huge number of UI cleanups 41 | * Via atom-debug-ui: Support for Atom dock functionality 42 | * Via atom-debug-ui: Floating/Overlay Actionbar 43 | * Via atom-debug-ui: Better access to settings for breakpoints 44 | * Via atom-debug-ui: Better highlighting for variables 45 | * Via atom-debug-ui: Better status messages 46 | * Via atom-debug-ui: Better console support 47 | 48 | ## 0.2.6 49 | * Fix bug(s) with new socket binding code, should fix Atom freezes 50 | * Fix for scrollbar styling [thanks pictus] 51 | * Add console history/log [thanks StAmourD and cgalvarez] 52 | * Add theming to panel php data [thanks StAmourD and cgalvarez] 53 | * Add better display/styling of object/arrays [thanks StAmourD and cgalvarez] 54 | * Sorting for objects/arrays [thanks StAmourD and cgalvarez] 55 | * Bug fixes [with thanks StAmourD and cgalvarez] 56 | * Documentation fix for ServerAddress [thanks ptrm04] 57 | 58 | ## 0.2.5 59 | * Implemented host specific listening [thanks lfarkas] 60 | * Add support for filtering file paths during debugging [thanks StAmourD] 61 | * Add support for activating the Atom window after a breakpoint triggers [thanks StAmourD] 62 | * Make adjustments to readme [thanks surfer190] 63 | * Add check on xdebug server for file to match breakpoints [thanks QwertyZW] 64 | * Adjustments to action bar button styling [thanks CraigGardener] 65 | * Many bug fixes 66 | 67 | ## 0.2.4 68 | * Allow main panel to be docked to the side or the bottom 69 | * Add an interactive console 70 | * Allow different views to be closed and restored 71 | * Many bug fixes 72 | 73 | ## 0.2.3 74 | * Change the way that scrolling works within the panel 75 | * Add ability to auto expand locals in the context 76 | * Support for resource data types from PHP 77 | * Classnames for objects in the context view 78 | * Allow port to be adjusted after php-debug has already been enabled once 79 | * Bug fixes 80 | 81 | ## 0.2.2 82 | * Add ability to toggle breakpoints from editor gutter 83 | * This can be enabled and configured via the settings 84 | * Attempt to resolve encoding issue by switching socket parsing to ASCII instead of UTF8 85 | * Fix paths bug 86 | * Better handling of socket in use errors 87 | * Bug fixes 88 | 89 | ## 0.2.1 90 | * Bug fixes 91 | 92 | ## 0.2.0 93 | * Bug fixes 94 | * Move unified panel into bottom panel 95 | * Change remote debugging configuration so it works 96 | 97 | ## 0.1.4 98 | * Bug fixes 99 | * Code cleanups 100 | * Data handling improvements (protocol) 101 | * Context preservation between break refreshes 102 | * UX improvements 103 | * Improved watchpoint handling 104 | 105 | ## 0.1.3 106 | * Bug fixes 107 | * UX Bug fixes ( breakpoints) 108 | * Improved exception handling 109 | * Stackframe selection 110 | 111 | ## 0.1.2 112 | * Add settings for selection Exceptions 113 | * Add custom Exceptions 114 | * Enable keybinding on debuggin panes 115 | * UX improvements 116 | 117 | ## 0.1.1 118 | * Optimizations 119 | * Bug fixes 120 | * Default changes for max depth / max children 121 | 122 | ## 0.1.0 123 | * UX improvements 124 | * Bug fixes 125 | * Additional configuration for Max Depth, Max children, Max data 126 | * Improved breakpoints 127 | 128 | ## 0.0.13 129 | * Bug fixes (protocol) 130 | * UX improvements 131 | 132 | ## 0.0.12 133 | * Bug fixes (buffer handling) 134 | 135 | ## 0.0.11 136 | * Additional type support 137 | * UX improvements 138 | * Bug fixes 139 | 140 | ## 0.0.10 141 | ## 0.0.9 142 | * Project rename 143 | 144 | ## 0.0.8 145 | * Breakpoint bug fixes 146 | * Fixes for stopping 147 | * Bug fixes 148 | 149 | ## 0.0.7 150 | * Formatting Adjustments 151 | * Bug fixes for max osx 152 | 153 | ## 0.0.6 154 | * Bug fixes (stepping,boolean values) 155 | 156 | ## 0.0.5 157 | * Readme Adjustments 158 | * Path conversion fixes 159 | * Better support for stopping 160 | 161 | ## 0.0.4 162 | * Improve watchpoints 163 | 164 | ## 0.0.3 165 | * Bug fixes for contexts 166 | 167 | ## 0.0.2 168 | * Bug fixes for xmljs 169 | 170 | ## 0.0.1 - First Release 171 | * Initial Release 172 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom PHP Debugging Package 2 | 3 | Debug PHP code using the [Xdebug PHP Extension](http://xdebug.org/). 4 | 5 | # Features 6 | - Interactive Console 7 | - Breakpoints 8 | - Watches 9 | - Multiple Debug Sessions 10 | - Path Mappings 11 | - Step through debugging (Over, In, Out) 12 | - Stack and Context views 13 | - UTF8 Support 14 | - Uses atom-debug-ui 15 | 16 | This is currently an alpha release, and still in active development. 17 | 18 | ![](https://raw.githubusercontent.com/gwomacks/php-debug/master/screenshot.gif) 19 | 20 | # Getting Started 21 | 22 | ## Install Xdebug ## 23 | You may already have Xdebug installed. Check the results of the [phpinfo function](http://php.net/manual/en/function.phpinfo.php) for xdebug information. 24 | If no xdebug section exists, you likely need to install this. *nix users can likely find it within their package manager of choice. 25 | Alternative installation or compiling instructions are available [here](http://xdebug.org/docs/install). 26 | 27 | ## Setting up Xdebug ## 28 | 29 | ``` 30 | xdebug.remote_enable=1 31 | xdebug.remote_host=127.0.0.1 32 | xdebug.remote_connect_back=1 # Not safe for production servers 33 | xdebug.remote_port=9000 34 | xdebug.remote_handler=dbgp 35 | xdebug.remote_mode=req 36 | xdebug.remote_autostart=true 37 | ``` 38 | 39 | With these settings, PHP will connect to your editor for every script it executes. 40 | The alternative is to switch xdebug.remote_autostart to false, and install an Xdebug helper extension for your browser of choice, such as: 41 | - [The easiest Xdebug](https://addons.mozilla.org/en-US/firefox/addon/the-easiest-xdebug/) for Mozilla Firefox 42 | - [Xdebug Helper](https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc) for Google Chrome 43 | 44 | These browser extensions will give you a button within your browser to enable/disable Xdebug. The extensions might have configuration options for an "IDE key" (which is used for an XDEBUG_SESSION cookie). The IDE key for Atom with PHP Debug is "xdebug-atom". 45 | 46 | It is also possible to run a php script from the command line with Xdebug enabled. 47 | You can find more information on this at the Xdebug documentation for [Starting The Debugger](http://xdebug.org/docs/remote#starting). 48 | See can find a complete list and explanation of Xdebug settings [here](http://xdebug.org/docs/all_settings). 49 | 50 | ## Start Debugging ## 51 | 52 | To begin debugging: 53 | 54 | 1. Open up your PHP file in atom 55 | 2. Add a breakpoint: 56 | 57 | Move the cursor to a line you want to break on and set a breakpoint by pressing `Alt+F9`, selecting Toggle Breakpoint from the Command Palette (`ctrl+shift+p`)or with the php-debug menu (`Packages -> php-debug->Toggle Breakpoint`). 58 | This will highlight the line number green, to indicate the presence of a breakpoint. 59 | 60 | 3. Open the debug view by pressing `ctrl+alt+d`, selecting 'Toggle Debugging' from the Command Palette or php-debug menu. 61 | 4. Start the script with Xdebug enabled. If everything is setup correctly, the entire line of the breakpoint will be highlighted in green, indicating the current line of the script. 62 | 63 | If everything worked correctly, you can now use the various buttons/commands to step through the script. 64 | 65 | # Settings 66 | 67 | ### Server Port ### 68 | This is the port that the atom client will listen on. 69 | Defaults to 9000 70 | 71 | ### Server Address ### 72 | This is the address that the atom client will listen on. 73 | Defaults to 127.0.0.1 74 | 75 | ### Xdebug DBGP Protocol Debugging ### 76 | Outputs protocol debugging messages to the atom debug console 77 | 78 | ### Xdebug: Max Depth ### 79 | Max depth for variable scopes 80 | 81 | ### Xdebug: Max children ### 82 | Maximum number of array elements to show in variables 83 | 84 | ### Xdebug: Max Data ### 85 | Maximum data for variables 86 | 87 | ### Display: Sort Arrays/Object alphabetically ### 88 | Sort Arrays/Object alphabetically instead of by php default 89 | 90 | ### Display: Status Bar ### 91 | Allow PHP Debug to be opened from the status bar 92 | 93 | ### Exceptions ### 94 | Default exceptions and errors for PHP-Debug to break on 95 | 96 | ### Path Maps ### 97 | Pathmaps are now configured for each project folder during connection 98 | -------------------------------------------------------------------------------- /extras/example.php: -------------------------------------------------------------------------------- 1 | member = array('key' => 'doom'); 21 | 22 | for($i = 0; $i < 10; $i++) 23 | $this->member = array('key' => $this->member); 24 | } 25 | 26 | function recursivePrintNumber($limit, $cur = 0) { 27 | 28 | print $cur."\n"; 29 | if($cur < $limit) 30 | $this->recursivePrintNumber($limit, $cur+1); 31 | } 32 | } 33 | 34 | $rs = new RecursivePrinter(); 35 | $rs->recursivePrintNumber(64); # Try setting a breakpoint on this line 36 | throw new Exception(); 37 | print "finished"; 38 | -------------------------------------------------------------------------------- /iconsets/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwomacks/php-debug/b586c08c50edd4fb0269db0a569a097e494d2355/iconsets/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /keymaps/php-debug.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-workspace': 11 | 'alt-f5': 'php-debug:run' 12 | 'alt-f6': 'php-debug:stepOver' 13 | 'alt-f7': 'php-debug:stepIn' 14 | 'alt-f8': 'php-debug:stepOut' 15 | 16 | # Add more specific keymap overrides for php files 17 | "atom-text-editor[data-grammar='text html php']": 18 | 'ctrl-alt-d': 'atom-debug-ui:toggleDebugging' 19 | 'ctrl-alt-f': 'atom-debug-ui:addWatch' 20 | 'ctrl-alt-w': 'atom-debug-ui:addWatchpoint' 21 | 'alt-f9': 'atom-debug-ui:toggleBreakpoint' 22 | 'alt-f5': 'php-debug:run' 23 | 'alt-f6': 'php-debug:stepOver' 24 | 'alt-f7': 'php-debug:stepIn' 25 | 'alt-f8': 'php-debug:stepOut' 26 | 27 | '.php-debug': 28 | 'ctrl-alt-d': 'atom-debug-ui:toggleDebugging' 29 | 'ctrl-alt-f': 'atom-debug-ui:addWatch' 30 | 'ctrl-alt-w': 'atom-debug-ui:addWatchpoint' 31 | 'alt-f9': 'atom-debug-ui:toggleBreakpoint' 32 | 'alt-f5': 'php-debug:run' 33 | 'alt-f6': 'php-debug:stepOver' 34 | 'alt-f7': 'php-debug:stepIn' 35 | 'alt-f8': 'php-debug:stepOut' 36 | -------------------------------------------------------------------------------- /lib/breakpoints/default-breakpoint-item-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import UiComponent from '../ui/component' 6 | 7 | export default class DefaultBreakpointItemView extends UiComponent { 8 | 9 | render () { 10 | const {breakpoint,type,status} = this.props; 11 | let attributes = { 12 | value: type 13 | }; 14 | if (status == true) { 15 | attributes.checked = "checked"; 16 | } 17 | return
  • 18 |
    19 | 20 | {breakpoint.title} 21 |
    22 |
  • 23 | } 24 | handleChange (event) { 25 | if (!this.props.onchange) { 26 | return 27 | } 28 | this.props.onchange(this.props.type, event.target.checked); 29 | } 30 | } 31 | DefaultBreakpointItemView.bindFns = ["handleChange"] 32 | -------------------------------------------------------------------------------- /lib/breakpoints/default-breakpoints-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import UiComponent from '../ui/component' 6 | import DefaultBreakpointItemView from './default-breakpoint-item-view' 7 | import helpers from '../helpers' 8 | 9 | export default class DefaultBreakpointListView extends UiComponent { 10 | 11 | constructor (props,children) { 12 | super(props,children) 13 | this._services = props.services; 14 | this.subscriptions.add(atom.config.onDidChange("php-debug.exceptions", this.breakpointsUpdated)) 15 | } 16 | 17 | render () { 18 | const {services,state} = this.props; 19 | const breakpointComponents = Object.keys(state.breakpoints).map((breakpointType,index) => { 20 | let breakpoint = state.breakpoints[breakpointType]; 21 | let status = atom.config.get('php-debug.exceptions.'+breakpointType) 22 | return 23 | }); 24 | return
    25 | Exceptions: 26 |
      27 | {breakpointComponents} 28 |
    29 |
    30 | } 31 | 32 | breakpointsReady() { 33 | } 34 | 35 | breakpointsUpdated(event) { 36 | var changed = false; 37 | for (let key in event.newValue) { 38 | if (event.newValue[key] != event.oldValue[key]) { 39 | changed = true; 40 | } 41 | } 42 | if (changed) { 43 | this.update({state:this.props.state}, this.children, true); 44 | } 45 | } 46 | 47 | init () { 48 | if (!this.props.state) { 49 | let config = atom.config.getSchema('php-debug.exceptions').properties; 50 | this.props.state = { 51 | breakpoints: config 52 | }; 53 | } 54 | super.init(); 55 | } 56 | 57 | breakpointChanged(type, checked) { 58 | atom.config.set('php-debug.exceptions.'+type, checked) 59 | } 60 | } 61 | DefaultBreakpointListView.bindFns = ["breakpointsReady","breakpointChanged","breakpointsUpdated"] 62 | -------------------------------------------------------------------------------- /lib/engines/dbgp/dbgp-instance.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import {parseString} from 'xml2js' 4 | import Promise from 'promise' 5 | import {Emitter, Disposable} from 'event-kit' 6 | import {escapeValue, localPathToRemote, remotePathToLocal, hasRemotePathMap, hasLocalPathMap, createPreventableEvent} from '../../helpers' 7 | import autoBind from 'auto-bind-inheritance' 8 | 9 | export default class DbgpInstance { 10 | constructor (params) { 11 | autoBind(this); 12 | this._isActive = false 13 | this._initComplete = false 14 | this._socket = params.socket 15 | this._emitter = params.emitter 16 | this._services = params.services 17 | this._context = params.context 18 | this._pathMaps = [] 19 | if (this._context == undefined || this._context == null || this._context.trim() == "") { 20 | throw new Error("Context cannot be empty") 21 | } 22 | this._activationQueue = [] 23 | this._promises = [] 24 | this._socket.on('data', this.handleData) 25 | this._bufferSize = 32 26 | this._buffers = new Array(this._bufferSize) 27 | this._bufferIdx = 0 28 | this._currentBufferReadIdx = 0 29 | this._bufferReadIdx = 0 30 | this._breakpointMap = {} 31 | this._sessionEnded = false 32 | } 33 | 34 | stop () { 35 | try { 36 | this._socket.end() 37 | } catch (err) { 38 | // Supress 39 | } 40 | if (!this._sessionEnded) { 41 | this._sessionEnded = true 42 | this._emitter.emit('php-debug.engine.internal.sessionEnd',{context:this.getContext()}) 43 | } 44 | } 45 | 46 | destroy() { 47 | this.executeStop() 48 | delete this._socket 49 | this._isActive = false 50 | delete this._services 51 | delete this._context 52 | delete this._promises 53 | delete this._buffers 54 | delete this._breakpointMap 55 | delete this._activationQueue 56 | } 57 | 58 | syncStack (depth) { 59 | let options = {} 60 | if (depth < 0) { 61 | depth = 0 62 | } 63 | return new Promise((fulfill,reject) => { 64 | this.executeCommand('stack_get', options).then((data) => { 65 | let stackFrames = [] 66 | if (data.response.stack) { 67 | for (frame of data.response.stack) { 68 | let csonFrame = { 69 | id: frame.$.level, 70 | label: frame.$.where, 71 | filepath: remotePathToLocal(decodeURI(frame.$.filename), this._pathMaps), 72 | level: frame.$.level, 73 | line: frame.$.lineno, 74 | active: parseInt(frame.$.level,10) == depth ? true : false 75 | } 76 | stackFrames.push(csonFrame) 77 | } 78 | } 79 | fulfill(stackFrames) 80 | }); 81 | }); 82 | } 83 | 84 | updateContextIdentifier(context) { 85 | if (context == undefined || context == null || context.trim() == "") { 86 | throw new Error("Context cannot be empty") 87 | } 88 | this._context = context 89 | } 90 | 91 | isActive() { 92 | return this._isActive 93 | } 94 | 95 | getContext() { 96 | return this._context 97 | } 98 | 99 | setPathMap(mapping) { 100 | for (let existing of this._pathMaps) { 101 | if (existing.remotePath == mapping.remotePath) { 102 | existing.localPath = mapping.localPath; 103 | return; 104 | } 105 | } 106 | this._pathMaps.push(mapping); 107 | } 108 | 109 | getPathMaps() { 110 | return this._pathMaps; 111 | } 112 | 113 | isInternallyActive() { 114 | return this._isActive || this._initComplete == false 115 | } 116 | 117 | activate() { 118 | this._isActive = true 119 | while (this._activationQueue.length > 0) { 120 | this.parseResponse(this._activationQueue.shift()) 121 | } 122 | } 123 | 124 | nextTransactionId () { 125 | if (!this._transaction_id) { 126 | this._transaction_id = 1 127 | } 128 | return this._transaction_id++ 129 | } 130 | 131 | parse () { 132 | var message = ""; 133 | var messageLength = 0; 134 | var tempBuffer = null; 135 | var writeIdx = 0 136 | while (this._bufferReadIdx < this._bufferIdx) { 137 | //console.log("buffers",this._buffers); 138 | //console.log("buffers",this._bufferReadIdx,this._bufferIdx); 139 | let buffer = this._buffers[this._bufferReadIdx % this._bufferSize]; 140 | //console.log("1.buffer.length",buffer.length, this._bufferReadIdx % this._bufferSize, this._bufferIdx % this._bufferSize); 141 | let idx = this._currentBufferReadIdx; 142 | //while (buffer.split("\0").length >= 2) { 143 | if (messageLength == 0) { 144 | // Read Length 145 | while (idx <= buffer.length) { 146 | if (buffer[idx] === 0) { 147 | messageLength = buffer.toString('utf8',this._currentBufferReadIdx, idx) 148 | break; 149 | } 150 | idx++ 151 | } 152 | messageLength = parseInt(messageLength); 153 | if (messageLength == 0) return; 154 | // Skip null zero 155 | idx++; 156 | } 157 | if ((buffer.length - idx) < (messageLength - writeIdx)) { 158 | // WARNING If a message is split over 3 or more packets 159 | // but the come two together and then one later 160 | // I believe this could result in a bug, we'd lose 161 | // tempBuffer. I'm not sure if this would actually 162 | // happen in practice though 163 | if (this._bufferIdx <= (this._bufferReadIdx + 1)) { 164 | return; 165 | } else { 166 | let available = (buffer.length - idx) + writeIdx; 167 | let tmpIdx = this._bufferReadIdx + 1; 168 | while (tmpIdx < this._bufferIdx) { 169 | available = available + this._buffers[tmpIdx % this._bufferSize].length; 170 | tmpIdx++; 171 | } 172 | if (available < messageLength) { 173 | return; 174 | } 175 | } 176 | if (tempBuffer == null) { 177 | tempBuffer = Buffer.allocUnsafe(messageLength); 178 | } 179 | buffer.copy(tempBuffer, writeIdx, idx, buffer.length); 180 | this._buffers[this._bufferReadIdx % this._bufferSize] = null; 181 | writeIdx = writeIdx + (buffer.length - idx); 182 | buffer = null; 183 | this._currentBufferReadIdx = 0; 184 | this._bufferReadIdx++; 185 | continue; 186 | } 187 | 188 | if (tempBuffer != null) { 189 | buffer.copy(tempBuffer, writeIdx, 0, (messageLength-writeIdx)); 190 | idx = idx + (messageLength-writeIdx); 191 | //console.log("tmp2:",tempBuffer.length, writeIdx, (messageLength-writeIdx), idx) 192 | //console.log(tempBuffer); 193 | message = tempBuffer.toString("utf8", 0, tempBuffer.length); 194 | } else { 195 | message = buffer.toString("utf8", idx, messageLength+idx); 196 | idx = messageLength+idx 197 | } 198 | // Post message null zero 199 | idx++; 200 | //console.log("messagelenght",messageLength); 201 | //console.log("message",message); 202 | //console.log("message.length",message.length); 203 | if (atom.config.get("php-debug.server.protocolDebugging")) { 204 | this._emitter.emit('php-debug.engine.internal.debugDBGPMessage',{context:this.getContext(),message:message,type:"recieved"}) 205 | } 206 | this.parseXml(message); 207 | //console.log("idx",idx) 208 | //console.log("buffer.length",buffer.length); 209 | // Complete message would leave one null zero left over 210 | if (buffer.length > idx) { 211 | this._currentBufferReadIdx = idx; 212 | this.parse() 213 | break; 214 | } else { 215 | //console.log("parse complete"); 216 | this._buffers[this._bufferReadIdx % this._bufferSize] = null; 217 | buffer = null; 218 | this._currentBufferReadIdx = 0; 219 | this._bufferReadIdx++; 220 | messageLength = 0; 221 | tempBuffer = null; 222 | writeIdx = 0 223 | } 224 | } 225 | //console.log("exiting parse"); 226 | } 227 | 228 | parseXml(message) { 229 | let o = parseString(message, (err, result) => { 230 | if (err) { 231 | console.error(err) 232 | } else { 233 | if (result == undefined || result == null) { 234 | console.error("An unexpected parse error occurred, received null result set", message); 235 | console.trace(); 236 | return; 237 | } 238 | if (typeof result !== "object" || Object.keys(result).length <= 0) { 239 | console.error("An unexpected parse error occurred, result set is not object", result, message); 240 | console.trace(); 241 | return; 242 | } 243 | const type = Object.keys(result)[0] 244 | switch (type) { 245 | case "init": 246 | this.onInit(result) 247 | break; 248 | case "response": 249 | if (this.isInternallyActive()) { 250 | this.parseResponse(result) 251 | } else { 252 | this._activationQueue.push(result) 253 | } 254 | break; 255 | case "stream": 256 | this.parseStream(result) 257 | break; 258 | } 259 | } 260 | }); 261 | } 262 | 263 | parseResponse (data) { 264 | const result = data.response.$ 265 | const transactionId = result.transaction_id 266 | 267 | if (this._promises[transactionId] != undefined) { 268 | this._promises[transactionId](data) 269 | delete this._promises[transactionId] 270 | } 271 | else { 272 | console.warn("Could not find promise for transaction " + transactionId) 273 | } 274 | } 275 | 276 | parseStream (data) { 277 | const result = data.stream._ 278 | var streamData = new Buffer(result, 'base64').toString('utf8') 279 | if (streamData != null && streamData != "") { 280 | if (this._services.hasService("Console")) { 281 | this._services.getConsoleService().addMessage(this.getContext(), streamData) 282 | } 283 | } 284 | } 285 | 286 | handleData (data) { 287 | if (this._buffers == undefined || this._buffers == null) { 288 | return; 289 | } 290 | if (data == null) { 291 | console.error("null data package"); 292 | return; 293 | } 294 | if (atom.config.get("php-debug.server.protocolDebugging")) { 295 | this._emitter.emit('php-debug.engine.internal.debugDBGPMessage',{context:this.getContext(),message:data.toString('hex'),type:"raw-recieved"}) 296 | } 297 | this._buffers[this._bufferIdx++ % this._bufferSize] = data 298 | try { 299 | this.parse() 300 | } catch (err) { 301 | console.error("An unexpected parse error occurred, exception during parsing"); 302 | console.error(err); 303 | } 304 | } 305 | 306 | executeCommand (command, options, data) { 307 | return this.command(command, options, data) 308 | } 309 | 310 | command (command, options, data) { 311 | let transactionId = this.nextTransactionId() 312 | return new Promise((fulfill,reject) => { 313 | if (this._promises == undefined || this._promises == null) { 314 | if (atom.config.get("php-debug.server.protocolDebugging")) { 315 | this._emitter.emit('php-debug.engine.internal.debugDBGPMessage',{context:this.getContext(),message:"socket already closed",type:"sent"}) 316 | } 317 | fulfill(null) 318 | return 319 | } 320 | this._promises[transactionId] = fulfill 321 | if (command == "stop" && (this._socket == undefined || this._socket == null || !this._socket.writable)) { 322 | if (atom.config.get("php-debug.server.protocolDebugging")) { 323 | this._emitter.emit('php-debug.engine.internal.debugDBGPMessage',{context:this.getContext(),message:"socket already closed",type:"sent"}) 324 | } 325 | delete this._promises[transactionId] 326 | fulfill(null) 327 | return 328 | } 329 | 330 | let payload = command + " -i " + transactionId 331 | if (options && Object.keys(options).length > 0) { 332 | let argu = [] 333 | for (arg in options) { 334 | let val = options[arg] 335 | argu.push("-"+(arg) + " " + escapeValue(val)) 336 | } 337 | //argu = ("-"+(arg) + " " + encodeURI(val) for arg, val of options) 338 | argu2 = argu.join(" ") 339 | payload += " " + argu2 340 | } 341 | if (data) { 342 | payload += " -- " + new Buffer(data, 'utf8').toString('base64') 343 | } 344 | if (this._socket != undefined && this._socket != null) { 345 | try { 346 | this._socket.write(payload + "\0") 347 | if (atom.config.get("php-debug.server.protocolDebugging")) { 348 | this._emitter.emit('php-debug.engine.internal.debugDBGPMessage',{context:this.getContext(),message:payload,type:"sent"}) 349 | } 350 | } catch (error) { 351 | console.error("Write error", error) 352 | delete this._promises[transactionId] 353 | reject(error) 354 | } 355 | } else { 356 | console.error("No socket found") 357 | delete this._promises[transactionId] 358 | reject("No socket found") 359 | } 360 | }) 361 | } 362 | 363 | getFeature (feature_name) { 364 | return this.command("feature_get", {n: feature_name}) 365 | } 366 | 367 | setFeature (feature_name, value) { 368 | return this.command("feature_set", {n: feature_name, v: value}) 369 | } 370 | 371 | setStdout (value) { 372 | return this.command("stdout", {c: value}) 373 | } 374 | 375 | setStderr (value) { 376 | return this.command("stderr", {c: value}) 377 | } 378 | 379 | onInit (data) { 380 | var handshakeStartedEvent = createPreventableEvent(); 381 | // Not sure exactly what xdebug is trying to accomplish here so just ignore 382 | // these connections 383 | if (data.init.$.fileuri == "dbgp://stdin") { 384 | return this.command("detach"); 385 | } 386 | this.setFeature('show_hidden', 1) 387 | .then( () => { 388 | handshakeStartedEvent = Object.assign(handshakeStartedEvent,{context:this.getContext(),fileuri:data.init.$.fileuri,idekey:data.init.$.idekey,appid:data.init.$.appid,"language":data.init.$.language,"version":data.init.$["xdebug:language_version"]}) 389 | this._emitter.emit('php-debug.engine.internal.handshakeStarted',handshakeStartedEvent) 390 | }) 391 | .then( () => { 392 | return this.getFeature('supported_encodings') 393 | }) 394 | .then( () => { 395 | return this.getFeature('supports_async') 396 | }) 397 | .then( () => { 398 | return this.setFeature('encoding', 'UTF-8') 399 | }) 400 | .then( () => { 401 | return this.setFeature('max_depth', atom.config.get('php-debug.xdebug.maxDepth')) 402 | }) 403 | .then( () => { 404 | return this.setFeature('max_data', atom.config.get('php-debug.xdebug.maxData')) 405 | }) 406 | .then( () => { 407 | return this.setFeature('max_children', atom.config.get('php-debug.xdebug.maxChildren')) 408 | }) 409 | .then( () => { 410 | if (atom.config.get('php-debug.xdebug.multipleSessions') === true || atom.config.get('php-debug.xdebug.multipleSessions') === 1) { 411 | return this.setFeature('multiple_sessions', 1) 412 | } else { 413 | return this.setFeature('multiple_sessions', 0) 414 | } 415 | }) 416 | .then( () => { 417 | return this.setStdout('stdout', atom.config.get('php-debug.server.redirectStdout') ? 1 : 0) 418 | }) 419 | .then( () => { 420 | return this.setStderr('stderr', atom.config.get('php-debug.server.redirectStderr') ? 1 : 0) 421 | }) 422 | .then( () => { 423 | return new Promise( (fulfill,reject) =>{ 424 | if (handshakeStartedEvent.isDefaultPrevented()) { 425 | if (handshakeStartedEvent.getPromise() == null) { 426 | reject("Session rejected") 427 | return; 428 | } 429 | handshakeStartedEvent.getPromise().then( () => { 430 | fulfill(this.sendAllBreakpoints()) 431 | }) 432 | } else { 433 | fulfill(this.sendAllBreakpoints()) 434 | } 435 | }) 436 | }) 437 | .then( () => { 438 | return this.sendAllWatchpoints() 439 | }) 440 | .then( () => { 441 | this._initComplete = true 442 | this._emitter.emit('php-debug.engine.internal.handshakeComplete',{context:this.getContext(),fileuri:data.init.$.fileuri,idekey:data.init.$.idekey,appid:data.init.$.appid,"language":data.init.$.language,"version":data.init.$["xdebug:language_version"]}) 443 | return this.executeRun() 444 | }).catch( (err) => { 445 | if (this._services != undefined && this._services != null && this._services.hasService("Logger")) { 446 | this._services.getLoggerService().error(err) 447 | } else { 448 | console.error(err) 449 | } 450 | }) 451 | } 452 | 453 | sendAllBreakpoints () { 454 | let commands = [] 455 | if (this._services.hasService("Breakpoints")) { 456 | let service = this._services.getBreakpointsService() 457 | let breakpoints = service.getBreakpoints() 458 | 459 | for (breakpoint of breakpoints) { 460 | commands.push(this.executeBreakpoint(breakpoint)) 461 | } 462 | 463 | if (atom.config.get('php-debug.exceptions.fatalError')) { 464 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Fatal error', stackDepth: -1}))) 465 | } 466 | if (atom.config.get('php-debug.exceptions.catchableFatalError')) { 467 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Catchable fatal error', stackDepth: -1}))) 468 | } 469 | if (atom.config.get('php-debug.exceptions.warning')) { 470 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Warning', stackDepth: -1}))) 471 | } 472 | if (atom.config.get('php-debug.exceptions.strictStandards')) { 473 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Strict standards', stackDepth: -1}))) 474 | } 475 | if (atom.config.get('php-debug.exceptions.xdebug')) { 476 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Xdebug', stackDepth: -1}))) 477 | } 478 | if (atom.config.get('php-debug.exceptions.unknownError')) { 479 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Unknown error', stackDepth: -1}))) 480 | } 481 | if (atom.config.get('php-debug.exceptions.notice')) { 482 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: 'Notice', stackDepth: -1}))) 483 | } 484 | if (atom.config.get('php-debug.exceptions.all')) { 485 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: '*', stackDepth: -1}))) 486 | } 487 | 488 | for (exception in atom.config.get('php-debug.exceptions.customExceptions')) { 489 | commands.push(this.executeBreakpoint(service.createBreakpoint(null, null, {type: "exception", exception: exception, stackDepth: -1}))) 490 | } 491 | } 492 | 493 | return Promise.all(commands) 494 | } 495 | 496 | sendAllWatchpoints () { 497 | let commands = [] 498 | if (this._services.hasService("Watchpoints")) { 499 | let service = this._services.getWatchpointsService() 500 | let watchpoints = service.getWatchpoints() 501 | 502 | for (watchpoint of watchpoints) { 503 | commands.push(this.executeWatchpoint(watchpoint)) 504 | } 505 | } 506 | 507 | return Promise.all(commands) 508 | } 509 | 510 | executeWatchpoint (watchpoint) { 511 | let commandOptions = null 512 | let commandData = null 513 | commandOptions = { 514 | t: 'watch', 515 | } 516 | commandData = watchpoint.getExpression() 517 | let p = this.command("breakpoint_set", commandOptions, commandData) 518 | return p.then( (data) => { 519 | this._breakpointMap[watchpoint.getId()] = data.response.$.id 520 | }); 521 | } 522 | 523 | executeBreakpoint (breakpoint) { 524 | let commandOptions = null 525 | let commandData = null 526 | switch (breakpoint.getSettingValue("type")) { 527 | case "exception": 528 | commandOptions = { 529 | t: 'exception', 530 | x: breakpoint.getSettingValue('exception') 531 | } 532 | break; 533 | case "line": 534 | default: 535 | let path = breakpoint.getPath() 536 | path = localPathToRemote(path, this._pathMaps) 537 | commandOptions = { 538 | t: 'line', 539 | f: encodeURI(path), 540 | n: breakpoint.getLine() 541 | } 542 | let conditional = "" 543 | let idx = 0 544 | for (let setting of breakpoint.getSettingValues("condition")) { 545 | if (idx++ > 1) { 546 | conditional += " && " 547 | } 548 | conditional += "(" + setting.value + ")" 549 | } 550 | if (conditional != "") { 551 | commandData = conditional 552 | } 553 | break; 554 | 555 | } 556 | let p = this.command("breakpoint_set", commandOptions, commandData) 557 | return p.then( (data) => { 558 | this._breakpointMap[breakpoint.getId()] = data.response.$.id 559 | // attempt to source a single line from the corresponding file where the breakpoint was made 560 | // if we're not successful then the user has probably screwed up their config and/or PathMaps 561 | if (breakpoint.getSettingValue("type") !== "exception") { 562 | let path = breakpoint.getPath() 563 | path = localPathToRemote(path, this._pathMaps) 564 | let options = { 565 | f : encodeURI(path), 566 | //beginnng line 567 | b : 1, 568 | //end line 569 | e : 1 570 | } 571 | // command documentation available at https://xdebug.org/docs-dbgp.php 572 | return this.command("source", options, null).then((data) => { 573 | if (data.response.hasOwnProperty("error")) { 574 | for (let error of data.response.error) { 575 | //handle other codes as appropriate, for now we have a generic handler 576 | if (error.$.code == "100") { 577 | atom.notifications.addError(`Breakpoints were set but the corresponding server side file ${path} couldn't be opened.` 578 | + ` Did you properly configure your PathMaps? Server message: ${error.message}, Code: ${error.$.code}`) 579 | } else { 580 | atom.notifications.addError(`A server side error occured. Please report this to https://github.com/gwomacks/php-debug. Server message: #{error.message}, Code: #{error.$.code}`) 581 | } 582 | } 583 | } 584 | }); 585 | } 586 | }); 587 | } 588 | 589 | executeBreakpointRemove (breakpoint) { 590 | options = { 591 | d: this._breakpointMap[breakpoint.getId()] 592 | } 593 | return this.command("breakpoint_remove", options) 594 | } 595 | 596 | executeWatchpointRemove (watchpoint) { 597 | options = { 598 | d: this._breakpointMap[watchpoint.getId()] 599 | } 600 | return this.command("breakpoint_remove", options) 601 | } 602 | 603 | continueExecution (type) { 604 | this._emitter.emit('php-debug.engine.internal.running', {context:this.getContext()}) 605 | return this.command(type).then( 606 | (data) => { 607 | let response = data.response 608 | switch (response.$.status) { 609 | case 'break': 610 | let messages = response["xdebug:message"] 611 | let message = messages[0] 612 | let messageData = message.$ 613 | //console.dir data 614 | let filepath = remotePathToLocal(decodeURI(messageData['filename']), this._pathMaps) 615 | 616 | let lineno = messageData['lineno'] 617 | var type = 'break' 618 | var exceptionType = null 619 | if (messageData.exception) { 620 | if (message._) { 621 | if (this._services.hasService("Console")) { 622 | this._services.getConsoleService().addMessage(this.getContext(), messageData.exception + ": " + message._) 623 | } 624 | } 625 | type = "error"; 626 | exceptionType = messageData.exception; 627 | } 628 | if (this._services.hasService("Breakpoints")) { 629 | const breakpoint = this._services.getBreakpointsService().createBreakpoint(filepath,lineno,{type:type,exceptionType:exceptionType}) 630 | this._emitter.emit('php-debug.engine.internal.break', {context:this.getContext(),breakpoint:breakpoint}) 631 | } 632 | this.syncCurrentContext(-1) 633 | break; 634 | case 'stopping': 635 | this._emitter.emit('php-debug.engine.internal.sessionEnding', {context:this.getContext()}) 636 | this.executeStop() 637 | break; 638 | default: 639 | console.dir(response) 640 | console.error("Unhandled status: " + response.$.status) 641 | } 642 | }).catch((err) => { 643 | if (this._services != undefined && this._services != null && this._services.hasService("Logger")) { 644 | this._services.getLoggerService().error(err) 645 | } else { 646 | console.error(err) 647 | } 648 | }); 649 | } 650 | 651 | syncCurrentContext (depth) { 652 | let p2 = this.getContextNames(depth).then( 653 | (data) => { 654 | return this.processContextNames(depth,data) 655 | }) 656 | 657 | let p3 = p2.then( 658 | (data) => { 659 | return this.updateWatches(data) 660 | }) 661 | 662 | let p4 = p3.then ( 663 | (data) => { 664 | return this.syncStack(depth) 665 | }) 666 | 667 | let p5 = p4.then( 668 | (data) => { 669 | if (this._services.hasService("Stack")) { 670 | this._services.getStackService().setStack(this.getContext(), data) 671 | } 672 | return 673 | }) 674 | 675 | return p5.done() 676 | } 677 | 678 | getContextNames (depth) { 679 | let options = {} 680 | if (depth >= 0) { 681 | options.d = depth 682 | } 683 | return this.command("context_names", options) 684 | } 685 | 686 | processContextNames (depth, data) { 687 | let commands = [] 688 | for (let context of data.response.context) { 689 | if (this._services.hasService("Scope")) { 690 | const scopeService = this._services.getScopeService() 691 | scopeService.registerScope(this.getContext(), context.$.id, context.$.name) 692 | commands.push(this.updateContext(depth, context.$.id)) 693 | } 694 | } 695 | return Promise.all(commands) 696 | } 697 | 698 | executeDetach () { 699 | this.command('detach').then((data) => { 700 | this.stop(); 701 | }); 702 | this.stop(); 703 | } 704 | 705 | executeStopDetach () { 706 | this.command('status').then((data) => { 707 | if (data.response.$.status == 'break') { 708 | if (this._services.hasService("Breakpoints")) { 709 | let breakpoints = this._services.getBreakpointsService().getBreakpoints() 710 | for (let breakpoint of breakpoints) { 711 | this.executeBreakpointRemove(breakpoint) 712 | } 713 | } 714 | this.command('run').then((data) => { 715 | this.command('detach').then((data) => { 716 | this.executeStop() 717 | }); 718 | }); 719 | } else if (data.response.$.status == 'stopped') { 720 | this.executeStop() 721 | } else { 722 | this.command('detach').then((data) => { 723 | this.executeStop() 724 | }); 725 | } 726 | }); 727 | } 728 | 729 | updateWatches (data) { 730 | let commands = [] 731 | if (this._services.hasService("Watches")) { 732 | for (watch of this._services.getWatchesService().getWatches()) { 733 | commands.push(this.evalWatch(watch)) 734 | } 735 | } 736 | return Promise.all(commands) 737 | } 738 | 739 | 740 | evalExpression (expression) { 741 | let p = this.command("eval", null, expression) 742 | return p.then((data) => { 743 | let datum = null 744 | if (data.response.error) { 745 | datum = { 746 | name : "Error", 747 | fullname : "Error", 748 | type: "error", 749 | value: data.response.error[0].message[0], 750 | label: "" 751 | } 752 | } else { 753 | datum = this.parseVariableExpression(data.response.property[0]) 754 | } 755 | if (typeof datum == "object" && datum.type == "error") { 756 | datum = datum.name + ": " + datum.value 757 | } 758 | //else 759 | // datum = datum.replace(/\\"/mg, "\"").replace(/\\'/mg, "'").replace(/\\n/mg, "\n"); 760 | return datum; 761 | //this.globalContext.notifyConsoleMessage(datum) 762 | }); 763 | } 764 | 765 | evalWatch(watch) { 766 | let p = this.command("eval", null, watch.getExpression()) 767 | return p.then((data) => { 768 | let datum = null 769 | if (data.response.error) { 770 | datum = { 771 | name : "Error", 772 | fullname : "Error", 773 | type: "error", 774 | value: data.response.error[0].message[0], 775 | label: "" 776 | } 777 | } else { 778 | datum = this.parseContextVariable(data.response.property[0]) 779 | } 780 | datum.label = watch.getExpression() 781 | watch.setValue(this.getContext(), datum) 782 | }); 783 | } 784 | 785 | updateContext (depth, scopeId) { 786 | let p = this.contextGet(depth, scopeId) 787 | return p.then( (data) => { 788 | let context = this.buildContext(data) 789 | if (this._services.hasService("Scope")) { 790 | this._services.getScopeService().setData(this.getContext(), scopeId, context) 791 | } 792 | }); 793 | } 794 | 795 | contextGet (depth, scopeId) { 796 | let options = { c : scopeId } 797 | if (depth >= 0) { 798 | options.d = depth 799 | } 800 | return this.command("context_get", options) 801 | } 802 | 803 | buildContext (response) { 804 | let data = {} 805 | data.type = 'context' 806 | data.context = response.response.$.context 807 | data.variables = [] 808 | if (response.response.property) { 809 | for (let property of response.response.property) { 810 | let v = this.parseContextVariable(property) 811 | data.variables.push(v) 812 | } 813 | } 814 | return data 815 | } 816 | 817 | executeRun () { 818 | return this.continueExecution("run") 819 | } 820 | 821 | executeStop () { 822 | if (this._socket != undefined && this._socket != null) { 823 | try { 824 | this.command("stop") 825 | } catch(err) { 826 | throw err 827 | } finally { 828 | this.stop() 829 | } 830 | } 831 | } 832 | 833 | parseVariableExpression (variable) { 834 | let result = "" 835 | if (variable.$.fullname) { 836 | result = "\"" + variable.$.fullname + "\" => " 837 | } else if (variable.$.name) { 838 | result = "\"" + variable.$.name + "\" => " 839 | } 840 | 841 | switch (variable.$.type) { 842 | case "string": 843 | switch (variable.$.encoding) { 844 | case "base64": 845 | if (!variable._) { 846 | return result + '(string)""' 847 | } else { 848 | return result + '(string)"' + new Buffer(variable._, 'base64').toString('utf8') + '"' 849 | } 850 | break; 851 | default: 852 | console.error("Unhandled context variable encoding: " + variable.$.encoding) 853 | } 854 | break; 855 | case "array": 856 | { 857 | let values = "" 858 | if (variable.property) { 859 | for (let property of variable.property) { 860 | values += this.parseVariableExpression(property) + ",\n" 861 | } 862 | values = values.substring(0,values.length-2) 863 | } 864 | return result + "(array)[" + values + "] size("+variable.$.numchildren+")" 865 | } 866 | break; 867 | case "object": 868 | { 869 | let values = "" 870 | let className = "stdClass" 871 | if (variable.$.classname) { 872 | className = variable.$.classname 873 | } 874 | if (variable.property) { 875 | for (let property of variable.property) { 876 | values += this.parseVariableExpression(property) + ",\n" 877 | } 878 | values = values.substring(0,values.length-2) 879 | } 880 | return result + "(object["+className+"])" + values 881 | } 882 | break; 883 | case "resource": 884 | return result + "(resource)" + variable._ 885 | break; 886 | case "int": 887 | return result + "(numeric)" + variable._ 888 | break; 889 | case "error": 890 | return result + "" 891 | break; 892 | case "uninitialized": 893 | return result + "(undefined)null" 894 | case "null": 895 | return result + "(null)null" 896 | case "bool": 897 | return result + "(bool)" + variable._ 898 | case "float": 899 | return result + "(numeric)" + variable._ 900 | default: 901 | console.dir(variable) 902 | console.error("Unhandled context variable type: " + variable.$.type) 903 | } 904 | return datum 905 | } 906 | 907 | parseContextVariable (variable) { 908 | let datum = { 909 | name : variable.$.name, 910 | fullname : variable.$.fullname, 911 | type: variable.$.type 912 | } 913 | 914 | if (variable.$.name) { 915 | datum.label = variable.$.name 916 | } else if (variable.$.fullname) { 917 | datum.label = variable.$.fullname 918 | } 919 | 920 | switch (variable.$.type) { 921 | case "string": 922 | switch (variable.$.encoding) { 923 | case "base64": 924 | if (!variable._) { 925 | datum.value = "" 926 | } else { 927 | datum.value = new Buffer(variable._, 'base64').toString('utf8') 928 | } 929 | break; 930 | default: 931 | console.error("Unhandled context variable encoding: " + variable.$.encoding) 932 | } 933 | break; 934 | case "array": 935 | datum.value = [] 936 | datum.length = variable.$.numchildren 937 | if (variable.property) { 938 | for (let property of variable.property) { 939 | datum.value.push(this.parseContextVariable(property)) 940 | } 941 | } 942 | break; 943 | case "object": 944 | datum.value = [] 945 | if (variable.$.classname) { 946 | datum.className = variable.$.classname 947 | } 948 | if (variable.property) { 949 | for (let property of variable.property) { 950 | datum.value.push(this.parseContextVariable(property)) 951 | } 952 | } 953 | break; 954 | case "resource": 955 | datum.type = "resource" 956 | datum.value = variable._ 957 | break; 958 | case "int": 959 | datum.type = "numeric" 960 | datum.value = variable._ 961 | break; 962 | case "error": 963 | datum.value = "" 964 | break; 965 | case "uninitialized": 966 | datum.value = undefined 967 | break; 968 | case "null": 969 | datum.value = null 970 | break; 971 | case "bool": 972 | datum.value = variable._ 973 | break; 974 | case "float": 975 | datum.type = "numeric" 976 | datum.value = variable._ 977 | break; 978 | default: 979 | console.dir(variable) 980 | console.error("Unhandled context variable type: " + variable.$.type) 981 | } 982 | return datum 983 | } 984 | } 985 | -------------------------------------------------------------------------------- /lib/engines/dbgp/debugging-context.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import autoBind from 'auto-bind-inheritance' 4 | import DbgpInstance from './dbgp-instance' 5 | import {Emitter, Disposable} from 'event-kit' 6 | import {CompositeDisposable} from 'atom' 7 | import path from 'path' 8 | import Promise from 'promise' 9 | import PathMapsView from '../../pathmaps/pathmaps-view' 10 | import helpers from '../../helpers' 11 | 12 | export default class DebbuggingContext { 13 | constructor (services, identifier, uiService) { 14 | autoBind(this); 15 | this._emitter = new Emitter() 16 | this._services = services 17 | this._identifier = identifier 18 | this._uiService = uiService 19 | this._socket = null 20 | this._instance = null 21 | this._redirectingStdout = atom.config.get('php-debug.server.redirectStdout'); 22 | this._redirectingStderr = atom.config.get('php-debug.server.redirectStderr'); 23 | this._subscriptions = new CompositeDisposable() 24 | this._persistentSubscriptions = new CompositeDisposable() 25 | this.handleUIEvents(); 26 | } 27 | 28 | getIdentifier() { 29 | return this._identifier 30 | } 31 | 32 | getUIService() { 33 | return this._uiService 34 | } 35 | 36 | setUIService(service) { 37 | this._uiService = service 38 | this.handleUIEvents(); 39 | if (this.isValidForContext(this._identifier)) { 40 | if (!this._instance.isActive()) { 41 | if (this._services.hasService("Stack")) { 42 | this._services.getStackService().registerStack(this._identifier) 43 | } 44 | if (this._services.hasService("Console")) { 45 | this._services.getConsoleService().addMessage(this._identifier,"Session Initialized: " + this._identifier) 46 | } 47 | this._instance.activate() 48 | } 49 | } 50 | } 51 | 52 | activate(socket) { 53 | if (socket == undefined || socket == null) return 54 | if (this._socket != undefined && this._socket != null) { 55 | throw new Error("Cannot assign socket to an already bound context") 56 | } 57 | this._socket = socket 58 | if (this._subscriptions == undefined || this._subscriptions == null) { 59 | this._subscriptions = new CompositeDisposable() 60 | } 61 | this.bindEvents() 62 | 63 | this._subscriptions.add(this.onHandshakeStarted((event) => { 64 | if (this._services.hasService("Actions")) { 65 | this._services.getActionsService().registerButton("pathmaps", this._identifier, "debug", "Path Maps",["btn","mdi","mdi-map","inline-block-tight", "btn-no-deactive","pathmaps-btn"], (clickEvent) => { 66 | this.handlePathMapsClick(clickEvent.context, event.fileuri); 67 | }) 68 | this._services.getActionsService().registerButton("redirect_stdout", this._identifier, "console", "Stdout",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stdout-btn", this._redirectingStdout ? 'btn-active':''], (clickEvent) => { 69 | if (this.isValidForContext(this._identifier)) { 70 | this._redirectingStdout = !this._redirectingStdout 71 | if (this._redirectingStdout) { 72 | this._services.getActionsService().updateButton("redirect_stdout", this._identifier, "console", "Stdout",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stdout-btn","btn-active"]); 73 | this._services.getLoggerService().info("Redirecting Stdout") 74 | this._instance.setStdout(1) 75 | } else { 76 | this._services.getActionsService().updateButton("redirect_stdout", this._identifier, "console", "Stdout",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stdout-btn"]); 77 | this._services.getLoggerService().info("No longer redirecting Stdout") 78 | this._instance.setStdout(0) 79 | } 80 | } 81 | }) 82 | this._services.getActionsService().registerButton("redirect_stderr", this._identifier, "console", "Stderr",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stderr-btn", this._redirectingStdout ? 'btn-active':''], (clickEvent) => { 83 | if (this.isValidForContext(this._identifier)) { 84 | this._redirectingStderr = !this._redirectingStderr 85 | if (this._redirectingStderr) { 86 | this._services.getActionsService().registerButton("redirect_stderr", this._identifier, "console", "Stderr",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stderr-btn","btn-active"]); 87 | this._services.getLoggerService().info("Redirecting Stderr") 88 | this._instance.setStderr(1) 89 | } else { 90 | this._services.getActionsService().registerButton("redirect_stderr", this._identifier, "console", "Stderr",["btn","mdi","mdi-shuffle-variant","inline-block-tight", "btn-no-deactive","redirect-stderr-btn"]); 91 | this._services.getLoggerService().info("No longer redirecting Stderr") 92 | this._instance.setStderr(0) 93 | } 94 | } 95 | }) 96 | } 97 | var currentMaps = atom.config.get('php-debug.xdebug.pathMaps') 98 | if (currentMaps == undefined || currentMaps == null || currentMaps == "") { 99 | currentMaps = [] 100 | } else { 101 | try { 102 | currentMaps = JSON.parse(currentMaps) 103 | } catch (err) { 104 | currentMaps = []; 105 | this._services.getLoggerService().info("Couldn't parse pathmaps"); 106 | } 107 | if (typeof currentMaps !== "object") { 108 | currentMaps = [] 109 | } 110 | } 111 | let requestListings = []; 112 | const rankedListing = helpers.generatePathMaps(event.fileuri, currentMaps) 113 | if (rankedListing.type !== undefined && rankedListing.type != null && rankedListing.type != "list") { 114 | if (rankedListing.results.localPath == "!") { 115 | event.preventDefault(null); 116 | if (this._instance != undefined && this._instance != null) { 117 | this._instance.executeDetach() 118 | } 119 | return; 120 | } 121 | this._instance.setPathMap(rankedListing.results) 122 | 123 | this.checkBreakpointPathMaps(requestListings, currentMaps); 124 | if (requestListings == 0) { 125 | return 126 | } 127 | } else { 128 | requestListings.push({path:event.fileuri, rankedListing: rankedListing}); 129 | this.checkBreakpointPathMaps(requestListings, currentMaps); 130 | } 131 | 132 | const serialize = (funcs) => { 133 | return funcs.reduce((promise,func) => 134 | promise.then((result) => func().then(Array.prototype.concat.bind(result))), 135 | Promise.resolve([])) 136 | }; 137 | 138 | const funcs = requestListings.map(rl => () => new Promise((fulfill,reject) => { 139 | if (helpers.hasLocalPathMap(rl.path,this._instance.getPathMaps())) { 140 | fulfill(); 141 | return; 142 | } 143 | this._pathMapsView = this.createPathmapsView(currentMaps, rl.rankedListing, fulfill, rl.path, true); 144 | this._pathMapsView.attach() 145 | }) 146 | ); 147 | 148 | var satisfyPromise = null 149 | var p = new Promise( (fulfill,reject) => { 150 | satisfyPromise = fulfill 151 | }) 152 | event.preventDefault(p); 153 | serialize(funcs).then( () => { 154 | satisfyPromise(); 155 | }); 156 | })) 157 | 158 | this._subscriptions.add(this.onRequestPathMap((event) => { 159 | var currentMaps = atom.config.get('php-debug.xdebug.pathMaps') 160 | if (currentMaps == undefined || currentMaps == null || currentMaps == "") { 161 | currentMaps = [] 162 | } else { 163 | try { 164 | currentMaps = JSON.parse(currentMaps) 165 | } catch (err) { 166 | currentMaps = [] 167 | this._services.getLoggerService().info("Couldn't parse pathmaps") 168 | } 169 | if (typeof currentMaps !== "object") { 170 | currentMaps = [] 171 | } 172 | } 173 | let searchPath = "" 174 | let flag = false; 175 | if (event.hasOwnProperty("remotePath")) { 176 | searchPath = event.remotePath; 177 | } else { 178 | searchPath = event.localPath; 179 | flag = true; 180 | } 181 | const rankedListing = helpers.generatePathMaps(searchPath, currentMaps, null, flag) 182 | if (rankedListing.type !== undefined && rankedListing.type != null && rankedListing.type != "list") { 183 | if (rankedListing.results.localPath == "!") { 184 | event.preventDefault(null); 185 | if (this._instance != undefined && this._instance != null) { 186 | this._instance.executeDetach() 187 | } 188 | return; 189 | } 190 | this._instance.setPathMap(rankedListing.results) 191 | return; 192 | } 193 | 194 | 195 | var satisfyPromise = null 196 | var p = new Promise( (fulfill,reject) => { 197 | satisfyPromise = fulfill 198 | }) 199 | event.preventDefault(p); 200 | this._pathMapsView = this.createPathmapsView(currentMaps, rankedListing, satisfyPromise, searchPath, true); 201 | this._pathMapsView.attach() 202 | })) 203 | 204 | 205 | this._subscriptions.add(this.onHandshakeComplete((event) => { 206 | this._emitter.emit('php-debug.engine.internal.sessionStart',event) 207 | if (this._uiService == undefined || this._uiService == null) { 208 | this._identifier = event.appid 209 | if (this.isValidForContext(this._identifier)) { 210 | this._instance.updateContextIdentifier(event.appid) 211 | } 212 | } else { 213 | if (this.isValidForContext(this._identifier)) { 214 | if (this._services.hasService("Stack")) { 215 | this._services.getStackService().registerStack(this._identifier) 216 | } 217 | this._instance.activate() 218 | if (this._services.hasService("Console")) { 219 | this._services.getConsoleService().addMessage(this._identifier,"Session Initialized: " + this._identifier) 220 | } 221 | } 222 | } 223 | })) 224 | this._instance = new DbgpInstance({socket:socket, emitter:this._emitter, services:this._services, context:this._identifier}) 225 | } 226 | 227 | checkBreakpointPathMaps(requestListings, currentMaps) { 228 | if (this._services.hasService("Breakpoints")) { 229 | let service = this._services.getBreakpointsService() 230 | let breakpoints = service.getBreakpoints() 231 | 232 | for (breakpoint of breakpoints) { 233 | let path = breakpoint.getPath() 234 | if (!helpers.hasLocalPathMap(path,this._instance.getPathMaps())) { 235 | const bprankedListing = helpers.generatePathMaps(path, currentMaps, null, true) 236 | if (bprankedListing.type !== undefined && bprankedListing.type != null && bprankedListing.type != "list") { 237 | this._instance.setPathMap(bprankedListing.results) 238 | } else { 239 | let add = true; 240 | for (let rl of requestListings) { 241 | if (rl.path == path) { 242 | add = false; 243 | break; 244 | } 245 | } 246 | if (add) { 247 | requestListings.push({path:path, rankedListing: bprankedListing}) 248 | } 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | handlePathMapsClick(context, fileuri) { 256 | if (this._pathMapsView != undefined || this._pathMapsView != null) { 257 | return; 258 | } 259 | if (this._identifier != context) { 260 | return 261 | } 262 | var currentMaps = atom.config.get('php-debug.xdebug.pathMaps') 263 | if (currentMaps == undefined || currentMaps == null || currentMaps == "") { 264 | currentMaps = [] 265 | } else { 266 | try { 267 | currentMaps = JSON.parse(currentMaps) 268 | } catch (err) { 269 | currentMaps = [] 270 | this._services.getLoggerService().info("Couldn't parse pathmaps") 271 | } 272 | if (typeof currentMaps !== "object") { 273 | currentMaps = [] 274 | } 275 | } 276 | const rankedListing = helpers.generatePathMaps(fileuri, currentMaps, true) 277 | this._pathMapsView = this.createPathmapsView(currentMaps, rankedListing, null, null, false) 278 | this._pathMapsView.attach() 279 | } 280 | 281 | createPathmapsView(currentMaps, rankedListing, completionPromise, uri, showDetach) { 282 | var pathOptions = []; 283 | if (rankedListing.hasOwnProperty("list")) { 284 | pathOptions = rankedListing["list"].results; 285 | } else if (rankedListing.hasOwnProperty('type') && rankedListing.type == "list") { 286 | pathOptions = rankedListing.results; 287 | } 288 | var defaultValue = null; 289 | if (rankedListing.hasOwnProperty("existing")) { 290 | defaultValue = rankedListing["existing"].results 291 | } else if (rankedListing.hasOwnProperty("direct")) { 292 | defaultValue = rankedListing["direct"].results 293 | } 294 | var options = { 295 | onCancel: () => { 296 | if (this._instance != undefined && this._instance != null) { 297 | this._instance.executeDetach() 298 | } 299 | }, 300 | onIgnore: (mapping, previous) => { 301 | atom.config.set('php-debug.xdebug.pathMaps',JSON.stringify(currentMaps)) 302 | if (this._instance != undefined && this._instance != null) { 303 | mapping.localPath = "!" 304 | this.updatePathmapsConfig(mapping, previous, currentMaps) 305 | this._instance.executeDetach() 306 | } 307 | }, 308 | showDetach: showDetach, 309 | pathOptions:pathOptions, 310 | default: defaultValue, 311 | uri: uri, 312 | onSave: (mapping, previous) => { 313 | this.updatePathmapsConfig(mapping, previous, currentMaps) 314 | if (this._instance != undefined && this._instance != null) { 315 | this._instance.setPathMap(mapping) 316 | } 317 | if (completionPromise != undefined && completionPromise != null) { 318 | completionPromise() 319 | } 320 | this._pathMapsView.destroy() 321 | delete this._pathMapsView 322 | } 323 | } 324 | return new PathMapsView(options) 325 | } 326 | 327 | updatePathmapsConfig(mapping, previous, currentMaps) { 328 | var replaced = false; 329 | if (previous != undefined && previous != null) { 330 | if (mapping.remotePath != "" || mapping.localPath != "") { 331 | for (let mapItem in currentMaps) { 332 | if (currentMaps[mapItem] == previous) { 333 | currentMaps[mapItem] = mapping 334 | replaced = true; 335 | break; 336 | } 337 | } 338 | } 339 | } 340 | if (!replaced) { 341 | if (mapping.remotePath != "" || mapping.localPath != "") { 342 | currentMaps.push(mapping) 343 | } 344 | } 345 | atom.config.set('php-debug.xdebug.pathMaps',JSON.stringify(currentMaps)) 346 | } 347 | 348 | bindEvents() { 349 | if (this._subscriptions == undefined || this._subscriptions == null) { 350 | this._subscriptions = new CompositeDisposable() 351 | } 352 | this._subscriptions.add(this.onSessionEnd( (event) => { 353 | if (this._services != undefined && this._services != null) { 354 | this._services.getLoggerService().info("Session Ended",event) 355 | if (this._services.hasService("Console")) { 356 | this._services.getConsoleService().addMessage(this._identifier,"Session Terminated: " + this._identifier) 357 | } 358 | } 359 | this.stop() 360 | })) 361 | this._subscriptions.add(this.onSessionStart( (event) => { 362 | if (this._services.hasService("Status")) { 363 | this._services.getStatusService().setStatus(this._identifier,"Session Started") 364 | } 365 | })) 366 | this._subscriptions.add(this.onBreak( (event) => { 367 | if (this._services.hasService("Console")) { 368 | if (event.breakpoint != undefined && event.breakpoint != null) { 369 | let exceptionType = event.breakpoint.getSettingValue("exceptionType"); 370 | if (exceptionType != null) { 371 | this._services.getConsoleService().addMessage(this._identifier,"Breakpoint hit: EXCEPTION " + exceptionType + " " + event.breakpoint.toString()) 372 | } else { 373 | this._services.getConsoleService().addMessage(this._identifier,"Breakpoint hit: " + event.breakpoint.toString()) 374 | } 375 | } 376 | } 377 | if (this._services.hasService("Status")) { 378 | if (event.breakpoint != undefined && event.breakpoint != null) { 379 | let exceptionType = event.breakpoint.getSettingValue("exceptionType"); 380 | if (exceptionType != null) { 381 | this._services.getStatusService().setStatus(this._identifier,"Session Active: BREAK - " + exceptionType) 382 | return; 383 | } 384 | } 385 | this._services.getStatusService().setStatus(this._identifier,"Session Active: BREAK") 386 | } 387 | })) 388 | this._subscriptions.add(this.onDebugDBGPMessage( (event) => { 389 | switch (event.type) { 390 | case "recieved": 391 | this._services.getLoggerService().debug("XDebug ->", event.context, event.message) 392 | break; 393 | case "raw-recieved": 394 | this._services.getLoggerService().debug("XDebug RAW -> ", event.context, event.message) 395 | break; 396 | case "sent": 397 | this._services.getLoggerService().debug("XDebug <-", event.context, event.message) 398 | break; 399 | default: 400 | this._services.getLoggerService().debug("XDebug <->", event.context, event.message) 401 | break; 402 | } 403 | })) 404 | if (this._services.hasService("Stack")) { 405 | this._subscriptions.add(this._services.getStackService().onFrameSelected((event) => { 406 | if (this._instance != undefined && this._instance != null) { 407 | if (this.isValidForContext(event.context)) { 408 | this._services.getLoggerService().debug("Changing Stack",event) 409 | this._instance.syncCurrentContext(event.codepoint.getStackDepth()); 410 | if (this._services.hasService("Breakpoints")) { 411 | let codepoint = this._services.getBreakpointsService().createCodepoint(helpers.remotePathToLocal(event.codepoint.getPath(), this._instance._pathMap), event.codepoint.getLine(), event.codepoint.getStackDepth()) 412 | this._services.getBreakpointsService().doCodePoint(event.context, codepoint); 413 | } 414 | } 415 | } 416 | })) 417 | } 418 | if (this._services.hasService("Console")) { 419 | this._subscriptions.add(this._services.getConsoleService().onExecuteExpression((event) => { 420 | if (this.isValidForContext(event.context)) { 421 | this._instance.evalExpression(event.expression).then((data) => { 422 | if (this._services.hasService("Console")) { 423 | this._services.getConsoleService().addMessage(this._identifier, data) 424 | } 425 | }); 426 | } 427 | })) 428 | } 429 | if (this._services.hasService("Breakpoints")) { 430 | let service = this._services.getBreakpointsService() 431 | this._subscriptions.add(service.onBreakpointAdded((event) => { 432 | if (this.isValidForContext(this._identifier)) { 433 | this._instance.executeBreakpoint(event.added) 434 | } 435 | })) 436 | this._subscriptions.add(service.onBreakpointChanged((event) => { 437 | if (this.isValidForContext(this._identifier)) { 438 | this._instance.executeBreakpointRemove(event.breakpoint) 439 | this._instance.executeBreakpoint(event.breakpoint) 440 | } 441 | })) 442 | this._subscriptions.add(service.onBreakpointRemoved((event) => { 443 | if (this.isValidForContext(this._identifier)) { 444 | this._instance.executeBreakpointRemove(event.removed) 445 | } 446 | })) 447 | this._subscriptions.add(service.onBreakpointsCleared((event) => { 448 | if (this.isValidForContext(this._identifier)) { 449 | for (let breakpoint of event.removed) { 450 | this._instance.executeBreakpointRemove(breakpoint) 451 | } 452 | } 453 | })) 454 | } 455 | if (this._services.hasService("Watchpoints")) { 456 | let service = this._services.getWatchpointsService() 457 | this._subscriptions.add(service.onWatchpointAdded((event) => { 458 | if (this.isValidForContext(this._identifier)) { 459 | this._instance.executeWatchpoint(event.added) 460 | } 461 | })) 462 | this._subscriptions.add(service.onWatchpointRemoved((event) => { 463 | if (this.isValidForContext(this._identifier)) { 464 | this._instance.executeWatchpointRemove(event.removed) 465 | } 466 | })) 467 | this._subscriptions.add(service.onWatchpointsCleared((event) => { 468 | if (this.isValidForContext(this._identifier)) { 469 | for (let watchpoint of event.removed) { 470 | this._instance.executeWatchpointRemove(watchpoint) 471 | } 472 | } 473 | })) 474 | } 475 | if (this._services.hasService("Actions")) { 476 | let service = this._services.getActionsService() 477 | this._subscriptions.add(service.onContinue((event) => { 478 | if (this.isValidForContext(event.context)) { 479 | this._instance.continueExecution('run') 480 | } 481 | })) 482 | this._subscriptions.add(service.onStepOver((event) => { 483 | if (this.isValidForContext(event.context)) { 484 | this._instance.continueExecution('step_over') 485 | } 486 | })) 487 | this._subscriptions.add(service.onDetach((event) => { 488 | if (this.isValidForContext(event.context)) { 489 | this._instance.executeDetach() 490 | } 491 | })) 492 | this._subscriptions.add(service.onStepInto((event) => { 493 | if (this.isValidForContext(event.context)) { 494 | this._instance.continueExecution('step_into') 495 | } 496 | })) 497 | this._subscriptions.add(service.onStepOut((event) => { 498 | if (this.isValidForContext(event.context)) { 499 | this._instance.continueExecution('step_out') 500 | } 501 | })) 502 | this._subscriptions.add(service.onStop((event) => { 503 | if (this.isValidForContext(event.context)) { 504 | this._instance.executeStopDetach() 505 | } 506 | })) 507 | } 508 | } 509 | 510 | handleUIEvents() { 511 | if (this.hasUIService()) { 512 | this._persistentSubscriptions.add(this._uiService.onDebuggerDeactivated((e) => { 513 | if (e.close) { 514 | if (!this._uiService.hasPanels()) { 515 | this._uiService.destroy() 516 | this.destroy() 517 | } 518 | } 519 | })); 520 | this._persistentSubscriptions.add(this._uiService.onConsoleDeactivated((e) => { 521 | if (e.close) { 522 | if (!this._uiService.hasPanels()) { 523 | this._uiService.destroy() 524 | this.destroy() 525 | } 526 | } 527 | })); 528 | this._persistentSubscriptions.add(this._uiService.onDestroyed((e) => { 529 | this.destroy() 530 | })); 531 | } 532 | } 533 | 534 | executeRun() { 535 | if (this.isValid()) { 536 | return this._instance.continueExecution('run') 537 | } 538 | } 539 | 540 | evalExpression(expression) { 541 | if (this.isValid()) { 542 | return this._instance.evalExpression(expression); 543 | } 544 | } 545 | 546 | executeStepInto() { 547 | if (this.isValid()) { 548 | return this._instance.continueExecution('step_into') 549 | } 550 | } 551 | 552 | executeStepOver() { 553 | if (this.isValid()) { 554 | return this._instance.continueExecution('step_over') 555 | } 556 | } 557 | 558 | executeStepOut() { 559 | if (this.isValid()) { 560 | return this._instance.continueExecution('step_out') 561 | } 562 | } 563 | 564 | isValid() { 565 | return (this._instance != undefined && this._instance != null && this.isActive()) 566 | } 567 | 568 | isValidForContext(context) { 569 | return (context == this._identifier && this._instance != undefined && this._instance != null && this.isActive()) 570 | } 571 | 572 | hasUIService() { 573 | return this._uiService != undefined && this._uiService != null 574 | } 575 | 576 | isActive() { 577 | return this._socket != undefined && this._socket != null 578 | } 579 | 580 | getSocket() { 581 | return this._socket 582 | } 583 | 584 | onSessionStart(callback) { 585 | return this._emitter.on('php-debug.engine.internal.sessionStart', callback) 586 | } 587 | 588 | onDebugDBGPMessage(callback) { 589 | return this._emitter.on('php-debug.engine.internal.debugDBGPMessage', callback) 590 | } 591 | 592 | onHandshakeStarted(callback) { 593 | return this._emitter.on('php-debug.engine.internal.handshakeStarted', callback) 594 | } 595 | 596 | onRequestPathMap(callback) { 597 | return this._emitter.on('php-debug.engine.internal.requestPathMap', callback) 598 | } 599 | 600 | onHandshakeComplete(callback) { 601 | return this._emitter.on('php-debug.engine.internal.handshakeComplete', callback) 602 | } 603 | 604 | onReceivedDBGPMessage(callback) { 605 | return this._emitter.on('php-debug.engine.internal.receivedDBGPMessage', callback) 606 | } 607 | 608 | onRunning(callback) { 609 | return this._emitter.on('php-debug.engine.internal.running', callback) 610 | } 611 | 612 | onBreak(callback) { 613 | return this._emitter.on('php-debug.engine.internal.break', callback) 614 | } 615 | 616 | onSessionEnd(callback) { 617 | return this._emitter.on('php-debug.engine.internal.sessionEnd', callback) 618 | } 619 | 620 | onDestroyed(callback) { 621 | return this._emitter.on('php-debug.engine.internal.destroyed', callback) 622 | } 623 | 624 | stop() { 625 | if (this.isValidForContext(this._identifier)) { 626 | if (this._services.hasService("Status")) { 627 | this._services.getStatusService().setStatus(this._identifier,"Session Ended, Listening for new sessions") 628 | } 629 | if (this._services.hasService("Scope")) { 630 | this._services.getScopeService().clearScopes(this._identifier) 631 | } 632 | if (this._services.hasService("Stack")) { 633 | this._services.getStackService().unregisterStack(this._identifier) 634 | } 635 | if (this._instance != undefined && this._instance != null) { 636 | this._instance.destroy() 637 | delete this._instance 638 | } 639 | if (this._socket != undefined && this._socket != null) { 640 | delete this._socket 641 | } 642 | if (this._subscriptions != undefined && this._subscriptions != null) { 643 | this._subscriptions.dispose() 644 | delete this._subscriptions; 645 | } 646 | } 647 | } 648 | 649 | destroy() { 650 | this.stop() 651 | delete this._uiService 652 | delete this._services 653 | if (this._emitter != undefined && this._emitter != null) { 654 | this._emitter.emit('php-debug.engine.internal.destroyed', {context:this._identifier}) 655 | } 656 | delete this._identifier 657 | delete this._subscriptions 658 | 659 | if (this._persistentSubscriptions != undefined && this._persistentSubscriptions != null) { 660 | this._persistentSubscriptions.dispose() 661 | delete this._persistentSubscriptions; 662 | } 663 | 664 | if (this._emitter != undefined && this._emitter != null) { 665 | if (typeof this._emitter.destroy === "function") { 666 | this._emitter.destroy() 667 | } 668 | this._emitter.dispose() 669 | } 670 | delete this._emitter 671 | 672 | } 673 | 674 | dispose() { 675 | this.destroy() 676 | } 677 | 678 | } 679 | -------------------------------------------------------------------------------- /lib/engines/dbgp/engine.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import helpers from '../../helpers' 4 | import DebugEngine from '../../models/debug-engine' 5 | import Server from './server' 6 | import DebuggingContext from './debugging-context' 7 | import {Emitter, Disposable} from 'event-kit' 8 | import autoBind from 'auto-bind-inheritance' 9 | import uuid from 'uuid' 10 | import {CompositeDisposable} from 'atom' 11 | 12 | export default class PHPDebugEngine extends DebugEngine { 13 | constructor () { 14 | super() 15 | autoBind(this); 16 | this._destroyed = false 17 | this._subscriptions = new CompositeDisposable() 18 | this._emitter = new Emitter() 19 | this.initializeServer() 20 | this._debugContexts = {} 21 | } 22 | 23 | initializeServer() { 24 | if (this._server == undefined || this._server == null) { 25 | this._server = new Server({serverAddress:atom.config.get('php-debug.server.serverAddress'), serverPort:atom.config.get('php-debug.server.serverPort')}) 26 | this._subscriptions.add(this._server.onServerListening(this.handleServerListening)) 27 | this._subscriptions.add(this._server.onServerError(this.handleServerError)) 28 | this._subscriptions.add(this._server.onlistenerClosed(this.handleListenerClosed)) 29 | this._subscriptions.add(this._server.onNewConnection(this.handleNewConnection)) 30 | this._subscriptions.add(this._server.onConnectionEnded(this.handleConnectionEnded)) 31 | this._subscriptions.add(this._server.onConnectionClosed(this.handleConnectionEnded)) 32 | this._subscriptions.add(this._server.onConnectionError(this.handleConnectionError)) 33 | } 34 | } 35 | 36 | setUIServices(services) { 37 | this._services = services 38 | } 39 | 40 | getUIServices() { 41 | return this._services 42 | } 43 | 44 | getName() { 45 | return "PHP Debug" 46 | } 47 | 48 | getContextForSocket(socket) { 49 | for (let context in this._debugContexts) { 50 | if (this._debugContexts[context].getSocket() == socket) { 51 | return this._debugContexts[context] 52 | } 53 | } 54 | return null 55 | } 56 | 57 | hasContext(context) { 58 | return this._debugContexts.hasOwnProperty(context) 59 | } 60 | 61 | hasUIServices() { 62 | return this._services != undefined && this._services != null; 63 | } 64 | 65 | tryGetContext() { 66 | // Maybe we'll get lucky 67 | let paneItem = atom.workspace.getActivePaneItem() 68 | if (paneItem != null && typeof paneItem.getURI === "function") { 69 | if (paneItem.getURI().indexOf("php-debug://debug-view") === 0) { 70 | let contextID = paneItem.getURI().substring(23) 71 | if (this.hasContext(contextID)) { 72 | return this._debugContexts[contextID] 73 | } 74 | } 75 | if (paneItem.getURI().indexOf("php-debug://console-view") === 0) { 76 | let contextID = paneItem.getURI().substring(25) 77 | if (this.hasContext(contextID)) { 78 | return this._debugContexts[contextID] 79 | } 80 | } 81 | } 82 | // Try each dock 83 | let docks = [atom.workspace.getBottomDock(),atom.workspace.getLeftDock(), atom.workspace.getRightDock()]; 84 | for (let dock of docks) { 85 | let paneItem = dock.getActivePaneItem() 86 | if (paneItem != null && typeof paneItem.getURI === "function") { 87 | if (paneItem.getURI().indexOf("php-debug://debug-view") === 0) { 88 | let contextID = paneItem.getURI().substring(23) 89 | if (this.hasContext(contextID)) { 90 | return this._debugContexts[contextID] 91 | } 92 | } 93 | if (paneItem.getURI().indexOf("php-debug://console-view") === 0) { 94 | let contextID = paneItem.getURI().substring(25) 95 | if (this.hasContext(contextID)) { 96 | return this._debugContexts[contextID] 97 | } 98 | } 99 | } 100 | } 101 | if (this.hasContext("default")) { 102 | return this._debugContexts["default"]; 103 | } 104 | return null; 105 | } 106 | 107 | onSessionStart(callback) { 108 | return this._emitter.on('php-debug.engine.sessionStart', callback) 109 | } 110 | 111 | onRunning(callback) { 112 | return this._emitter.on('php-debug.engine.running', callback) 113 | } 114 | 115 | onBreak(callback) { 116 | return this._emitter.on('php-debug.engine.break', callback) 117 | } 118 | 119 | onSessionEnd(callback) { 120 | return this._emitter.on('php-debug.engine.sessionEnd', callback) 121 | } 122 | 123 | assignSocketToContext(socket) { 124 | if (!this.hasUIServices()) { 125 | return 126 | } 127 | if (!this._debugContexts.hasOwnProperty("default")) { 128 | this.createDebuggingContext("default", socket) 129 | this._services.getLoggerService().debug("Assigned socket to default context") 130 | return 131 | } 132 | if (!this._debugContexts.default.isActive()) { 133 | this._debugContexts.default.activate(socket) 134 | this._services.getLoggerService().debug("Assigned socket to default context") 135 | return 136 | } else { 137 | // Create a new context with a temporary ID 138 | const identifier = uuid.v4() 139 | this._debugContexts[identifier] = new DebuggingContext(this.getUIServices(),identifier) 140 | const disposable = this._debugContexts[identifier].onSessionStart((event) => { 141 | // Update it with a DBGP context identifier 142 | this._debugContexts[event.appid] = this._debugContexts[identifier] 143 | delete this._debugContexts[identifier] 144 | this.createUIContext(event.appid).then( (context) => { 145 | context.getUIService().activateDebugger() 146 | }).catch( (err) => { 147 | console.log('Failed to property initialize active debugger') 148 | }); 149 | this._services.getLoggerService().debug("New context with identifier: " + event.appid + " from " + identifier) 150 | disposable.dispose() 151 | }) 152 | this.bindContextEvents(this._debugContexts[identifier]) 153 | this._debugContexts[identifier].activate(socket) 154 | this._services.getLoggerService().debug("Created new context with temporary identifier: " + identifier) 155 | } 156 | } 157 | 158 | createUIContext(identifier) { 159 | return new Promise ((fulfill,reject) => { 160 | if (!this.hasUIServices()) { 161 | reject("no ui services") 162 | return 163 | } 164 | 165 | this._services.getDebugViewService().createContext(identifier,{'allowAutoClose':true,'allowActionBar':false}).then((service) => { 166 | this._debugContexts[identifier].setUIService(service) 167 | service.activateDebugger() 168 | service.activateConsole() 169 | fulfill(this._debugContexts[identifier]) 170 | }).catch((err) => { 171 | this._services.getLoggerService().error(err) 172 | reject(err) 173 | }) 174 | }) 175 | } 176 | 177 | createDebuggingContext(identifier, socket) { 178 | return new Promise( (fulfill,reject) => { 179 | 180 | if (!this.hasUIServices()) { 181 | reject("no ui services") 182 | return 183 | } 184 | 185 | if (this._debugContexts.hasOwnProperty(identifier)) { 186 | if (this._debugContexts[identifier].hasUIService()) { 187 | fulfill(this._debugContexts[identifier]) 188 | return 189 | } 190 | } 191 | let options = {'allowAutoClose':true,'allowActionBar':false} 192 | if (identifier == "default") { 193 | options.allowActionBar = true 194 | options.allowAutoClose = false 195 | } 196 | this._services.getDebugViewService().createContext(identifier, options).then((service) => { 197 | this._debugContexts[identifier] = new DebuggingContext(this.getUIServices(), identifier, service) 198 | this.bindContextEvents(this._debugContexts[identifier]) 199 | if (socket != undefined && socket != null) { 200 | this._debugContexts[identifier].activate(socket) 201 | } 202 | if (!this._server.isListening()) { 203 | this._server.listen() 204 | } 205 | service.activateDebugger() 206 | service.activateConsole() 207 | fulfill(this._debugContexts[identifier]) 208 | }).catch((err) => { 209 | this._services.getLoggerService().error(err) 210 | reject(err) 211 | }) 212 | }) 213 | } 214 | 215 | bindContextEvents(context) { 216 | this._subscriptions.add(context.onRunning((event) => { 217 | this._emitter.emit('php-debug.engine.running',event) 218 | })) 219 | this._subscriptions.add(context.onBreak((event) => { 220 | this._emitter.emit('php-debug.engine.break',event) 221 | })) 222 | this._subscriptions.add(context.onSessionEnd((event) => { 223 | this._emitter.emit('php-debug.engine.sessionEnd',event) 224 | })) 225 | this._subscriptions.add(context.onSessionStart((event) => { 226 | this._emitter.emit('php-debug.engine.sessionStart',event) 227 | })) 228 | this._subscriptions.add(context.onDestroyed((event) => { 229 | if (this._debugContexts != undefined && this._debugContexts != null) { 230 | if (this._services != undefined && this._services != null) { 231 | this._services.getDebugViewService().removeContext(event.context); 232 | } 233 | delete this._debugContexts[event.context]; 234 | remainingContexts = Object.keys(this._debugContexts).length; 235 | if (remainingContexts == 0) { 236 | if (atom.config.get('php-debug.server.keepAlive') === true || atom.config.get('php-debug.server.keepAlive') === 1) { 237 | return; 238 | } 239 | if (this._server != undefined && this._server != null) { 240 | this._server.close() 241 | } 242 | } 243 | } 244 | })) 245 | } 246 | 247 | getGrammars() { 248 | return ["text.html.php"] 249 | } 250 | 251 | handleServerError(err) { 252 | if (this._services != undefined && this._services != null) { 253 | this._services.getLoggerService().warn("Server Error", err) 254 | if (this._services.hasService("Console")) { 255 | this._services.getConsoleService().broadcastMessage("Server Error: " + err) 256 | } 257 | for (let context in this._debugContexts) { 258 | this._debugContexts[context].stop() 259 | } 260 | } 261 | } 262 | handleServerListening () { 263 | if (this._services != undefined && this._services != null) { 264 | this._services.getLoggerService().info("Listening on " + this._server.getAddress() + ':' + this._server.getPort()) 265 | if (this._services.hasService("Console")) { 266 | this._services.getConsoleService().broadcastMessage("Listening on Address:Port " + this._server.getAddress() +":" + this._server.getPort()) 267 | } 268 | } 269 | } 270 | handleNewConnection(socket) { 271 | if (this._services != undefined && this._services != null) { 272 | this._services.getLoggerService().info("Session initiated") 273 | this.assignSocketToContext(socket) 274 | } 275 | } 276 | handleListenerClosed() { 277 | if (this._services != undefined && this._services != null) { 278 | this._services.getLoggerService().info("Listener Closed") 279 | if (this._services.hasService("Console")) { 280 | this._services.getConsoleService().broadcastMessage("No longer listening on Address:Port " + this._server.getAddress() +":" + this._server.getPort()) 281 | } 282 | for (let context in this._debugContexts) { 283 | this._debugContexts[context].stop() 284 | } 285 | } 286 | } 287 | handleConnectionEnded(socket) { 288 | if (this._services != undefined && this._services != null) { 289 | this._services.getLoggerService().info("Connection Closed") 290 | const context = this.getContextForSocket(socket) 291 | if (context != null) { 292 | context.stop() 293 | } 294 | } 295 | } 296 | handleConnectionError(event) { 297 | if (this._services != undefined && this._services != null) { 298 | this._services.getLoggerService().warn("Connection error",event.error) 299 | const context = this.getContextForSocket(event.socket) 300 | if (context != null) { 301 | context.stop() 302 | } 303 | } 304 | } 305 | 306 | isDestroyed() { 307 | return this._isDestroyed 308 | } 309 | 310 | destroy(fromServiceManager) { 311 | this._destroyed = true 312 | if (this._server != undefined && this._server != null) { 313 | this._server.destroy() 314 | delete this._server 315 | } 316 | for (let context in this._debugContexts) { 317 | this._debugContexts[context].destroy() 318 | delete this._debugContexts[context] 319 | } 320 | delete this._debugContexts 321 | if (typeof this._emitter.destroy === "function") { 322 | this._emitter.destroy() 323 | } 324 | this._emitter.dispose() 325 | this._subscriptions.dispose() 326 | } 327 | 328 | dispose() { 329 | this.destroy() 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /lib/engines/dbgp/server.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | 4 | import {Emitter, Disposable} from 'event-kit' 5 | import net from "net" 6 | import autoBind from 'auto-bind-inheritance' 7 | export default class Server { 8 | constructor (params) { 9 | autoBind(this) 10 | this._emitter = new Emitter() 11 | this._serverPort = params.serverPort; 12 | this._serverAddress = params.serverAddress; 13 | this._sockets = [] 14 | } 15 | 16 | getPort() { 17 | return this._serverPort 18 | } 19 | 20 | getAddress() { 21 | return this._serverAddress 22 | } 23 | 24 | setPort (port) { 25 | if (port != this._serverPort) { 26 | this._serverPort = port 27 | if (this.isListening()) { 28 | this.close() 29 | this.listen() 30 | } 31 | } 32 | } 33 | 34 | setAddress (address) { 35 | if (address != this._serverAddress) { 36 | this._serverAddress = address 37 | if (this.isListening()) { 38 | this.close() 39 | this.listen() 40 | } 41 | } 42 | } 43 | 44 | setAddressPort (address,port) { 45 | if (port != this._serverPort || address != this._serverAddress) { 46 | this._serverPort = port 47 | this._serverAddress = address 48 | if (this.isListening()) { 49 | this.close() 50 | this.listen() 51 | } 52 | } 53 | } 54 | 55 | isListening () { 56 | return this._server != undefined; 57 | } 58 | 59 | listen (options) { 60 | try { 61 | if (this.isListening()) { 62 | this.close() 63 | } 64 | this._sockets = [] 65 | this._server = net.createServer( (socket) => { 66 | //socket.setEncoding('utf8'); 67 | socket.on('error', (err) => { 68 | this._emitter.emit('php-debug.engine.internal.connectionError', {socket:socket,error:err}) 69 | try { 70 | socket.end() 71 | } catch (err) { 72 | // Supress 73 | } 74 | if (this._sockets == undefined || this._sockets == null) return; 75 | this._sockets = this._sockets.filter(item => item !== socket) 76 | }) 77 | socket.on('close', () => { 78 | this._emitter.emit('php-debug.engine.internal.connectionClosed', socket) 79 | if (this._sockets == undefined || this._sockets == null) return; 80 | this._sockets = this._sockets.filter(item => item !== socket) 81 | }) 82 | socket.on('end', () => { 83 | this._emitter.emit('php-debug.engine.internal.connectionEnded', socket) 84 | if (this._sockets == undefined || this._sockets == null) return; 85 | this._sockets = this._sockets.filter(item => item !== socket) 86 | }) 87 | if (atom.config.get('php-debug.xdebug.multipleSessions') !== true && atom.config.get('php-debug.xdebug.multipleSessions') !== 1) { 88 | if (this._sockets.length >= 1) { 89 | console.log("Rejecting session") 90 | try { 91 | socket.end(); 92 | } catch (err) { 93 | console.error(err); 94 | } 95 | return; 96 | } 97 | } 98 | this._sockets.push(socket) 99 | this._emitter.emit('php-debug.engine.internal.newConnection', socket) 100 | }); 101 | 102 | if (this._server) { 103 | this._server.on('error', (err) => { 104 | this._emitter.emit('php-debug.engine.internal.serverError', err) 105 | this.close() 106 | return false 107 | }); 108 | } 109 | 110 | let serverOptions = {} 111 | serverOptions.port = this._serverPort 112 | if (this._serverAddress != "*") { 113 | serverOptions.host = this._serverAddress 114 | } 115 | if (this._server) { 116 | this._server.listen(serverOptions, () => { 117 | this._emitter.emit('php-debug.engine.internal.serverListening') 118 | }); 119 | } 120 | return true 121 | } catch (e) { 122 | this._emitter.emit('php-debug.engine.internal.serverError', e) 123 | this.close() 124 | return false 125 | } 126 | } 127 | 128 | close () { 129 | this._emitter.emit('php-debug.engine.internal.listenerClosing') 130 | if (this._sockets != undefined && this._sockets != null) { 131 | for (let socket of this._sockets) { 132 | try { 133 | socket.end() 134 | } catch (err) { 135 | // Supress 136 | } 137 | } 138 | delete this._sockets 139 | } 140 | if (this._server != undefined && this._server != null) { 141 | this._server.close() 142 | delete this._server 143 | } 144 | this._emitter.emit('php-debug.engine.internal.listenerClosed') 145 | } 146 | 147 | onServerListening(callback) { 148 | return this._emitter.on('php-debug.engine.internal.serverListening', callback) 149 | } 150 | onServerError(callback) { 151 | return this._emitter.on('php-debug.engine.internal.serverError', callback) 152 | } 153 | onlistenerClosed(callback) { 154 | return this._emitter.on('php-debug.engine.internal.listenerClosed', callback) 155 | } 156 | onlistenerClosing(callback) { 157 | return this._emitter.on('php-debug.engine.internal.listenerClosing', callback) 158 | } 159 | onNewConnection(callback) { 160 | return this._emitter.on('php-debug.engine.internal.newConnection', callback) 161 | } 162 | onConnectionError(callback) { 163 | return this._emitter.on('php-debug.engine.internal.connectionError', callback) 164 | } 165 | onConnectionClosed(callback) { 166 | return this._emitter.on('php-debug.engine.internal.connectionClosed', callback) 167 | } 168 | onConnectionEnded(callback) { 169 | return this._emitter.on('php-debug.engine.internal.connectionEnded', callback) 170 | } 171 | 172 | destroy() { 173 | this.close() 174 | if (typeof this._emitter.destroy === "function") { 175 | this._emitter.destroy() 176 | } 177 | this._emitter.dispose() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import path from 'path' 4 | import fastGlob from 'fast-glob' 5 | import os from 'os' 6 | 7 | exports.getInsertIndex = function(sortedArray, object) { 8 | var curObject, index; 9 | if (sortedArray.length === 0) { 10 | return 0; 11 | } 12 | for (index in sortedArray) { 13 | curObject = sortedArray[index]; 14 | if (object.isLessThan(curObject)) { 15 | return index; 16 | } 17 | } 18 | return sortedArray.length; 19 | }; 20 | 21 | exports.generatePathMaps = function(remoteUri, existingPathMaps, aggregate, reverse) { 22 | if (existingPathMaps == undefined || existingPathMaps == null) { 23 | existingPathMaps = [] 24 | } 25 | var aggregateResults = {}; 26 | let uri = remoteUri; 27 | uri = decodeURI(uri).replace('file:///','') 28 | // Try to do the right thing if it's a windows path vs *nix 29 | if (uri[1].charCodeAt(0) != 58 && uri[0].charCodeAt(0) != 47) { 30 | uri = "/" + uri; 31 | } 32 | let baseName = path.basename(uri) 33 | let uriParent = path.dirname(uri) 34 | var uriParts = uriParent.split('/') 35 | var bestMatchExisting = null 36 | if (reverse != undefined && reverse != null && reverse === true) { 37 | let uriParentAlt = uriParent.replace(/\\/g,"/") 38 | let uriAlt = uri.replace(/\\/g,"/") 39 | for (let mapping of existingPathMaps) { 40 | if (mapping.localPath == uriParent || mapping.localPath == uri || mapping.localPath == uriParentAlt || mapping.localPath == uriAlt) { 41 | if (aggregate == undefined || aggregate == null) { 42 | return {type:"existing", results:mapping} 43 | } 44 | aggregateResults["existing"] = {type:"existing", results:mapping} 45 | break; 46 | } else if (mapping.localPath.endsWith("*")) { 47 | var wildcardPath = mapping.localPath.substring(0, mapping.localPath.length-1); 48 | if (uriParent.startsWith(wildcardPath) || (uriParent + "/").startsWith(wildcardPath) || uriParentAlt.startsWith(wildcardPath) || (uriParentAlt + "/").startsWith(wildcardPath) ) { 49 | if (aggregate == undefined || aggregate == null) { 50 | return {type:"existing", results:{remotePath:mapping.remotePath,localPath:wildcardPath}} 51 | } 52 | aggregateResults["existing"] = {type:"existing", results:{remotePath:mapping.remotePath,localPath:wildcardPath}} 53 | break; 54 | } 55 | } else if (uriParent.indexOf(mapping.localPath) == 0 || uriParentAlt.indexOf(mapping.localPath) == 0) { 56 | if (bestMatchExisting != null && mapping.localPath.length < bestMatchExisting.results.localPath.length) { 57 | bestMatchExisting = {type:"existing", results:mapping} 58 | } if (bestMatchExisting == null) { 59 | bestMatchExisting = {type:"existing", results:mapping} 60 | } 61 | } 62 | } 63 | if (bestMatchExisting != null) { 64 | if (aggregate == undefined || aggregate == null) { 65 | return bestMatchExisting 66 | } 67 | aggregateResults["existing"] = bestMatchExisting 68 | } 69 | return {type:"list",results:[{remotePath:"",localPath:uriParentAlt}]} 70 | } 71 | 72 | 73 | for (let mapping of existingPathMaps) { 74 | if (mapping.remotePath == uriParent || mapping.remotePath == uri) { 75 | if (aggregate == undefined || aggregate == null) { 76 | return {type:"existing", results:mapping} 77 | } 78 | aggregateResults["existing"] = {type:"existing", results:mapping} 79 | break; 80 | } else if (mapping.remotePath.endsWith("*")) { 81 | var wildcardPath = mapping.remotePath.substring(0, mapping.remotePath.length-1); 82 | if (uriParent.startsWith(wildcardPath) || (uriParent + "/").startsWith(wildcardPath)) { 83 | if (aggregate == undefined || aggregate == null) { 84 | return {type:"existing", results:{remotePath:wildcardPath,localPath:mapping.localPath}} 85 | } 86 | aggregateResults["existing"] = {type:"existing", results:{remotePath:wildcardPath,localPath:mapping.localPath}} 87 | break; 88 | } 89 | } else if (uriParent.indexOf(mapping.remotePath) == 0) { 90 | if (bestMatchExisting != null && mapping.remotePath.length < bestMatchExisting.results.remotePath.length) { 91 | bestMatchExisting = {type:"existing", results:mapping} 92 | } if (bestMatchExisting == null) { 93 | bestMatchExisting = {type:"existing", results:mapping} 94 | } 95 | } 96 | } 97 | if (bestMatchExisting != null) { 98 | if (aggregate == undefined || aggregate == null) { 99 | return bestMatchExisting 100 | } 101 | aggregateResults["existing"] = bestMatchExisting 102 | } 103 | 104 | let projectDirs = atom.project.rootDirectories 105 | let possibleMatches = [] 106 | let rankedListing = [] 107 | let matchedFiles = [] 108 | if (atom.config.get('php-debug.xdebug.projectScan') === true || atom.config.get('php-debug.xdebug.projectScan') === 1) { 109 | for (let project of projectDirs) { 110 | if (project.realPath != undefined && project.realPath != null) { 111 | const joined = path.join(project.realPath,'/**/',baseName) 112 | const normal = [path.normalize(joined).replace(/\\/gi,'/')] 113 | const ignorePaths = atom.config.get('php-debug.pathMapsSearchIgnore') 114 | const globSearch = normal.concat(ignorePaths); 115 | const files = fastGlob.sync(globSearch); 116 | matchedFiles = matchedFiles.concat(files) 117 | } 118 | } 119 | } 120 | 121 | for (let possible of matchedFiles) { 122 | // Direct match 123 | if (possible == uri) { 124 | possible = path.dirname(possible); 125 | if (aggregate == undefined || aggregate == null) { 126 | 127 | return {type:"direct", results:{remotePath:possible,localPath:possible}} 128 | } 129 | //aggregateResults["direct"] = {type:"direct", results:{remotePath:possible,localPath:possible}} 130 | rankedListing.push({remotePath:possible,localPath:possible}); 131 | } else { 132 | // Traverse up the tree until the paths diverge, use as possible 133 | 134 | let pathParent = path.dirname(possible) 135 | 136 | var pathParts = pathParent.split('/') 137 | var uriIdx = uriParts.length - 1 138 | var pathIdx = pathParts.length - 1 139 | var nextUriBase = uriParts[uriIdx] 140 | var nextPathBase = pathParts[pathIdx] 141 | var matches = -1 142 | while (nextUriBase == nextPathBase && uriIdx >= 0 && pathIdx >= 0) { 143 | uriIdx-- 144 | pathIdx-- 145 | nextUriBase = uriParts[uriIdx] 146 | nextPathBase = pathParts[pathIdx] 147 | matches++ 148 | } 149 | if (matches == -1) { 150 | possibleMatches.push({slices: 0, remote:uriParts,local:pathParts}) 151 | } else { 152 | possibleMatches.push({slices:matches, remote:uriParts,local:pathParts}) 153 | } 154 | } 155 | } 156 | for (let listing of possibleMatches) { 157 | for (let x = listing.slices; x >= 0; x--) { 158 | let remoteParts = listing.remote.slice(0, listing.remote.length-x) 159 | let localParts = listing.local.slice(0, listing.local.length-x) 160 | rankedListing.push({remotePath:remoteParts.join('/'),localPath:localParts.join('/')}) 161 | } 162 | } 163 | if (aggregate == undefined || aggregate == null) { 164 | if (rankedListing.length == 0) { 165 | rankedListing.push({remotePath:uriParent,localPath:""}) 166 | } 167 | return {type:"list",results:rankedListing} 168 | } 169 | aggregateResults["list"] = {type:"list", results:rankedListing} 170 | return aggregateResults; 171 | } 172 | 173 | exports.createPreventableEvent = function() { 174 | return { 175 | preventDefault : function(promise) { 176 | this._defaultPrevented = true; 177 | this._promise = promise; 178 | }, 179 | isDefaultPrevented : function() { 180 | return this._defaultPrevented == true; 181 | }, 182 | getPromise : function() { 183 | return this._promise 184 | } 185 | }; 186 | } 187 | 188 | exports.insertOrdered = function(sortedArray, object) { 189 | var index; 190 | index = exports.getInsertIndex(sortedArray, object); 191 | return sortedArray.splice(index, 0, object); 192 | }; 193 | 194 | exports.escapeValue = function(object) { 195 | if (typeof object === "string") { 196 | return "\"" + object.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; 197 | } 198 | return object; 199 | }; 200 | 201 | exports.escapeHtml = function(string) { 202 | var entityMap, result; 203 | entityMap = { 204 | "<": "<", 205 | ">": ">" 206 | }; 207 | result = String(string).replace(/[<>]/g, function(s) { 208 | return entityMap[s]; 209 | }); 210 | return result; 211 | }; 212 | 213 | exports.arraySearch = function(array, object) { 214 | var curObject, index; 215 | if (array.length === 0) { 216 | return false; 217 | } 218 | for (index in array) { 219 | curObject = array[index]; 220 | if (object.isEqual(curObject)) { 221 | return index; 222 | } 223 | } 224 | return false; 225 | }; 226 | 227 | exports.arrayRemove = function(array, object) { 228 | var index, removed; 229 | index = exports.arraySearch(array, object); 230 | if (index === false) { 231 | return; 232 | } 233 | removed = array.splice(index, 1); 234 | if (removed.length > 0) { 235 | return removed[0]; 236 | } 237 | }; 238 | 239 | exports.serializeArray = function(array) { 240 | var curObject, index, object, ret; 241 | ret = []; 242 | for (index in array) { 243 | curObject = array[index]; 244 | object = curObject.serialize(); 245 | if (object === void 0) { 246 | continue; 247 | } 248 | ret.push(object); 249 | } 250 | return ret; 251 | }; 252 | 253 | exports.shallowEqual = function(oldProps, newProps) { 254 | var newKeys, oldKeys; 255 | newKeys = Object.keys(newProps).sort(); 256 | oldKeys = Object.keys(oldProps).sort(); 257 | if (!newKeys.every((function(_this) { 258 | return function(key) { 259 | return oldKeys.includes(key); 260 | }; 261 | })(this))) { 262 | return false; 263 | } 264 | return newKeys.every((function(_this) { 265 | return function(key) { 266 | return newProps[key] === oldProps[key]; 267 | }; 268 | })(this)); 269 | }; 270 | 271 | exports.deserializeArray = function(array) { 272 | var curObject, error, index, object, ret; 273 | ret = []; 274 | for (index in array) { 275 | curObject = array[index]; 276 | try { 277 | object = atom.deserializers.deserialize(curObject); 278 | if (object === void 0) { 279 | continue; 280 | } 281 | ret.push(object); 282 | } catch (_error) { 283 | error = _error; 284 | console.error("Could not deserialize object"); 285 | console.dir(curObject); 286 | } 287 | } 288 | return ret; 289 | }; 290 | 291 | exports.hasLocalPathMap = function(localPath, pathMaps) { 292 | if (!Array.isArray(pathMaps)) { 293 | pathMaps = [pathMaps]; 294 | } 295 | for (let pathMap of pathMaps) { 296 | if (pathMap == undefined || pathMap == null || !pathMap.hasOwnProperty('localPath') || !pathMap.hasOwnProperty('remotePath') ) { 297 | return false; 298 | } 299 | localPath = localPath.replace(/\\/g, '/'); 300 | let mapLocal = path.posix.normalize(pathMap.localPath.replace(/\\/g, '/')); 301 | localPath = path.posix.normalize(localPath.replace('file://','')) 302 | if (localPath.startsWith(mapLocal)) { 303 | return true; 304 | } 305 | } 306 | return false; 307 | } 308 | exports.hasRemotePathMap = function(remotePath, pathMaps) { 309 | if (!Array.isArray(pathMaps)) { 310 | pathMaps = [pathMaps]; 311 | } 312 | for (let pathMap of pathMaps) { 313 | if (pathMap == undefined || pathMap == null || !pathMap.hasOwnProperty('localPath') || !pathMap.hasOwnProperty('remotePath') ) { 314 | return false; 315 | } 316 | remotePath = path.posix.normalize(remotePath.replace(/\//g, '/').replace("file://",'')); 317 | let mapRemote = path.posix.normalize(pathMap.remotePath.replace(/\//g, '/')); 318 | if (remotePath.startsWith(mapRemote)) { 319 | return true; 320 | } 321 | } 322 | return false; 323 | } 324 | 325 | exports.localPathToRemote = function(localPath, pathMaps) { 326 | if (!Array.isArray(pathMaps)) { 327 | pathMaps = [pathMaps]; 328 | } 329 | for (let pathMap of pathMaps) { 330 | if (pathMap == undefined || pathMap == null || !pathMap.hasOwnProperty('localPath') || !pathMap.hasOwnProperty('remotePath') ) { 331 | if (localPath.indexOf('/') !== 0) { 332 | localPath = '/' + localPath 333 | } 334 | if (localPath.indexOf('file://') !== 0) { 335 | return 'file://' + localPath 336 | } 337 | return localPath 338 | } 339 | // Unify to unix line seperators 340 | localPath = localPath.replace(/\\/g, '/'); 341 | let mapLocal = path.posix.normalize(pathMap.localPath.replace(/\\/g, '/')); 342 | localPath = path.posix.normalize(localPath.replace('file://','')) 343 | if (!localPath.startsWith(mapLocal)) { 344 | continue; 345 | } 346 | let resultPath = localPath.replace(mapLocal, path.posix.normalize(pathMap.remotePath)); 347 | if (resultPath.indexOf('/') !== 0) { 348 | resultPath = '/' + resultPath 349 | } 350 | if (resultPath.indexOf('file://') !== 0) { 351 | return 'file://' + resultPath 352 | } 353 | return resultPath 354 | } 355 | if (localPath.indexOf('/') !== 0) { 356 | localPath = '/' + localPath 357 | } 358 | if (localPath.indexOf('file://') !== 0) { 359 | return 'file://' + localPath 360 | } 361 | }; 362 | 363 | exports.remotePathToLocal = function(remotePath, pathMaps) { 364 | if (!Array.isArray(pathMaps)) { 365 | pathMaps = [pathMaps]; 366 | } 367 | for (let pathMap of pathMaps) { 368 | if (pathMap == undefined || pathMap == null || !pathMap.hasOwnProperty('localPath') || !pathMap.hasOwnProperty('remotePath')) { 369 | return remotePath.replace("file://",''); 370 | } 371 | remotePath = path.posix.normalize(remotePath.replace(/\//g, '/').replace("file://",'')); 372 | let mapRemote = path.posix.normalize(pathMap.remotePath.replace(/\//g, '/')); 373 | if (!remotePath.startsWith(mapRemote) && !remotePath.substring(1).startsWith(mapRemote)) { 374 | if (!remotePath.startsWith(mapRemote.replace(/\\/g, '/')) && !remotePath.substring(1).startsWith(mapRemote.replace(/\\/g, '/'))) { 375 | continue; 376 | } else { 377 | mapRemote = mapRemote.replace(/\\/g, '/'); 378 | } 379 | } 380 | let resultPath = remotePath.replace(mapRemote, path.posix.normalize(pathMap.localPath)); 381 | if (os.type() == "Windows_NT") { 382 | resultPath = resultPath.replace(/\//g, '\\') 383 | if (resultPath.indexOf('\\') === 0) { 384 | resultPath = resultPath.substring(1) 385 | } 386 | } 387 | return resultPath 388 | } 389 | return remotePath.replace("file://",''); 390 | }; 391 | -------------------------------------------------------------------------------- /lib/models/debug-engine.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | 4 | export default class DebugEngine { 5 | getName() { 6 | console.error('engine not implemented') 7 | } 8 | 9 | getGrammars() { 10 | console.error('engine not implemented') 11 | } 12 | 13 | setUIServices (services) { 14 | console.error('engine not implemented') 15 | } 16 | 17 | getUIServices () { 18 | console.error('engine not implemented') 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/pathmaps/pathmaps-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import UiComponent from '../ui/component' 6 | import SelectList from '../ui/double-filter-list-view' 7 | import Editor from '../ui/editor' 8 | import fs from 'fs' 9 | import path from 'path' 10 | 11 | export default class PathMapsView extends UiComponent { 12 | 13 | constructor (props,children) { 14 | super(props,children) 15 | } 16 | 17 | render () { 18 | const {pathOptions,entry,state,showDetach, uri} = this.props; 19 | let cancelButton = [] 20 | let uriInfo = [] 21 | if (showDetach) { 22 | cancelButton = 23 | } 24 | if (uri != undefined && uri != null && uri != "") { 25 | let decodedUri = decodeURI(uri).replace('file:///','') 26 | // Try to do the right thing if it's a windows path vs *nix 27 | if (decodedUri[1].charCodeAt(0) != 58 && decodedUri[0].charCodeAt(0) != 47) { 28 | decodedUri = "/" + decodedUri; 29 | } 30 | let urifile = path.basename(decodedUri) 31 | let uribase = path.dirname(decodedUri) 32 | uriInfo = Entry point: {uribase}/{urifile} 33 | } 34 | const settings = pathOptions 35 | return
    36 | Setup a path map for this project 37 | {uriInfo} 38 |
    39 |
    40 | 41 |
    42 |
    43 |
    44 | 45 | {cancelButton} 46 | 47 |
    48 |
    49 |
    50 |
      51 |
    • Use a local path of ! to auto disconnnect when this path is hit
    • 52 |
    • Use a local path of ? to skip over these paths when debugging
    • 53 |
    54 |
    55 |
    56 |
    57 | } 58 | 59 | getFilterKeys(item) { 60 | return {first:item.remotePath, second:item.localPath} 61 | } 62 | 63 | getSettingElement(item) { 64 | const li = document.createElement('li') 65 | const remoteSpan = document.createElement('span') 66 | const localSpan = document.createElement('span') 67 | const remoteLabel = document.createElement('span') 68 | const localLabel = document.createElement('span') 69 | const remoteDiv = document.createElement('div') 70 | const localDiv = document.createElement('div') 71 | remoteDiv.className = 'pathmap-path-container path-container remote-path-container' 72 | localDiv.className = 'pathmap-path-container path-container local-path-container' 73 | remoteSpan.className ='pathmap-path remote-path' 74 | remoteSpan.textContent = item.remotePath 75 | localSpan.textContent = item.localPath 76 | localSpan.className = 'pathmap-path local-path' 77 | 78 | remoteLabel.className ='pathmap-path-label remote-path-label' 79 | remoteLabel.textContent = "Remote Path:" 80 | localLabel.textContent = "Local Path:" 81 | localLabel.className = 'pathmap-path-label local-path-label' 82 | li.className ='pathmap-choice paths-container' 83 | remoteDiv.appendChild(remoteLabel) 84 | remoteDiv.appendChild(remoteSpan) 85 | localDiv.appendChild(localLabel) 86 | localDiv.appendChild(localSpan) 87 | li.appendChild(remoteDiv) 88 | li.appendChild(localDiv) 89 | 90 | return li 91 | } 92 | 93 | didChangeQuery() { 94 | if (!this.canSave()) { 95 | this.refs.saveButton.setAttribute("disabled","disabled") 96 | } else { 97 | this.refs.saveButton.removeAttribute("disabled") 98 | } 99 | } 100 | 101 | handleSelectElement(item) { 102 | const state = Object.assign({}, this.props.state); 103 | state.selected = item 104 | this.update({state:state}); 105 | } 106 | 107 | canSave(selected) { 108 | if (!this.refs || !this.refs.selectList) { 109 | return false 110 | } 111 | const selectList = this.refs.selectList; 112 | 113 | const valueData = selectList.getQuery() 114 | if (valueData.first == "" || valueData.second == "") { 115 | if (selected == undefined || selected == null || selected.remotePath == "" || selected.localPath == "") { 116 | return false 117 | } 118 | } 119 | return true 120 | } 121 | 122 | handleSaveClick () { 123 | const selectList = this.refs.selectList; 124 | 125 | const valueData = selectList.getQuery() 126 | let pathMap = {remotePath:valueData.first, localPath:valueData.second} 127 | const onSave = this.props.onSave 128 | const selected = this.props.state.selected 129 | // Make sure the possible user entry *looks* valid if not 130 | // fall back to their last selection 131 | if (onSave != undefined && onSave != null) { 132 | if (valueData.second != "?" && valueData.second != "!") { 133 | fs.access(valueData.second,fs.constants.F_OK, (err) => { 134 | if (err == undefined || err == null) { 135 | onSave(pathMap, this.props.default) 136 | } else { 137 | onSave(selected, this.props.default) 138 | } 139 | }); 140 | } else { 141 | onSave(pathMap, this.props.default) 142 | } 143 | } 144 | this.destroy() 145 | } 146 | 147 | handleCancelDetachClick () { 148 | const onCancel = this.props.onCancel 149 | if (onCancel != undefined && onCancel != null) { 150 | onCancel() 151 | } 152 | this.destroy() 153 | } 154 | 155 | handleIgnoreClick () { 156 | const onIgnore = this.props.onIgnore 157 | if (onIgnore != undefined && onIgnore != null) { 158 | 159 | const selectList = this.refs.selectList; 160 | 161 | const valueData = selectList.getQuery() 162 | if (valueData.first == undefined || valueData.first == null || valueData.first == "") { 163 | let uri = decodeURI(this.props.uri).replace('file:///','') 164 | // Try to do the right thing if it's a windows path vs *nix 165 | if (uri[1].charCodeAt(0) != 58 && uri[0].charCodeAt(0) != 47) { 166 | uri = "/" + uri; 167 | } 168 | valueData.first = uri; 169 | } 170 | if (valueData.first == undefined || valueData.first == null || valueData.first == "") { 171 | return; 172 | } 173 | 174 | let pathMap = {remotePath:valueData.first, localPath:valueData.second} 175 | onIgnore(pathMap, this.props.default) 176 | } 177 | this.destroy() 178 | } 179 | 180 | 181 | init () { 182 | if (!this.props.state) { 183 | this.props.state = { 184 | selected : {remotePath:"",localPath:""}, 185 | canSave: false, 186 | initialSelection: null 187 | } 188 | if (this.props.default) { 189 | this.props.state.selected = this.props.default 190 | for (var idx in this.props.pathOptions) { 191 | if (this.props.pathOptions[idx].remotePath == this.props.default.remotePath && this.props.pathOptions[idx].localPath == this.props.default.localPath) { 192 | this.props.state.initialSelection = idx; 193 | break; 194 | } 195 | } 196 | } else { 197 | if (this.props.pathOptions.length == 1) { 198 | this.props.state.selected = this.props.pathOptions[0]; 199 | this.props.state.initialSelection = 0; 200 | } 201 | } 202 | } 203 | super.init(); 204 | } 205 | 206 | 207 | attach () { 208 | this.panel = atom.workspace.addModalPanel({item: this.element}); 209 | } 210 | 211 | destroy() { 212 | super.destroy(); 213 | if (this.panel != undefined && this.panel != null) { 214 | this.panel.destroy(); 215 | delete this.panel 216 | } 217 | } 218 | 219 | 220 | handleCloseClick (event) { 221 | if (this.panel != undefined && this.panel != null) { 222 | this.panel.destroy(); 223 | delete this.panel; 224 | } 225 | } 226 | 227 | } 228 | PathMapsView.bindFns = ["handleCloseClick","handleSaveClick","getSettingElement","handleSelectElement","didChangeQuery", "handleCancelDetachClick", "handleIgnoreClick"] 229 | -------------------------------------------------------------------------------- /lib/php-debug.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import os from 'os' 6 | import {CompositeDisposable} from 'atom' 7 | import DebugEngine from './engines/dbgp/engine' 8 | import DecoratorService from './services/decorator' 9 | import StatusBarDebugView from './status/debug-view' 10 | import StatusBarConsoleView from './status/console-view' 11 | import DefaultBreakpointsView from './breakpoints/default-breakpoints-view' 12 | import compareVersions from 'compare-versions' 13 | 14 | class PHPDebug { 15 | constructor() { 16 | this._fullyActivated = false; 17 | } 18 | activate(state) { 19 | this._subscriptions = new CompositeDisposable(); 20 | this._engine = new DebugEngine() 21 | this.showUpgradeNotice() 22 | this.requirePackages() 23 | this._state = this.deserialize(state); 24 | this.registerSubscriptions(); 25 | } 26 | 27 | registerSubscriptions() { 28 | this._subscriptions.add(atom.commands.add('atom-workspace', { 29 | 'php-debug:run': (event) => { 30 | if (this._engine != undefined && this._engine != null) { 31 | let context = this._engine.tryGetContext() 32 | if (context != null) { 33 | context.executeRun(); 34 | } 35 | } 36 | } 37 | })); 38 | this._subscriptions.add(atom.commands.add('atom-workspace', { 39 | 'php-debug:stepOver': (event) => { 40 | if (this._engine != undefined && this._engine != null) { 41 | let context = this._engine.tryGetContext() 42 | if (context != null) { 43 | context.executeStepOver(); 44 | } 45 | } 46 | } 47 | })); 48 | this._subscriptions.add(atom.commands.add('atom-workspace', { 49 | 'php-debug:stepIn': (event) => { 50 | if (this._engine != undefined && this._engine != null) { 51 | let context = this._engine.tryGetContext() 52 | if (context != null) { 53 | context.executeStepInto(); 54 | } 55 | } 56 | } 57 | })); 58 | this._subscriptions.add(atom.commands.add('atom-workspace', { 59 | 'php-debug:stepOut': (event) => { 60 | if (this._engine != undefined && this._engine != null) { 61 | let context = this._engine.tryGetContext() 62 | if (context != null) { 63 | context.executeStepOut(); 64 | } 65 | } 66 | } 67 | })); 68 | } 69 | 70 | showUpgradeNotice() { 71 | const showWelcome = atom.config.get('php-debug.showWelcome') 72 | if (!showWelcome) { 73 | return 74 | } 75 | const notification = atom.notifications.addInfo('php-debug', { 76 | dismissable: true, 77 | icon: 'bug', 78 | detail: 'Welcome to the new PHP-Debug! Check out some of the new Features:\n* Multiple Debugging Sessions\n* Cleaner UI\n* Overlay Debugging Command Bar\n* Tool tips for variables while debugging', 79 | description: 'Many of your previous settings may have changes or may no longer be available. You will likely need to reconfigure the package.', 80 | buttons: [{ 81 | text: 'Open Settings Now', 82 | onDidClick: () => { 83 | notification.dismiss() 84 | atom.config.set('php-debug.showWelcome', false) 85 | this.openSettingsView() 86 | } 87 | },{ 88 | text: 'Got It', 89 | onDidClick: () => { 90 | notification.dismiss() 91 | atom.config.set('php-debug.showWelcome', false) 92 | } 93 | }, { 94 | text: 'Remind Me Next Time', 95 | onDidClick: () => { 96 | notification.dismiss() 97 | } 98 | }] 99 | }) 100 | } 101 | 102 | openSettingsView() { 103 | atom.workspace.open('atom://config/packages/php-debug') 104 | } 105 | 106 | requirePackages() { 107 | const pkg = 'atom-debug-ui' 108 | const detail = 'It provides IDE/UI features for debugging inside atom' 109 | const packages = new Map() 110 | packages.set("atom-debug-ui",{required:true,details:'It provides IDE/UI features for debugging inside atom'}) 111 | packages.set("ide-php",{required:false,details:'It provides IDE features for PHP'}) 112 | packages.set("atom-ide-ui",{required:false,details:'It provides IDE features for Atom that will be used by ide-php'}) 113 | for (let [pkg, info] of packages) { 114 | 115 | const existingPkg = atom.packages.getLoadedPackage(pkg) 116 | if (existingPkg != null) { 117 | continue; 118 | } 119 | 120 | const preDisabledBundledPackages = atom.config.get('php-debug.noPackageInstallPrompt') 121 | if (preDisabledBundledPackages.includes(pkg)) { 122 | continue; 123 | } 124 | 125 | const notification = atom.notifications.addInfo('php-debug', { 126 | dismissable: true, 127 | icon: 'cloud-download', 128 | detail: 'This package '+ (info.required ? 'requires' : 'works better with') + ' the ' + pkg + ' package. ' + info.details, 129 | description: 'Would you like to install **' + pkg + '**?' + (info.required ? ' **It is _required_ for this plugin to work.**' : ''), 130 | buttons: [{ 131 | text: 'Yes', 132 | onDidClick: () => { 133 | notification.dismiss() 134 | this.installPkg(pkg) 135 | } 136 | }, { 137 | text: 'Not Now', 138 | onDidClick: () => { 139 | notification.dismiss() 140 | } 141 | }, { 142 | text: 'Never', 143 | onDidClick: () => { 144 | notification.dismiss() 145 | const disabledBundledPackages = atom.config.get('php-debug.noPackageInstallPrompt') 146 | if (!disabledBundledPackages.includes(pkg)) { 147 | disabledBundledPackages.push(pkg) 148 | atom.config.set('php-debug.noPackageInstallPrompt', disabledBundledPackages) 149 | } 150 | } 151 | }] 152 | }) 153 | } 154 | } 155 | 156 | showVersionNotice() { 157 | const notification = atom.notifications.addInfo('php-debug', { 158 | dismissable: true, 159 | icon: 'bug', 160 | detail: 'Please install the latest version of atom-debug-ui and restart Atom.\nPHP-Debug will not work without the latest version', 161 | description: 'It looks like the version of **atom-debug-ui** you have installed is too old!', 162 | buttons: [{ 163 | text: 'Okay', 164 | onDidClick: () => { 165 | notification.dismiss() 166 | } 167 | }] 168 | }) 169 | } 170 | 171 | installPkg(pkg) { 172 | console.debug(`Attempting to install installing package ${pkg}`) 173 | const p = atom.packages.activatePackage('settings-view') 174 | if (!p) { 175 | return 176 | } 177 | p.then( (settingsPkg) => { 178 | if (!settingsPkg || !settingsPkg.mainModule) { 179 | console.warn("Could not find settings view") 180 | return 181 | } 182 | const settingsview = settingsPkg.mainModule.createSettingsView({uri: settingsPkg.mainModule.configUri}) 183 | settingsview.packageManager.install({name: pkg}, (error) => { 184 | if (!error) { 185 | console.info(`The ${pkg} package has been installed`) 186 | atom.notifications.addInfo(`Installed the ${pkg} package`) 187 | } else { 188 | let content = '' 189 | if (error.stdout) { 190 | content = error.stdout 191 | } 192 | if (error.stderr) { 193 | content = content + os.EOL + error.stderr 194 | } 195 | content = content.trim() 196 | atom.notifications.addError(content) 197 | console.error(error) 198 | } 199 | }) 200 | }).catch( (err) => { 201 | console.warn("Could not find settings view package",err) 202 | }) 203 | } 204 | 205 | provideDebugEngineService() { 206 | if (this._engine.isDestroyed) { 207 | this._engine = new DebugEngine() 208 | } 209 | return this._engine 210 | } 211 | 212 | consumeDebugUI(services) { 213 | try { 214 | const existingPkg = atom.packages.getLoadedPackage("atom-debug-ui") 215 | if (existingPkg == null) { 216 | return 217 | } 218 | let debugUiVersion = existingPkg.metadata.version; 219 | if (compareVersions(debugUiVersion,'1.0.3') < 0) { 220 | this.showVersionNotice() 221 | return; 222 | } 223 | this._fullyActivated = true; 224 | 225 | if (this._services != undefined || this._services != null) { 226 | this._services.destroy() 227 | this._subscriptions.dispose() 228 | this._subscriptions = new CompositeDisposable() 229 | } 230 | this._services = services 231 | this._engine.setUIServices(services) 232 | this._services.activate(this._engine.getName(), this._state.services) 233 | this._subscriptions.add(this._services.onServiceRegistered(this.serviceRegistered.bind(this))) 234 | this._services.registerService('Decorator',new DecoratorService(this._services,{})) 235 | this._services.requestService('actions',{},this.actionsServiceActivated.bind(this)) 236 | this._services.requestService('stack') 237 | this._services.requestService('console') 238 | this._services.requestService('status') 239 | this._services.requestService('scope') 240 | this._services.requestService('watches') 241 | //xDebug does not support watchpoints 242 | //this._services.requestService('watchpoints') 243 | const view = 244 | const breakpointsOptions = { 245 | attachedViews: view 246 | } 247 | this._services.requestService('breakpoints',breakpointsOptions) 248 | const viewOptions = { 249 | allowDefaultConsole: true, 250 | allowDefaultDebugger: true, 251 | uriPrefix:'php-debug', 252 | debugViewTitle:'PHP Debug', 253 | consoleViewTitle:'PHP Console', 254 | combineBreakpointsWatchpoints: false 255 | } 256 | this._services.requestService('debugview',viewOptions,this.debugViewServiceActivated.bind(this)) 257 | this._services.getLoggerService().info("Received Debug UI Services") 258 | } catch (err) { 259 | atom.notifications.addError(`Failed to load PHP-Debug`, { 260 | detail: err, 261 | dismissable: true 262 | }) 263 | throw err 264 | } 265 | } 266 | 267 | getGrammarScopes() { 268 | return ['text.html.php']; 269 | } 270 | 271 | consumeDatatip(service) { 272 | let provider = { 273 | providerName: "PHPDebugLanguageClient", 274 | priority: 12, 275 | grammarScopes: this.getGrammarScopes(), 276 | validForScope: (scopeName) => { 277 | return this.getGrammarScopes().includes(scopeName) 278 | }, 279 | datatip: this.getDatatip.bind(this) 280 | } 281 | service.addProvider(provider); 282 | } 283 | 284 | getDatatip(tipEditor, tipPoint) { 285 | return new Promise((resolve,reject) => { 286 | if (this._engine == undefined || this._engine == null) { 287 | reject(); 288 | return; 289 | } 290 | let pkg = atom.packages.getActivePackage("ide-php") 291 | if (pkg == undefined || pkg == null) { 292 | reject(); 293 | return; 294 | } 295 | pkg.mainModule.getDatatip(tipEditor,tipPoint).then((result) => { 296 | let editor = atom.workspace.getActivePaneItem(); 297 | if (editor == undefined || editor == null || editor.getTextInBufferRange == undefined || result == undefined || result == null) { 298 | reject(); 299 | return; 300 | } else { 301 | let highlight = editor.getTextInBufferRange(result.range); 302 | let debugContext = this._engine.tryGetContext(); 303 | if (debugContext == undefined || debugContext == null) { 304 | reject(); 305 | return; 306 | } 307 | if (highlight == undefined || highlight == null) { 308 | reject(); 309 | return; 310 | } else { 311 | let p = debugContext.evalExpression(highlight) 312 | p.then((data) => { 313 | let markedString = { 314 | grammar: editor.getGrammar(), 315 | type: "snippet", 316 | value: "\"" + data + "\"" 317 | } 318 | if (result.markedStrings != undefined && result.markedStrings != null) { 319 | result.markedStrings = []; 320 | result.markedStrings.push(markedString); 321 | } 322 | resolve(result) 323 | return; 324 | }); 325 | } 326 | } 327 | }).catch((error) => { 328 | reject(error); 329 | return; 330 | }); 331 | }); 332 | } 333 | 334 | 335 | debugViewServiceActivated(service,options) { 336 | this._subscriptions.add(service.onDefaultDebuggerRequested(() => { 337 | this._engine.createDebuggingContext("default").then( (context) => { 338 | const uiService = context.getUIService() 339 | if (uiService != null) { 340 | uiService.activateDebugger() 341 | } 342 | }) 343 | })) 344 | this._subscriptions.add(service.onDefaultConsoleRequested(() => { 345 | this._engine.createDebuggingContext("default").then( (context) => { 346 | const uiService = context.getUIService() 347 | if (uiService != null) { 348 | uiService.activateDebugger() 349 | } 350 | }) 351 | })) 352 | this._subscriptions.add(service.onContextCreated(this.debugContextViewServiceCreated.bind(this))) 353 | } 354 | 355 | actionsServiceActivated(service,options) { 356 | service.hideActionButtons(['attach','run']) 357 | } 358 | 359 | debugContextViewServiceCreated(event) { 360 | event.service.enablePanels(['watches','breakpoints','context','stack']) 361 | } 362 | 363 | serviceRegistered(event) { 364 | this._services.getLoggerService().info('Service registered: ' + event.name) 365 | } 366 | 367 | consumeStatusBar (statusBar) { 368 | this._subscriptions.add(atom.config.observe("php-debug.display.enableStatusbarButtons", (enable) => { 369 | if (enable) { 370 | this._debugView = new StatusBarDebugView({statusBar:statusBar, engine:this._engine}) 371 | this._consoleView = new StatusBarConsoleView({statusBar:statusBar, engine:this._engine}) 372 | } 373 | else { 374 | if (this._consoleView != undefined && this._consoleView != null) { 375 | this._consoleView.destroy() 376 | delete this._consoleView 377 | } 378 | if (this._debugView != undefined && this._debugView != null) { 379 | this._debugView.destroy() 380 | delete this._debugView 381 | } 382 | } 383 | })) 384 | } 385 | 386 | deactivate() { 387 | if (this._engine != undefined && this._engine != null) { 388 | this._engine.destroy() 389 | delete this._services 390 | } 391 | if (this._service != undefined && this._engine != null) { 392 | this._services.destroy() 393 | delete this._services 394 | } 395 | if (this._subscriptions != undefined && this._subscriptions != null) { 396 | this._subscriptions.dispose() 397 | delete this._subscriptions 398 | } 399 | } 400 | 401 | 402 | serialize() { 403 | if (this._services != undefined && this._services != null) { 404 | var results = { 405 | "deserializer": "PHPDebug", 406 | "data": { 407 | "services" : this._services.serialize() 408 | } 409 | } 410 | return results; 411 | } 412 | if (!this._fullyActivated) { 413 | return this._state; 414 | } 415 | return {}; 416 | } 417 | deserialize(state) { 418 | if (state != null) { 419 | if (typeof state.data === "object") { 420 | return state.data; 421 | } else { 422 | return {"services":null} 423 | } 424 | } 425 | } 426 | } 427 | atom.deserializers.add(PHPDebug); 428 | module.exports = new PHPDebug() 429 | -------------------------------------------------------------------------------- /lib/services/decorator.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | import helpers from '../helpers' 4 | import {Emitter, Disposable} from 'event-kit' 5 | import autoBind from 'auto-bind-inheritance' 6 | 7 | export default class DecoratorService { 8 | constructor(services,options) { 9 | autoBind(this); 10 | this._services = services 11 | this._options = options 12 | this._emitter = new Emitter() 13 | } 14 | 15 | decorate(type,ref,data) { 16 | switch (type) { 17 | case "breakpointMarker": 18 | return this.decorateBreakpointMarker(ref,data) 19 | case "debuggerTitle": 20 | return this.decorateDebuggerTitle(ref,data) 21 | case "consoleTitle": 22 | return this.decorateConsoleTitle(ref,data) 23 | case "scopeContextArraySort": 24 | return this.decorateScopeContextArraySort(ref,data) 25 | case "variableLabels": 26 | return this.decorateVariableLabels(ref, data) 27 | case "variableRenderer": 28 | return this.decorateVariableRenderer(ref,data) 29 | default: 30 | return data 31 | } 32 | } 33 | 34 | decorateVariableRenderer(ref,data) { 35 | // let this be handled by the default renderer 36 | return data 37 | } 38 | 39 | decorateScopeContextArraySort(ref,data) { 40 | if (atom.config.get('php-debug.display.sortArray')) { 41 | this.fnWalkVar(data.data.variables); 42 | } 43 | return data 44 | } 45 | 46 | fnWalkVar (contextVar) { 47 | if (Array.isArray(contextVar)) { 48 | for (let item in contextVar) { 49 | if (Array.isArray(item.value)) { 50 | this.fnWalkVar(item.value) 51 | } 52 | } 53 | contextVar.sort(this.cbDeepNaturalSort) 54 | } 55 | } 56 | 57 | cbDeepNaturalSort (a,b) { 58 | let aIsNumeric = /^\d+$/.test(a.name) 59 | let bIsNumeric = /^\d+$/.test(b.name) 60 | // cannot exist two equal keys, so skip case of returning 0 61 | if (aIsNumeric && bIsNumeric) { // order numbers 62 | if (parseInt(a.name, 10) < parseInt(b.name, 10)) { 63 | return -1 64 | } else { 65 | return 1 66 | } 67 | } else if (!aIsNumeric && !bIsNumeric) { // order strings 68 | if (a.name < b.name) { 69 | return -1 70 | } else { 71 | return 1 72 | } 73 | } else { // string first (same behavior that PHP's `ksort`) 74 | if (aIsNumeric) { 75 | return 1 76 | } else { 77 | return -1 78 | } 79 | } 80 | } 81 | 82 | decorateVariableLabels(ref,data) { 83 | let variable = ref.variable; 84 | let parent = ref.parent; 85 | if (parent == undefined || parent == null) { 86 | parent = "" 87 | } 88 | let labels = []; 89 | let identifierClasses = 'variable php syntax--php'; 90 | if (!variable.label && variable.name) { 91 | var identifier = variable.name; 92 | } else { 93 | var identifier = variable.label; 94 | } 95 | const numericIdentifier = /^\d+$/.test(identifier) 96 | if (!parent) { // root taxonomy (Locals,Globals) 97 | identifierClasses += ' syntax--type' 98 | } else if (parent == 'User derfined constants') { 99 | identifierClasses += ' syntax--constant' 100 | } else if (parent.indexOf('.') == -1) { // Variable 101 | identifierClasses += ' syntax--variable' 102 | } else { 103 | identifierClasses += ' syntax--property' 104 | if (numericIdentifier) { 105 | identifierClasses += ' syntax--constant syntax--numeric' 106 | } else { 107 | identifierClasses += ' syntax--string' 108 | } 109 | label = '"' + identifier + '"' 110 | } 111 | 112 | let typeClasses = 'type php syntax--php syntax--' + variable.type; 113 | switch (variable.type) { 114 | case "array": 115 | typeClasses += ' syntax--support syntax--function' 116 | break; 117 | case "object": 118 | typeClasses += ' syntax--entity syntax--name syntax--type' 119 | break; 120 | } 121 | 122 | labels.push({text:identifier,classes:identifierClasses}) 123 | if (variable.type) { 124 | switch (variable.type) { 125 | case "array": 126 | labels.push({text:'array[' + (variable.length ? variable.length : variable.value.length) + ']',classes:typeClasses}); 127 | break; 128 | case "object": 129 | labels.push({text:'object',classes:typeClasses}); 130 | labels.push({text:"["+variable.className+"]",classes:'variable php syntax--php syntax--entity syntax--name syntax--class'}); 131 | break; 132 | } 133 | 134 | let value = null; 135 | let valueClasses = 'syntax--php'; 136 | switch (variable.type) { 137 | case "string": 138 | valueClasses += ' syntax--quoted syntax--string syntax--double ' 139 | value = '"' + helpers.escapeHtml(variable.value) + '"'; 140 | break; 141 | case 'resource': 142 | case 'error': 143 | valueClasses += ' syntax--quoted syntax--double syntax--constant' 144 | value = '"' + helpers.escapeHtml(variable.value) + '"'; 145 | break; 146 | case 'bool': 147 | if (variable.value == 0) { 148 | value = 'false' 149 | } else { 150 | value = 'true'; 151 | } 152 | valueClasses += ' syntax--constant syntax--language syntax--bool' 153 | break; 154 | case 'null': 155 | value = 'null'; 156 | valueClasses += ' syntax--constant syntax--language syntax--null' 157 | break; 158 | case 'numeric': 159 | value = variable.value; 160 | valueClasses += ' syntax--constant syntax--numeric' 161 | break; 162 | case 'uninitialized': 163 | value = '?'; 164 | valueClasses += ' syntax--constant syntax--language' 165 | break; 166 | } 167 | if (value) { 168 | labels.push({text:value,classes:valueClasses}) 169 | } 170 | } 171 | 172 | 173 | 174 | return labels; 175 | } 176 | 177 | decorateDebuggerTitle(ref,data) { 178 | if (ref != "default") { 179 | data = data + "("+ ref +")" 180 | } 181 | return data 182 | } 183 | 184 | decorateConsoleTitle(ref,data) { 185 | if (ref != "default") { 186 | data = data + "("+ ref +")" 187 | } 188 | return data 189 | } 190 | 191 | decorateBreakpointMarker(ref, data) { 192 | if (typeof ref.getSettingValue === "function") { 193 | switch (ref.getSettingValue("type")) { 194 | case "line": 195 | data.class= 'debug-break-line'; 196 | break; 197 | case "exception": 198 | case "error": 199 | data.class= 'debug-break-exception'; 200 | break; 201 | } 202 | } 203 | return data 204 | } 205 | 206 | destroy() { 207 | if (typeof this._emitter.destroy === "function") { 208 | this._emitter.destroy() 209 | } 210 | this._emitter.dispose() 211 | delete this._emitter; 212 | delete this._services; 213 | delete this._options; 214 | 215 | 216 | } 217 | 218 | dispose() { 219 | this.destroy() 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /lib/status/console-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import UiComponent from '../ui/component' 6 | 7 | export default class ConsoleView extends UiComponent { 8 | render() { 9 | const {state} = this.props; 10 | let classes = 'php-debug-console-view-toggle' 11 | if (state.active) { 12 | classes += ' active' 13 | } 14 | return
    15 | PHP Console 16 |
    17 | } 18 | 19 | constructor (props,children) { 20 | super(props,children) 21 | this._engine = props.engine 22 | this._statusBar = props.statusBar 23 | } 24 | 25 | init () { 26 | if (!this.props.state) { 27 | this.props.state = { 28 | active: false 29 | } 30 | } 31 | super.init() 32 | this._tile = this.props.statusBar.addLeftTile({item: this.element, priority: -99}) 33 | } 34 | 35 | toggleConsole() { 36 | this._engine.createDebuggingContext("default").then( (context) => { 37 | const uiService = context.getUIService() 38 | if (uiService != null) { 39 | uiService.toggleConsole() 40 | } 41 | }) 42 | } 43 | 44 | setActive (active) { 45 | const state = Object.assign({}, this.props.state); 46 | state._active = active; 47 | this.update({state:state}); 48 | } 49 | 50 | destroy() { 51 | if (this._tile) { 52 | this._tile.destroy() 53 | this._tile = null 54 | } 55 | super.destroy() 56 | } 57 | } 58 | ConsoleView.bindFns = ["toggleConsole","setActive"] 59 | -------------------------------------------------------------------------------- /lib/status/debug-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import UiComponent from '../ui/component' 6 | 7 | export default class DebugView extends UiComponent { 8 | render() { 9 | const {state} = this.props; 10 | let classes = 'php-debug-debug-view-toggle' 11 | if (state.active) { 12 | classes += ' active' 13 | } 14 | return
    15 | PHP Debug 16 |
    17 | } 18 | 19 | constructor (props,children) { 20 | super(props,children) 21 | this._engine = props.engine 22 | this._statusBar = props.statusBar 23 | } 24 | 25 | init () { 26 | if (!this.props.state) { 27 | this.props.state = { 28 | active: false 29 | } 30 | } 31 | super.init() 32 | this._tile = this.props.statusBar.addLeftTile({item: this.element, priority: -99}) 33 | } 34 | 35 | toggleDebugging() { 36 | this._engine.createDebuggingContext("default").then( (context) => { 37 | const uiService = context.getUIService() 38 | if (uiService != null) { 39 | uiService.toggleDebugger() 40 | } 41 | }) 42 | } 43 | 44 | setActive (active) { 45 | const state = Object.assign({}, this.props.state); 46 | state._active = active; 47 | this.update({state:state}); 48 | } 49 | 50 | destroy() { 51 | if (this._tile) { 52 | this._tile.destroy() 53 | this._tile = null 54 | } 55 | super.destroy() 56 | } 57 | } 58 | DebugView.bindFns = ["toggleDebugging","setActive"] 59 | -------------------------------------------------------------------------------- /lib/ui/component.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import { CompositeDisposable } from 'atom' 5 | import etch from 'etch' 6 | import { shallowEqual } from '../helpers' 7 | 8 | export default class UiComponent { 9 | constructor (props, children) { 10 | this.subscriptions = new CompositeDisposable(); 11 | this.props = props 12 | this.children = children 13 | 14 | const { bindFns } = this.constructor 15 | if (bindFns) { 16 | bindFns.forEach((fn) => { this[fn] = this[fn].bind(this) }) 17 | } 18 | 19 | this.init() 20 | } 21 | 22 | init () { 23 | etch.initialize(this) 24 | } 25 | 26 | shouldUpdate (newProps) { 27 | return !shallowEqual(this.props, newProps) 28 | } 29 | 30 | update (props, children, force) { 31 | if ((force == undefined || force == false) && !this.shouldUpdate(props)) { 32 | return Promise.resolve() 33 | } 34 | this.props = Object.assign({}, this.props, props) 35 | this.children = children 36 | return etch.update(this) 37 | } 38 | 39 | destroy (removeNode = false) { 40 | this.subscriptions.dispose(); 41 | etch.destroy(this, removeNode) 42 | } 43 | 44 | dispose () { 45 | this.destroy() 46 | } 47 | 48 | render () { 49 | throw new Error('Ui components must implement a `render` method') 50 | } 51 | } 52 | 53 | etch.setScheduler(atom.views) 54 | -------------------------------------------------------------------------------- /lib/ui/double-filter-list-view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | const {Disposable, CompositeDisposable, TextEditor} = require('atom') 5 | const etch = require('etch') 6 | const $ = etch.dom 7 | const fuzzaldrin = require('fuzzaldrin') 8 | 9 | export default class DoubleFilterListView { 10 | static setScheduler (scheduler) { 11 | etch.setScheduler(scheduler) 12 | } 13 | 14 | static getScheduler (scheduler) { 15 | return etch.getScheduler() 16 | } 17 | 18 | constructor (props) { 19 | this.props = props 20 | if (!this.props.hasOwnProperty('initialSelectionIndex')) { 21 | this.props.initialSelectionIndex = 0 22 | } 23 | if (props.initiallyVisibleItemCount) { 24 | this.initializeVisibilityObserver() 25 | } 26 | this.computeItems(false) 27 | this.disposables = new CompositeDisposable() 28 | etch.initialize(this) 29 | this.element.classList.add('select-list') 30 | if (props.hasOwnProperty('firstQuery') && props.firstQuery != null && props.firstQuery != "") { 31 | this.refs.firstQueryEditor.setText(props.firstQuery) 32 | } 33 | 34 | if (props.hasOwnProperty('secondQuery') && props.secondQuery != null && props.secondQuery != "") { 35 | this.refs.secondQueryEditor.setText(props.secondQuery) 36 | } 37 | this.disposables.add(this.refs.firstQueryEditor.onDidChange(this.didChangeQuery.bind(this))) 38 | this.disposables.add(this.refs.secondQueryEditor.onDidChange(this.didChangeQuery.bind(this))) 39 | if (!props.skipCommandsRegistration) { 40 | this.disposables.add(this.registerAtomCommands()) 41 | } 42 | const firstEditor = this.refs.firstQueryEditor.element 43 | const secondEditor = this.refs.secondQueryEditor.element 44 | const didLoseFocus = this.didLoseFocus.bind(this) 45 | //firstEditor.addEventListener('blur', didLoseFocus) 46 | //secondEditor.addEventListener('blur', didLoseFocus) 47 | //this.disposables.add(new Disposable(() => { firstEditor.removeEventListener('blur', didLoseFocus) })) 48 | //this.disposables.add(new Disposable(() => { secondEditor.removeEventListener('blur', didLoseFocus) })) 49 | // Should be fixed in https://github.com/atom/atom/pull/16492 50 | this.refs.firstQueryEditor.component.refs.cursorsAndInput.refs.hiddenInput.setAttribute("tabindex","1") 51 | this.refs.secondQueryEditor.component.refs.cursorsAndInput.refs.hiddenInput.setAttribute("tabindex","2") 52 | 53 | } 54 | 55 | initializeVisibilityObserver () { 56 | this.visibilityObserver = new IntersectionObserver(changes => { 57 | // Since we observe fake items only, whenever it changes, just render to real item. 58 | // No need to check change.intersectionRatio. 59 | for (const change of changes) { 60 | const element = change.target 61 | this.visibilityObserver.unobserve(element) 62 | const index = Array.from(this.refs.items.children).indexOf(element) 63 | if (index >= 0) { 64 | this.renderItemAtIndex(index) 65 | } 66 | } 67 | }) 68 | } 69 | 70 | focus () { 71 | this.refs.firstQueryEditor.element.focus() 72 | } 73 | 74 | didLoseFocus (event) { 75 | if (this.element.contains(event.relatedTarget)) { 76 | this.refs.firstQueryEditor.element.focus() 77 | } else if (document.hasFocus()) { 78 | this.cancelSelection() 79 | } 80 | } 81 | 82 | reset () { 83 | this.refs.firstQueryEditor.setText('') 84 | this.refs.secondQueryEditor.setText('') 85 | } 86 | 87 | destroy () { 88 | this.disposables.dispose() 89 | if (this.visibilityObserver) this.visibilityObserver.disconnect() 90 | return etch.destroy(this) 91 | } 92 | 93 | registerAtomCommands () { 94 | return global.atom.commands.add(this.element, { 95 | 'core:move-up': (event) => { 96 | this.selectPrevious() 97 | event.stopPropagation() 98 | }, 99 | 'core:move-down': (event) => { 100 | this.selectNext() 101 | event.stopPropagation() 102 | }, 103 | 'core:move-to-top': (event) => { 104 | this.selectFirst() 105 | event.stopPropagation() 106 | }, 107 | 'core:move-to-bottom': (event) => { 108 | this.selectLast() 109 | event.stopPropagation() 110 | }, 111 | 'core:confirm': (event) => { 112 | this.confirmSelection() 113 | event.stopPropagation() 114 | }, 115 | 'core:cancel': (event) => { 116 | this.cancelSelection() 117 | event.stopPropagation() 118 | } 119 | }) 120 | } 121 | 122 | update (props = {}) { 123 | let shouldComputeItems = false 124 | 125 | if (props.hasOwnProperty('items')) { 126 | this.props.items = props.items 127 | shouldComputeItems = true 128 | } 129 | 130 | if (props.hasOwnProperty('maxResults')) { 131 | this.props.maxResults = props.maxResults 132 | shouldComputeItems = true 133 | } 134 | 135 | if (props.hasOwnProperty('filter')) { 136 | this.props.filter = props.filter 137 | shouldComputeItems = true 138 | } 139 | 140 | if (props.hasOwnProperty('filterQuery')) { 141 | this.props.filterQuery = props.filterQuery 142 | shouldComputeItems = true 143 | } 144 | 145 | if (props.hasOwnProperty('firstQuery')) { 146 | // Items will be recomputed as part of the change event handler, so we 147 | // don't need to recompute them again at the end of this function. 148 | this.refs.firstQueryEditor.setText(props.firstQuery) 149 | shouldComputeItems = false 150 | } 151 | 152 | if (props.hasOwnProperty('secondQuery')) { 153 | // Items will be recomputed as part of the change event handler, so we 154 | // don't need to recompute them again at the end of this function. 155 | this.refs.secondQueryEditor.setText(props.secondQuery) 156 | shouldComputeItems = false 157 | } 158 | 159 | if (props.hasOwnProperty('selectFirstQuery')) { 160 | if (props.selectFirstQuery) { 161 | this.refs.firstQueryEditor.selectAll() 162 | } else { 163 | this.refs.secondQueryEditor.clearSelections() 164 | } 165 | } 166 | 167 | if (props.hasOwnProperty('selectSecondQuery')) { 168 | if (props.selectSecondQuery) { 169 | this.refs.secondQueryEditor.selectAll() 170 | } else { 171 | this.refs.secondQueryEditor.clearSelections() 172 | } 173 | } 174 | 175 | if (props.hasOwnProperty('order')) { 176 | this.props.order = props.order 177 | } 178 | 179 | if (props.hasOwnProperty('emptyMessage')) { 180 | this.props.emptyMessage = props.emptyMessage 181 | } 182 | 183 | if (props.hasOwnProperty('errorMessage')) { 184 | this.props.errorMessage = props.errorMessage 185 | } 186 | 187 | if (props.hasOwnProperty('infoMessage')) { 188 | this.props.infoMessage = props.infoMessage 189 | } 190 | 191 | if (props.hasOwnProperty('loadingMessage')) { 192 | this.props.loadingMessage = props.loadingMessage 193 | } 194 | 195 | if (props.hasOwnProperty('loadingBadge')) { 196 | this.props.loadingBadge = props.loadingBadge 197 | } 198 | 199 | if (props.hasOwnProperty('itemsClassList')) { 200 | this.props.itemsClassList = props.itemsClassList 201 | } 202 | 203 | if (props.hasOwnProperty('initialSelectionIndex')) { 204 | this.props.initialSelectionIndex = props.initialSelectionIndex 205 | } 206 | 207 | if (shouldComputeItems) { 208 | this.computeItems() 209 | } 210 | 211 | return etch.update(this) 212 | } 213 | 214 | render () { 215 | return $.div( 216 | {}, 217 | this.renderFirstLabel(), 218 | $(TextEditor, {ref: 'firstQueryEditor', mini: true}), 219 | this.renderSecondLabel(), 220 | $(TextEditor, {ref: 'secondQueryEditor', mini: true}), 221 | this.renderLoadingMessage(), 222 | this.renderInfoMessage(), 223 | this.renderErrorMessage(), 224 | this.renderItems() 225 | ) 226 | } 227 | 228 | renderFirstLabel () { 229 | if (this.props.hasOwnProperty("firstLabel")) { 230 | return $.span({ref: 'firstLabel'}, this.props.firstLabel) 231 | } else { 232 | return '' 233 | } 234 | } 235 | 236 | renderSecondLabel () { 237 | if (this.props.hasOwnProperty("secondLabel")) { 238 | return $.span({ref: 'secondLabel'}, this.props.secondLabel) 239 | } else { 240 | return '' 241 | } 242 | } 243 | 244 | renderItems () { 245 | if (this.items.length > 0) { 246 | const className = ['list-group'].concat(this.props.itemsClassList || []).join(' ') 247 | 248 | if (this.visibilityObserver) { 249 | etch.getScheduler().updateDocument(() => { 250 | Array.from(this.refs.items.children).slice(this.props.initiallyVisibleItemCount).forEach(element => { 251 | this.visibilityObserver.observe(element) 252 | }) 253 | }) 254 | } 255 | 256 | this.listItems = this.items.map((item, index) => { 257 | const selected = this.getSelectedItem() === item 258 | const visible = !this.props.initiallyVisibleItemCount || index < this.props.initiallyVisibleItemCount 259 | return $(ListItemView, { 260 | element: this.props.elementForItem(item, {selected, index, visible}), 261 | selected: selected, 262 | onclick: () => this.didClickItem(index) 263 | }) 264 | }) 265 | 266 | return $.ol( 267 | {className, ref: 'items'}, 268 | ...this.listItems 269 | ) 270 | } else if (!this.props.loadingMessage && this.props.emptyMessage) { 271 | return $.span({ref: 'emptyMessage'}, this.props.emptyMessage) 272 | } else { 273 | return "" 274 | } 275 | } 276 | 277 | renderErrorMessage () { 278 | if (this.props.errorMessage) { 279 | return $.span({ref: 'errorMessage'}, this.props.errorMessage) 280 | } else { 281 | return '' 282 | } 283 | } 284 | 285 | renderInfoMessage () { 286 | if (this.props.infoMessage) { 287 | return $.span({ref: 'infoMessage'}, this.props.infoMessage) 288 | } else { 289 | return '' 290 | } 291 | } 292 | 293 | renderLoadingMessage () { 294 | if (this.props.loadingMessage) { 295 | return $.div( 296 | {className: 'loading'}, 297 | $.span({ref: 'loadingMessage', className: 'loading-message'}, this.props.loadingMessage), 298 | this.props.loadingBadge ? $.span({ref: 'loadingBadge', className: 'badge'}, this.props.loadingBadge) : '' 299 | ) 300 | } else { 301 | return '' 302 | } 303 | } 304 | 305 | getQuery () { 306 | if (this.refs && this.refs.firstQueryEditor) { 307 | return {first:this.refs.firstQueryEditor.getText(), second:this.refs.secondQueryEditor.getText()} 308 | } else { 309 | return {first:'',second:''} 310 | } 311 | } 312 | 313 | getFilterQuery () { 314 | return this.props.filterQuery ? this.props.filterQuery(this.getQuery()) : this.getQuery() 315 | } 316 | 317 | didChangeQuery () { 318 | this.initialSelectionChanged = true 319 | if (this.props.didChangeQuery) { 320 | this.props.didChangeQuery(this.getFilterQuery()) 321 | } 322 | 323 | this.computeItems() 324 | } 325 | 326 | didClickItem (itemIndex) { 327 | this.selectIndex(itemIndex) 328 | this.confirmSelection() 329 | } 330 | 331 | computeItems (updateComponent) { 332 | this.listItems = null 333 | if (this.visibilityObserver) this.visibilityObserver.disconnect() 334 | const filterFn = this.props.filter || this.fuzzyFilter.bind(this) 335 | this.items = filterFn(this.props.items.slice(), this.getFilterQuery()) 336 | if (this.props.order) { 337 | this.items.sort(this.props.order) 338 | } 339 | if (this.props.maxResults) { 340 | this.items = this.items.slice(0, this.props.maxResults) 341 | } 342 | 343 | this.selectIndex(this.props.initialSelectionIndex, updateComponent) 344 | } 345 | 346 | 347 | fuzzyFilter (items, query) { 348 | if (query.first.length === 0 && query.second.length === 0) { 349 | return items 350 | } else { 351 | const scoredItems = [] 352 | for (const item of items) { 353 | const filterKeys = this.props.filterKeysForItem ? this.props.filterKeysForItem(item) : item 354 | const firstString = filterKeys.first 355 | const secondString = filterKeys.second 356 | let firstScore = 0 357 | let secondScore = 0 358 | if (query.first.length > 0) { 359 | firstScore = fuzzaldrin.score(firstString, query.first) 360 | } 361 | if (query.second.length > 0) { 362 | secondScore = fuzzaldrin.score(secondString, query.second) 363 | } 364 | let score = firstScore + secondScore 365 | if (score > 0) { 366 | if (firstString.toLowerCase().includes(query.first.toLowerCase()) && secondString.toLowerCase().includes(query.second.toLowerCase())) { 367 | scoredItems.push({item, score}) 368 | } 369 | } 370 | } 371 | scoredItems.sort((a, b) => b.score - a.score) 372 | return scoredItems.map((i) => i.item) 373 | } 374 | } 375 | 376 | getSelectedItem () { 377 | if (this.selectionIndex === undefined) return null 378 | return this.items[this.selectionIndex] 379 | } 380 | 381 | renderItemAtIndex (index) { 382 | const item = this.items[index] 383 | const selected = this.getSelectedItem() === item 384 | /*const component = this.listItems[index].component 385 | if (this.visibilityObserver) this.visibilityObserver.unobserve(component.element) 386 | component.update({ 387 | element: this.props.elementForItem(item, {selected, index, visible: true}), 388 | selected: selected, 389 | onclick: () => this.didClickItem(index) 390 | })*/ 391 | } 392 | 393 | selectPrevious () { 394 | if (this.selectionIndex === undefined) return this.selectLast() 395 | return this.selectIndex(this.selectionIndex - 1) 396 | } 397 | 398 | selectNext () { 399 | if (this.selectionIndex === undefined) return this.selectFirst() 400 | return this.selectIndex(this.selectionIndex + 1) 401 | } 402 | 403 | selectFirst () { 404 | return this.selectIndex(0) 405 | } 406 | 407 | selectLast () { 408 | return this.selectIndex(this.items.length - 1) 409 | } 410 | 411 | selectNone () { 412 | return this.selectIndex(undefined) 413 | } 414 | 415 | selectIndex (index, updateComponent = true) { 416 | if (index >= this.items.length) { 417 | index = 0 418 | } else if (index < 0) { 419 | index = this.items.length - 1 420 | } 421 | 422 | const oldIndex = this.selectionIndex 423 | 424 | this.selectionIndex = index 425 | if (index !== undefined && this.props.didChangeSelection) { 426 | this.props.didChangeSelection(this.getSelectedItem()) 427 | } 428 | 429 | if (updateComponent) { 430 | if (this.listItems) { 431 | if (oldIndex >= 0) this.renderItemAtIndex(oldIndex) 432 | if (index >= 0) this.renderItemAtIndex(index) 433 | return etch.getScheduler().getNextUpdatePromise() 434 | } else { 435 | return etch.update(this) 436 | } 437 | } else { 438 | return Promise.resolve() 439 | } 440 | } 441 | 442 | selectItem (item) { 443 | const index = this.items.indexOf(item) 444 | if (index === -1) { 445 | throw new Error('Cannot select the specified item because it does not exist.') 446 | } else { 447 | return this.selectIndex(index) 448 | } 449 | } 450 | 451 | confirmSelection () { 452 | const selectedItem = this.getSelectedItem() 453 | if (selectedItem != null) { 454 | if (this.props.didConfirmSelection) { 455 | this.props.didConfirmSelection(selectedItem) 456 | } 457 | } else { 458 | if (this.props.didConfirmEmptySelection) { 459 | this.props.didConfirmEmptySelection() 460 | } 461 | } 462 | } 463 | 464 | cancelSelection () { 465 | if (this.props.didCancelSelection) { 466 | this.props.didCancelSelection() 467 | } 468 | } 469 | } 470 | 471 | class ListItemView { 472 | constructor (props) { 473 | this.mouseDown = this.mouseDown.bind(this) 474 | this.mouseUp = this.mouseUp.bind(this) 475 | this.didClick = this.didClick.bind(this) 476 | this.selected = props.selected 477 | this.onclick = props.onclick 478 | this.element = props.element 479 | this.element.addEventListener('mousedown', this.mouseDown) 480 | this.element.addEventListener('mouseup', this.mouseUp) 481 | this.element.addEventListener('click', this.didClick) 482 | if (this.selected) { 483 | this.element.classList.add('selected') 484 | } 485 | this.domEventsDisposable = new Disposable(() => { 486 | this.element.removeEventListener('mousedown', this.mouseDown) 487 | this.element.removeEventListener('mouseup', this.mouseUp) 488 | this.element.removeEventListener('click', this.didClick) 489 | }) 490 | etch.getScheduler().updateDocument(this.scrollIntoViewIfNeeded.bind(this)) 491 | } 492 | 493 | mouseDown (event) { 494 | event.preventDefault() 495 | } 496 | 497 | mouseUp (event) { 498 | event.preventDefault() 499 | } 500 | 501 | didClick (event) { 502 | event.preventDefault() 503 | this.onclick() 504 | } 505 | 506 | destroy () { 507 | this.element.remove() 508 | this.domEventsDisposable.dispose() 509 | } 510 | 511 | update (props) { 512 | this.element.removeEventListener('mousedown', this.mouseDown) 513 | this.element.removeEventListener('mouseup', this.mouseUp) 514 | this.element.removeEventListener('click', this.didClick) 515 | 516 | this.element.parentNode.replaceChild(props.element, this.element) 517 | this.element = props.element 518 | this.element.addEventListener('mousedown', this.mouseDown) 519 | this.element.addEventListener('mouseup', this.mouseUp) 520 | this.element.addEventListener('click', this.didClick) 521 | if (props.selected) { 522 | this.element.classList.add('selected') 523 | } 524 | 525 | this.selected = props.selected 526 | this.onclick = props.onclick 527 | etch.getScheduler().updateDocument(this.scrollIntoViewIfNeeded.bind(this)) 528 | } 529 | 530 | scrollIntoViewIfNeeded () { 531 | if (this.selected) { 532 | this.element.scrollIntoViewIfNeeded(false) 533 | } 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /lib/ui/editor.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | import { TextEditor } from 'atom' 6 | 7 | import UiComponent from './component' 8 | 9 | export default class Editor extends UiComponent { 10 | constructor () { 11 | super(...arguments) 12 | 13 | let text = '' 14 | const explore = (elt) => { 15 | if (elt.text) { 16 | text += elt.text 17 | } 18 | 19 | if (elt.children) { 20 | for (const ch of elt.children) { 21 | explore(ch) 22 | } 23 | } 24 | } 25 | explore(this) 26 | 27 | this.model = this.refs.editor 28 | this.element = this.model.element 29 | this.model.setText(text) 30 | this.subscribeToEvents() 31 | if (this.props.grammar) { 32 | this.model.setGrammar(atom.grammars.grammarForScopeName(this.props.grammar)) 33 | } 34 | } 35 | 36 | getText() { 37 | return this.model.getText() 38 | } 39 | 40 | setText(text) { 41 | this.model.setText(text) 42 | } 43 | 44 | render () { 45 | return 46 | } 47 | 48 | subscribeToEvents() { 49 | // event subscription! 50 | if (this.props) { 51 | for (const evt in this.props.on) { 52 | const modelEvent = `on${evt[0].toUpperCase()}${evt.substring(1)}`; 53 | if (this.model[modelEvent]) { 54 | const handler = this.props.on[evt] 55 | this.subscriptions.add(this.model[modelEvent](handler)) 56 | this.props.on[evt] = null 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /menus/php-debugx.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'PHP-Debug' 7 | 'submenu': [ 8 | { 9 | 'label': 'Run' 10 | 'command': 'php-debug:run' 11 | }, 12 | { 13 | 'label': 'Step Over' 14 | 'command': 'php-debug:stepOver' 15 | }, 16 | { 17 | 'label': 'Step In' 18 | 'command': 'php-debug:stepIn' 19 | }, 20 | { 21 | 'label': 'Step Out' 22 | 'command': 'php-debug:stepOut' 23 | }, 24 | ] 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-debug", 3 | "main": "./lib/php-debug", 4 | "version": "0.3.5", 5 | "description": "A package for the Atom Text Editor for debugging PHP code using the XDebug extension", 6 | "repository": "https://github.com/gwomacks/php-debug", 7 | "bugs": { 8 | "url" :"https://github.com/gwomacks/php-debug/issues" 9 | }, 10 | "license": "MIT", 11 | "engines": { 12 | "atom": ">=1.17.0 <2.0.0" 13 | }, 14 | "keywords" : [ 15 | "php", 16 | "phpdebug", 17 | "php-debug", 18 | "debugging", 19 | "debugger", 20 | "debug", 21 | "xdebug" 22 | ], 23 | "dependencies": { 24 | "etch": "^0.12.6", 25 | "event-kit": "^1.0.3", 26 | "q": "^1.1.2", 27 | "xml2js": ">= 0.4.6 < 0.5", 28 | "uuid": "3.0.1", 29 | "auto-bind-inheritance" : "^1.0.6", 30 | "promise" : "^8.0.1", 31 | "fast-glob": "^2.2.1", 32 | "fuzzaldrin": "^2.1.0", 33 | "compare-versions": "^3.1.0" 34 | }, 35 | "providedServices" : { 36 | "debug-engine" : { 37 | "versions" : { 38 | "0.1.0" : "provideDebugEngineService" 39 | } 40 | } 41 | }, 42 | "consumedServices": { 43 | "debug-ui": { 44 | "versions": { 45 | "0.1.0": "consumeDebugUI" 46 | } 47 | }, 48 | "status-bar": { 49 | "versions": { 50 | "^1.0.0": "consumeStatusBar" 51 | } 52 | }, 53 | "datatip": { 54 | "versions": { 55 | "0.1.0": "consumeDatatip" 56 | } 57 | } 58 | }, 59 | "configSchema" : { 60 | "server" : { 61 | "type" : "object", 62 | "order": 1, 63 | "properties" : { 64 | "serverAddress" : { 65 | "title" : "Server Listen Address", 66 | "default" : "*", 67 | "type" : "string", 68 | "description" : "The IP for the debug server to listen on. Use * for any (0.0.0.0)", 69 | "order": 1 70 | }, 71 | "serverPort" : { 72 | "title" : "Server Listen Port", 73 | "default" : 9000, 74 | "type" : "integer", 75 | "description" : "The port for the debug server to listen on", 76 | "order": 2 77 | }, 78 | "keepAlive" : { 79 | "title" : "Continue to listen for debug sessions even if the debugger windows are all closed", 80 | "default" : false, 81 | "type" : "boolean", 82 | "description" : "Allow PHP-Debug to continue to handle sessions after closing the windows. They will be reopened on new connections", 83 | "order": 4 84 | }, 85 | "protocolDebugging" : { 86 | "title" : "Xdebug DBGP Protocol Debugging", 87 | "default" : false, 88 | "type" : "boolean", 89 | "description" : "Output received DBGP messsages to the Atom console", 90 | "order": 5 91 | }, 92 | "redirectStdout" : { 93 | "title" : "Automatically redirect Stdout to the console", 94 | "default" : false, 95 | "type" : "boolean", 96 | "description" : "Output all PHP Stdout data to the console", 97 | "order": 6 98 | }, 99 | "redirectStderr" : { 100 | "title" : "Automatically redirect Stderr to the console", 101 | "default" : false, 102 | "type" : "boolean", 103 | "description" : "Output all PHP Stderr data to the console", 104 | "order": 7 105 | } 106 | } 107 | }, 108 | "xdebug" : { 109 | "title" : "Xdebug", 110 | "type" : "object", 111 | "order": 2, 112 | "properties" : { 113 | "maxDepth" : { 114 | "title" : "Max Depth", 115 | "default" : 4, 116 | "type" : "integer", 117 | "description" : "", 118 | "order": 1 119 | }, 120 | "maxChildren" : { 121 | "title" : "Max Children", 122 | "default" : 32, 123 | "type" : "integer", 124 | "description" : "", 125 | "order": 2 126 | }, 127 | "maxData" : { 128 | "title" : "Max Data", 129 | "default" : 1024, 130 | "type" : "integer", 131 | "description" : "", 132 | "order": 3 133 | }, 134 | "pathMaps" : { 135 | "title" : "Path Mappings", 136 | "type" : "string", 137 | "default" : "", 138 | "description" : "JSON Object of Path Mappings", 139 | "order": 4 140 | }, 141 | "projectScan" : { 142 | "title" : "Automatically scan projects in Atom to try and find path maps", 143 | "default" : true, 144 | "type" : "boolean", 145 | "description" : "When a project has no path map set automatically look through all the projects in Atom to try and find a match", 146 | "order": 5 147 | }, 148 | "multipleSessions" : { 149 | "title" : "Allow for multiple debug sessions at once", 150 | "default" : true, 151 | "type" : "boolean", 152 | "description" : "Allows PHP-Debug to handle multiple debug sessions at the same time", 153 | "order": 6 154 | } 155 | } 156 | }, 157 | "display" : { 158 | "title" : "Display", 159 | "type" : "object", 160 | "order": 3, 161 | "properties" : { 162 | "arraySort" : { 163 | "title" : "Sort Array/Object Elements Alphabetically", 164 | "default" : false, 165 | "type" : "boolean", 166 | "description" : "", 167 | "order": 1 168 | }, 169 | "enableStatusbarButtons" : { 170 | "title" : "Allow PHP-Debug to be opened from the Atom status bar", 171 | "default" : true, 172 | "type" : "boolean", 173 | "description" : "", 174 | "order": 2 175 | }, 176 | "views" : { 177 | "title" : "Views", 178 | "type" : "object", 179 | "properties" : { 180 | "breakpoints" : { 181 | "title" : "Breakpoints", 182 | "default" : true, 183 | "type" : "boolean", 184 | "description" : "Show breakpoints panel", 185 | "order": 1 186 | }, 187 | "watches" : { 188 | "title" : "Watches", 189 | "default" : true, 190 | "type" : "boolean", 191 | "description" : "Show watches panel", 192 | "order": 2 193 | }, 194 | "context" : { 195 | "title" : "Context", 196 | "default" : true, 197 | "type" : "boolean", 198 | "description" : "Show context panel", 199 | "order": 3 200 | }, 201 | "scope" : { 202 | "title" : "Scope", 203 | "default" : true, 204 | "type" : "boolean", 205 | "description" : "Show scope panel", 206 | "order": 5 207 | } 208 | } 209 | } 210 | } 211 | }, 212 | "exceptions" : { 213 | "title" : "Exceptions", 214 | "type" : "object", 215 | "order": 4, 216 | "properties" : { 217 | "fatalError" : { 218 | "title" : "Fatal Errors", 219 | "default" : true, 220 | "type" : "boolean", 221 | "description" : "Break on fatal error exceptions", 222 | "order": 1 223 | }, 224 | "catchableFatalError" : { 225 | "title" : "Catchable Fatal Errors", 226 | "default" : true, 227 | "type" : "boolean", 228 | "description" : "Break on catchable fatal error exceptions", 229 | "order": 2 230 | }, 231 | "warning" : { 232 | "title" : "Warnings", 233 | "default" : true, 234 | "type" : "boolean", 235 | "description" : "Break on PHP warnings", 236 | "order": 3 237 | }, 238 | "strictStandards" : { 239 | "title" : "Strict Standards", 240 | "default" : true, 241 | "type" : "boolean", 242 | "description" : "Break on strict standards messages", 243 | "order": 4 244 | }, 245 | "xdebug" : { 246 | "title" : "Xdebug Exceptions", 247 | "default" : true, 248 | "type" : "boolean", 249 | "description" : "Break on xdebug messages", 250 | "order": 5 251 | }, 252 | "unknownError" : { 253 | "title" : "Unknown Errors", 254 | "default" : true, 255 | "type" : "boolean", 256 | "description" : "Break on unknown errors", 257 | "order": 6 258 | }, 259 | "notice" : { 260 | "title" : "Notice", 261 | "default" : true, 262 | "type" : "boolean", 263 | "description" : "Break on notice messages", 264 | "order": 7 265 | }, 266 | "all": { 267 | "title": "All Exceptions/Errors", 268 | "default": false, 269 | "type": "boolean", 270 | "description": "Break on all thrown errors and exceptions", 271 | "order": 8 272 | } 273 | } 274 | }, 275 | "noPackageInstallPrompt": { 276 | "title": "Disabled Prompting to install Packages", 277 | "description": "php-debug requires some third party packages, this disables prompting to install for them", 278 | "type": "array", 279 | "default": [], 280 | "items": { 281 | "type": "string" 282 | }, 283 | "order": 5 284 | }, 285 | "showWelcome": { 286 | "title": "Show welcome message for new version", 287 | "description": "", 288 | "type": "boolean", 289 | "default": true, 290 | "order": 6 291 | }, 292 | "pathMapsSearchIgnore": { 293 | "title": "Ignore paths during pathmaps search", 294 | "description": "Comma separated list of paths to ignore during pathmaps search", 295 | "type": "array", 296 | "default": ["!**/.git/**","!**/.*/**"], 297 | "items": { 298 | "type": "string" 299 | }, 300 | "order": 7 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwomacks/php-debug/b586c08c50edd4fb0269db0a569a097e494d2355/screenshot.gif -------------------------------------------------------------------------------- /spec/php-debug-spec.coffee: -------------------------------------------------------------------------------- 1 | PhpDebug = require '../lib/php-debug' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "PhpDebug", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('php-debug') 14 | 15 | describe "when the php-debug:toggle event is triggered", -> 16 | it "hides and shows the modal panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.php-debug')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'php-debug:toggleDebugging' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.php-debug')).toExist() 30 | 31 | phpDebugElement = workspaceElement.querySelector('.php-debug') 32 | expect(phpDebugElement).toExist() 33 | 34 | phpDebugPanel = atom.workspace.panelForItem(phpDebugElement) 35 | expect(phpDebugPanel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'php-debug:toggleDebugging' 37 | expect(phpDebugPanel.isVisible()).toBe false 38 | 39 | it "hides and shows the view", -> 40 | # This test shows you an integration test testing at the view level. 41 | 42 | # Attaching the workspaceElement to the DOM is required to allow the 43 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 44 | # requires that the workspaceElement is on the DOM. Tests that attach the 45 | # workspaceElement to the DOM are generally slower than those off DOM. 46 | jasmine.attachToDOM(workspaceElement) 47 | 48 | expect(workspaceElement.querySelector('.php-debug')).not.toExist() 49 | 50 | # This is an activation event, triggering it causes the package to be 51 | # activated. 52 | atom.commands.dispatch workspaceElement, 'php-debug:toggleDebugging' 53 | 54 | waitsForPromise -> 55 | activationPromise 56 | 57 | runs -> 58 | # Now we can test for view visibility 59 | phpDebugElement = workspaceElement.querySelector('.php-debug') 60 | expect(phpDebugElement).toBeVisible() 61 | atom.commands.dispatch workspaceElement, 'php-debug:toggleDebugging' 62 | expect(phpDebugElement).not.toBeVisible() 63 | -------------------------------------------------------------------------------- /spec/php-debug-view-spec.coffee: -------------------------------------------------------------------------------- 1 | PhpDebugView = require '../lib/php-debug-view' 2 | 3 | describe "PhpDebugView", -> 4 | it "has one valid test", -> 5 | expect("life").toBe "easy" 6 | -------------------------------------------------------------------------------- /styles/php-debug.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | 4 | .php-debug-debug-view-toggle, 5 | .php-debug-console-view-toggle { 6 | display: inline-block; 7 | color: @text-color-subtle; 8 | border: 1px solid @button-border-color; 9 | background: fade(@button-background-color, 33%); 10 | cursor: pointer; 11 | vertical-align: top; 12 | height:100%; 13 | position: relative; 14 | padding: 0 .6em; 15 | line-height: 2.0em; 16 | margin-right: 0.6em; 17 | 18 | &:active { 19 | background: transparent; 20 | } 21 | &.active { 22 | color: @text-color-highlight; 23 | background: @button-background-color; 24 | } 25 | } 26 | 27 | .php-debug-pathmaps-view .btn-ignore { 28 | float: right; 29 | } 30 | 31 | .php-debug-pathmaps-view .pathmaps-extra { 32 | margin-top: 0.5em; 33 | } 34 | .php-debug-pathmaps-view .pathmaps-important { 35 | font-weight:bold; 36 | } 37 | 38 | .php-debug-pathmaps-view .pathmaps-settings-actions { 39 | margin-top: 1.5em; 40 | } 41 | 42 | .php-debug-pathmaps-view .pathmaps-info { 43 | display: block; 44 | } 45 | 46 | .php-debug-pathmaps-view .pathmaps-uri-info { 47 | display:block; 48 | } 49 | 50 | .php-debug-pathmaps-view .pathmaps-uri-info .pathmaps-uri-base { 51 | font-weight:bold; 52 | } 53 | -------------------------------------------------------------------------------- /styles/php-debug.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/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | @import (inline) "../iconsets/materialdesignicons.css"; 7 | 8 | .atom-debug-ui { 9 | .action-bar { 10 | .pathmaps-btn { 11 | float:left; 12 | } 13 | } 14 | .default-breakpoints-view { 15 | .breakpoint-enabled { 16 | margin-right: 1em; 17 | } 18 | .default-breakpoints-header { 19 | display:block; 20 | font-weight:bold; 21 | margin-left: 1em; 22 | margin-bottom: 0.5em; 23 | } 24 | } 25 | } 26 | .atom-debug-ui-console { 27 | .redirect-stderr-btn.btn-active, 28 | .redirect-stdout-btn.btn-active { 29 | background-color: @background-color-info; 30 | background-image: -webkit-linear-gradient(@background-color-info, darken(@background-color-info, 8%)); 31 | color: #FFF; 32 | } 33 | } 34 | --------------------------------------------------------------------------------