Skip to content
Source
Sign inSign up
Open History to see the last commit

diffFileActions.ts

112 lines3.8 KB

1import type { ScopedThreadRef } from "@t3tools/contracts";
2import {
3 isAbsolutePath,
4 isWindowsAbsolutePath,
5 normalizeProjectPathForComparison,
6 workspaceRelativeFilePath,
7} from "@t3tools/shared/path";
8​
9import { useRightPanelStore } from "./rightPanelStore";
10import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks";
11​
12interface OpenDiffFilePrimaryActionInput {
13 readonly threadRef: ScopedThreadRef | null;
14 readonly filePath: string;
15 readonly activeCwd: string | undefined;
16 readonly repositoryRoot?: string | undefined;
17 readonly openInEditor: (targetPath: string) => void;
19​
20function normalizedRelativePathSegments(filePath: string): ReadonlyArray<string> | null {
21 if (filePath.startsWith("/") || isWindowsAbsolutePath(filePath) || /^[a-zA-Z]:/.test(filePath)) {
22 return null;
23 }
24​
25 const segments = filePath
26 .replaceAll("\\", "/")
27 .split("/")
28 .filter((segment) => segment.length > 0 && segment !== ".");
29 if (segments.length === 0 || segments.includes("..")) return null;
30 return segments;
32​
33function repositoryRelativeWorkspaceSegments(
34 workspaceRoot: string | undefined,
35 repositoryRoot: string | undefined,
36): ReadonlyArray<string> | null {
37 if (!workspaceRoot || !repositoryRoot) return null;
38​
39 const normalizedWorkspaceRoot = normalizeProjectPathForComparison(workspaceRoot);
40 const normalizedRepositoryRoot = normalizeProjectPathForComparison(repositoryRoot);
41 if (normalizedWorkspaceRoot === normalizedRepositoryRoot) return [];
42​
43 const separator = normalizedRepositoryRoot.includes("\\") ? "\\" : "/";
44 const repositoryPrefix = normalizedRepositoryRoot.endsWith(separator)
45 ? normalizedRepositoryRoot
46 : `${normalizedRepositoryRoot}${separator}`;
47 if (!normalizedWorkspaceRoot.startsWith(repositoryPrefix)) return null;
48​
49 return normalizedWorkspaceRoot
50 .slice(repositoryPrefix.length)
51 .split(/[\\/]+/)
52 .filter(Boolean);
54​
55export function resolveDiffPathForWorkspace(input: {
56 readonly filePath: string;
57 readonly workspaceRoot: string | undefined;
58 readonly repositoryRoot: string | undefined;
59}): string | null {
60 // Claude and ACP tool-call diffs name files by absolute path.
61 if (isAbsolutePath(input.filePath)) {
62 const relativePath = workspaceRelativeFilePath(input.filePath, input.workspaceRoot);
63 const segments = relativePath === null ? null : normalizedRelativePathSegments(relativePath);
64 return segments ? segments.join("/") : null;
65 }
66​
67 const fileSegments = normalizedRelativePathSegments(input.filePath);
68 if (!fileSegments) return null;
69​
70 const workspaceSegments = repositoryRelativeWorkspaceSegments(
71 input.workspaceRoot,
72 input.repositoryRoot,
73 );
74 if (!workspaceSegments || workspaceSegments.length === 0) {
75 return fileSegments.join("/");
76 }
77​
78 const caseInsensitive = input.repositoryRoot
79 ? isWindowsAbsolutePath(input.repositoryRoot)
80 : false;
81 const belongsToWorkspace = workspaceSegments.every((segment, index) => {
82 const candidate = fileSegments[index];
83 if (candidate === undefined) return false;
84 return caseInsensitive ? candidate.toLowerCase() === segment : candidate === segment;
85 });
86 if (!belongsToWorkspace) return null;
87​
88 const relativeSegments = fileSegments.slice(workspaceSegments.length);
89 return relativeSegments.length > 0 ? relativeSegments.join("/") : null;
91​
92export function openDiffFilePrimaryAction({
93 threadRef,
94 filePath,
95 activeCwd,
96 repositoryRoot,
97 openInEditor,
98}: OpenDiffFilePrimaryActionInput): void {
99 const workspaceFilePath = resolveDiffPathForWorkspace({
100 filePath,
101 workspaceRoot: activeCwd,
102 repositoryRoot,
103 });
104 if (!workspaceFilePath) return;
105​
106 if (threadRef) {
107 useRightPanelStore.getState().openFile(threadRef, workspaceFilePath);
108 return;
109 }
110​
111 openInEditor(activeCwd ? resolvePathLinkTarget(workspaceFilePath, activeCwd) : workspaceFilePath);