From e9f9759ba88fce274251537db962ca8284864fc6 Mon Sep 17 00:00:00 2001 From: Bjoern147 <43368639+bjspi@users.noreply.github.com> Date: Wed, 10 Jun 2026 06:18:25 +0200 Subject: [PATCH] Fix Windows Explorer opening Documents instead of the workspace Closes #476 --- .../src/features/editor-targets.test.ts | 67 ++++++++++++++++++- .../desktop/src/features/editor-targets.ts | 15 ++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/features/editor-targets.test.ts b/packages/desktop/src/features/editor-targets.test.ts index 857cbb138..66467d8d8 100644 --- a/packages/desktop/src/features/editor-targets.test.ts +++ b/packages/desktop/src/features/editor-targets.test.ts @@ -162,7 +162,45 @@ describe("desktop editor targets", () => { expect(recorder.calls[0]?.args).toEqual(["-R", "/tmp/repo/src/index.ts"]); }); - it("reveals files in Explorer on Windows", async () => { + it("opens the workspace directory in Explorer using Windows path separators", async () => { + const recorder = createSpawnRecorder(); + + await openEditorTarget( + { editorId: "explorer", path: "C:/Users/me/project" }, + { + platform: "win32", + env: { PATH: "C:/Windows" }, + existsSync: createExistsSync(["C:/Users/me/project", "C:/Windows/explorer.exe"]), + spawn: recorder.spawn, + }, + ); + + // explorer.exe reads each "/segment" of a forward-slash arg as a switch and + // falls back to the Documents folder. The path must use backslashes. + expect(recorder.calls[0]?.command).toBe("C:/Windows/explorer.exe"); + expect(recorder.calls[0]?.args).toEqual(["C:\\Users\\me\\project"]); + expect(recorder.calls[0]?.options.shell).toBe(false); + }); + + it("opens UNC workspace directories in Explorer using Windows path separators", async () => { + const recorder = createSpawnRecorder(); + + await openEditorTarget( + { editorId: "explorer", path: "//server/share/project" }, + { + platform: "win32", + env: { PATH: "C:/Windows" }, + existsSync: createExistsSync(["//server/share/project", "C:/Windows/explorer.exe"]), + spawn: recorder.spawn, + }, + ); + + // UNC paths must become \\server\share\... — the leading // is preserved by + // the app's path normalization and explorer accepts the backslash form. + expect(recorder.calls[0]?.args).toEqual(["\\\\server\\share\\project"]); + }); + + it("reveals files in Explorer on Windows using Windows path separators", async () => { const recorder = createSpawnRecorder(); await openEditorTarget( @@ -176,7 +214,7 @@ describe("desktop editor targets", () => { ); expect(recorder.calls[0]?.command).toBe("C:/Windows/explorer.exe"); - expect(recorder.calls[0]?.args).toEqual(["/select,", "C:/repo/src/index.ts"]); + expect(recorder.calls[0]?.args).toEqual(["/select,", "C:\\repo\\src\\index.ts"]); expect(recorder.calls[0]?.options.shell).toBe(false); }); @@ -196,13 +234,36 @@ describe("desktop editor targets", () => { }, ); + // Separators flip to backslashes; "&" stays literal and the path stays a + // separate arg token (Node only quotes the path, not the "/select," switch). expect(recorder.calls[0]).toMatchObject({ command: "C:/Windows/explorer.exe", - args: ["/select,", "C:/repo/src/file & calculator.ts"], + args: ["/select,", "C:\\repo\\src\\file & calculator.ts"], options: { shell: false }, }); }); + it("does not convert separators for editor targets on Windows", async () => { + const recorder = createSpawnRecorder(); + + await openEditorTarget( + { editorId: "vscode", path: "C:/repo/src/index.ts", cwd: "C:/repo" }, + { + platform: "win32", + env: { PATH: "C:/Program Files/Microsoft VS Code/bin" }, + existsSync: createExistsSync([ + "C:/repo/src/index.ts", + "C:/repo", + "C:/Program Files/Microsoft VS Code/bin/code.exe", + ]), + spawn: recorder.spawn, + }, + ); + + // Editors accept forward slashes; the backslash conversion is Explorer-scoped. + expect(recorder.calls[0]?.args).toEqual(["C:/repo", "C:/repo/src/index.ts"]); + }); + it("prefers Windows command shims over extensionless shell launchers", async () => { const recorder = createSpawnRecorder(); const vscodeBin = "C:/Users/me/AppData/Local/Programs/Microsoft VS Code/bin"; diff --git a/packages/desktop/src/features/editor-targets.ts b/packages/desktop/src/features/editor-targets.ts index 175d87c56..1cbf00ea6 100644 --- a/packages/desktop/src/features/editor-targets.ts +++ b/packages/desktop/src/features/editor-targets.ts @@ -190,6 +190,16 @@ function dirnameForPlatform(value: string, platform: NodeJS.Platform): string { return platform === "win32" ? win32.dirname(value) : posix.dirname(value); } +// App paths are normalized to forward slashes everywhere (see file-open), but +// explorer.exe parses each "/segment" of an argument as a command-line switch. +// Given a POSIX-style path it finds no path token and silently falls back to +// the default shell folder (the user's Documents). Hand it native backslashes. +// Only the argument needs converting — CreateProcess resolves the command path +// fine with forward slashes. +function toWindowsPathSeparators(value: string): string { + return value.replace(/\//g, "\\"); +} + function isWindowsCommandScript(executable: string, platform: NodeJS.Platform): boolean { if (platform !== "win32") { return false; @@ -237,7 +247,7 @@ function buildLaunch(input: { return { command: input.executable, args: ["-R", input.path] }; } if (input.target.id === "explorer" && input.platform === "win32") { - return { command: input.executable, args: ["/select,", input.path] }; + return { command: input.executable, args: ["/select,", toWindowsPathSeparators(input.path)] }; } if (input.target.id === "file-manager") { return { command: input.executable, args: [dirnameForPlatform(input.path, input.platform)] }; @@ -247,6 +257,9 @@ function buildLaunch(input: { if (input.target.kind === "editor" && input.cwd && input.cwd !== input.path) { return { command: input.executable, args: [input.cwd, input.path] }; } + if (input.target.id === "explorer" && input.platform === "win32") { + return { command: input.executable, args: [toWindowsPathSeparators(input.path)] }; + } return { command: input.executable, args: [input.path] }; }