Open workspace files in desktop targets

This commit is contained in:
Mohamed Boudra
2026-06-03 18:34:24 +08:00
committed by GitHub
parent 45f412456c
commit 3681fd132b
30 changed files with 1830 additions and 822 deletions
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
createWorkspaceFileTabTarget,
normalizeWorkspaceFileLocation,
resolveWorkspaceFilePaths,
workspaceFileLocationsEqual,
} from ".";
@@ -61,3 +62,93 @@ describe("workspace file tab targets", () => {
).toBe(false);
});
});
describe("resolveWorkspaceFilePaths", () => {
it("joins workspace-relative paths against the workspace root", () => {
expect(
resolveWorkspaceFilePaths({ path: "src/app.ts", workspaceRoot: "/Users/me/repo" }),
).toEqual({
absolutePath: "/Users/me/repo/src/app.ts",
relativePath: "src/app.ts",
});
});
it("derives the relative path for an absolute file inside the workspace", () => {
expect(
resolveWorkspaceFilePaths({
path: "/Users/me/repo/src/app.ts",
workspaceRoot: "/Users/me/repo",
}),
).toEqual({
absolutePath: "/Users/me/repo/src/app.ts",
relativePath: "src/app.ts",
});
});
it("keeps the absolute path but drops the relative path when outside the workspace", () => {
expect(
resolveWorkspaceFilePaths({
path: "/etc/hosts",
workspaceRoot: "/Users/me/repo",
}),
).toEqual({
absolutePath: "/etc/hosts",
relativePath: null,
});
});
it("normalizes Windows separators in the file path", () => {
expect(
resolveWorkspaceFilePaths({ path: "src\\app.ts", workspaceRoot: "/Users/me/repo" }),
).toEqual({
absolutePath: "/Users/me/repo/src/app.ts",
relativePath: "src/app.ts",
});
});
it("normalizes dot segments in workspace-relative paths", () => {
expect(
resolveWorkspaceFilePaths({ path: "./src/../app.ts", workspaceRoot: "/Users/me/repo" }),
).toEqual({
absolutePath: "/Users/me/repo/app.ts",
relativePath: "app.ts",
});
});
it("rejects workspace-relative paths that escape the workspace", () => {
expect(
resolveWorkspaceFilePaths({ path: "../outside.ts", workspaceRoot: "/Users/me/repo" }),
).toBeNull();
expect(
resolveWorkspaceFilePaths({ path: "src/../../outside.ts", workspaceRoot: "/Users/me/repo" }),
).toBeNull();
});
it("derives the relative path for a Windows absolute file inside the workspace", () => {
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("returns null for home-relative paths that cannot be anchored", () => {
expect(
resolveWorkspaceFilePaths({ path: "~/notes.md", workspaceRoot: "/Users/me/repo" }),
).toBeNull();
});
it("returns null when the workspace root is not absolute", () => {
expect(resolveWorkspaceFilePaths({ path: "src/app.ts", workspaceRoot: "repo" })).toBeNull();
});
it("returns null when the resolved file equals the workspace root", () => {
expect(
resolveWorkspaceFilePaths({ path: "/Users/me/repo", workspaceRoot: "/Users/me/repo" }),
).toBeNull();
});
});
@@ -1,3 +1,5 @@
import { isAbsolutePath } from "@/utils/path";
export type OpenFileDisposition = "main" | "side";
export interface WorkspaceFileLocation {
@@ -57,3 +59,103 @@ function normalizeLineNumber(value: number | null | undefined): number | undefin
? Math.floor(value)
: undefined;
}
function trimTrailingSlashes(value: string): string {
if (value === "/" || /^\/+$/.test(value)) {
return "/";
}
if (/^[A-Za-z]:\/+$/.test(value)) {
return `${value.slice(0, 2)}/`;
}
return value.replace(/\/+$/, "");
}
function normalizePathSegments(value: string, rejectEscape: boolean): string | null {
const segments: string[] = [];
for (const segment of value.split("/")) {
if (!segment || segment === ".") {
continue;
}
if (segment === "..") {
if (segments.length === 0) {
if (rejectEscape) {
return null;
}
continue;
}
segments.pop();
continue;
}
segments.push(segment);
}
return segments.join("/");
}
function normalizeAbsolutePath(value: string): string | null {
const normalizedInput = trimTrailingSlashes(value);
if (!isAbsolutePath(normalizedInput)) {
return null;
}
const drivePath = /^([A-Za-z]:)\/(.*)$/.exec(normalizedInput);
if (drivePath) {
const normalizedBody = normalizePathSegments(drivePath[2], false);
return trimTrailingSlashes(`${drivePath[1]}/${normalizedBody}`);
}
const prefix = normalizedInput.startsWith("//") ? "//" : "/";
const normalizedBody = normalizePathSegments(normalizedInput.replace(/^\/+/, ""), false);
return trimTrailingSlashes(`${prefix}${normalizedBody}`);
}
function normalizeRelativePath(value: string): string | null {
const normalized = normalizePathSegments(value.replace(/^\/+/, ""), true);
return normalized || null;
}
export interface ResolvedWorkspaceFilePaths {
/** Absolute path on the host, suitable for opening in an editor / file manager. */
absolutePath: string;
/** Path relative to the workspace root, or null when the file lives outside it. */
relativePath: string | null;
}
/**
* Resolves a file tab's path (which may be workspace-relative) against the workspace
* root. Returns null when an absolute host path cannot be derived — e.g. a `~`-relative
* path or a relative path with no workspace root to anchor it.
*/
export function resolveWorkspaceFilePaths(input: {
path: string;
workspaceRoot: string;
}): ResolvedWorkspaceFilePaths | null {
const filePath = input.path.trim().replace(/\\/g, "/");
const workspaceRoot = normalizeAbsolutePath(input.workspaceRoot.trim().replace(/\\/g, "/"));
if (!filePath || !workspaceRoot) {
return null;
}
if (isAbsolutePath(filePath)) {
const normalizedFile = normalizeAbsolutePath(filePath);
if (!normalizedFile) {
return null;
}
if (normalizedFile === workspaceRoot) {
return null;
}
const relativePath = normalizedFile.startsWith(`${workspaceRoot}/`)
? normalizedFile.slice(workspaceRoot.length + 1)
: null;
return { absolutePath: normalizedFile, relativePath };
}
if (filePath.startsWith("~")) {
return null;
}
const relativePath = normalizeRelativePath(filePath);
if (!relativePath) {
return null;
}
return { absolutePath: `${workspaceRoot}/${relativePath}`, relativePath };
}