| 1 | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 2 | import { isWindowsAbsolutePath, normalizeProjectPathForComparison } from "@t3tools/shared/path"; |
| 3 | |
| 4 | import { useRightPanelStore } from "./rightPanelStore"; |
| 5 | import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; |
| 6 | |
| 7 | interface 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 | |
| 15 | function 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 | |
| 28 | function 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 | |
| 50 | export 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 | |
| 80 | export 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 | } |