mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
* fix(app): open folders from inline path links in the file explorer Clicking a folder path in an assistant message opened a file tab that failed with "Requested path is not a file". Extension-less workspace-relative paths without a line number are now probed with a directory listing before opening: directories open the Files explorer navigated to that folder, everything else opens as a file like before. Lexical directories (trailing slash, workspace root) now navigate the explorer to the folder instead of only preloading its listing. * fix(app): reveal linked folders and their contents Resolve workspace folder links from directory metadata and expand their ancestor paths in Files. Preserve file and line-reference opening, including extensionless files. Co-authored-by: Moy <moyfoxther@gmail.com> * fix(app): cancel stale path opens and reveal hidden folders Keep pending directory checks inside the path-opening model so a later click or navigation supersedes them. Explicitly revealing a hidden directory also enables the existing hidden-file visibility preference. --------- Co-authored-by: Moy <moyfoxther@gmail.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import { mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, expect, it } from "vitest";
|
|
import { resolveInlinePathOpen } from "./path-open";
|
|
|
|
const roots: string[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
});
|
|
|
|
it("distinguishes dotted and extensionless folders from files using the filesystem", async () => {
|
|
const workspaceRoot = await mkdtemp(path.join(tmpdir(), "folder-link-"));
|
|
roots.push(workspaceRoot);
|
|
await mkdir(path.join(workspaceRoot, "docs/guides"), { recursive: true });
|
|
await mkdir(path.join(workspaceRoot, "docs/reports.v1"));
|
|
await writeFile(path.join(workspaceRoot, "Makefile"), "all:\n");
|
|
await writeFile(path.join(workspaceRoot, "README.md"), "# Readme\n");
|
|
const client = {
|
|
listDirectory: (root: string, file: string) => readdir(path.resolve(root, file)),
|
|
};
|
|
for (const file of ["docs/guides", "docs/reports.v1"]) {
|
|
await expect(
|
|
resolveInlinePathOpen({ file, directory: "docs", workspaceRoot, client }),
|
|
).resolves.toEqual({ kind: "directory", directoryPath: file });
|
|
}
|
|
for (const file of ["Makefile", "README.md", "missing.md"]) {
|
|
await expect(
|
|
resolveInlinePathOpen({ file, directory: ".", workspaceRoot, client }),
|
|
).resolves.toEqual({ kind: "file" });
|
|
}
|
|
});
|
|
|
|
it("preserves explicit directory destinations and file line references", async () => {
|
|
const workspaceRoot = "/repo";
|
|
await expect(
|
|
resolveInlinePathOpen({ directory: "docs/guides", workspaceRoot, client: null }),
|
|
).resolves.toEqual({ kind: "directory", directoryPath: "docs/guides" });
|
|
await expect(
|
|
resolveInlinePathOpen({
|
|
file: "docs/guides",
|
|
directory: "docs",
|
|
lineStart: 12,
|
|
workspaceRoot,
|
|
client: null,
|
|
}),
|
|
).resolves.toEqual({ kind: "file" });
|
|
});
|
|
|
|
it("keeps unavailable probes and external paths on the existing file-open path", async () => {
|
|
for (const file of ["Makefile", "/external/folder", "C:/external/folder"]) {
|
|
await expect(
|
|
resolveInlinePathOpen({ file, directory: ".", workspaceRoot: "/repo", client: null }),
|
|
).resolves.toEqual({ kind: "file" });
|
|
}
|
|
});
|
|
|
|
it("ignores an older probe after a newer file link opens", async () => {
|
|
const { createInlinePathOpener } = await import("./path-open");
|
|
let rejectProbe!: (error: Error) => void;
|
|
const pendingProbe = new Promise<never>((_, reject) => {
|
|
rejectProbe = reject;
|
|
});
|
|
const opened: string[] = [];
|
|
const opener = createInlinePathOpener({
|
|
workspaceRoot: "/repo",
|
|
client: { listDirectory: () => pendingProbe },
|
|
openFile: (location) => {
|
|
opened.push(location.path);
|
|
},
|
|
openDirectory: (directory) => {
|
|
opened.push(directory);
|
|
},
|
|
});
|
|
const first = opener.open({ raw: "./first.md", path: "/repo/first.md" }, "preferred");
|
|
await opener.open({ raw: "./second.md:2", path: "/repo/second.md", lineStart: 2 }, "preferred");
|
|
rejectProbe(new Error("Not a directory"));
|
|
await first;
|
|
expect(opened).toEqual(["second.md"]);
|
|
});
|
|
|
|
it("ignores a pending probe when its view is left", async () => {
|
|
const { createInlinePathOpener } = await import("./path-open");
|
|
let resolveProbe!: (value: unknown) => void;
|
|
const pendingProbe = new Promise((resolve) => {
|
|
resolveProbe = resolve;
|
|
});
|
|
const opened: string[] = [];
|
|
const opener = createInlinePathOpener({
|
|
workspaceRoot: "/repo",
|
|
client: { listDirectory: () => pendingProbe },
|
|
openFile: (location) => {
|
|
opened.push(location.path);
|
|
},
|
|
openDirectory: (directory) => {
|
|
opened.push(directory);
|
|
},
|
|
});
|
|
const pending = opener.open({ raw: "./docs", path: "/repo/docs" }, "preferred");
|
|
opener.cancel();
|
|
resolveProbe({});
|
|
await pending;
|
|
expect(opened).toEqual([]);
|
|
});
|