From a5e14550993a65026197247b7432fd2725f83670 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 17 Feb 2016 18:35:30 +0100 Subject: [PATCH] Adapt VS Code January release 0.10.8, implement SourceRequest --- .vscode/launch.json | 10 ++-- README.md | 4 +- package.json | 14 ++--- src/phpDebug.ts | 116 ++++++++++++++++++++-------------------- src/xdebugConnection.ts | 15 ++++++ 5 files changed, 87 insertions(+), 72 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c5a7e715..68d27665 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "runtimeArgs": [ "--harmony" ], - "program": "./src/phpDebug.ts", + "program": "${workspaceRoot}/src/phpDebug.ts", "stopOnEntry": false, "args": [ "--server=4711" @@ -17,7 +17,7 @@ "NODE_ENV": "development" }, "sourceMaps": true, - "outDir": "./out" + "outDir": "${workspaceRoot}/out" }, { "name": "Launch Extension", @@ -28,13 +28,13 @@ "--extensionDevelopmentPath=${workspaceRoot}" ], "sourceMaps": true, - "outDir": "./out" + "outDir": "${workspaceRoot}/out" }, { "name": "Run Tests", "type": "node", "request": "launch", - "program": "node_modules/mocha/bin/_mocha", + "program": "${workspaceRoot}node_modules/mocha/bin/_mocha", "args": [ "./out/tests", "--timeout", @@ -42,7 +42,7 @@ "--colors" ], "sourceMaps": true, - "outDir": "./out" + "outDir": "${workspaceRoot}/out" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 71b65019..9c2c50f5 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,9 @@ If you want to debug a running application on a remote host, you have to set the Example: ```json "serverSourceRoot": "/var/www/myproject", -"localSourceRoot": "./src" +"localSourceRoot": "${workspaceRoot}/src" ``` -`localSourceRoot` is resolved relative to the project root (the currently opened folder in VS Code). Both paths are normalized, so you can use slashes or backslashes no matter of the OS you're running. -If no `localSourceRoot` is specified, the project root is assumed. Troubleshooting --------------- diff --git a/package.json b/package.json index 1dc0cd33..53bf3754 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "email": "felix.b@outlook.com" }, "engines": { - "vscode": "^0.10.6", + "vscode": "^0.10.8", "node": "^4.1.1" }, "icon": "images/logo.svg", @@ -34,14 +34,14 @@ }, "dependencies": { "iconv-lite": "^0.4.13", - "moment": "^2.10.6", + "moment": "^2.11.2", "url-relative": "^1.0.0", - "vscode-debugadapter": "^1.0.3", - "vscode-debugprotocol": "^1.0.1", - "xmldom": "^0.1.19" + "vscode-debugadapter": "^1.5.0", + "vscode-debugprotocol": "^1.5.0", + "xmldom": "^0.1.22" }, "devDependencies": { - "typescript": "^1.6.2" + "typescript": "^1.7.5" }, "scripts": { "compile": "tsc -p ./src", @@ -82,7 +82,7 @@ }, "localSourceRoot": { "type": "string", - "description": "The source root on this machine that is the equivalent to the serverSourceRoot on the server. May be relative to the project root." + "description": "The source root on this machine that is the equivalent to the serverSourceRoot on the server." }, "cwd": { "type": "string", diff --git a/src/phpDebug.ts b/src/phpDebug.ts index eede1664..17e67607 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -49,17 +49,17 @@ function formatPropertyValue(property: xdebug.BaseProperty): string { */ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArguments { /** The port where the adapter should listen for XDebug connections (default: 9000) */ - port: number; + port?: number; /** Automatically stop target after launch. If not specified, target does not stop. */ stopOnEntry?: boolean; /** The source root on the server when doing remote debugging on a different host */ serverSourceRoot?: string; - /** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. May be relative to cwd. */ + /** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. */ localSourceRoot?: string; /** The current working directory, by default the project root */ cwd?: string; /** If true, will log all communication between VS Code and the adapter to the console */ - log: boolean; + log?: boolean; } class PhpDebugSession extends vscode.DebugSession { @@ -72,16 +72,19 @@ class PhpDebugSession extends vscode.DebugSession { /** * A map from VS Code thread IDs to XDebug Connections. - * XDebug makes a new connection for each request to the webserver, we present hese as threads to VS Code. + * XDebug makes a new connection for each request to the webserver, we present these as threads to VS Code. * The threadId key is equal to the id attribute of the connection. */ private _connections = new Map(); - /** A set of connecitons which still need to be initialized with exception breakpoints before _runOrStopOnEntry can be called. */ - private _connectionsAwaitingExceptionBreakpoints = new Set(); + /** A set of connections which are not yet running and are waiting for configurationDoneRequest */ + private _waitingConnections = new Set(); - /** A set of connecitons which still need to be initialized with exception breakpoints before _runOrStopOnEntry can be called. */ - private _connectionsAwaitingBreakpoints = new Set(); + /** A counter for unique source IDs */ + private _sourceIdCounter = 1; + + /** A map of VS Code source IDs to XDebug file URLs for virtual files (dpgp://whatever) and the corresponding connection */ + private _sources = new Map(); /** A counter for unique stackframe IDs */ private _stackFrameIdCounter = 1; @@ -105,28 +108,25 @@ class PhpDebugSession extends vscode.DebugSession { super(debuggerLinesStartAt1, isServer); } + protected initializeRequest(response: VSCodeDebugProtocol.InitializeResponse, args: VSCodeDebugProtocol.InitializeRequestArguments): void { + response.body.supportsConfigurationDoneRequest = true; + response.body.supportsEvaluateForHovers = true; + this.sendResponse(response); + } + protected attachRequest(response: VSCodeDebugProtocol.AttachResponse, args: VSCodeDebugProtocol.AttachRequestArguments) { this.sendErrorResponse(response, 0, 'Attach requests are not supported'); this.shutdown(); } protected launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - if (args.serverSourceRoot) { - // use cwd by default for localSourceRoot - if (!args.localSourceRoot) { - args.localSourceRoot = '.'; - } - // resolve localSourceRoot relative to the project root - args.localSourceRoot = path.resolve(args.cwd, args.localSourceRoot); - } this._args = args; const server = this._server = net.createServer(); server.on('connection', (socket: net.Socket) => { // new XDebug connection const connection = new xdebug.Connection(socket); this._connections.set(connection.id, connection); - this._connectionsAwaitingBreakpoints.add(connection); - this._connectionsAwaitingExceptionBreakpoints.add(connection); + this._waitingConnections.add(connection); connection.waitForInitPacket() .then(() => { this.sendEvent(new vscode.ThreadEvent('started', connection.id)); @@ -146,16 +146,6 @@ class PhpDebugSession extends vscode.DebugSession { this.sendResponse(response); } - /** is called after all breakpoints etc. are initialized and either runs the script or notifies VS Code that we stopped on entry, depending on launch settings */ - private _runOrStopOnEntry(connection: xdebug.Connection): void { - // either tell VS Code we stopped on entry or run the script - if (this._args.stopOnEntry) { - this.sendEvent(new vscode.StoppedEvent('entry', connection.id)); - } else { - connection.sendRunCommand().then(response => this._checkStatus(response)); - } - } - /** Checks the status of a StatusResponse and notifies VS Code accordingly */ private _checkStatus(response: xdebug.StatusResponse): void { const connection = response.connection; @@ -182,9 +172,12 @@ class PhpDebugSession extends vscode.DebugSession { } /** converts a server-side XDebug file URI to a local path for VS Code with respect to source root settings */ - protected convertDebuggerPathToClient(fileUri: string): string { + protected convertDebuggerPathToClient(fileUri: string|url.Url): string { + if (typeof fileUri === 'string') { + fileUri = url.parse(fileUri); + } // convert the file URI to a path - let serverPath = decodeURI(url.parse(fileUri).pathname); + let serverPath = decodeURI((fileUri).pathname); // strip the trailing slash from Windows paths (indicated by a drive letter with a colon) if (/^\/[a-zA-Z]:\//.test(serverPath)) { serverPath = serverPath.substr(1); @@ -225,7 +218,7 @@ class PhpDebugSession extends vscode.DebugSession { } /** Logs all requests before dispatching */ - protected dispatchRequest(request: VSCodeDebugProtocol.Request) { + protected dispatchRequest(request: VSCodeDebugProtocol.Request): void { const log = `-> ${request.command}Request\n${util.inspect(request, {depth: null})}\n\n`; console.log(log); if (this._args && this._args.log) { @@ -243,7 +236,7 @@ class PhpDebugSession extends vscode.DebugSession { super.sendEvent(event); } - public sendResponse(response: VSCodeDebugProtocol.Response) { + public sendResponse(response: VSCodeDebugProtocol.Response): void { const log = `<- ${response.command}Response\n${util.inspect(response, {depth: null})}\n\n`; console[response.success ? 'log' : 'error'](log); if (this._args && this._args.log) { @@ -276,15 +269,6 @@ class PhpDebugSession extends vscode.DebugSession { .then(() => Promise.all(args.lines.map(line => connection.sendBreakpointSetCommand({type: 'line', fileUri, line}) .then(xdebugResponse => { - // has this connection finally received its long-awaited breakpoints? - if (this._connectionsAwaitingBreakpoints.has(connection)) { - // remember that the breakpoints for this connection have been set - this._connectionsAwaitingBreakpoints.delete(connection); - // if this connection has already received exception breakpoints, run it now - if (!this._connectionsAwaitingExceptionBreakpoints.has(connection)) { - this._runOrStopOnEntry(connection); - } - } // only capture each breakpoint once if (connectionIndex === 0) { breakpoints.push(new vscode.Breakpoint(true, line)); @@ -333,17 +317,6 @@ class PhpDebugSession extends vscode.DebugSession { return connection.sendBreakpointSetCommand({type: 'exception', exception: '*'}); } }) - .then(() => { - // has this connection finally received its long-awaited exception breakpoints? - if (this._connectionsAwaitingExceptionBreakpoints.has(connection)) { - // remember that the exception breakpoints for this connection have been set - this._connectionsAwaitingExceptionBreakpoints.delete(connection); - // if this connection has already received line breakpoints, run it now - if (!this._connectionsAwaitingBreakpoints.has(connection)) { - this._runOrStopOnEntry(connection); - } - } - }) )).then(() => { this.sendResponse(response); }).catch(error => { @@ -351,6 +324,19 @@ class PhpDebugSession extends vscode.DebugSession { }); } + /** Executed after all breakpoints have been set by VS Code */ + protected configurationDoneRequest(response: VSCodeDebugProtocol.ConfigurationDoneResponse, args: VSCodeDebugProtocol.ConfigurationDoneArguments): void { + for (const connection of Array.from(this._waitingConnections)) { + // either tell VS Code we stopped on entry or run the script + if (this._args.stopOnEntry) { + this.sendEvent(new vscode.StoppedEvent('entry', connection.id)); + } else { + connection.sendRunCommand().then(response => this._checkStatus(response)); + } + } + this.sendResponse(response); + } + /** Executed after a successfull launch or attach request and after a ThreadEvent */ protected threadsRequest(response: VSCodeDebugProtocol.ThreadsResponse): void { // PHP doesn't have threads, but it may have multiple requests in parallel. @@ -374,10 +360,18 @@ class PhpDebugSession extends vscode.DebugSession { // this._contexts.clear(); response.body = { stackFrames: xdebugResponse.stack.map(stackFrame => { - // XDebug paths are URIs, VS Code file paths - const filePath = this.convertDebuggerPathToClient(stackFrame.fileUri); - // "Name" of the source and the actual file path - const source = new vscode.Source(path.basename(filePath), filePath); + let source: vscode.Source; + const urlObject = url.parse(stackFrame.fileUri); + if (urlObject.protocol === 'dbgp:') { + const sourceReference = this._sourceIdCounter++; + this._sources.set(sourceReference, {connection, url: stackFrame.fileUri}); + source = new vscode.Source(stackFrame.name, stackFrame.fileUri.substr('dbgp://'.length), sourceReference, stackFrame.type); + } else { + // XDebug paths are URIs, VS Code file paths + const filePath = this.convertDebuggerPathToClient(urlObject); + // "Name" of the source and the actual file path + source = new vscode.Source(path.basename(filePath), filePath); + } // a new, unique ID for scopeRequests const stackFrameId = this._stackFrameIdCounter++; // save the connection this stackframe belongs to and the level of the stackframe under the stacktrace id @@ -393,6 +387,14 @@ class PhpDebugSession extends vscode.DebugSession { }); } + protected sourceRequest(response: VSCodeDebugProtocol.SourceResponse, args: VSCodeDebugProtocol.SourceArguments): void { + const {connection, url} = this._sources.get(args.sourceReference); + connection.sendSourceCommand(url).then(xdebugResponse => { + response.body.content = xdebugResponse.source; + this.sendResponse(response); + }); + } + protected scopesRequest(response: VSCodeDebugProtocol.ScopesResponse, args: VSCodeDebugProtocol.ScopesArguments): void { const stackFrame = this._stackFrames.get(args.frameId); stackFrame.getContexts() @@ -428,7 +430,7 @@ class PhpDebugSession extends vscode.DebugSession { } else if (this._evalResultProperties.has(variablesReference)) { // the children of properties returned from an eval command are always inlined, so we simply resolve them const property = this._evalResultProperties.get(variablesReference); - propertiesPromise = Promise.resolve(property.children); + propertiesPromise = Promise.resolve(property.hasChildren ? property.children : []); } else { console.error('Unknown variable reference: ' + variablesReference); console.error('Known variables: ' + JSON.stringify(Array.from(this._properties))); diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index 5128e7f1..e82f68c1 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -170,6 +170,8 @@ export class BreakpointListResponse extends Response { export class StackFrame { /** The UI-friendly name of this stack frame, like a function name or "{main}" */ name: string; + /** The type of stack frame. Valid values are "file" and "eval" */ + type: string; /** The file URI where the stackframe was entered */ fileUri: string; /** The line number inside file where the stackframe was entered */ @@ -185,6 +187,7 @@ export class StackFrame { constructor(stackFrameNode: Element, connection: Connection) { this.name = stackFrameNode.getAttribute('where'); this.fileUri = stackFrameNode.getAttribute('filename'); + this.type = stackFrameNode.getAttribute('type'); this.line = parseInt(stackFrameNode.getAttribute('lineno')); this.level = parseInt(stackFrameNode.getAttribute('level')); this.connection = connection; @@ -209,6 +212,14 @@ export class StackGetResponse extends Response { } } +export class SourceResponse extends Response { + source: string; + constructor(document: XMLDocument, connection: Connection) { + super(document, connection); + this.source = document.documentElement.textContent; + } +} + /** A context inside a stack frame, like "Local" or "Superglobals" */ export class Context { /** Unique id that is used for further commands */ @@ -615,6 +626,10 @@ export class Connection extends DbgpConnection { return this._enqueueCommand('stack_get').then(document => new StackGetResponse(document, this)); } + public sendSourceCommand(uri: string): Promise { + return this._enqueueCommand('source', `-f ${uri}`).then(document => new SourceResponse(document, this)); + } + // ------------------------------ context -------------------------------------- /** Sends a context_names command. */