From f380458bae94d5dec1c8bdbc8161453d3ce99c73 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 18 Jul 2025 04:19:15 -0700 Subject: [PATCH] Add remoteRoot/localRoot mapping for VSCode (#19884) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: robobun --- packages/bun-vscode/README.md | 3 + packages/bun-vscode/package.json | 8 ++ packages/bun-vscode/src/features/debug.ts | 106 +++++++++++++++++++--- 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/packages/bun-vscode/README.md b/packages/bun-vscode/README.md index f373c5a4b8..3e63cef04a 100644 --- a/packages/bun-vscode/README.md +++ b/packages/bun-vscode/README.md @@ -95,6 +95,9 @@ You can use the following configurations to debug JavaScript and TypeScript file // The URL of the WebSocket inspector to attach to. // This value can be retrieved by using `bun --inspect`. "url": "ws://localhost:6499/", + // Optional path mapping for remote debugging + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app", }, ], } diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json index 6128d9bdd6..09f49e1d57 100644 --- a/packages/bun-vscode/package.json +++ b/packages/bun-vscode/package.json @@ -279,6 +279,14 @@ "type": "boolean", "description": "If the debugger should stop on the first line of the program.", "default": false + }, + "localRoot": { + "type": "string", + "description": "The local path that maps to \"remoteRoot\" when attaching to a remote Bun process." + }, + "remoteRoot": { + "type": "string", + "description": "The remote path to the code when attaching. File paths reported by Bun that start with this path will be mapped back to 'localRoot'." } } } diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts index 94ff64280b..a653fd676f 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -1,5 +1,6 @@ import { DebugSession, OutputEvent } from "@vscode/debugadapter"; import { tmpdir } from "node:os"; +import * as path from "node:path"; import { join } from "node:path"; import * as vscode from "vscode"; import { @@ -220,7 +221,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory session: vscode.DebugSession, ): Promise> { const { configuration } = session; - const { request, url, __untitledName } = configuration; + const { request, url, __untitledName, localRoot, remoteRoot } = configuration; if (request === "attach") { for (const [adapterUrl, adapter] of adapters) { @@ -230,7 +231,10 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory } } - const adapter = new FileDebugSession(session.id, __untitledName); + const adapter = new FileDebugSession(session.id, __untitledName, { + localRoot, + remoteRoot, + }); await adapter.initialize(); return new vscode.DebugAdapterInlineImplementation(adapter); } @@ -275,6 +279,11 @@ interface RuntimeExceptionThrownEvent { }; } +interface PathMapping { + localRoot?: string; + remoteRoot?: string; +} + class FileDebugSession extends DebugSession { // If these classes are moved/published, we should make sure // we remove these non-null assertions so consumers of @@ -283,18 +292,60 @@ class FileDebugSession extends DebugSession { sessionId?: string; untitledDocPath?: string; bunEvalPath?: string; + localRoot?: string; + remoteRoot?: string; + #isWindowsRemote = false; - constructor(sessionId?: string, untitledDocPath?: string) { + constructor(sessionId?: string, untitledDocPath?: string, mapping?: PathMapping) { super(); this.sessionId = sessionId; this.untitledDocPath = untitledDocPath; + if (mapping) { + this.localRoot = mapping.localRoot; + this.remoteRoot = mapping.remoteRoot; + if (typeof mapping.remoteRoot === "string") { + this.#isWindowsRemote = mapping.remoteRoot.includes("\\"); + } + } + if (untitledDocPath) { const cwd = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? process.cwd(); this.bunEvalPath = join(cwd, "[eval]"); } } + mapRemoteToLocal(p: string | undefined): string | undefined { + if (!p || !this.remoteRoot || !this.localRoot) return p; + const remoteModule = this.#isWindowsRemote ? path.win32 : path.posix; + let remoteRoot = remoteModule.normalize(this.remoteRoot); + if (!remoteRoot.endsWith(remoteModule.sep)) remoteRoot += remoteModule.sep; + let target = remoteModule.normalize(p); + const starts = this.#isWindowsRemote + ? target.toLowerCase().startsWith(remoteRoot.toLowerCase()) + : target.startsWith(remoteRoot); + if (starts) { + const rel = target.slice(remoteRoot.length); + const localRel = rel.split(remoteModule.sep).join(path.sep); + return path.join(this.localRoot, localRel); + } + return p; + } + + mapLocalToRemote(p: string | undefined): string | undefined { + if (!p || !this.remoteRoot || !this.localRoot) return p; + let localRoot = path.normalize(this.localRoot); + if (!localRoot.endsWith(path.sep)) localRoot += path.sep; + let localPath = path.normalize(p); + if (localPath.startsWith(localRoot)) { + const rel = localPath.slice(localRoot.length); + const remoteModule = this.#isWindowsRemote ? path.win32 : path.posix; + const remoteRel = rel.split(path.sep).join(remoteModule.sep); + return remoteModule.join(this.remoteRoot, remoteRel); + } + return p; + } + async initialize() { const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2); const url = @@ -307,14 +358,20 @@ class FileDebugSession extends DebugSession { if (untitledDocPath) { this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => { - if (response.body?.source?.path === bunEvalPath) { - response.body.source.path = untitledDocPath; + if (response.body?.source?.path) { + if (response.body.source.path === bunEvalPath) { + response.body.source.path = untitledDocPath; + } else { + response.body.source.path = this.mapRemoteToLocal(response.body.source.path); + } } if (Array.isArray(response.body?.breakpoints)) { for (const bp of response.body.breakpoints) { if (bp.source?.path === bunEvalPath) { bp.source.path = untitledDocPath; bp.verified = true; + } else if (bp.source?.path) { + bp.source.path = this.mapRemoteToLocal(bp.source.path); } } } @@ -322,14 +379,35 @@ class FileDebugSession extends DebugSession { }); this.adapter.on("Adapter.event", (event: DebugProtocolEvent) => { - if (event.body?.source?.path === bunEvalPath) { - event.body.source.path = untitledDocPath; + if (event.body?.source?.path) { + if (event.body.source.path === bunEvalPath) { + event.body.source.path = untitledDocPath; + } else { + event.body.source.path = this.mapRemoteToLocal(event.body.source.path); + } } this.sendEvent(event); }); } else { - this.adapter.on("Adapter.response", response => this.sendResponse(response)); - this.adapter.on("Adapter.event", event => this.sendEvent(event)); + this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => { + if (response.body?.source?.path) { + response.body.source.path = this.mapRemoteToLocal(response.body.source.path); + } + if (Array.isArray(response.body?.breakpoints)) { + for (const bp of response.body.breakpoints) { + if (bp.source?.path) { + bp.source.path = this.mapRemoteToLocal(bp.source.path); + } + } + } + this.sendResponse(response); + }); + this.adapter.on("Adapter.event", (event: DebugProtocolEvent) => { + if (event.body?.source?.path) { + event.body.source.path = this.mapRemoteToLocal(event.body.source.path); + } + this.sendEvent(event); + }); } this.adapter.on("Adapter.reverseRequest", ({ command, arguments: args }) => @@ -345,11 +423,15 @@ class FileDebugSession extends DebugSession { if (type === "request") { const { untitledDocPath, bunEvalPath } = this; const { command } = message; - if (untitledDocPath && (command === "setBreakpoints" || command === "breakpointLocations")) { + if (command === "setBreakpoints" || command === "breakpointLocations") { const args = message.arguments as any; - if (args.source?.path === untitledDocPath) { + if (untitledDocPath && args.source?.path === untitledDocPath) { args.source.path = bunEvalPath; + } else if (args.source?.path) { + args.source.path = this.mapLocalToRemote(args.source.path); } + } else if (command === "source" && message.arguments?.source?.path) { + message.arguments.source.path = this.mapLocalToRemote(message.arguments.source.path); } this.adapter.emit("Adapter.request", message); @@ -367,7 +449,7 @@ class TerminalDebugSession extends FileDebugSession { signal!: TCPSocketSignal | UnixSignal; constructor() { - super(); + super(undefined, undefined); } async initialize() {