Skip to content

Commit 2f64866

Browse files
committed
Implement conditional breakpoints
1 parent a58eb54 commit 2f64866

File tree

3 files changed

+149
-53
lines changed

3 files changed

+149
-53
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Now, when you make a request to `localhost` with your webbrowser, XDebug will co
3535
What is supported?
3636
------------------
3737
- Line breakpoints
38+
- Conditional breakpoints
3839
- Step over, step in, step out
3940
- Break on entry
4041
- Breaking on uncaught exceptions and errors / warnings / notices
@@ -46,7 +47,6 @@ What is supported?
4647

4748
What is not supported?
4849
----------------------
49-
- Conditional breakpoints (not yet implemented)
5050
- Breaking on _caught_ exceptions, this is not supported by XDebug and the setting is ignored
5151
- Attach requests, there is no such thing because the lifespan of PHP scripts is short
5252

src/phpDebug.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -249,44 +249,58 @@ class PhpDebugSession extends vscode.DebugSession {
249249
protected setBreakPointsRequest(response: VSCodeDebugProtocol.SetBreakpointsResponse, args: VSCodeDebugProtocol.SetBreakpointsArguments) {
250250
const fileUri = this.convertClientPathToDebugger(args.source.path);
251251
const connections = Array.from(this._connections.values());
252-
let breakpoints: vscode.Breakpoint[];
252+
let xdebugBreakpoints: Array<xdebug.ConditionalBreakpoint|xdebug.LineBreakpoint>;
253+
response.body = {breakpoints: []};
254+
// this is returned to VS Code
255+
let vscodeBreakpoints: vscode.Breakpoint[];
253256
let breakpointsSetPromise: Promise<any>;
254257
if (connections.length === 0) {
255258
// if there are no connections yet, we cannot verify any breakpoint
256-
breakpoints = args.lines.map(line => new vscode.Breakpoint(false, line));
259+
vscodeBreakpoints = args.breakpoints.map(breakpoint => new vscode.Breakpoint(false, breakpoint.line));
257260
breakpointsSetPromise = Promise.resolve();
258261
} else {
259-
breakpoints = [];
262+
vscodeBreakpoints = [];
263+
// create XDebug breakpoints from the arguments
264+
xdebugBreakpoints = args.breakpoints.map(breakpoint => {
265+
if (breakpoint.condition) {
266+
return new xdebug.ConditionalBreakpoint(breakpoint.condition, fileUri, breakpoint.line);
267+
} else {
268+
return new xdebug.LineBreakpoint(fileUri, breakpoint.line);
269+
}
270+
});
271+
// for all connections
260272
breakpointsSetPromise = Promise.all(connections.map((connection, connectionIndex) =>
261273
// clear breakpoints for this file
262274
connection.sendBreakpointListCommand()
263275
.then(response => Promise.all(
264276
response.breakpoints
265-
.filter(breakpoint => breakpoint.type === 'line' && breakpoint.fileUri === fileUri)
277+
// filte to only include line breakpoints for this file
278+
.filter(breakpoint => breakpoint instanceof xdebug.LineBreakpoint && breakpoint.fileUri === fileUri)
279+
// remove them
266280
.map(breakpoint => breakpoint.remove())
267281
))
268-
// set them
269-
.then(() => Promise.all(args.lines.map(line =>
270-
connection.sendBreakpointSetCommand({type: 'line', fileUri, line})
282+
// set new breakpoints
283+
.then(() => Promise.all(xdebugBreakpoints.map(breakpoint =>
284+
connection.sendBreakpointSetCommand(breakpoint)
271285
.then(xdebugResponse => {
272286
// only capture each breakpoint once
273287
if (connectionIndex === 0) {
274-
breakpoints.push(new vscode.Breakpoint(true, line));
288+
vscodeBreakpoints.push(new vscode.Breakpoint(true, breakpoint.line));
275289
}
276290
})
277291
.catch(error => {
278292
// only capture each breakpoint once
279293
if (connectionIndex === 0) {
280-
console.error('breakpoint could not be set: ', error);
281-
breakpoints.push(new vscode.Breakpoint(false, line));
294+
console.error('breakpoint could not be set: ', error.message);
295+
vscodeBreakpoints.push(new vscode.Breakpoint(false, breakpoint.line));
282296
}
283297
})
284298
)))
285299
));
286300
}
287301
breakpointsSetPromise
288302
.then(() => {
289-
response.body = {breakpoints};
303+
response.body = {breakpoints: vscodeBreakpoints};
290304
this.sendResponse(response);
291305
})
292306
.catch(error => {
@@ -302,10 +316,10 @@ class PhpDebugSession extends vscode.DebugSession {
302316
this.sendEvent(new vscode.OutputEvent('breaking on caught exceptions is not supported by XDebug', 'stderr'));
303317
}
304318
const connections = Array.from(this._connections.values());
305-
// remove all exception breakpoints
306319
Promise.all(connections.map(connection =>
307-
// remove all exception breakpoints
320+
// get all breakpoints
308321
connection.sendBreakpointListCommand()
322+
// remove all exception breakpoints
309323
.then(response => Promise.all(
310324
response.breakpoints
311325
.filter(breakpoint => breakpoint.type === 'exception')
@@ -314,7 +328,7 @@ class PhpDebugSession extends vscode.DebugSession {
314328
.then(() => {
315329
// if enabled, set exception breakpoint for all exceptions
316330
if (breakOnExceptions) {
317-
return connection.sendBreakpointSetCommand({type: 'exception', exception: '*'});
331+
return connection.sendBreakpointSetCommand(new xdebug.ExceptionBreakpoint('*'));
318332
}
319333
})
320334
)).then(() => {
@@ -433,8 +447,6 @@ class PhpDebugSession extends vscode.DebugSession {
433447
const property = this._evalResultProperties.get(variablesReference);
434448
propertiesPromise = Promise.resolve(property.hasChildren ? property.children : []);
435449
} else {
436-
console.error('Unknown variable reference: ' + variablesReference);
437-
console.error('Known variables: ' + JSON.stringify(Array.from(this._properties)));
438450
this.sendErrorResponse(response, 0, 'Unknown variable reference');
439451
return;
440452
}

src/xdebugConnection.ts

Lines changed: 120 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class XDebugError extends Error {
3939
constructor(message: string, code: number) {
4040
super(message);
4141
this.code = code;
42+
this.name = 'XDebugError';
4243
}
4344
}
4445

@@ -108,42 +109,124 @@ export class StatusResponse extends Response {
108109
}
109110
}
110111

111-
/** Returned by a breakpoint_list command */
112-
export class Breakpoint {
113-
/** Unique ID which is used for modifying the breakpoint */
112+
/** Abstract base class for all breakpoints */
113+
export abstract class Breakpoint {
114+
/** Unique ID which is used for modifying the breakpoint (only when received through breakpoint_list) */
114115
id: number;
115116
/** The type of the breakpoint: line, call, return, exception, conditional or watch */
116117
type: string;
117118
/** State of the breakpoint: enabled, disabled */
118119
state: string;
119-
/** File URI, if type is line */
120-
fileUri: string;
121-
/** Line, if type is line */
122-
line: number;
123-
/** Exception, if type is exception */
124-
exception: string;
125120
/** The connection this breakpoint is set on */
126121
connection: Connection;
127-
/**
128-
* @param {Element} breakpointNode
129-
* @param {Connection} connection
130-
*/
131-
constructor(breakpointNode: Element, connection: Connection) {
132-
this.id = parseInt(breakpointNode.getAttribute('id'));
133-
this.type = breakpointNode.getAttribute('type');
134-
if (this.type === 'line') {
135-
this.line = parseInt(breakpointNode.getAttribute('line'));
136-
} else if (this.type === 'exception') {
137-
this.exception = breakpointNode.getAttribute('exception');
122+
/** dynamically detects the type of breakpoint and returns the appropiate object */
123+
public static fromXml(breakpointNode: Element, connection: Connection): Breakpoint {
124+
switch (breakpointNode.getAttribute('type')) {
125+
case 'exception': return new ExceptionBreakpoint(breakpointNode, connection);
126+
case 'line': return new LineBreakpoint(breakpointNode, connection);
127+
case 'conditional': return new ConditionalBreakpoint(breakpointNode, connection);
128+
}
129+
}
130+
/** Constructs a breakpoint object from an XML node from a XDebug response */
131+
constructor(breakpointNode: Element, connection: Connection);
132+
/** To create a new breakpoint in derived classes */
133+
constructor(type: string);
134+
constructor() {
135+
if (typeof arguments[0] === 'object') {
136+
// from XML
137+
const breakpointNode: Element = arguments[0];
138+
this.connection = arguments[1];
139+
this.type = breakpointNode.getAttribute('type');
140+
this.id = parseInt(breakpointNode.getAttribute('id'));
141+
this.state = breakpointNode.getAttribute('state');
142+
} else {
143+
this.type = arguments[0];
138144
}
139-
this.connection = connection;
140145
}
141146
/** Removes the breakpoint by sending a breakpoint_remove command */
142147
public remove() {
143148
return this.connection.sendBreakpointRemoveCommand(this);
144149
}
145150
}
146151

152+
/** class for line breakpoints. Returned from a breakpoint_list or passed to sendBreakpointSetCommand */
153+
export class LineBreakpoint extends Breakpoint {
154+
/** File URI of the file in which to break */
155+
fileUri: string;
156+
/** Line to break on */
157+
line: number;
158+
/** constructs a line breakpoint from an XML node */
159+
constructor(breakpointNode: Element, connection: Connection);
160+
/** contructs a line breakpoint for passing to sendSetBreakpointCommand */
161+
constructor(fileUri: string, line: number);
162+
constructor() {
163+
if (typeof arguments[0] === 'object') {
164+
const breakpointNode: Element = arguments[0];
165+
const connection: Connection = arguments[1];
166+
super(breakpointNode, connection);
167+
this.line = parseInt(breakpointNode.getAttribute('line'));
168+
} else {
169+
// construct from arguments
170+
this.fileUri = arguments[0];
171+
this.line = arguments[1];
172+
super('line');
173+
}
174+
}
175+
}
176+
177+
/** class for exception breakpoints. Returned from a breakpoint_list or passed to sendBreakpointSetCommand */
178+
export class ExceptionBreakpoint extends Breakpoint {
179+
/** The Exception name to break on. Can also contain wildcards. */
180+
exception: string;
181+
/** Constructs a breakpoint object from an XML node from a XDebug response */
182+
constructor(breakpointNode: Element, connection: Connection);
183+
/** Constructs a breakpoint for passing it to sendSetBreakpointCommand */
184+
constructor(exception: string);
185+
constructor() {
186+
if (typeof arguments[0] === 'object') {
187+
// from XML
188+
const breakpointNode: Element = arguments[0];
189+
const connection: Connection = arguments[1];
190+
super(breakpointNode, connection);
191+
this.exception = breakpointNode.getAttribute('exception');
192+
} else {
193+
// from arguments
194+
super('exception');
195+
this.exception = arguments[0];
196+
}
197+
}
198+
}
199+
200+
/** class for conditional breakpoints. Returned from a breakpoint_list or passed to sendBreakpointSetCommand */
201+
export class ConditionalBreakpoint extends Breakpoint {
202+
/** File URI */
203+
fileUri: string;
204+
/** Line (optional)*/
205+
line: number;
206+
/** The PHP expression under which to break on */
207+
expression: string;
208+
/** Constructs a breakpoint object from an XML node from a XDebug response */
209+
constructor(breakpointNode: Element, connection: Connection);
210+
/** Contructs a breakpoint object for passing to sendSetBreakpointCommand */
211+
constructor(expression: string, fileUri: string, line?: number);
212+
constructor() {
213+
if (typeof arguments[0] === 'object') {
214+
// from XML
215+
const breakpointNode: Element = arguments[0];
216+
const connection: Connection = arguments[1];
217+
super(breakpointNode, connection);
218+
this.expression = breakpointNode.getAttribute('expression'); // Base64 encoded?
219+
} else {
220+
// from arguments
221+
super('conditional');
222+
this.expression = arguments[0];
223+
this.fileUri = arguments[1];
224+
this.line = arguments[2];
225+
}
226+
}
227+
}
228+
229+
/** Response to a breakpoint_set command */
147230
export class BreakpointSetResponse extends Response {
148231
breakpointId: number;
149232
constructor(document: XMLDocument, connection: Connection) {
@@ -162,7 +245,7 @@ export class BreakpointListResponse extends Response {
162245
*/
163246
constructor(document: XMLDocument, connection: Connection) {
164247
super(document, connection);
165-
this.breakpoints = Array.from(document.documentElement.childNodes).map((breakpointNode: Element) => new Breakpoint(breakpointNode, connection));
248+
this.breakpoints = Array.from(document.documentElement.childNodes).map((breakpointNode: Element) => Breakpoint.fromXml(breakpointNode, connection));
166249
}
167250
}
168251

@@ -563,23 +646,24 @@ export class Connection extends DbgpConnection {
563646

564647
/**
565648
* Sends a breakpoint_set command that sets a breakpoint.
566-
* @param {object} breakpoint
567-
* @param {string} breakpoint.type - the type of breakpoint. Can be 'line' or 'exception'
568-
* @param {string} [breakpoint.fileUri] - the file URI to break on if type is 'line'
569-
* @param {number} [breakpoint.line] - the line to break on if type is 'line'
570-
* @param {string} [breakpoint.exception] - the exception class name to break on if type is 'exception'
649+
* @param {Breakpoint} breakpoint - an instance of LineBreakpoint, ConditionalBreakpoint or ExceptionBreakpoint
571650
* @returns Promise.<BreakpointSetResponse>
572651
*/
573-
public sendBreakpointSetCommand(breakpoint: {type: string, fileUri?: string, line?: number, exception?: string}): Promise<BreakpointSetResponse> {
574-
let args = `-t ${breakpoint.type} `;
575-
if (breakpoint.type === 'line') {
576-
args += `-f ${breakpoint.fileUri} -n ${breakpoint.line}`;
577-
} else if (breakpoint.type === 'exception') {
578-
args += `-x ${breakpoint.exception}`;
579-
} else {
580-
return Promise.reject<BreakpointSetResponse>(new Error('unsupported breakpoint type'));
652+
public sendBreakpointSetCommand(breakpoint: Breakpoint): Promise<BreakpointSetResponse> {
653+
let args = `-t ${breakpoint.type}`;
654+
let data: string;
655+
if (breakpoint instanceof LineBreakpoint) {
656+
args += ` -f ${breakpoint.fileUri} -n ${breakpoint.line}`;
657+
} else if (breakpoint instanceof ExceptionBreakpoint) {
658+
args += ` -x ${breakpoint.exception}`;
659+
} else if (breakpoint instanceof ConditionalBreakpoint) {
660+
args += ` -f ${breakpoint.fileUri}`;
661+
if (typeof breakpoint.line === 'number') {
662+
args += ` -n ${breakpoint.line}`;
663+
}
664+
data = breakpoint.expression;
581665
}
582-
return this._enqueueCommand('breakpoint_set', args).then(document => new BreakpointSetResponse(document, this));
666+
return this._enqueueCommand('breakpoint_set', args, data).then(document => new BreakpointSetResponse(document, this));
583667
}
584668

585669
/** sends a breakpoint_list command */

0 commit comments

Comments
 (0)