Skip to content

Commit

Permalink
Adapt VS Code January release 0.10.8, implement SourceRequest
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfbecker committed Feb 17, 2016
1 parent 938e7fb commit a5e1455
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 72 deletions.
10 changes: 5 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"runtimeArgs": [
"--harmony"
],
"program": "./src/phpDebug.ts",
"program": "${workspaceRoot}/src/phpDebug.ts",
"stopOnEntry": false,
"args": [
"--server=4711"
Expand All @@ -17,7 +17,7 @@
"NODE_ENV": "development"
},
"sourceMaps": true,
"outDir": "./out"
"outDir": "${workspaceRoot}/out"
},
{
"name": "Launch Extension",
Expand All @@ -28,21 +28,21 @@
"--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",
"999999",
"--colors"
],
"sourceMaps": true,
"outDir": "./out"
"outDir": "${workspaceRoot}/out"
}
]
}
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"email": "[email protected]"
},
"engines": {
"vscode": "^0.10.6",
"vscode": "^0.10.8",
"node": "^4.1.1"
},
"icon": "images/logo.svg",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
116 changes: 59 additions & 57 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<number, xdebug.Connection>();

/** A set of connecitons which still need to be initialized with exception breakpoints before _runOrStopOnEntry can be called. */
private _connectionsAwaitingExceptionBreakpoints = new Set<xdebug.Connection>();
/** A set of connections which are not yet running and are waiting for configurationDoneRequest */
private _waitingConnections = new Set<xdebug.Connection>();

/** A set of connecitons which still need to be initialized with exception breakpoints before _runOrStopOnEntry can be called. */
private _connectionsAwaitingBreakpoints = new Set<xdebug.Connection>();
/** 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<number, {connection: xdebug.Connection, url: string}>();

/** A counter for unique stackframe IDs */
private _stackFrameIdCounter = 1;
Expand All @@ -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));
Expand All @@ -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;
Expand All @@ -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(<string>fileUri);
}
// convert the file URI to a path
let serverPath = decodeURI(url.parse(fileUri).pathname);
let serverPath = decodeURI((<url.Url>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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -333,24 +317,26 @@ 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 => {
this.sendErrorResponse(response, error.code, error.message);
});
}

/** 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.
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)));
Expand Down
15 changes: 15 additions & 0 deletions src/xdebugConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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;
Expand All @@ -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 */
Expand Down Expand Up @@ -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<SourceResponse> {
return this._enqueueCommand('source', `-f ${uri}`).then(document => new SourceResponse(document, this));
}

// ------------------------------ context --------------------------------------

/** Sends a context_names command. */
Expand Down

0 comments on commit a5e1455

Please sign in to comment.