Skip to content

Commit a7e542f

Browse files
authored
fix(breakpoints): Breakpoints could get overwritten when multiple TS files map to a single JS file. (#285)
Maintain a map of source breakpoints per file. VSCode's `setBreakpointRequest` is triggered per file whenever breakpoints are added or removed. Since it does not provide all breakpoints for all files, we need to maintain our own record of every breakpoint for every file. This way, we have all inputs available when constructing the complete list of BPs for a given generated file.
1 parent 66fcf8d commit a7e542f

File tree

1 file changed

+80
-50
lines changed

1 file changed

+80
-50
lines changed

src/session.ts

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class Session extends DebugSession {
131131
private _sourceFileWatcher?: FileSystemWatcher;
132132
private _activeThreadId: number = 0; // the one being debugged
133133
private _localRoot: string = '';
134+
private _sourceBreakpointsMap: Map<string, DebugProtocol.SourceBreakpoint[] | undefined> = new Map();
134135
private _sourceMapRoot?: string;
135136
private _generatedSourceRoot?: string;
136137
private _inlineSourceMap: boolean = false;
@@ -246,7 +247,7 @@ export class Session extends DebugSession {
246247
// VSCode extension has been activated due to the 'onDebug' activation request defined in packages.json
247248
protected initializeRequest(
248249
response: DebugProtocol.InitializeResponse,
249-
args: DebugProtocol.InitializeRequestArguments
250+
_args: DebugProtocol.InitializeRequestArguments
250251
): void {
251252
const capabilities: DebugProtocol.Capabilities = {
252253
// indicates VSCode should send the configurationDoneRequest
@@ -269,9 +270,9 @@ export class Session extends DebugSession {
269270

270271
// VSCode starts MC exe, then waits for MC to boot and connect back to a listening VSCode
271272
protected async launchRequest(
272-
response: DebugProtocol.LaunchResponse,
273-
args: DebugProtocol.LaunchRequestArguments,
274-
request?: DebugProtocol.Request
273+
_response: DebugProtocol.LaunchResponse,
274+
_args: DebugProtocol.LaunchRequestArguments,
275+
_request?: DebugProtocol.Request
275276
) {
276277
// not implemented
277278
}
@@ -280,7 +281,7 @@ export class Session extends DebugSession {
280281
protected async attachRequest(
281282
response: DebugProtocol.AttachResponse,
282283
args: IAttachRequestArguments,
283-
request?: DebugProtocol.Request
284+
_request?: DebugProtocol.Request
284285
) {
285286
this.closeSession();
286287

@@ -337,7 +338,7 @@ export class Session extends DebugSession {
337338
protected async setBreakPointsRequest(
338339
response: DebugProtocol.SetBreakpointsResponse,
339340
args: DebugProtocol.SetBreakpointsArguments,
340-
request?: DebugProtocol.Request
341+
_request?: DebugProtocol.Request
341342
) {
342343
response.body = {
343344
breakpoints: [],
@@ -348,52 +349,81 @@ export class Session extends DebugSession {
348349
return;
349350
}
350351

351-
let originalLocalAbsolutePath = path.normalize(args.source.path);
352+
// store source breakpoints per file
353+
this._sourceBreakpointsMap.set(args.source.path, args.breakpoints);
352354

353-
const originalBreakpoints = args.breakpoints || [];
354-
const generatedBreakpoints: DebugProtocol.SourceBreakpoint[] = [];
355-
let generatedRemoteLocalPath = undefined;
355+
// rebuild the generated breakpoints map each time a breakpoint is changed in any file
356+
let generatedBreakpointsMap: Map<string, DebugProtocol.SourceBreakpoint[]> = new Map();
356357

357-
try {
358-
// first get generated remote file path, will throw if fails
359-
generatedRemoteLocalPath = await this._sourceMaps.getGeneratedRemoteRelativePath(originalLocalAbsolutePath);
360-
361-
// for all breakpoint positions set on the source file, get generated/mapped positions
362-
if (originalBreakpoints.length) {
363-
for (let originalBreakpoint of originalBreakpoints) {
364-
const generatedPosition = await this._sourceMaps.getGeneratedPositionFor({
365-
source: originalLocalAbsolutePath,
366-
column: originalBreakpoint.column || 0,
367-
line: originalBreakpoint.line,
368-
});
369-
generatedBreakpoints.push({
370-
line: generatedPosition.line || 0,
371-
column: 0,
372-
});
358+
// get generated breakpoints from all sources
359+
for (let [sourcePath, sourceBreakpoints] of this._sourceBreakpointsMap) {
360+
let originalLocalAbsolutePath = path.normalize(sourcePath);
361+
362+
const originalBreakpoints = sourceBreakpoints ?? [];
363+
let generatedRemoteLocalPath = undefined;
364+
365+
try {
366+
// first get generated remote file path, will throw if fails
367+
generatedRemoteLocalPath = await this._sourceMaps.getGeneratedRemoteRelativePath(
368+
originalLocalAbsolutePath
369+
);
370+
371+
// append to any existing breakpoints for this generated file
372+
if (!generatedBreakpointsMap.has(generatedRemoteLocalPath)) {
373+
generatedBreakpointsMap.set(generatedRemoteLocalPath, []);
373374
}
375+
const generatedBreakpoints = generatedBreakpointsMap.get(generatedRemoteLocalPath)!;
376+
377+
// for all breakpoint positions set on the source file, get generated/mapped positions
378+
if (originalBreakpoints.length) {
379+
for (let originalBreakpoint of originalBreakpoints) {
380+
const generatedPosition = await this._sourceMaps.getGeneratedPositionFor({
381+
source: originalLocalAbsolutePath,
382+
column: originalBreakpoint.column ?? 0,
383+
line: originalBreakpoint.line,
384+
});
385+
generatedBreakpoints.push({
386+
line: generatedPosition.line ?? 0,
387+
column: 0,
388+
});
389+
}
390+
}
391+
} catch (e) {
392+
this.log((e as Error).message, LogLevel.Error);
393+
this.sendErrorResponse(
394+
response,
395+
1002,
396+
`Failed to resolve breakpoint for ${originalLocalAbsolutePath}.`
397+
);
398+
continue;
374399
}
375-
} catch (e) {
376-
this.log((e as Error).message, LogLevel.Error);
377-
this.sendErrorResponse(response, 1002, `Failed to resolve breakpoint for ${originalLocalAbsolutePath}.`);
378-
return;
379400
}
380401

381-
const envelope = {
382-
type: 'breakpoints',
383-
breakpoints: {
384-
path: generatedRemoteLocalPath,
385-
breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined,
386-
},
387-
};
402+
// send full set of breakpoints for each generated file, a message per file
403+
for (let [generatedRemoteLocalPath, generatedBreakpoints] of generatedBreakpointsMap) {
404+
const envelope = {
405+
type: 'breakpoints',
406+
breakpoints: {
407+
path: generatedRemoteLocalPath,
408+
breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined,
409+
},
410+
};
411+
this.sendDebuggeeMessage(envelope);
412+
}
413+
414+
// if all bps are removed from this file, ok to remove map entry after sending empty list to client
415+
if (args.breakpoints === undefined || args.breakpoints.length === 0) {
416+
this._sourceBreakpointsMap.delete(args.source.path);
417+
}
388418

389-
this.sendDebuggeeMessage(envelope);
419+
// notify vscode breakpoints have been set
390420
this.sendResponse(response);
391421
}
392422

393423
protected setExceptionBreakPointsRequest(
394424
response: DebugProtocol.SetExceptionBreakpointsResponse,
395425
args: DebugProtocol.SetExceptionBreakpointsArguments,
396-
request?: DebugProtocol.Request
426+
_request?: DebugProtocol.Request
397427
): void {
398428
this.sendDebuggeeMessage({
399429
type: 'stopOnException',
@@ -405,8 +435,8 @@ export class Session extends DebugSession {
405435

406436
protected configurationDoneRequest(
407437
response: DebugProtocol.ConfigurationDoneResponse,
408-
args: DebugProtocol.ConfigurationDoneArguments,
409-
request?: DebugProtocol.Request
438+
_args: DebugProtocol.ConfigurationDoneArguments,
439+
_request?: DebugProtocol.Request
410440
): void {
411441
this.sendDebuggeeMessage({
412442
type: 'resume',
@@ -416,7 +446,7 @@ export class Session extends DebugSession {
416446
}
417447

418448
// VSCode wants current threads (substitute JS contexts)
419-
protected threadsRequest(response: DebugProtocol.ThreadsResponse, request?: DebugProtocol.Request): void {
449+
protected threadsRequest(response: DebugProtocol.ThreadsResponse, _request?: DebugProtocol.Request): void {
420450
response.body = {
421451
threads: Array.from(this._threads.keys()).map(
422452
thread => new Thread(thread, `thread 0x${thread.toString(16)}`)
@@ -478,7 +508,7 @@ export class Session extends DebugSession {
478508
protected variablesRequest(
479509
response: DebugProtocol.VariablesResponse,
480510
args: DebugProtocol.VariablesArguments,
481-
request?: DebugProtocol.Request
511+
_request?: DebugProtocol.Request
482512
) {
483513
// get variables at this reference (all vars in scope or vars in object/array)
484514
this.sendDebugeeRequest(this._activeThreadId, response, args, (body: any) => {
@@ -513,7 +543,7 @@ export class Session extends DebugSession {
513543
protected pauseRequest(
514544
response: DebugProtocol.PauseResponse,
515545
args: DebugProtocol.PauseArguments,
516-
request?: DebugProtocol.Request
546+
_request?: DebugProtocol.Request
517547
) {
518548
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
519549
response.body = body;
@@ -531,7 +561,7 @@ export class Session extends DebugSession {
531561
protected stepInRequest(
532562
response: DebugProtocol.StepInResponse,
533563
args: DebugProtocol.StepInArguments,
534-
request?: DebugProtocol.Request
564+
_request?: DebugProtocol.Request
535565
) {
536566
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
537567
response.body = body;
@@ -542,7 +572,7 @@ export class Session extends DebugSession {
542572
protected stepOutRequest(
543573
response: DebugProtocol.StepOutResponse,
544574
args: DebugProtocol.StepOutArguments,
545-
request?: DebugProtocol.Request
575+
_request?: DebugProtocol.Request
546576
) {
547577
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
548578
response.body = body;
@@ -552,8 +582,8 @@ export class Session extends DebugSession {
552582

553583
protected disconnectRequest(
554584
response: DebugProtocol.DisconnectResponse,
555-
args: DebugProtocol.DisconnectArguments,
556-
request?: DebugProtocol.Request
585+
_args: DebugProtocol.DisconnectArguments,
586+
_request?: DebugProtocol.Request
557587
): void {
558588
// closeSession triggers the 'close' event on the socket which will call terminateSession
559589
this.closeServer();
@@ -719,7 +749,7 @@ export class Session extends DebugSession {
719749
// ------------------------------------------------------------------------
720750

721751
// async send message of type 'request' with promise and await results.
722-
private sendDebugeeRequestAsync(thread: number, response: DebugProtocol.Response, args: any): Promise<any> {
752+
private sendDebugeeRequestAsync(_thread: number, response: DebugProtocol.Response, args: any): Promise<any> {
723753
let promise = new Promise((resolve, reject) => {
724754
let requestSeq = response.request_seq;
725755
this._requests.set(requestSeq, {
@@ -733,7 +763,7 @@ export class Session extends DebugSession {
733763
}
734764

735765
// send message of type 'request' and callback with results.
736-
private sendDebugeeRequest(thread: number, response: DebugProtocol.Response, args: any, callback: Function) {
766+
private sendDebugeeRequest(_thread: number, response: DebugProtocol.Response, args: any, callback: Function) {
737767
let requestSeq = response.request_seq;
738768
this._requests.set(requestSeq, {
739769
onSuccess: callback,

0 commit comments

Comments
 (0)