Skip to content

Commit 0fc2c5a

Browse files
committed
fix(mobile): open home-relative file links
1 parent 5cbe82a commit 0fc2c5a

5 files changed

Lines changed: 70 additions & 5 deletions

File tree

‎apps/mobile/src/features/files/filePath.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
import { describe, expect, it } from "vite-plus/test";
22

33
import {
4+
expandHomeFilePath,
45
fileRoutePathSegments,
56
isSvgImagePreviewFile,
67
resolveWorkspaceRelativeFilePath,
78
fileHeaderSubtitle,
89
} from "./filePath";
910

11+
describe("expandHomeFilePath", () => {
12+
it.each([
13+
["/Users/tommy/repo", "/Users/tommy/.zshrc"],
14+
["/home/tommy/repo", "/home/tommy/.zshrc"],
15+
["C:\\Users\\tommy\\repo", "C:\\Users\\tommy\\.zshrc"],
16+
])("expands host-home paths inferred from %s", (workspaceRoot, expected) => {
17+
expect(expandHomeFilePath("~/.zshrc", workspaceRoot)).toBe(expected);
18+
});
19+
20+
it.each([null, undefined, "/repo"])(
21+
"keeps home paths when home cannot be inferred from %s",
22+
(root) => {
23+
expect(expandHomeFilePath("~/.zshrc", root)).toBe("~/.zshrc");
24+
},
25+
);
26+
27+
it.each(["src/app.ts", "/tmp/report.md"])("preserves existing file paths: %s", (path) => {
28+
expect(expandHomeFilePath(path, "/Users/tommy/repo")).toBe(path);
29+
});
30+
31+
it("routes a home path inside the workspace as a workspace file", () => {
32+
const root = "/Users/tommy/repo";
33+
expect(
34+
resolveWorkspaceRelativeFilePath(root, expandHomeFilePath("~/repo/src/app.ts", root)),
35+
).toBe("src/app.ts");
36+
});
37+
});
38+
1039
describe("fileRoutePathSegments", () => {
1140
it("round-trips workspace-relative and host paths through the route", () => {
1241
expect(fileRoutePathSegments("src/main.ts")).toEqual(["src", "main.ts"]);

‎apps/mobile/src/features/files/filePath.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
12
import {
23
isWorkspaceAudioPreviewPath,
34
isWorkspaceVideoPreviewPath,
@@ -18,6 +19,11 @@ export function isAbsolutePath(value: string): boolean {
1819
return value.startsWith("/") || isWindowsAbsolutePath(value);
1920
}
2021

22+
/** Expand host-home paths using the same workspace-root inference as web. */
23+
export function expandHomeFilePath(path: string, workspaceRoot: string | null | undefined): string {
24+
return workspaceRoot && path.startsWith("~/") ? resolvePathLinkTarget(path, workspaceRoot) : path;
25+
}
26+
2127
/** Route segments that `normalizeRoutePath` joins back into the same path, root included. */
2228
export function fileRoutePathSegments(path: string): string[] {
2329
const segments = path.split("/").filter((segment) => segment.length > 0);

‎apps/mobile/src/features/threads/ThreadFeed.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ import { useAtomCommand } from "../../state/use-atom-command";
213213
import { useV2ItemSupport } from "../../state/v2-item-support";
214214
import {
215215
basename,
216+
expandHomeFilePath,
216217
fileRoutePathSegments,
217218
isAbsolutePath,
218219
resolveWorkspaceRelativeFilePath,
@@ -2343,7 +2344,14 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
23432344
});
23442345
return;
23452346
}
2346-
const presentation = resolveMarkdownLinkPresentation(href);
2347+
const linkPresentation = resolveMarkdownLinkPresentation(href);
2348+
const presentation =
2349+
linkPresentation.kind === "file"
2350+
? {
2351+
...linkPresentation,
2352+
path: expandHomeFilePath(linkPresentation.path, props.workspaceRoot),
2353+
}
2354+
: linkPresentation;
23472355
if (presentation.kind === "file") {
23482356
const relativePath = resolveWorkspaceRelativeFilePath(
23492357
props.workspaceRoot,

‎apps/mobile/src/features/threads/fileChipMenu.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ describe("resolveFileChipTarget", () => {
1717
});
1818
});
1919

20+
it("expands home paths outside the workspace and strips their position suffix", () => {
21+
const target = resolveFileChipTarget("~/.agents/AGENTS.md:4:2", "/Users/tommy/repo");
22+
expect(target).toEqual({ fullPath: "/Users/tommy/.agents/AGENTS.md" });
23+
expect(fileChipMenu(target!).actions).toEqual([
24+
{ id: "copy-full-path", title: "Copy full path" },
25+
{ id: "open-file", title: "Open in file viewer" },
26+
]);
27+
});
28+
29+
it("resolves home paths inside the workspace to both paths", () => {
30+
expect(resolveFileChipTarget("~/repo/src/app.ts", "/Users/tommy/repo")).toEqual({
31+
fullPath: "/Users/tommy/repo/src/app.ts",
32+
relativePath: "src/app.ts",
33+
});
34+
});
35+
36+
it.each([null, undefined, "/repo"])("rejects home paths without a known home for %s", (root) => {
37+
expect(resolveFileChipTarget("~/.zshrc", root)).toBeNull();
38+
});
39+
2040
it("keeps only the relative path when the workspace root is unknown", () => {
2141
expect(resolveFileChipTarget("src/app.ts", null)).toEqual({ relativePath: "src/app.ts" });
2242
});

‎apps/mobile/src/features/threads/fileChipMenu.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { MarkdownFileContextMenu } from "@t3tools/mobile-markdown-text/type
55
import { hostPreviewMimeTypeFromExtension } from "@t3tools/shared/filePreview";
66

77
import {
8+
expandHomeFilePath,
89
isAbsolutePath,
910
resolveWorkspaceFilePath,
1011
resolveWorkspaceRelativeFilePath,
@@ -19,16 +20,17 @@ export interface FileChipTarget {
1920
readonly relativePath?: string;
2021
}
2122

22-
/** Null when the link is not a file or resolves nowhere the feed can open, such as `~/x` or `../x`. */
23+
/** Null when the link is not a file or resolves nowhere the feed can open, such as `../x`. */
2324
export function resolveFileChipTarget(
2425
href: string,
2526
workspaceRoot: string | null | undefined,
2627
): FileChipTarget | null {
2728
const presentation = resolveMarkdownLinkPresentation(href);
2829
if (presentation.kind !== "file") return null;
29-
const relativePath = resolveWorkspaceRelativeFilePath(workspaceRoot, presentation.path);
30-
const fullPath = isAbsolutePath(presentation.path)
31-
? presentation.path
30+
const path = expandHomeFilePath(presentation.path, workspaceRoot);
31+
const relativePath = resolveWorkspaceRelativeFilePath(workspaceRoot, path);
32+
const fullPath = isAbsolutePath(path)
33+
? path
3234
: workspaceRoot && relativePath
3335
? resolveWorkspaceFilePath(workspaceRoot, relativePath)
3436
: undefined;

0 commit comments

Comments
 (0)