diff --git a/packages/app/src/workspace/file-open/index.test.ts b/packages/app/src/workspace/file-open/index.test.ts index 6a31ac6ab..d7d2dd397 100644 --- a/packages/app/src/workspace/file-open/index.test.ts +++ b/packages/app/src/workspace/file-open/index.test.ts @@ -140,6 +140,37 @@ describe("resolveWorkspaceFilePaths", () => { expect( resolveWorkspaceFilePaths({ path: "~/notes.md", workspaceRoot: "/Users/me/repo" }), ).toBeNull(); + expect(resolveWorkspaceFilePaths({ path: "~", workspaceRoot: "/Users/me/repo" })).toBeNull(); + }); + + it("treats filenames that merely start with ~ as workspace-relative", () => { + expect(resolveWorkspaceFilePaths({ path: "~env.ts", workspaceRoot: "/Users/me/repo" })).toEqual( + { + absolutePath: "/Users/me/repo/~env.ts", + relativePath: "~env.ts", + }, + ); + }); + + it("matches Windows workspace paths case-insensitively", () => { + expect( + resolveWorkspaceFilePaths({ + path: "C:\\Users\\Me\\Repo\\src\\app.ts", + workspaceRoot: "c:\\users\\me\\repo", + }), + ).toEqual({ + absolutePath: "C:/Users/Me/Repo/src/app.ts", + relativePath: "src/app.ts", + }); + }); + + it("treats a case-only difference from the Windows root as the root itself", () => { + expect( + resolveWorkspaceFilePaths({ + path: "C:\\Users\\Me\\Repo", + workspaceRoot: "c:\\users\\me\\repo", + }), + ).toBeNull(); }); it("returns null when the workspace root is not absolute", () => { diff --git a/packages/app/src/workspace/file-open/index.ts b/packages/app/src/workspace/file-open/index.ts index 4a14a78a5..a823edd5d 100644 --- a/packages/app/src/workspace/file-open/index.ts +++ b/packages/app/src/workspace/file-open/index.ts @@ -113,6 +113,22 @@ function normalizeRelativePath(value: string): string | null { return normalized || null; } +function isWindowsPath(value: string): boolean { + return /^[A-Za-z]:\//.test(value); +} + +function pathsEqual(left: string, right: string): boolean { + return isWindowsPath(left) || isWindowsPath(right) + ? left.toLowerCase() === right.toLowerCase() + : left === right; +} + +function startsWithPath(value: string, prefix: string): boolean { + return isWindowsPath(value) || isWindowsPath(prefix) + ? value.toLowerCase().startsWith(prefix.toLowerCase()) + : value.startsWith(prefix); +} + export interface ResolvedWorkspaceFilePaths { /** Absolute path on the host, suitable for opening in an editor / file manager. */ absolutePath: string; @@ -140,16 +156,17 @@ export function resolveWorkspaceFilePaths(input: { if (!normalizedFile) { return null; } - if (normalizedFile === workspaceRoot) { + if (pathsEqual(normalizedFile, workspaceRoot)) { return null; } - const relativePath = normalizedFile.startsWith(`${workspaceRoot}/`) - ? normalizedFile.slice(workspaceRoot.length + 1) + const prefix = `${workspaceRoot}/`; + const relativePath = startsWithPath(normalizedFile, prefix) + ? normalizedFile.slice(prefix.length) : null; return { absolutePath: normalizedFile, relativePath }; } - if (filePath.startsWith("~")) { + if (filePath === "~" || filePath.startsWith("~/")) { return null; }