Skip to content

Commit c8f881b

Browse files
waltossclaude
andcommitted
Fix break-rm for function breakpoints and unify DAP breakpoint storage
- removeBreakpoint() now handles both file and function breakpoints - Consolidate 3 breakpoint properties into single DapBreakpoint[] with discriminated union - getRuntimeConfig() resolves aliases and fails fast on unknown runtimes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e627e89 commit c8f881b

2 files changed

Lines changed: 65 additions & 91 deletions

File tree

src/dap/runtimes/index.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const RUNTIME_CONFIGS: Record<string, DapRuntimeConfig> = {
1616
const RUNTIME_ALIASES: Record<string, keyof typeof RUNTIME_CONFIGS> = {
1717
jdwp: "java",
1818
debugpy: "python",
19-
"lldb-dap": "lldb"
19+
"lldb-dap": "lldb",
2020
};
2121

2222
/**
@@ -33,13 +33,14 @@ export const KNOWN_DAP_RUNTIMES = new Set([
3333
...Object.keys(RUNTIME_ALIASES),
3434
]);
3535

36-
const DEFAULT_CONFIG: DapRuntimeConfig = {
37-
getAdapterCommand: () => {
38-
throw new Error("Unknown runtime");
39-
},
40-
buildLaunchArgs: ({ program, args, cwd }) => ({ program, args, cwd }),
41-
};
42-
4336
export function getRuntimeConfig(runtime: string): DapRuntimeConfig {
44-
return RUNTIME_CONFIGS[runtime] ?? { ...DEFAULT_CONFIG, getAdapterCommand: () => [runtime] };
37+
const canonical = resolveRuntime(runtime);
38+
const config = RUNTIME_CONFIGS[canonical];
39+
if (!config) {
40+
const available = [...Object.keys(RUNTIME_CONFIGS), ...Object.keys(RUNTIME_ALIASES)]
41+
.sort()
42+
.join(", ");
43+
throw new Error(`Unknown DAP runtime "${runtime}". Available: ${available}`);
44+
}
45+
return config;
4546
}

src/dap/session.ts

Lines changed: 55 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,28 @@ export function getManagedAdaptersDir(): string {
1818
return join(home, ".debug-that", "adapters");
1919
}
2020

21-
interface DapBreakpointEntry {
21+
interface DapBreakpointBase {
2222
ref: string;
23-
dapId?: number;
24-
file: string;
25-
line: number;
2623
condition?: string;
2724
hitCondition?: string;
2825
verified: boolean;
26+
}
27+
28+
interface DapFileBreakpoint extends DapBreakpointBase {
29+
kind: "file";
30+
dapId?: number;
31+
file: string;
32+
line: number;
2933
actualLine?: number;
3034
}
3135

32-
interface DapFunctionBreakpointEntry {
33-
ref: string;
36+
interface DapFunctionBreakpoint extends DapBreakpointBase {
37+
kind: "function";
3438
name: string;
35-
condition?: string;
36-
hitCondition?: string;
37-
verified: boolean;
3839
}
3940

41+
type DapBreakpoint = DapFileBreakpoint | DapFunctionBreakpoint;
42+
4043
interface DapStackFrame {
4144
id: number;
4245
name: string;
@@ -58,10 +61,8 @@ export class DapSession extends BaseSession {
5861
private _stackFrames: DapStackFrame[] = [];
5962
private adapterCapabilities: DebugProtocol.Capabilities = {};
6063

61-
// Breakpoints: DAP requires sending ALL breakpoints per file at once
62-
private breakpoints = new Map<string, DapBreakpointEntry[]>();
63-
private allBreakpoints: DapBreakpointEntry[] = [];
64-
private functionBreakpoints: DapFunctionBreakpointEntry[] = [];
64+
// Breakpoints: DAP requires sending ALL breakpoints per file/function at once
65+
private breakpoints: DapBreakpoint[] = [];
6566

6667
// Stored config (applied on launch/restart, and immediately if connected+paused)
6768
private _remaps: [string, string][] = [];
@@ -255,9 +256,7 @@ export class DapSession extends BaseSession {
255256
this.resetState();
256257
this._stackFrames = [];
257258
this._isAttached = false;
258-
this.breakpoints.clear();
259-
this.allBreakpoints = [];
260-
this.functionBreakpoints = [];
259+
this.breakpoints = [];
261260
}
262261

263262
// ── Execution control ─────────────────────────────────────────────
@@ -331,7 +330,8 @@ export class DapSession extends BaseSession {
331330
// Resolve short filenames (e.g. "User.java") to full paths via sourcePaths
332331
file = this.resolveSourceFile(file);
333332

334-
const entry: DapBreakpointEntry = {
333+
const entry: DapFileBreakpoint = {
334+
kind: "file",
335335
ref: "", // will be set by RefTable
336336
file,
337337
line,
@@ -340,14 +340,7 @@ export class DapSession extends BaseSession {
340340
verified: false,
341341
};
342342

343-
// Add to per-file tracking
344-
let fileBreakpoints = this.breakpoints.get(file);
345-
if (!fileBreakpoints) {
346-
fileBreakpoints = [];
347-
this.breakpoints.set(file, fileBreakpoints);
348-
}
349-
fileBreakpoints.push(entry);
350-
this.allBreakpoints.push(entry);
343+
this.breakpoints.push(entry);
351344

352345
// Register ref
353346
const ref = this.refs.addBreakpoint(`dap-bp:${file}:${line}`, {
@@ -368,40 +361,33 @@ export class DapSession extends BaseSession {
368361
async removeBreakpoint(ref: string): Promise<void> {
369362
this.requireConnected();
370363

371-
const entry = this.allBreakpoints.find((bp) => bp.ref === ref);
372-
if (!entry) {
364+
const idx = this.breakpoints.findIndex((bp) => bp.ref === ref);
365+
if (idx === -1) {
373366
throw new Error(`Unknown breakpoint ref: ${ref}`);
374367
}
375-
376-
// Remove from per-file tracking
377-
const fileBreakpoints = this.breakpoints.get(entry.file);
378-
if (fileBreakpoints) {
379-
const idx = fileBreakpoints.indexOf(entry);
380-
if (idx !== -1) fileBreakpoints.splice(idx, 1);
381-
if (fileBreakpoints.length === 0) {
382-
this.breakpoints.delete(entry.file);
383-
}
384-
}
385-
386-
// Remove from all-breakpoints list
387-
const allIdx = this.allBreakpoints.indexOf(entry);
388-
if (allIdx !== -1) this.allBreakpoints.splice(allIdx, 1);
389-
390-
// Remove from ref table
368+
// biome-ignore lint/style/noNonNullAssertion: idx validated above
369+
const entry = this.breakpoints[idx]!;
370+
this.breakpoints.splice(idx, 1);
391371
this.refs.remove(ref);
392372

393-
// Re-sync file breakpoints (or clear them if none left)
394-
await this.syncFileBreakpoints(entry.file);
373+
if (entry.kind === "file") {
374+
await this.syncFileBreakpoints(entry.file);
375+
} else {
376+
await this.syncFunctionBreakpoints();
377+
}
395378
}
396379

397380
async removeAllBreakpoints(): Promise<void> {
398381
this.requireConnected();
399382

400-
// Clear all files
401-
const files = [...this.breakpoints.keys()];
402-
this.breakpoints.clear();
403-
this.allBreakpoints = [];
404-
this.functionBreakpoints = [];
383+
const files = new Set(
384+
this.breakpoints
385+
.filter((bp): bp is DapFileBreakpoint => bp.kind === "file")
386+
.map((bp) => bp.file),
387+
);
388+
const hadFunctionBps = this.breakpoints.some((bp) => bp.kind === "function");
389+
390+
this.breakpoints = [];
405391

406392
// Remove all BP refs
407393
for (const entry of this.refs.list("BP")) {
@@ -417,7 +403,9 @@ export class DapSession extends BaseSession {
417403
}
418404

419405
// Clear function breakpoints
420-
await this.getDap().send("setFunctionBreakpoints", { breakpoints: [] });
406+
if (hadFunctionBps) {
407+
await this.getDap().send("setFunctionBreakpoints", { breakpoints: [] });
408+
}
421409
}
422410

423411
/** DAP breakpoints are always bound — the pending filter is ignored. */
@@ -428,21 +416,13 @@ export class DapSession extends BaseSession {
428416
line: number;
429417
condition?: string;
430418
}> {
431-
const fileBps = this.allBreakpoints.map((bp) => ({
432-
ref: bp.ref,
433-
type: "BP" as const,
434-
url: bp.file,
435-
line: bp.actualLine ?? bp.line,
436-
condition: bp.condition,
437-
}));
438-
const fnBps = this.functionBreakpoints.map((bp) => ({
419+
return this.breakpoints.map((bp) => ({
439420
ref: bp.ref,
440421
type: "BP" as const,
441-
url: bp.name,
442-
line: 0,
422+
url: bp.kind === "file" ? bp.file : bp.name,
423+
line: bp.kind === "file" ? (bp.actualLine ?? bp.line) : 0,
443424
condition: bp.condition,
444425
}));
445-
return [...fileBps, ...fnBps];
446426
}
447427

448428
/**
@@ -455,15 +435,16 @@ export class DapSession extends BaseSession {
455435
): Promise<{ ref: string }> {
456436
this.requireConnected();
457437

458-
const entry: DapFunctionBreakpointEntry = {
438+
const entry: DapFunctionBreakpoint = {
439+
kind: "function",
459440
ref: "",
460441
name,
461442
condition: options?.condition,
462443
hitCondition: options?.hitCount ? String(options.hitCount) : undefined,
463444
verified: false,
464445
};
465446

466-
this.functionBreakpoints.push(entry);
447+
this.breakpoints.push(entry);
467448

468449
const ref = this.refs.addBreakpoint(`dap-fn:${name}`, {
469450
url: name,
@@ -475,21 +456,11 @@ export class DapSession extends BaseSession {
475456
return { ref };
476457
}
477458

478-
async removeFunctionBreakpoint(ref: string): Promise<void> {
479-
this.requireConnected();
480-
481-
const idx = this.functionBreakpoints.findIndex((bp) => bp.ref === ref);
482-
if (idx === -1) {
483-
throw new Error(`Unknown function breakpoint ref: ${ref}`);
484-
}
485-
486-
this.functionBreakpoints.splice(idx, 1);
487-
this.refs.remove(ref);
488-
await this.syncFunctionBreakpoints();
489-
}
490-
491459
private async syncFunctionBreakpoints(): Promise<void> {
492-
const dapBps = this.functionBreakpoints.map((bp) => ({
460+
const fnBps = this.breakpoints.filter(
461+
(bp): bp is DapFunctionBreakpoint => bp.kind === "function",
462+
);
463+
const dapBps = fnBps.map((bp) => ({
493464
name: bp.name,
494465
condition: bp.condition,
495466
hitCondition: bp.hitCondition,
@@ -503,8 +474,8 @@ export class DapSession extends BaseSession {
503474
| { breakpoints?: Array<{ id?: number; verified?: boolean }> }
504475
| undefined;
505476
const resultBps = body?.breakpoints ?? [];
506-
for (let i = 0; i < this.functionBreakpoints.length; i++) {
507-
const entry = this.functionBreakpoints[i];
477+
for (let i = 0; i < fnBps.length; i++) {
478+
const entry = fnBps[i];
508479
const result = resultBps[i];
509480
if (entry && result) {
510481
entry.verified = result.verified ?? false;
@@ -791,7 +762,7 @@ export class DapSession extends BaseSession {
791762
}
792763

793764
if (options.breakpoints !== false) {
794-
snapshot.breakpointCount = this.allBreakpoints.length;
765+
snapshot.breakpointCount = this.breakpoints.length;
795766
}
796767

797768
return snapshot;
@@ -1314,7 +1285,9 @@ export class DapSession extends BaseSession {
13141285
}
13151286

13161287
private async syncFileBreakpoints(file: string): Promise<void> {
1317-
const entries = this.breakpoints.get(file) ?? [];
1288+
const entries = this.breakpoints.filter(
1289+
(bp): bp is DapFileBreakpoint => bp.kind === "file" && bp.file === file,
1290+
);
13181291

13191292
const dapBreakpoints = entries.map((bp) => {
13201293
const sbp: Record<string, unknown> = { line: bp.line };

0 commit comments

Comments
 (0)