mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(terminal): open the file a link names, not a same-named one (#711)
resolvePath only understood POSIX paths, so on Windows the direct lookup never matched and every click fell back to a project-wide search by bare file name, which opened any same-named file. Resolve Windows drive paths and backslashes, and make the fallback query include the link's directory and check the hit's project-relative path ends with it; a bare-name match is used only for links without a directory. Fixes #705. Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
This commit is contained in:
co-authored by
a-rookie-of-C-language
parent
fb55847afa
commit
c5ff0de1ee
@@ -60,14 +60,14 @@ export function findTerminalLinks(text: string): TerminalLink[] {
|
||||
return links.sort((a, b) => a.start - b.start);
|
||||
}
|
||||
|
||||
/** Resolves `path` against `cwd` (POSIX); `~` expands with `home` when known. */
|
||||
/** Resolves `path` against `cwd` (POSIX, or Windows drive paths); `~` expands with `home` when known. */
|
||||
export function resolvePath(path: string, cwd: string, home?: string | null): string {
|
||||
let p = path;
|
||||
let p = path.replace(/\\/g, "/");
|
||||
if (p === "~" || p.startsWith("~/")) {
|
||||
if (!home) return p;
|
||||
p = home + p.slice(1);
|
||||
} else if (!p.startsWith("/")) {
|
||||
p = `${cwd.replace(/\/+$/, "")}/${p}`;
|
||||
p = home.replace(/\\/g, "/") + p.slice(1);
|
||||
} else if (!p.startsWith("/") && !/^[A-Za-z]:\//.test(p)) {
|
||||
p = `${cwd.replace(/\\/g, "/").replace(/\/+$/, "")}/${p}`;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
for (const seg of p.split("/")) {
|
||||
@@ -75,5 +75,7 @@ export function resolvePath(path: string, cwd: string, home?: string | null): st
|
||||
if (seg === "..") parts.pop();
|
||||
else parts.push(seg);
|
||||
}
|
||||
return "/" + parts.join("/");
|
||||
const joined = parts.join("/");
|
||||
// A Windows drive prefix is its own root; POSIX results keep the leading slash.
|
||||
return /^[A-Za-z]:\//.test(joined) ? joined : `/${joined}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { findTerminalLinks, type TerminalLink } from "./links";
|
||||
import { openFileLink } from "./session";
|
||||
|
||||
const ipc = vi.hoisted(() => ({
|
||||
call: vi.fn(async (v: unknown) => v),
|
||||
commands: { fsReadDir: vi.fn(), findFiles: vi.fn() },
|
||||
}));
|
||||
const workbench = vi.hoisted(() => ({
|
||||
openFile: vi.fn(async () => undefined),
|
||||
notify: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/platform/ipc", () => ({ call: ipc.call, commands: ipc.commands, isTauri: () => true }));
|
||||
vi.mock("@/workbench/contributions", () => ({
|
||||
editorService: () => ({ openFile: workbench.openFile }),
|
||||
notifications: () => ({ notify: workbench.notify }),
|
||||
OWNS_FILE_DROP: { "data-owner": true },
|
||||
}));
|
||||
|
||||
const fileLink = (text: string): Extract<TerminalLink, { kind: "file" }> => {
|
||||
const [l] = findTerminalLinks(text);
|
||||
if (!l || l.kind !== "file") throw new Error(`not a file link: ${text}`);
|
||||
return l;
|
||||
};
|
||||
|
||||
describe("openFileLink", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("opens the resolved path when the directory lists it", async () => {
|
||||
ipc.commands.fsReadDir.mockResolvedValue([{ name: "main.php", kind: "file" }]);
|
||||
await openFileLink(fileLink("a/main.php:2"), "C:\\Users\\me\\proj");
|
||||
expect(ipc.commands.fsReadDir).toHaveBeenCalledWith("C:/Users/me/proj/a");
|
||||
expect(workbench.openFile).toHaveBeenCalledWith("C:/Users/me/proj/a/main.php", {
|
||||
selection: { line: 1, col: 0 },
|
||||
focus: true,
|
||||
});
|
||||
expect(ipc.commands.findFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ranks the link's directory over the bare name in the project-wide fallback (#705)", async () => {
|
||||
ipc.commands.fsReadDir.mockResolvedValue([]);
|
||||
ipc.commands.findFiles.mockImplementation(async (q: string) =>
|
||||
q === "a/main.php"
|
||||
? [
|
||||
{ path: "/p/b/main.php", relPath: "b/main.php" },
|
||||
{ path: "/p/a/main.php", relPath: "a/main.php" },
|
||||
]
|
||||
: [{ path: "/p/c/main.php", relPath: "c/main.php" }],
|
||||
);
|
||||
await openFileLink(fileLink("a/main.php:2"), "/p");
|
||||
expect(ipc.commands.findFiles).toHaveBeenCalledWith("a/main.php", 5);
|
||||
expect(workbench.openFile).toHaveBeenCalledWith("/p/a/main.php", {
|
||||
selection: { line: 1, col: 0 },
|
||||
focus: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the bare name for a basename-only link", async () => {
|
||||
ipc.commands.fsReadDir.mockResolvedValue([]);
|
||||
ipc.commands.findFiles.mockResolvedValue([{ path: "/p/x/main.php", relPath: "x/main.php" }]);
|
||||
await openFileLink(fileLink("main.php:1"), "/p");
|
||||
expect(ipc.commands.findFiles).toHaveBeenCalledTimes(1);
|
||||
expect(workbench.openFile).toHaveBeenCalledWith("/p/x/main.php", { selection: { line: 0, col: 0 }, focus: true });
|
||||
});
|
||||
|
||||
it("warns when nothing matches", async () => {
|
||||
ipc.commands.fsReadDir.mockResolvedValue([]);
|
||||
ipc.commands.findFiles.mockResolvedValue([]);
|
||||
await openFileLink(fileLink("gone.php:1"), "/p");
|
||||
expect(workbench.openFile).not.toHaveBeenCalled();
|
||||
expect(workbench.notify).toHaveBeenCalledWith({ level: "warning", title: "File not found", body: "gone.php" });
|
||||
});
|
||||
});
|
||||
@@ -137,25 +137,32 @@ export const LINK_HANDLER: ILinkHandler = {
|
||||
export async function openFileLink(link: Extract<TerminalLink, { kind: "file" }>, cwd: string) {
|
||||
const selection = link.line ? { line: link.line - 1, col: Math.max(0, (link.col ?? 1) - 1) } : undefined;
|
||||
const path = resolvePath(link.path, cwd);
|
||||
const name = link.path.split("/").pop() ?? link.path;
|
||||
const rel = link.path.replace(/\\/g, "/");
|
||||
const name = rel.split("/").pop() ?? rel;
|
||||
try {
|
||||
const slash = path.lastIndexOf("/");
|
||||
const entries = await call(commands.fsReadDir(path.slice(0, slash) || "/"));
|
||||
const entries = await call(commands.fsReadDir(slash > 0 ? path.slice(0, slash) : "/"));
|
||||
if (entries.some((e) => e.name === name && e.kind !== "dir")) {
|
||||
await editorService().openFile(path, { selection, focus: true });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* fall back to a project-wide lookup by file name */
|
||||
/* fall back to a project-wide lookup */
|
||||
}
|
||||
try {
|
||||
const [hit] = await call(commands.findFiles(name, 1));
|
||||
if (hit && hit.path.endsWith(name)) {
|
||||
await editorService().openFile(hit.path, { selection, focus: true });
|
||||
return;
|
||||
// The basename alone is ambiguous (`a/x.ts` vs `b/x.ts`): rank the link's
|
||||
// directory part too and verify the hit actually sits on that path, settling
|
||||
// for a basename match only when the link names no directory.
|
||||
for (const q of rel === name ? [name] : [rel, name]) {
|
||||
try {
|
||||
const hits = await call(commands.findFiles(q, 5));
|
||||
const hit = hits.find((h) => h.relPath === q || h.relPath.endsWith(`/${q}`));
|
||||
if (hit) {
|
||||
await editorService().openFile(hit.path, { selection, focus: true });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
notifications().notify({ level: "warning", title: "File not found", body: link.path });
|
||||
}
|
||||
|
||||
@@ -96,6 +96,14 @@ describe("terminal links", () => {
|
||||
expect(resolvePath("/abs/c.ts", "/p")).toBe("/abs/c.ts");
|
||||
expect(resolvePath("~/d.ts", "/p", "/Users/me")).toBe("/Users/me/d.ts");
|
||||
});
|
||||
|
||||
it("resolves paths against a Windows cwd (#705)", () => {
|
||||
expect(resolvePath("a/main.php", "C:\\Users\\me\\proj")).toBe("C:/Users/me/proj/a/main.php");
|
||||
expect(resolvePath("a\\main.php", "C:/Users/me/proj")).toBe("C:/Users/me/proj/a/main.php");
|
||||
expect(resolvePath("C:\\abs\\d.ts", "C:/p")).toBe("C:/abs/d.ts");
|
||||
expect(resolvePath("~/e.ts", "C:/p", "C:\\Users\\me")).toBe("C:/Users/me/e.ts");
|
||||
expect(resolvePath("../f.ts", "C:/p/web")).toBe("C:/p/f.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("terminal keys", () => {
|
||||
|
||||
Reference in New Issue
Block a user