Skip to content

Commit

Permalink
fix(breakpoints): Breakpoints could get overwritten when multiple TS …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
chmeyer-ms authored Jan 28, 2025
1 parent 66fcf8d commit a7e542f
Showing 1 changed file with 80 additions and 50 deletions.
130 changes: 80 additions & 50 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class Session extends DebugSession {
private _sourceFileWatcher?: FileSystemWatcher;
private _activeThreadId: number = 0; // the one being debugged
private _localRoot: string = '';
private _sourceBreakpointsMap: Map<string, DebugProtocol.SourceBreakpoint[] | undefined> = new Map();
private _sourceMapRoot?: string;
private _generatedSourceRoot?: string;
private _inlineSourceMap: boolean = false;
Expand Down Expand Up @@ -246,7 +247,7 @@ export class Session extends DebugSession {
// VSCode extension has been activated due to the 'onDebug' activation request defined in packages.json
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments
_args: DebugProtocol.InitializeRequestArguments
): void {
const capabilities: DebugProtocol.Capabilities = {
// indicates VSCode should send the configurationDoneRequest
Expand All @@ -269,9 +270,9 @@ export class Session extends DebugSession {

// VSCode starts MC exe, then waits for MC to boot and connect back to a listening VSCode
protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: DebugProtocol.LaunchRequestArguments,
request?: DebugProtocol.Request
_response: DebugProtocol.LaunchResponse,
_args: DebugProtocol.LaunchRequestArguments,
_request?: DebugProtocol.Request
) {
// not implemented
}
Expand All @@ -280,7 +281,7 @@ export class Session extends DebugSession {
protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: IAttachRequestArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
this.closeSession();

Expand Down Expand Up @@ -337,7 +338,7 @@ export class Session extends DebugSession {
protected async setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
response.body = {
breakpoints: [],
Expand All @@ -348,52 +349,81 @@ export class Session extends DebugSession {
return;
}

let originalLocalAbsolutePath = path.normalize(args.source.path);
// store source breakpoints per file
this._sourceBreakpointsMap.set(args.source.path, args.breakpoints);

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

try {
// first get generated remote file path, will throw if fails
generatedRemoteLocalPath = await this._sourceMaps.getGeneratedRemoteRelativePath(originalLocalAbsolutePath);

// for all breakpoint positions set on the source file, get generated/mapped positions
if (originalBreakpoints.length) {
for (let originalBreakpoint of originalBreakpoints) {
const generatedPosition = await this._sourceMaps.getGeneratedPositionFor({
source: originalLocalAbsolutePath,
column: originalBreakpoint.column || 0,
line: originalBreakpoint.line,
});
generatedBreakpoints.push({
line: generatedPosition.line || 0,
column: 0,
});
// get generated breakpoints from all sources
for (let [sourcePath, sourceBreakpoints] of this._sourceBreakpointsMap) {
let originalLocalAbsolutePath = path.normalize(sourcePath);

const originalBreakpoints = sourceBreakpoints ?? [];
let generatedRemoteLocalPath = undefined;

try {
// first get generated remote file path, will throw if fails
generatedRemoteLocalPath = await this._sourceMaps.getGeneratedRemoteRelativePath(
originalLocalAbsolutePath
);

// append to any existing breakpoints for this generated file
if (!generatedBreakpointsMap.has(generatedRemoteLocalPath)) {
generatedBreakpointsMap.set(generatedRemoteLocalPath, []);
}
const generatedBreakpoints = generatedBreakpointsMap.get(generatedRemoteLocalPath)!;

// for all breakpoint positions set on the source file, get generated/mapped positions
if (originalBreakpoints.length) {
for (let originalBreakpoint of originalBreakpoints) {
const generatedPosition = await this._sourceMaps.getGeneratedPositionFor({
source: originalLocalAbsolutePath,
column: originalBreakpoint.column ?? 0,
line: originalBreakpoint.line,
});
generatedBreakpoints.push({
line: generatedPosition.line ?? 0,
column: 0,
});
}
}
} catch (e) {
this.log((e as Error).message, LogLevel.Error);
this.sendErrorResponse(
response,
1002,
`Failed to resolve breakpoint for ${originalLocalAbsolutePath}.`
);
continue;
}
} catch (e) {
this.log((e as Error).message, LogLevel.Error);
this.sendErrorResponse(response, 1002, `Failed to resolve breakpoint for ${originalLocalAbsolutePath}.`);
return;
}

const envelope = {
type: 'breakpoints',
breakpoints: {
path: generatedRemoteLocalPath,
breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined,
},
};
// send full set of breakpoints for each generated file, a message per file
for (let [generatedRemoteLocalPath, generatedBreakpoints] of generatedBreakpointsMap) {
const envelope = {
type: 'breakpoints',
breakpoints: {
path: generatedRemoteLocalPath,
breakpoints: generatedBreakpoints.length ? generatedBreakpoints : undefined,
},
};
this.sendDebuggeeMessage(envelope);
}

// if all bps are removed from this file, ok to remove map entry after sending empty list to client
if (args.breakpoints === undefined || args.breakpoints.length === 0) {
this._sourceBreakpointsMap.delete(args.source.path);
}

this.sendDebuggeeMessage(envelope);
// notify vscode breakpoints have been set
this.sendResponse(response);
}

protected setExceptionBreakPointsRequest(
response: DebugProtocol.SetExceptionBreakpointsResponse,
args: DebugProtocol.SetExceptionBreakpointsArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
): void {
this.sendDebuggeeMessage({
type: 'stopOnException',
Expand All @@ -405,8 +435,8 @@ export class Session extends DebugSession {

protected configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments,
request?: DebugProtocol.Request
_args: DebugProtocol.ConfigurationDoneArguments,
_request?: DebugProtocol.Request
): void {
this.sendDebuggeeMessage({
type: 'resume',
Expand All @@ -416,7 +446,7 @@ export class Session extends DebugSession {
}

// VSCode wants current threads (substitute JS contexts)
protected threadsRequest(response: DebugProtocol.ThreadsResponse, request?: DebugProtocol.Request): void {
protected threadsRequest(response: DebugProtocol.ThreadsResponse, _request?: DebugProtocol.Request): void {
response.body = {
threads: Array.from(this._threads.keys()).map(
thread => new Thread(thread, `thread 0x${thread.toString(16)}`)
Expand Down Expand Up @@ -478,7 +508,7 @@ export class Session extends DebugSession {
protected variablesRequest(
response: DebugProtocol.VariablesResponse,
args: DebugProtocol.VariablesArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
// get variables at this reference (all vars in scope or vars in object/array)
this.sendDebugeeRequest(this._activeThreadId, response, args, (body: any) => {
Expand Down Expand Up @@ -513,7 +543,7 @@ export class Session extends DebugSession {
protected pauseRequest(
response: DebugProtocol.PauseResponse,
args: DebugProtocol.PauseArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
response.body = body;
Expand All @@ -531,7 +561,7 @@ export class Session extends DebugSession {
protected stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
response.body = body;
Expand All @@ -542,7 +572,7 @@ export class Session extends DebugSession {
protected stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments,
request?: DebugProtocol.Request
_request?: DebugProtocol.Request
) {
this.sendDebugeeRequest(args.threadId, response, args, (body: any) => {
response.body = body;
Expand All @@ -552,8 +582,8 @@ export class Session extends DebugSession {

protected disconnectRequest(
response: DebugProtocol.DisconnectResponse,
args: DebugProtocol.DisconnectArguments,
request?: DebugProtocol.Request
_args: DebugProtocol.DisconnectArguments,
_request?: DebugProtocol.Request
): void {
// closeSession triggers the 'close' event on the socket which will call terminateSession
this.closeServer();
Expand Down Expand Up @@ -719,7 +749,7 @@ export class Session extends DebugSession {
// ------------------------------------------------------------------------

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

// send message of type 'request' and callback with results.
private sendDebugeeRequest(thread: number, response: DebugProtocol.Response, args: any, callback: Function) {
private sendDebugeeRequest(_thread: number, response: DebugProtocol.Response, args: any, callback: Function) {
let requestSeq = response.request_seq;
this._requests.set(requestSeq, {
onSuccess: callback,
Expand Down

0 comments on commit a7e542f

Please sign in to comment.