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