diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f352081f..56b7a53e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Fixed +- Fix breakpoint filtering fallback in debugger logging ([#1989](https://github.com/swiftlang/vscode-swift/pull/1989)) + +### Fixed + - Fix extension failing to activate when Swiftly was installed via Homebrew ([#1975](https://github.com/swiftlang/vscode-swift/pull/1975)) - Fix running `swift package` commands with `swift.disableSwiftPMIntegration` enabled ([#1969](https://github.com/swiftlang/vscode-swift/pull/1969)) - Fixed an issue where `lldb-dap` could not be found in Command Line Tools toolchains ([#1936](https://github.com/swiftlang/vscode-swift/pull/1936)) diff --git a/src/debugger/logTracker.ts b/src/debugger/logTracker.ts index 5b34b9c27..42e9aaf69 100644 --- a/src/debugger/logTracker.ts +++ b/src/debugger/logTracker.ts @@ -11,6 +11,7 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import { DebugProtocol } from "@vscode/debugprotocol"; import * as vscode from "vscode"; import { SwiftLogger } from "../logging/SwiftLogger"; @@ -72,6 +73,7 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { private exitHandler?: (exitCode: number) => void; private output: string[] = []; private exitCode: number | undefined; + private logger?: SwiftLogger; constructor(public id: string) { LoggingDebugAdapterTracker.debugSessionIdMap[id] = this; @@ -90,6 +92,7 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { cb(o); } if (loggingDebugAdapter.exitCode) { + loggingDebugAdapter.logger = logger; exitHandler(loggingDebugAdapter.exitCode); } loggingDebugAdapter.output = []; @@ -114,6 +117,8 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { return; } + this.handleBreakpointEvent(debugMessage); + if (debugMessage.event === "exited" && debugMessage.body.exitCode) { this.exitCode = debugMessage.body.exitCode; this.exitHandler?.(debugMessage.body.exitCode); @@ -131,6 +136,126 @@ export class LoggingDebugAdapterTracker implements vscode.DebugAdapterTracker { } } + private handleBreakpointEvent(rawMsg: unknown): void { + // Narrow-bodied shapes for just the fields we use (local to this function). + type StoppedBodyLike = { + reason?: string; + thread?: { + frames?: { + source?: { path?: string }; + line?: number; + }[]; + }; + }; + + type BreakpointBodyLike = { + source?: { path?: string }; + line?: number; + }; + + try { + const msg = rawMsg as DebugProtocol.ProtocolMessage; + const eventMsg = msg as DebugProtocol.Event; + + if (!msg || msg.type !== "event") { + return; + } + + const normalizePath = (p: string): string => { + try { + // If adapter sends a URI-like path, parse it + if (p.startsWith("file://") || p.includes("://")) { + return vscode.Uri.parse(p).fsPath; + } + // Otherwise treat as filesystem path + return vscode.Uri.file(p).fsPath; + } catch { + // Fallback: return raw + return p; + } + }; + + // Case A: stopped event with reason = "breakpoint" + if ( + eventMsg.event === "stopped" && + (eventMsg.body as StoppedBodyLike)?.reason === "breakpoint" + ) { + const frames = (eventMsg.body as StoppedBodyLike)?.thread?.frames; + if (!Array.isArray(frames) || frames.length === 0) { + return; + } + + const top = frames[0]; + const sourcePath = top?.source?.path; + const line = top?.line; + if (!sourcePath || typeof line !== "number") { + return; + } + + const bpLine0 = line - 1; // VS Code uses 0-based lines + + const breakpoints = vscode.debug.breakpoints.filter( + b => !!(b as vscode.SourceBreakpoint).location + ) as vscode.SourceBreakpoint[]; + + for (const bp of breakpoints) { + const loc = bp.location; + if (!loc) { + continue; + } + if (loc.uri.fsPath !== normalizePath(sourcePath)) { + continue; + } + if (loc.range.start.line !== bpLine0) { + continue; + } + + // Force a UI refresh so the breakpoint shows as installed. + vscode.debug.removeBreakpoints([bp]); + vscode.debug.addBreakpoints([bp]); + break; + } + return; + } + + // Case B: explicit "breakpoint" event that carries source+line info + if (eventMsg.event === "breakpoint" && eventMsg.body) { + const body = eventMsg.body as BreakpointBodyLike; + const sourcePath = body.source?.path; + const line = body.line; + if (!sourcePath || typeof line !== "number") { + return; + } + + const bpLine0 = line - 1; + const breakpoints = vscode.debug.breakpoints.filter( + b => b instanceof vscode.SourceBreakpoint + ) as vscode.SourceBreakpoint[]; + + for (const bp of breakpoints) { + const loc = bp.location; + if (!loc) { + continue; + } + if (loc.uri.fsPath !== normalizePath(sourcePath)) { + continue; + } + if (loc.range.start.line !== bpLine0) { + continue; + } + + vscode.debug.removeBreakpoints([bp]); + vscode.debug.addBreakpoints([bp]); + break; + } + return; + } + } catch (err) { + // eslint-disable-next-line no-console + this.logger?.error(`Breakpoint fallback error: ${String(err)}`); + } + } + /** * The debug adapter session is about to be stopped. Delete the session from * the tracker