Skip to content

Commit ef9c83b

Browse files
committed
Show errors as a virtual scope
1 parent 1781fa3 commit ef9c83b

File tree

2 files changed

+153
-88
lines changed

2 files changed

+153
-88
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,6 @@ If it fails with your ultra-awesome MVC app, please first try it on a dead-simpl
6969
FAQ
7070
---
7171

72-
#### How can I get the error message when breaking on an error/warning?
73-
Set a watch for `error_get_last()`
74-
7572
#### Where are the variables of the parent scope?
7673
In opposite to Javascript, PHP does not have closures.
7774
A scope contains only variables that have been declared, parameters and imported globals with `global` or `use`.

src/phpDebug.ts

Lines changed: 153 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,18 @@ class PhpDebugSession extends vscode.DebugSession {
109109
/** A map from unique stackframe IDs (even across connections) to XDebug stackframes */
110110
private _stackFrames = new Map<number, xdebug.StackFrame>();
111111

112+
/** A map from XDebug connections to their current status */
113+
private _statuses = new Map<xdebug.Connection, xdebug.StatusResponse>();
114+
112115
/** A counter for unique context, property and eval result properties (as these are all requested by a VariableRequest from VS Code) */
113116
private _variableIdCounter = 1;
114117

118+
/** A map from unique VS Code variable IDs to XDebug statuses for virtual error stack frames */
119+
private _errorStackFrames = new Map<number, xdebug.StatusResponse>();
120+
121+
/** A map from unique VS Code variable IDs to XDebug statuses for virtual error scopes */
122+
private _errorScopes = new Map<number, xdebug.StatusResponse>();
123+
115124
/** A map from unique VS Code variable IDs to an XDebug contexts */
116125
private _contexts = new Map<number, xdebug.Context>();
117126

@@ -203,6 +212,7 @@ class PhpDebugSession extends vscode.DebugSession {
203212
/** Checks the status of a StatusResponse and notifies VS Code accordingly */
204213
private _checkStatus(response: xdebug.StatusResponse): void {
205214
const connection = response.connection;
215+
this._statuses.set(connection, response);
206216
if (response.status === 'stopping') {
207217
connection.sendStopCommand().then(response => this._checkStatus(response));
208218
} else if (response.status === 'stopped') {
@@ -427,31 +437,59 @@ class PhpDebugSession extends vscode.DebugSession {
427437
// this._stackFrames.clear();
428438
// this._properties.clear();
429439
// this._contexts.clear();
430-
response.body = {
431-
stackFrames: xdebugResponse.stack.map(stackFrame => {
432-
let source: vscode.Source;
433-
let line = stackFrame.line;
434-
const urlObject = url.parse(stackFrame.fileUri);
435-
if (urlObject.protocol === 'dbgp:') {
436-
const sourceReference = this._sourceIdCounter++;
437-
this._sources.set(sourceReference, {connection, url: stackFrame.fileUri});
438-
// for eval code, we need to include .php extension to get syntax highlighting
439-
source = new vscode.Source(stackFrame.type === 'eval' ? 'eval.php' : stackFrame.name, null, sourceReference, stackFrame.type);
440-
// for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
441-
line++;
442-
} else {
443-
// XDebug paths are URIs, VS Code file paths
444-
const filePath = this.convertDebuggerPathToClient(urlObject);
445-
// "Name" of the source and the actual file path
446-
source = new vscode.Source(path.basename(filePath), filePath);
447-
}
448-
// a new, unique ID for scopeRequests
449-
const stackFrameId = this._stackFrameIdCounter++;
450-
// save the connection this stackframe belongs to and the level of the stackframe under the stacktrace id
451-
this._stackFrames.set(stackFrameId, stackFrame);
452-
// prepare response for VS Code (column is always 1 since XDebug doesn't tell us the column)
453-
return new vscode.StackFrame(stackFrameId, stackFrame.name, source, line, 1);
454-
})
440+
const status = this._statuses.get(connection);
441+
if (xdebugResponse.stack.length === 0 && status.exception) {
442+
// special case: if a fatal error occurs (for example after an uncaught exception), the stack trace is EMPTY.
443+
// in that case, VS Code would normally not show any information to the user at all
444+
// to avoid this, we create a virtual stack frame with the info from the last status response we got
445+
const status = this._statuses.get(connection);
446+
const id = this._stackFrameIdCounter++;
447+
const name = status.exception.name;
448+
let line = status.line;
449+
let source: vscode.Source;
450+
const urlObject = url.parse(status.fileUri);
451+
if (urlObject.protocol === 'dbgp:') {
452+
const sourceReference = this._sourceIdCounter++;
453+
this._sources.set(sourceReference, {connection, url: status.fileUri});
454+
// for eval code, we need to include .php extension to get syntax highlighting
455+
source = new vscode.Source(status.exception.name + '.php', null, sourceReference, status.exception.name);
456+
// for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
457+
line++;
458+
} else {
459+
// XDebug paths are URIs, VS Code file paths
460+
const filePath = this.convertDebuggerPathToClient(urlObject);
461+
// "Name" of the source and the actual file path
462+
source = new vscode.Source(path.basename(filePath), filePath);
463+
}
464+
this._errorStackFrames.set(id, status);
465+
response.body = {stackFrames: [new vscode.StackFrame(id, name, source, status.line, 1)]};
466+
} else {
467+
response.body = {
468+
stackFrames: xdebugResponse.stack.map(stackFrame => {
469+
let source: vscode.Source;
470+
let line = stackFrame.line;
471+
const urlObject = url.parse(stackFrame.fileUri);
472+
if (urlObject.protocol === 'dbgp:') {
473+
const sourceReference = this._sourceIdCounter++;
474+
this._sources.set(sourceReference, {connection, url: stackFrame.fileUri});
475+
// for eval code, we need to include .php extension to get syntax highlighting
476+
source = new vscode.Source(stackFrame.type === 'eval' ? 'eval.php' : stackFrame.name, null, sourceReference, stackFrame.type);
477+
// for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
478+
line++;
479+
} else {
480+
// XDebug paths are URIs, VS Code file paths
481+
const filePath = this.convertDebuggerPathToClient(urlObject);
482+
// "Name" of the source and the actual file path
483+
source = new vscode.Source(path.basename(filePath), filePath);
484+
}
485+
// a new, unique ID for scopeRequests
486+
const stackFrameId = this._stackFrameIdCounter++;
487+
// save the connection this stackframe belongs to and the level of the stackframe under the stacktrace id
488+
this._stackFrames.set(stackFrameId, stackFrame);
489+
// prepare response for VS Code (column is always 1 since XDebug doesn't tell us the column)
490+
return new vscode.StackFrame(stackFrameId, stackFrame.name, source, line, 1);
491+
})
492+
};
455493
}
456494
this.sendResponse(response);
457495
})
@@ -474,73 +512,103 @@ class PhpDebugSession extends vscode.DebugSession {
474512
}
475513

476514
protected scopesRequest(response: VSCodeDebugProtocol.ScopesResponse, args: VSCodeDebugProtocol.ScopesArguments): void {
477-
const stackFrame = this._stackFrames.get(args.frameId);
478-
stackFrame.getContexts()
479-
.then(contexts => {
480-
response.body = {
481-
scopes: contexts.map(context => {
515+
if (this._errorStackFrames.has(args.frameId)) {
516+
// VS Code is requesting the scopes for a virtual error stack frame
517+
const status = this._errorStackFrames.get(args.frameId);
518+
if (status && status.exception) {
519+
const variableId = this._variableIdCounter++;
520+
this._errorScopes.set(variableId, status);
521+
response.body = {scopes: [new vscode.Scope(status.exception.name, variableId)]};
522+
}
523+
this.sendResponse(response);
524+
} else {
525+
const stackFrame = this._stackFrames.get(args.frameId);
526+
stackFrame.getContexts()
527+
.then(contexts => {
528+
response.body = {
529+
scopes: contexts.map(context => {
530+
const variableId = this._variableIdCounter++;
531+
// remember that this new variable ID is assigned to a SCOPE (in XDebug "context"), not a variable (in XDebug "property"),
532+
// so when VS Code does a variablesRequest with that ID we do a context_get and not a property_get
533+
this._contexts.set(variableId, context);
534+
// send VS Code the variable ID as identifier
535+
return new vscode.Scope(context.name, variableId);
536+
})
537+
};
538+
const status = this._statuses.get(stackFrame.connection);
539+
if (status && status.exception) {
482540
const variableId = this._variableIdCounter++;
483-
// remember that this new variable ID is assigned to a SCOPE (in XDebug "context"), not a variable (in XDebug "property"),
484-
// so when VS Code does a variablesRequest with that ID we do a context_get and not a property_get
485-
this._contexts.set(variableId, context);
486-
// send VS Code the variable ID as identifier
487-
return new vscode.Scope(context.name, variableId);
488-
})
489-
};
490-
this.sendResponse(response);
491-
})
492-
.catch(error => {
493-
this.sendErrorResponse(response, error.code, error.message);
494-
});
541+
this._errorScopes.set(variableId, status);
542+
response.body.scopes.unshift(new vscode.Scope(status.exception.name, variableId));
543+
}
544+
this.sendResponse(response);
545+
})
546+
.catch(error => {
547+
this.sendErrorResponse(response, error.code, error.message);
548+
});
549+
}
495550
}
496551

497552
protected variablesRequest(response: VSCodeDebugProtocol.VariablesResponse, args: VSCodeDebugProtocol.VariablesArguments): void {
498553
const variablesReference = args.variablesReference;
499-
let propertiesPromise: Promise<xdebug.BaseProperty[]>;
500-
if (this._contexts.has(variablesReference)) {
501-
// VS Code is requesting the variables for a SCOPE, so we have to do a context_get
502-
const context = this._contexts.get(variablesReference);
503-
propertiesPromise = context.getProperties();
504-
} else if (this._properties.has(variablesReference)) {
505-
// VS Code is requesting the subelements for a variable, so we have to do a property_get
506-
const property = this._properties.get(variablesReference);
507-
propertiesPromise = property.hasChildren ? property.getChildren() : Promise.resolve([]);
508-
} else if (this._evalResultProperties.has(variablesReference)) {
509-
// the children of properties returned from an eval command are always inlined, so we simply resolve them
510-
const property = this._evalResultProperties.get(variablesReference);
511-
propertiesPromise = Promise.resolve(property.hasChildren ? property.children : []);
554+
if (this._errorScopes.has(variablesReference)) {
555+
// this is a virtual error scope
556+
const status = this._errorScopes.get(variablesReference);
557+
response.body = {
558+
variables: [
559+
new vscode.Variable('name', '"' + status.exception.name + '"'),
560+
new vscode.Variable('message', '"' + status.exception.message + '"')
561+
]
562+
};
563+
this.sendResponse(response);
512564
} else {
513-
this.sendErrorResponse(response, 0, 'Unknown variable reference');
514-
return;
515-
}
516-
propertiesPromise
517-
.then(properties => {
518-
response.body = {
519-
variables: properties.map(property => {
520-
const displayValue = formatPropertyValue(property);
521-
let variablesReference: number;
522-
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
523-
// if the property has children, we have to send a variableReference back to VS Code
524-
// so it can receive the child elements in another request.
525-
// for arrays and objects we do it even when it does not have children so the user can still expand/collapse the entry
526-
variablesReference = this._variableIdCounter++;
527-
if (property instanceof xdebug.Property) {
528-
this._properties.set(variablesReference, property);
529-
} else if (property instanceof xdebug.EvalResultProperty) {
530-
this._evalResultProperties.set(variablesReference, property);
565+
// it is a real scope
566+
let propertiesPromise: Promise<xdebug.BaseProperty[]>;
567+
if (this._contexts.has(variablesReference)) {
568+
// VS Code is requesting the variables for a SCOPE, so we have to do a context_get
569+
const context = this._contexts.get(variablesReference);
570+
propertiesPromise = context.getProperties();
571+
} else if (this._properties.has(variablesReference)) {
572+
// VS Code is requesting the subelements for a variable, so we have to do a property_get
573+
const property = this._properties.get(variablesReference);
574+
propertiesPromise = property.hasChildren ? property.getChildren() : Promise.resolve([]);
575+
} else if (this._evalResultProperties.has(variablesReference)) {
576+
// the children of properties returned from an eval command are always inlined, so we simply resolve them
577+
const property = this._evalResultProperties.get(variablesReference);
578+
propertiesPromise = Promise.resolve(property.hasChildren ? property.children : []);
579+
} else {
580+
this.sendErrorResponse(response, 0, 'Unknown variable reference');
581+
return;
582+
}
583+
propertiesPromise
584+
.then(properties => {
585+
response.body = {
586+
variables: properties.map(property => {
587+
const displayValue = formatPropertyValue(property);
588+
let variablesReference: number;
589+
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
590+
// if the property has children, we have to send a variableReference back to VS Code
591+
// so it can receive the child elements in another request.
592+
// for arrays and objects we do it even when it does not have children so the user can still expand/collapse the entry
593+
variablesReference = this._variableIdCounter++;
594+
if (property instanceof xdebug.Property) {
595+
this._properties.set(variablesReference, property);
596+
} else if (property instanceof xdebug.EvalResultProperty) {
597+
this._evalResultProperties.set(variablesReference, property);
598+
}
599+
} else {
600+
variablesReference = 0;
531601
}
532-
} else {
533-
variablesReference = 0;
534-
}
535-
return new vscode.Variable(property.name, displayValue, variablesReference);
536-
})
537-
}
538-
this.sendResponse(response);
539-
})
540-
.catch(error => {
541-
console.error(util.inspect(error));
542-
this.sendErrorResponse(response, error.code, error.message);
543-
})
602+
return new vscode.Variable(property.name, displayValue, variablesReference);
603+
})
604+
}
605+
this.sendResponse(response);
606+
})
607+
.catch(error => {
608+
console.error(util.inspect(error));
609+
this.sendErrorResponse(response, error.code, error.message);
610+
});
611+
}
544612
}
545613

546614
protected continueRequest(response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments): void {

0 commit comments

Comments
 (0)