Allow previews to open readable files (#1214)
* Allow previews to open readable files * Fix Windows file preview path test * Address file preview review comments
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveFilePreviewReadTarget } from "./preview-target";
|
||||
|
||||
describe("resolveFilePreviewReadTarget", () => {
|
||||
it("uses the workspace cwd for relative paths", () => {
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "packages/app/src/message.tsx",
|
||||
workspaceRoot: "/Users/test/project",
|
||||
}),
|
||||
).toEqual({
|
||||
cwd: "/Users/test/project",
|
||||
path: "packages/app/src/message.tsx",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the workspace cwd for absolute paths inside the workspace", () => {
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "/Users/test/project/packages/app/src/message.tsx",
|
||||
workspaceRoot: "/Users/test/project",
|
||||
}),
|
||||
).toEqual({
|
||||
cwd: "/Users/test/project",
|
||||
path: "/Users/test/project/packages/app/src/message.tsx",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the filesystem root for absolute paths outside the workspace", () => {
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "/tmp/paseo-preview.txt",
|
||||
workspaceRoot: "/Users/test/project",
|
||||
}),
|
||||
).toEqual({
|
||||
cwd: "/",
|
||||
path: "/tmp/paseo-preview.txt",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the home root for tilde paths", () => {
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "~/.paseo/plans/file-preview.md",
|
||||
workspaceRoot: "/Users/test/project",
|
||||
}),
|
||||
).toEqual({
|
||||
cwd: "~",
|
||||
path: "~/.paseo/plans/file-preview.md",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the drive root for Windows absolute paths outside the workspace", () => {
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "C:/Users/test/Desktop/file.txt",
|
||||
workspaceRoot: "D:/repo",
|
||||
}),
|
||||
).toEqual({
|
||||
cwd: "C:/",
|
||||
path: "C:/Users/test/Desktop/file.txt",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects relative paths without an absolute workspace root", () => {
|
||||
expect(resolveFilePreviewReadTarget({ path: "src/app.ts" })).toBeNull();
|
||||
expect(
|
||||
resolveFilePreviewReadTarget({
|
||||
path: "src/app.ts",
|
||||
workspaceRoot: "relative/root",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
|
||||
export interface FilePreviewReadTarget {
|
||||
cwd: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function trimTrailingSeparators(value: string): string {
|
||||
if (value === "/" || /^[A-Za-z]:[\\/]?$/.test(value)) {
|
||||
return value.replace(/\\/g, "/");
|
||||
}
|
||||
return value.replace(/[\\/]+$/, "");
|
||||
}
|
||||
|
||||
function normalizeForPathComparison(value: string): string {
|
||||
const normalized = trimTrailingSeparators(value.replace(/\\/g, "/"));
|
||||
if (/^[A-Za-z]:\//.test(normalized)) {
|
||||
return `${normalized.slice(0, 1).toUpperCase()}${normalized.slice(1)}`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isPathWithinRoot(candidatePath: string, rootPath: string): boolean {
|
||||
const candidate = normalizeForPathComparison(candidatePath);
|
||||
const root = normalizeForPathComparison(rootPath);
|
||||
if (!candidate || !root) {
|
||||
return false;
|
||||
}
|
||||
if (root === "/") {
|
||||
return candidate.startsWith("/");
|
||||
}
|
||||
if (candidate === root) {
|
||||
return true;
|
||||
}
|
||||
return candidate.startsWith(`${root}/`);
|
||||
}
|
||||
|
||||
function deriveFilesystemRootFromAbsolutePath(value: string): string | null {
|
||||
if (value.startsWith("/")) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
const driveMatch = /^([A-Za-z]:)[\\/]/.exec(value);
|
||||
if (driveMatch?.[1]) {
|
||||
return `${driveMatch[1]}/`;
|
||||
}
|
||||
|
||||
const uncMatch = /^(\\\\[^\\]+\\[^\\]+)/.exec(value);
|
||||
if (uncMatch?.[1]) {
|
||||
return uncMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isHomeRelativePath(value: string): boolean {
|
||||
return value === "~" || value.startsWith("~/") || value.startsWith("~\\");
|
||||
}
|
||||
|
||||
export function resolveFilePreviewReadTarget(input: {
|
||||
path: string;
|
||||
workspaceRoot?: string;
|
||||
}): FilePreviewReadTarget | null {
|
||||
const previewPath = input.path.trim();
|
||||
if (!previewPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isHomeRelativePath(previewPath)) {
|
||||
return {
|
||||
cwd: "~",
|
||||
path: previewPath,
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceRoot = input.workspaceRoot?.trim();
|
||||
if (!isAbsolutePath(previewPath)) {
|
||||
if (!workspaceRoot || !isAbsolutePath(workspaceRoot)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
cwd: workspaceRoot,
|
||||
path: previewPath,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
workspaceRoot &&
|
||||
isAbsolutePath(workspaceRoot) &&
|
||||
isPathWithinRoot(previewPath, workspaceRoot)
|
||||
) {
|
||||
return {
|
||||
cwd: workspaceRoot,
|
||||
path: previewPath,
|
||||
};
|
||||
}
|
||||
|
||||
const filesystemRoot = deriveFilesystemRootFromAbsolutePath(previewPath);
|
||||
if (!filesystemRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cwd: filesystemRoot,
|
||||
path: previewPath,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user