Repository navigation
Expand file tree
/
Copy pathdiffFileTree.logic.ts
More file actions
178 lines (164 loc) · 6.39 KB
/
Copy pathdiffFileTree.logic.ts
File metadata and controls
178 lines (164 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import type { FileDiffMetadata } from "@pierre/diffs";
import type {
FileTreeBatchOperation,
FileTreeIconConfig,
FileTreeSortComparator,
GitStatus,
} from "@pierre/trees";
import { resolveFileDiffPath } from "~/lib/diffRendering";
import { basenameOfPath, resolvePierreIconForEntry, T3_PIERRE_ICONS } from "~/pierre-icons";
export interface DiffFileTreeEntry {
readonly path: string;
readonly status: GitStatus;
}
function toGitStatus(file: FileDiffMetadata): GitStatus {
switch (file.type) {
case "new":
return "added";
case "deleted":
return "deleted";
case "rename-pure":
case "rename-changed":
return "renamed";
case "change":
return "modified";
}
}
export function diffFileTreeEntries(
files: ReadonlyArray<FileDiffMetadata>,
): ReadonlyArray<DiffFileTreeEntry> {
const statusByPath = new Map<string, GitStatus>();
for (const file of files) {
const path = resolveFileDiffPath(file);
const status = toGitStatus(file);
const previous = statusByPath.get(path);
statusByPath.set(path, previous === undefined || previous === status ? status : "modified");
}
return [...statusByPath].map(([path, status]) => ({ path, status }));
}
// Pierre cannot store a file and a directory at the same path. Give only those
// files an invisible suffix, keeping real paths at the diff/viewer boundary.
export function diffFileTreeModel(paths: ReadonlyArray<string>) {
const directories = new Set(collectDirectoryPaths(paths));
// Icon overrides match basenames case-insensitively, so aliases must also avoid
// real names in other directories, including names that already contain the suffix.
const occupiedNames = new Set(paths.flatMap((path) => path.toLowerCase().split("/")));
const selectionByModelPath = new Map<string, string>();
const modelBySelectionPath = new Map<string, string>();
const byFileName: FileTreeIconConfig["byFileName"] = { ...T3_PIERRE_ICONS.byFileName };
const modelPaths = paths.map((path) => {
if (!directories.has(`${path}/`)) return path;
let modelPath = path;
do {
modelPath += "\u200b";
} while (occupiedNames.has(basenameOfPath(modelPath).toLowerCase()));
occupiedNames.add(basenameOfPath(modelPath).toLowerCase());
selectionByModelPath.set(modelPath, path);
modelBySelectionPath.set(path, modelPath);
const icon = resolvePierreIconForEntry(path, "file");
if (icon !== null) byFileName[basenameOfPath(modelPath)] = icon;
return modelPath;
});
return {
paths: modelBySelectionPath.size === 0 ? paths : modelPaths,
selectionPath: (modelPath: string) => selectionByModelPath.get(modelPath) ?? modelPath,
modelPath: (path: string) => modelBySelectionPath.get(path) ?? path,
icons: modelBySelectionPath.size === 0 ? T3_PIERRE_ICONS : { ...T3_PIERRE_ICONS, byFileName },
};
}
export function collectDirectoryPaths(paths: ReadonlyArray<string>): ReadonlyArray<string> {
const directories = new Set<string>();
for (const path of paths) {
const segments = path.split("/");
let directory = "";
for (const segment of segments.slice(0, -1)) {
directory += `${segment}/`;
directories.add(directory);
}
}
return [...directories];
}
export function diffFileTreePositions(paths: ReadonlyArray<string>): ReadonlyMap<string, number> {
const positions = new Map<string, number>();
paths.forEach((path, index) => {
positions.set(path, index);
let directory = "";
for (const segment of path.split("/").slice(0, -1)) {
directory += `${segment}/`;
if (!positions.has(directory)) positions.set(directory, index);
}
});
return positions;
}
// A flat list in tree order: each directory stays contiguous, siblings ranked as
// compareDiffFileTreeEntries ranks them, so the list never disagrees with the tree.
export function orderDiffFilesLikeTree(
files: ReadonlyArray<FileDiffMetadata>,
): ReadonlyArray<FileDiffMetadata> {
const paths = files.map(resolveFileDiffPath);
const positions = diffFileTreePositions(paths);
const keys = paths.map((path) => {
const key: number[] = [];
let directory = "";
for (const segment of path.split("/").slice(0, -1)) {
directory += `${segment}/`;
key.push(positions.get(directory)!);
}
key.push(positions.get(path)!);
return key;
});
return files
.map((file, index) => ({ file, key: keys[index]! }))
.toSorted((left, right) => {
const length = Math.min(left.key.length, right.key.length);
for (let index = 0; index < length; index++) {
const difference = left.key[index]! - right.key[index]!;
if (difference !== 0) return difference;
}
return left.key.length - right.key.length;
})
.map(({ file }) => file);
}
export function compareDiffFileTreeEntries(
getPositions: () => ReadonlyMap<string, number>,
): FileTreeSortComparator {
return (left, right) => {
const positions = getPositions();
return (
(positions.get(left.path) ?? Number.MAX_SAFE_INTEGER) -
(positions.get(right.path) ?? Number.MAX_SAFE_INTEGER) ||
left.depth - right.depth ||
left.path.localeCompare(right.path)
);
};
}
function pathDepth(path: string): number {
return path.split("/").filter(Boolean).length;
}
export function buildDiffFileTreeUpdates(
previousPaths: ReadonlyArray<string>,
nextPaths: ReadonlyArray<string>,
): FileTreeBatchOperation[] {
const previousDirectories = new Set(collectDirectoryPaths(previousPaths));
const nextDirectories = new Set(collectDirectoryPaths(nextPaths));
const previous = new Set(previousPaths);
const next = new Set(nextPaths);
const updates: FileTreeBatchOperation[] = [];
for (const path of previousPaths) {
if (!next.has(path)) updates.push({ type: "remove", path });
}
const removedDirectories = [...previousDirectories]
.filter((directory) => !nextDirectories.has(directory))
.toSorted((left, right) => pathDepth(right) - pathDepth(left));
for (const directory of removedDirectories) {
updates.push({ type: "remove", path: directory, recursive: true });
}
const addedDirectories = [...nextDirectories]
.filter((directory) => !previousDirectories.has(directory))
.toSorted((left, right) => pathDepth(left) - pathDepth(right));
for (const directory of addedDirectories) updates.push({ type: "add", path: directory });
for (const path of nextPaths) {
if (!previous.has(path)) updates.push({ type: "add", path });
}
return updates;
}