@youtyan/code-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +39 -0
- package/package.json +54 -0
- package/web/app.js +1762 -0
- package/web/index.html +97 -0
- package/web/style.css +1584 -0
- package/web/vendor/README.md +12 -0
- package/web/vendor/diff2html/LICENSE.md +14 -0
- package/web/vendor/diff2html/diff2html-ui.min.js +1 -0
- package/web/vendor/diff2html/diff2html.min.css +1 -0
- package/web/vendor/highlight.js/LICENSE +29 -0
- package/web/vendor/highlight.js/highlight.min.js +1213 -0
- package/web/vendor/highlight.js/styles/github-dark.min.css +10 -0
- package/web/vendor/highlight.js/styles/github.min.css +10 -0
- package/web-src/server/git.ts +197 -0
- package/web-src/server/preview.ts +414 -0
- package/web-src/server/runtime.d.ts +40 -0
- package/web-src/types.ts +86 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,1762 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// web-src/expand-logic.ts
|
|
3
|
+
function initExpandState(prevHunkEndNew, hunkNewStart) {
|
|
4
|
+
return {
|
|
5
|
+
topExpandedStart: hunkNewStart,
|
|
6
|
+
bottomExpandedEnd: prevHunkEndNew - 1
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function remainingGap(state, prevHunkEndNew) {
|
|
10
|
+
const remainingStart = Math.max(1, prevHunkEndNew, state.bottomExpandedEnd + 1);
|
|
11
|
+
const remainingEnd = state.topExpandedStart - 1;
|
|
12
|
+
if (remainingStart > remainingEnd)
|
|
13
|
+
return null;
|
|
14
|
+
return { start: remainingStart, end: remainingEnd };
|
|
15
|
+
}
|
|
16
|
+
function isFullyExpanded(state, prevHunkEndNew) {
|
|
17
|
+
return remainingGap(state, prevHunkEndNew) == null;
|
|
18
|
+
}
|
|
19
|
+
function upClickRange(state, prevHunkEndNew, step) {
|
|
20
|
+
const gap = remainingGap(state, prevHunkEndNew);
|
|
21
|
+
return gap ? { start: gap.start, end: Math.min(gap.end, gap.start + step - 1) } : null;
|
|
22
|
+
}
|
|
23
|
+
function downClickRange(state, prevHunkEndNew, step) {
|
|
24
|
+
const gap = remainingGap(state, prevHunkEndNew);
|
|
25
|
+
return gap ? { start: Math.max(gap.start, gap.end - step + 1), end: gap.end } : null;
|
|
26
|
+
}
|
|
27
|
+
function applyUp(state, range) {
|
|
28
|
+
return Object.assign({}, state, { bottomExpandedEnd: range.end });
|
|
29
|
+
}
|
|
30
|
+
function applyDown(state, range) {
|
|
31
|
+
return Object.assign({}, state, { topExpandedStart: range.start });
|
|
32
|
+
}
|
|
33
|
+
function mapNewToOld(newLine, prevHunkEndNew, prevHunkEndOld) {
|
|
34
|
+
return prevHunkEndOld + (newLine - prevHunkEndNew);
|
|
35
|
+
}
|
|
36
|
+
var GdpExpandLogic = {
|
|
37
|
+
initExpandState,
|
|
38
|
+
remainingGap,
|
|
39
|
+
isFullyExpanded,
|
|
40
|
+
upClickRange,
|
|
41
|
+
downClickRange,
|
|
42
|
+
applyUp,
|
|
43
|
+
applyDown,
|
|
44
|
+
mapNewToOld
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// web-src/ws-highlight.ts
|
|
48
|
+
function isWhitespaceOnlyInlineHighlight(text) {
|
|
49
|
+
return !!text && !/\S/.test(text);
|
|
50
|
+
}
|
|
51
|
+
function suppressWhitespaceOnlyInlineHighlights(root) {
|
|
52
|
+
root.querySelectorAll("ins, del").forEach((el) => {
|
|
53
|
+
if (!isWhitespaceOnlyInlineHighlight(el.textContent))
|
|
54
|
+
return;
|
|
55
|
+
const parent = el.parentNode;
|
|
56
|
+
if (!parent)
|
|
57
|
+
return;
|
|
58
|
+
parent.replaceChild(document.createTextNode(el.textContent || ""), el);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// web-src/app.ts
|
|
63
|
+
window.GdpExpandLogic = GdpExpandLogic;
|
|
64
|
+
(() => {
|
|
65
|
+
const $ = (sel) => document.querySelector(sel);
|
|
66
|
+
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
67
|
+
const diffCardSelector = (path) => '.gdp-file-shell[data-path="' + (window.CSS && CSS.escape ? CSS.escape(path) : path) + '"]';
|
|
68
|
+
const HIGHLIGHT_SRC = "/vendor/highlight.js/highlight.min.js";
|
|
69
|
+
let highlightLoadPromise = null;
|
|
70
|
+
let highlightConfigured = false;
|
|
71
|
+
const STATE = (() => {
|
|
72
|
+
const igRaw = localStorage.getItem("gdp:ignore-ws");
|
|
73
|
+
return {
|
|
74
|
+
layout: localStorage.getItem("gdp:layout") || "side-by-side",
|
|
75
|
+
theme: localStorage.getItem("gdp:theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"),
|
|
76
|
+
sbView: localStorage.getItem("gdp:sbview") || "tree",
|
|
77
|
+
sbWidth: parseInt(localStorage.getItem("gdp:sbwidth")) || 308,
|
|
78
|
+
collapsedDirs: new Set(JSON.parse(localStorage.getItem("gdp:collapsed-dirs") || "[]")),
|
|
79
|
+
ignoreWs: igRaw === null ? true : igRaw === "1",
|
|
80
|
+
from: localStorage.getItem("gdp:from") || "HEAD",
|
|
81
|
+
to: localStorage.getItem("gdp:to") || "worktree",
|
|
82
|
+
collapsed: false,
|
|
83
|
+
files: [],
|
|
84
|
+
activeFile: null,
|
|
85
|
+
autoReload: localStorage.getItem("gdp:auto-reload") !== "0",
|
|
86
|
+
hideTests: localStorage.getItem("gdp:hide-tests") === "1",
|
|
87
|
+
syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0"
|
|
88
|
+
};
|
|
89
|
+
})();
|
|
90
|
+
function setStatus(s) {
|
|
91
|
+
const el = $("#status");
|
|
92
|
+
el.classList.remove("live", "refreshing", "error");
|
|
93
|
+
if (s)
|
|
94
|
+
el.classList.add(s);
|
|
95
|
+
}
|
|
96
|
+
function applyTheme() {
|
|
97
|
+
document.documentElement.dataset.theme = STATE.theme;
|
|
98
|
+
$("#hljs-light").disabled = STATE.theme === "dark";
|
|
99
|
+
$("#hljs-dark").disabled = STATE.theme !== "dark";
|
|
100
|
+
}
|
|
101
|
+
function getHljs() {
|
|
102
|
+
const hljsRef = window.hljs || window.Diff2HtmlUI && window.Diff2HtmlUI.hljs;
|
|
103
|
+
if (!hljsRef)
|
|
104
|
+
return null;
|
|
105
|
+
if (!highlightConfigured && typeof hljsRef.configure === "function") {
|
|
106
|
+
hljsRef.configure({ ignoreUnescapedHTML: true });
|
|
107
|
+
highlightConfigured = true;
|
|
108
|
+
}
|
|
109
|
+
return hljsRef;
|
|
110
|
+
}
|
|
111
|
+
function setHighlightButton(state) {
|
|
112
|
+
const btn = $("#syntax-highlight");
|
|
113
|
+
if (!btn)
|
|
114
|
+
return;
|
|
115
|
+
btn.classList.toggle("active", STATE.syntaxHighlight);
|
|
116
|
+
btn.classList.toggle("loading", state === "loading");
|
|
117
|
+
btn.textContent = state === "loading" ? "loading..." : STATE.syntaxHighlight ? "syntax on" : "syntax off";
|
|
118
|
+
btn.setAttribute("aria-pressed", STATE.syntaxHighlight ? "true" : "false");
|
|
119
|
+
btn.title = STATE.syntaxHighlight ? "syntax highlighting on" : state === "loading" ? "loading syntax highlighter" : state === "error" ? "failed to load syntax highlighter" : "syntax highlighting off";
|
|
120
|
+
}
|
|
121
|
+
function loadSyntaxHighlighter() {
|
|
122
|
+
const existing = getHljs();
|
|
123
|
+
if (existing) {
|
|
124
|
+
setHighlightButton("loaded");
|
|
125
|
+
return Promise.resolve(existing);
|
|
126
|
+
}
|
|
127
|
+
if (highlightLoadPromise)
|
|
128
|
+
return highlightLoadPromise;
|
|
129
|
+
setHighlightButton("loading");
|
|
130
|
+
highlightLoadPromise = new Promise((resolve, reject) => {
|
|
131
|
+
const script = document.createElement("script");
|
|
132
|
+
script.src = HIGHLIGHT_SRC;
|
|
133
|
+
script.async = true;
|
|
134
|
+
script.onload = () => {
|
|
135
|
+
const hljsRef = getHljs();
|
|
136
|
+
if (hljsRef) {
|
|
137
|
+
setHighlightButton("loaded");
|
|
138
|
+
resolve(hljsRef);
|
|
139
|
+
} else {
|
|
140
|
+
setHighlightButton("error");
|
|
141
|
+
reject(new Error("highlight.js did not expose window.hljs"));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
script.onerror = () => {
|
|
145
|
+
setHighlightButton("error");
|
|
146
|
+
reject(new Error("failed to load highlight.js"));
|
|
147
|
+
};
|
|
148
|
+
document.head.appendChild(script);
|
|
149
|
+
}).catch(() => {
|
|
150
|
+
highlightLoadPromise = null;
|
|
151
|
+
return null;
|
|
152
|
+
});
|
|
153
|
+
return highlightLoadPromise;
|
|
154
|
+
}
|
|
155
|
+
function rerenderLoadedDiffs() {
|
|
156
|
+
document.querySelectorAll(".gdp-file-shell.loaded").forEach((card) => {
|
|
157
|
+
const data = card._diffData;
|
|
158
|
+
const file = card._file;
|
|
159
|
+
if (!data || !file)
|
|
160
|
+
return;
|
|
161
|
+
mountDiff(card, file, data);
|
|
162
|
+
if (data.truncated && data.mode === "preview") {
|
|
163
|
+
addExpandHunksUI(file, data, card);
|
|
164
|
+
}
|
|
165
|
+
scheduleIdleHighlight(card, file);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function setLayout(layout) {
|
|
169
|
+
STATE.layout = layout;
|
|
170
|
+
localStorage.setItem("gdp:layout", layout);
|
|
171
|
+
$$("#topbar .seg button").forEach((b) => {
|
|
172
|
+
b.classList.toggle("active", b.dataset.layout === layout);
|
|
173
|
+
});
|
|
174
|
+
document.querySelectorAll(".gdp-file-shell.loaded").forEach((card) => {
|
|
175
|
+
const data = card._diffData;
|
|
176
|
+
const file = card._file;
|
|
177
|
+
if (!data || !file)
|
|
178
|
+
return;
|
|
179
|
+
mountDiff(card, file, data);
|
|
180
|
+
if (data.truncated && data.mode === "preview") {
|
|
181
|
+
addExpandHunksUI(file, data, card);
|
|
182
|
+
}
|
|
183
|
+
scheduleIdleHighlight(card, file);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function fileBadge(status) {
|
|
187
|
+
const ch = (status || "M")[0].toUpperCase();
|
|
188
|
+
const span = document.createElement("span");
|
|
189
|
+
span.className = "badge " + ch;
|
|
190
|
+
span.textContent = ch;
|
|
191
|
+
span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
|
|
192
|
+
return span;
|
|
193
|
+
}
|
|
194
|
+
function buildTree(files) {
|
|
195
|
+
const root = { name: "", dirs: {}, files: [], path: "", minOrder: Infinity };
|
|
196
|
+
for (const f of files) {
|
|
197
|
+
const parts = f.path.split("/");
|
|
198
|
+
let node = root;
|
|
199
|
+
let acc = "";
|
|
200
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
201
|
+
const p = parts[i];
|
|
202
|
+
acc = acc ? acc + "/" + p : p;
|
|
203
|
+
if (!node.dirs[p]) {
|
|
204
|
+
node.dirs[p] = { name: p, dirs: {}, files: [], path: acc, minOrder: Infinity };
|
|
205
|
+
}
|
|
206
|
+
node = node.dirs[p];
|
|
207
|
+
if (typeof f.order === "number" && f.order < node.minOrder)
|
|
208
|
+
node.minOrder = f.order;
|
|
209
|
+
}
|
|
210
|
+
node.files.push(f);
|
|
211
|
+
}
|
|
212
|
+
function compress(node) {
|
|
213
|
+
const ks = Object.keys(node.dirs);
|
|
214
|
+
while (ks.length === 1 && node.files.length === 0 && node !== root) {
|
|
215
|
+
const only = node.dirs[ks[0]];
|
|
216
|
+
node.name = node.name ? node.name + "/" + only.name : only.name;
|
|
217
|
+
node.dirs = only.dirs;
|
|
218
|
+
node.files = only.files;
|
|
219
|
+
node.path = only.path;
|
|
220
|
+
node.minOrder = Math.min(node.minOrder, only.minOrder);
|
|
221
|
+
ks.length = 0;
|
|
222
|
+
Object.keys(node.dirs).forEach((k) => ks.push(k));
|
|
223
|
+
}
|
|
224
|
+
Object.values(node.dirs).forEach(compress);
|
|
225
|
+
}
|
|
226
|
+
Object.values(root.dirs).forEach(compress);
|
|
227
|
+
return root;
|
|
228
|
+
}
|
|
229
|
+
function renderTreeNode(node, depth, ul) {
|
|
230
|
+
const items = [];
|
|
231
|
+
for (const k of Object.keys(node.dirs)) {
|
|
232
|
+
const d = node.dirs[k];
|
|
233
|
+
items.push({ kind: "dir", sortKey: d.minOrder, dir: d });
|
|
234
|
+
}
|
|
235
|
+
for (const f of node.files) {
|
|
236
|
+
items.push({ kind: "file", sortKey: f.order != null ? f.order : Infinity, file: f });
|
|
237
|
+
}
|
|
238
|
+
items.sort((a, b) => a.sortKey - b.sortKey);
|
|
239
|
+
for (const item of items) {
|
|
240
|
+
if (item.kind === "dir") {
|
|
241
|
+
const dir = item.dir;
|
|
242
|
+
const li = document.createElement("li");
|
|
243
|
+
li.className = "tree-dir";
|
|
244
|
+
li.dataset.dirpath = dir.path;
|
|
245
|
+
li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
|
|
246
|
+
const chev = document.createElement("span");
|
|
247
|
+
chev.className = "chev";
|
|
248
|
+
chev.textContent = "▾";
|
|
249
|
+
li.appendChild(chev);
|
|
250
|
+
const dirIcon = document.createElement("span");
|
|
251
|
+
dirIcon.className = "dir-icon";
|
|
252
|
+
li.appendChild(dirIcon);
|
|
253
|
+
const dn = document.createElement("span");
|
|
254
|
+
dn.className = "dir-name";
|
|
255
|
+
dn.textContent = dir.name;
|
|
256
|
+
dn.title = dir.path;
|
|
257
|
+
li.appendChild(dn);
|
|
258
|
+
const collapsed = STATE.collapsedDirs.has(dir.path);
|
|
259
|
+
if (collapsed)
|
|
260
|
+
li.classList.add("collapsed");
|
|
261
|
+
const updateIcon = () => {
|
|
262
|
+
dirIcon.textContent = li.classList.contains("collapsed") ? "\uD83D\uDCC1" : "\uD83D\uDCC2";
|
|
263
|
+
};
|
|
264
|
+
updateIcon();
|
|
265
|
+
const childUl = document.createElement("ul");
|
|
266
|
+
childUl.className = "tree-children";
|
|
267
|
+
renderTreeNode(dir, depth + 1, childUl);
|
|
268
|
+
li.addEventListener("click", (e) => {
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
li.classList.toggle("collapsed");
|
|
271
|
+
updateIcon();
|
|
272
|
+
if (li.classList.contains("collapsed"))
|
|
273
|
+
STATE.collapsedDirs.add(dir.path);
|
|
274
|
+
else
|
|
275
|
+
STATE.collapsedDirs.delete(dir.path);
|
|
276
|
+
localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
|
|
277
|
+
});
|
|
278
|
+
ul.appendChild(li);
|
|
279
|
+
ul.appendChild(childUl);
|
|
280
|
+
} else {
|
|
281
|
+
const f = item.file;
|
|
282
|
+
const li = document.createElement("li");
|
|
283
|
+
li.className = "tree-file";
|
|
284
|
+
li.dataset.path = f.path;
|
|
285
|
+
li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
|
|
286
|
+
li.appendChild(fileBadge(f.status));
|
|
287
|
+
const name = document.createElement("span");
|
|
288
|
+
name.className = "name";
|
|
289
|
+
name.textContent = f.path.split("/").pop();
|
|
290
|
+
name.title = f.path;
|
|
291
|
+
li.appendChild(name);
|
|
292
|
+
li.addEventListener("click", () => scrollToFile(f.path));
|
|
293
|
+
li.addEventListener("mouseenter", () => prefetchByPath(f.path), { passive: true });
|
|
294
|
+
ul.appendChild(li);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function renderFlat(files, ul) {
|
|
299
|
+
files.forEach((f, i) => {
|
|
300
|
+
const li = document.createElement("li");
|
|
301
|
+
li.dataset.index = String(i);
|
|
302
|
+
li.dataset.path = f.path;
|
|
303
|
+
li.appendChild(fileBadge(f.status));
|
|
304
|
+
const name = document.createElement("span");
|
|
305
|
+
name.className = "name";
|
|
306
|
+
name.textContent = f.path;
|
|
307
|
+
name.title = f.path;
|
|
308
|
+
li.appendChild(name);
|
|
309
|
+
li.addEventListener("click", () => scrollToFile(f.path));
|
|
310
|
+
li.addEventListener("mouseenter", () => prefetchByPath(f.path), { passive: true });
|
|
311
|
+
ul.appendChild(li);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function renderSidebar(files) {
|
|
315
|
+
const ul = $("#filelist");
|
|
316
|
+
ul.innerHTML = "";
|
|
317
|
+
ul.classList.toggle("tree", STATE.sbView === "tree");
|
|
318
|
+
STATE.files = files;
|
|
319
|
+
if (STATE.sbView === "tree") {
|
|
320
|
+
const root = buildTree(files);
|
|
321
|
+
renderTreeNode(root, 0, ul);
|
|
322
|
+
} else {
|
|
323
|
+
renderFlat(files, ul);
|
|
324
|
+
}
|
|
325
|
+
$("#totals").textContent = files.length ? files.length + " file" + (files.length === 1 ? "" : "s") : "";
|
|
326
|
+
$$(".sb-view-seg button").forEach((b) => {
|
|
327
|
+
b.classList.toggle("active", b.dataset.view === STATE.sbView);
|
|
328
|
+
});
|
|
329
|
+
if (STATE.activeFile)
|
|
330
|
+
markActive(STATE.activeFile);
|
|
331
|
+
applyFilter();
|
|
332
|
+
}
|
|
333
|
+
function renderMeta(meta) {
|
|
334
|
+
const el = $("#meta");
|
|
335
|
+
if (!meta) {
|
|
336
|
+
el.textContent = "";
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
document.title = (meta.project ? meta.project + " - " : "") + "git diff preview";
|
|
340
|
+
el.innerHTML = "";
|
|
341
|
+
if (meta.range) {
|
|
342
|
+
const r = document.createElement("span");
|
|
343
|
+
r.className = "ref";
|
|
344
|
+
r.textContent = meta.range;
|
|
345
|
+
el.appendChild(r);
|
|
346
|
+
}
|
|
347
|
+
if (meta.branch) {
|
|
348
|
+
const b = document.createElement("span");
|
|
349
|
+
b.className = "ref";
|
|
350
|
+
b.textContent = "⎇ " + meta.branch;
|
|
351
|
+
el.appendChild(b);
|
|
352
|
+
}
|
|
353
|
+
if (meta.totals) {
|
|
354
|
+
const t = document.createElement("span");
|
|
355
|
+
t.className = "num";
|
|
356
|
+
t.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span> " + "<span>" + meta.totals.files + " files</span>";
|
|
357
|
+
el.appendChild(t);
|
|
358
|
+
}
|
|
359
|
+
const u = document.createElement("span");
|
|
360
|
+
u.className = "updated-at";
|
|
361
|
+
u.title = "last updated";
|
|
362
|
+
u.textContent = "updated " + new Date().toLocaleTimeString([], { hour12: false });
|
|
363
|
+
el.appendChild(u);
|
|
364
|
+
}
|
|
365
|
+
let SUPPRESS_SPY_UNTIL = 0;
|
|
366
|
+
function prefetchByPath(path) {
|
|
367
|
+
const card = document.querySelector(diffCardSelector(path));
|
|
368
|
+
if (!card || !card.classList.contains("pending"))
|
|
369
|
+
return;
|
|
370
|
+
const f = STATE.files.find((x) => x.path === path);
|
|
371
|
+
if (!f)
|
|
372
|
+
return;
|
|
373
|
+
enqueueLoad(f, card, 5);
|
|
374
|
+
}
|
|
375
|
+
function scrollToFile(path) {
|
|
376
|
+
const card = document.querySelector(diffCardSelector(path));
|
|
377
|
+
if (!card)
|
|
378
|
+
return;
|
|
379
|
+
markActive(path);
|
|
380
|
+
SUPPRESS_SPY_UNTIL = performance.now() + 1500;
|
|
381
|
+
const onEnd = () => {
|
|
382
|
+
SUPPRESS_SPY_UNTIL = 0;
|
|
383
|
+
window.removeEventListener("scrollend", onEnd);
|
|
384
|
+
};
|
|
385
|
+
window.addEventListener("scrollend", onEnd, { once: true });
|
|
386
|
+
if (card.classList.contains("pending")) {
|
|
387
|
+
const f = STATE.files.find((x) => x.path === path);
|
|
388
|
+
if (f)
|
|
389
|
+
enqueueLoad(f, card, 10);
|
|
390
|
+
}
|
|
391
|
+
card.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
392
|
+
}
|
|
393
|
+
function markActive(path) {
|
|
394
|
+
STATE.activeFile = path;
|
|
395
|
+
$$("#filelist li").forEach((li) => {
|
|
396
|
+
if (li.dataset.path)
|
|
397
|
+
li.classList.toggle("active", li.dataset.path === path);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function applyFilter() {
|
|
401
|
+
const q = ($("#filter").value || "").toLowerCase().trim();
|
|
402
|
+
$$("#filelist li[data-path]").forEach((li) => {
|
|
403
|
+
const match = !q || li.dataset.path.toLowerCase().includes(q);
|
|
404
|
+
li.classList.toggle("hidden", !match);
|
|
405
|
+
});
|
|
406
|
+
$$("#filelist .tree-dir").forEach((dir) => {
|
|
407
|
+
const childUl = dir.nextElementSibling;
|
|
408
|
+
if (!childUl || !childUl.classList.contains("tree-children"))
|
|
409
|
+
return;
|
|
410
|
+
const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden)");
|
|
411
|
+
const fullVisible = !q;
|
|
412
|
+
dir.classList.toggle("hidden", !(fullVisible || anyVisible));
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
let SERVER_GENERATION = 0;
|
|
416
|
+
let CLIENT_REQ_SEQ = 0;
|
|
417
|
+
const LOAD_QUEUE = [];
|
|
418
|
+
let ACTIVE_LOADS = 0;
|
|
419
|
+
const MAX_PARALLEL = 2;
|
|
420
|
+
let lazyObserver = null;
|
|
421
|
+
let IN_FLIGHT = 0;
|
|
422
|
+
function updateLoadBar() {
|
|
423
|
+
const el = $("#load-bar");
|
|
424
|
+
if (el)
|
|
425
|
+
el.classList.toggle("active", IN_FLIGHT > 0);
|
|
426
|
+
}
|
|
427
|
+
function trackLoad(promise) {
|
|
428
|
+
IN_FLIGHT++;
|
|
429
|
+
updateLoadBar();
|
|
430
|
+
const done = () => {
|
|
431
|
+
IN_FLIGHT = Math.max(0, IN_FLIGHT - 1);
|
|
432
|
+
updateLoadBar();
|
|
433
|
+
};
|
|
434
|
+
return Promise.resolve(promise).then((v) => {
|
|
435
|
+
done();
|
|
436
|
+
return v;
|
|
437
|
+
}, (e) => {
|
|
438
|
+
done();
|
|
439
|
+
throw e;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function escapeHtml(s) {
|
|
443
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
444
|
+
}
|
|
445
|
+
function renderShell(meta) {
|
|
446
|
+
const newFiles = meta.files || [];
|
|
447
|
+
STATE.files = newFiles;
|
|
448
|
+
SERVER_GENERATION = meta.generation || 0;
|
|
449
|
+
window._lastMeta = meta;
|
|
450
|
+
renderMeta(meta);
|
|
451
|
+
renderSidebar(newFiles);
|
|
452
|
+
const target = $("#diff");
|
|
453
|
+
const empty = $("#empty");
|
|
454
|
+
if (!newFiles.length) {
|
|
455
|
+
empty.classList.remove("hidden");
|
|
456
|
+
target.replaceChildren();
|
|
457
|
+
LOAD_QUEUE.length = 0;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
empty.classList.add("hidden");
|
|
461
|
+
const oldByKey = new Map;
|
|
462
|
+
document.querySelectorAll(".gdp-file-shell").forEach((c) => {
|
|
463
|
+
if (c.dataset.key)
|
|
464
|
+
oldByKey.set(c.dataset.key, c);
|
|
465
|
+
});
|
|
466
|
+
const ordered = [];
|
|
467
|
+
newFiles.forEach((f) => {
|
|
468
|
+
const key = f.key || f.path;
|
|
469
|
+
const old = oldByKey.get(key);
|
|
470
|
+
if (old) {
|
|
471
|
+
oldByKey.delete(key);
|
|
472
|
+
const sizeChanged = old.dataset.sizeClass !== (f.size_class || "small");
|
|
473
|
+
const statusChanged = old.dataset.status !== (f.status || "M");
|
|
474
|
+
if (sizeChanged || statusChanged) {
|
|
475
|
+
old.classList.remove("loaded", "error");
|
|
476
|
+
old.classList.add("pending");
|
|
477
|
+
old.replaceChildren();
|
|
478
|
+
const tmp = createPlaceholder(f);
|
|
479
|
+
while (tmp.firstChild)
|
|
480
|
+
old.appendChild(tmp.firstChild);
|
|
481
|
+
old.dataset.sizeClass = f.size_class || "small";
|
|
482
|
+
old.dataset.status = f.status || "M";
|
|
483
|
+
delete old.dataset.manualRendered;
|
|
484
|
+
delete old.dataset.manualLoad;
|
|
485
|
+
delete old.dataset.manualMode;
|
|
486
|
+
old.style.minHeight = (f.estimated_height_px || 80) + "px";
|
|
487
|
+
old._diffData = null;
|
|
488
|
+
old._file = null;
|
|
489
|
+
} else {
|
|
490
|
+
const stats = old.querySelector(".gdp-shell-header .stats");
|
|
491
|
+
if (stats) {
|
|
492
|
+
stats.innerHTML = '<span class="a">+' + (f.additions || 0) + "</span>" + '<span class="d">−' + (f.deletions || 0) + "</span>";
|
|
493
|
+
}
|
|
494
|
+
old._file = f;
|
|
495
|
+
}
|
|
496
|
+
ordered.push(old);
|
|
497
|
+
} else {
|
|
498
|
+
ordered.push(createPlaceholder(f));
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
oldByKey.forEach((c) => c.remove());
|
|
502
|
+
target.replaceChildren(...ordered);
|
|
503
|
+
for (let i = LOAD_QUEUE.length - 1;i >= 0; i--) {
|
|
504
|
+
if (!LOAD_QUEUE[i].card.isConnected)
|
|
505
|
+
LOAD_QUEUE.splice(i, 1);
|
|
506
|
+
}
|
|
507
|
+
setupLazyObserver();
|
|
508
|
+
enqueueInitialLoads();
|
|
509
|
+
setupScrollSpy();
|
|
510
|
+
if (typeof applyHideTests === "function")
|
|
511
|
+
applyHideTests();
|
|
512
|
+
}
|
|
513
|
+
function createPlaceholder(f) {
|
|
514
|
+
const card = document.createElement("div");
|
|
515
|
+
card.className = "gdp-file-shell pending";
|
|
516
|
+
card.dataset.path = f.path;
|
|
517
|
+
card.dataset.key = f.key || f.path;
|
|
518
|
+
card.dataset.sizeClass = f.size_class || "small";
|
|
519
|
+
card.dataset.status = f.status || "M";
|
|
520
|
+
if (f.estimated_height_px) {
|
|
521
|
+
card.style.minHeight = f.estimated_height_px + "px";
|
|
522
|
+
}
|
|
523
|
+
const head = document.createElement("div");
|
|
524
|
+
head.className = "gdp-shell-header";
|
|
525
|
+
head.innerHTML = '<span class="status-pill ' + escapeHtml(f.status || "M") + '">' + escapeHtml(f.status || "M") + "</span>" + '<span class="path">' + escapeHtml(f.display_path || f.path) + "</span>" + '<span class="stats">' + '<span class="a">+' + (f.additions || 0) + "</span>" + '<span class="d">−' + (f.deletions || 0) + "</span>" + "</span>" + '<span class="size-tag ' + escapeHtml(f.size_class || "") + '">' + escapeHtml(f.size_class || "") + "</span>" + '<span class="loading-indicator" hidden>loading…</span>';
|
|
526
|
+
card.appendChild(head);
|
|
527
|
+
const body = document.createElement("div");
|
|
528
|
+
body.className = "gdp-shell-body";
|
|
529
|
+
card.appendChild(body);
|
|
530
|
+
return card;
|
|
531
|
+
}
|
|
532
|
+
function setupLazyObserver() {
|
|
533
|
+
if (lazyObserver)
|
|
534
|
+
lazyObserver.disconnect();
|
|
535
|
+
lazyObserver = new IntersectionObserver((entries) => {
|
|
536
|
+
entries.forEach((entry) => {
|
|
537
|
+
if (!entry.isIntersecting)
|
|
538
|
+
return;
|
|
539
|
+
const card = entry.target;
|
|
540
|
+
if (card.classList.contains("loaded") || card.classList.contains("loading"))
|
|
541
|
+
return;
|
|
542
|
+
const f = STATE.files.find((x) => x.path === card.dataset.path);
|
|
543
|
+
if (!f)
|
|
544
|
+
return;
|
|
545
|
+
enqueueLoad(f, card, 0);
|
|
546
|
+
});
|
|
547
|
+
}, { rootMargin: "1200px 0px 1600px 0px" });
|
|
548
|
+
document.querySelectorAll(".gdp-file-shell.pending").forEach((c) => lazyObserver.observe(c));
|
|
549
|
+
}
|
|
550
|
+
window.addEventListener("scroll", () => enqueueInitialLoads(), { passive: true });
|
|
551
|
+
window.addEventListener("resize", () => enqueueInitialLoads(), { passive: true });
|
|
552
|
+
document.addEventListener("visibilitychange", () => {
|
|
553
|
+
if (!document.hidden)
|
|
554
|
+
enqueueInitialLoads();
|
|
555
|
+
});
|
|
556
|
+
function enqueueInitialLoads() {
|
|
557
|
+
const viewportBottom = window.innerHeight + 1600;
|
|
558
|
+
document.querySelectorAll(".gdp-file-shell.pending").forEach((card) => {
|
|
559
|
+
const rect = card.getBoundingClientRect();
|
|
560
|
+
if (rect.top > viewportBottom)
|
|
561
|
+
return;
|
|
562
|
+
const f = STATE.files.find((x) => x.path === card.dataset.path);
|
|
563
|
+
if (f)
|
|
564
|
+
enqueueLoad(f, card, 0);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function enqueueLoad(file, card, priority) {
|
|
568
|
+
if (manualLoadReason(file) && card.dataset.manualLoad !== "1") {
|
|
569
|
+
renderManualLoadPlaceholder(card, file);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (LOAD_QUEUE.find((item) => item.card === card))
|
|
573
|
+
return;
|
|
574
|
+
LOAD_QUEUE.push({ file, card, priority: priority || 0 });
|
|
575
|
+
LOAD_QUEUE.sort((a, b) => b.priority - a.priority);
|
|
576
|
+
pumpQueue();
|
|
577
|
+
}
|
|
578
|
+
function pumpQueue() {
|
|
579
|
+
while (ACTIVE_LOADS < MAX_PARALLEL && LOAD_QUEUE.length) {
|
|
580
|
+
const item = LOAD_QUEUE.shift();
|
|
581
|
+
if (item.card.classList.contains("loaded") || item.card.classList.contains("loading"))
|
|
582
|
+
continue;
|
|
583
|
+
ACTIVE_LOADS++;
|
|
584
|
+
loadFile(item.file, item.card).finally(() => {
|
|
585
|
+
ACTIVE_LOADS--;
|
|
586
|
+
pumpQueue();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function manualLoadReason(file) {
|
|
591
|
+
const path = file.path || "";
|
|
592
|
+
if (file.size_class === "huge")
|
|
593
|
+
return "huge diff";
|
|
594
|
+
if (/\.(min|bundle)\.(js|mjs|css)$/i.test(path))
|
|
595
|
+
return "minified or bundled file";
|
|
596
|
+
if (/\.map$/i.test(path))
|
|
597
|
+
return "source map";
|
|
598
|
+
if (/(^|\/)(vendor|node_modules|dist|build|out)\//i.test(path))
|
|
599
|
+
return "generated or vendored path";
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function renderManualLoadPlaceholder(card, file) {
|
|
603
|
+
if (card.dataset.manualRendered === "1")
|
|
604
|
+
return;
|
|
605
|
+
card.dataset.manualRendered = "1";
|
|
606
|
+
card.classList.remove("loading");
|
|
607
|
+
card.classList.add("pending", "manual-load");
|
|
608
|
+
if (lazyObserver)
|
|
609
|
+
lazyObserver.unobserve(card);
|
|
610
|
+
const indicator = card.querySelector(".loading-indicator");
|
|
611
|
+
if (indicator)
|
|
612
|
+
indicator.hidden = true;
|
|
613
|
+
const body = card.querySelector(".gdp-shell-body");
|
|
614
|
+
body.innerHTML = "";
|
|
615
|
+
const wrap = document.createElement("div");
|
|
616
|
+
wrap.className = "gdp-manual-load";
|
|
617
|
+
const note = document.createElement("div");
|
|
618
|
+
note.className = "gdp-manual-note";
|
|
619
|
+
note.textContent = manualLoadReason(file) + " - click to load diff";
|
|
620
|
+
const previewBtn = document.createElement("button");
|
|
621
|
+
previewBtn.className = "gdp-show-full";
|
|
622
|
+
previewBtn.textContent = "Load preview";
|
|
623
|
+
previewBtn.addEventListener("click", () => {
|
|
624
|
+
body.innerHTML = "";
|
|
625
|
+
card.dataset.manualLoad = "1";
|
|
626
|
+
card.dataset.manualMode = "preview";
|
|
627
|
+
card.classList.remove("manual-load");
|
|
628
|
+
loadFile(file, card, buildPreviewUrl(file, 3));
|
|
629
|
+
});
|
|
630
|
+
const fullBtn = document.createElement("button");
|
|
631
|
+
fullBtn.className = "gdp-show-full secondary";
|
|
632
|
+
fullBtn.textContent = "Load full";
|
|
633
|
+
fullBtn.addEventListener("click", () => {
|
|
634
|
+
body.innerHTML = "";
|
|
635
|
+
card.dataset.manualLoad = "1";
|
|
636
|
+
card.dataset.manualMode = "full";
|
|
637
|
+
card.classList.remove("manual-load");
|
|
638
|
+
loadFile(file, card, file.load_url);
|
|
639
|
+
});
|
|
640
|
+
wrap.appendChild(note);
|
|
641
|
+
wrap.appendChild(previewBtn);
|
|
642
|
+
wrap.appendChild(fullBtn);
|
|
643
|
+
body.appendChild(wrap);
|
|
644
|
+
}
|
|
645
|
+
function nextIdle(timeout = 500) {
|
|
646
|
+
return new Promise((resolve) => {
|
|
647
|
+
let done = false;
|
|
648
|
+
const finish = () => {
|
|
649
|
+
if (done)
|
|
650
|
+
return;
|
|
651
|
+
done = true;
|
|
652
|
+
resolve();
|
|
653
|
+
};
|
|
654
|
+
const ric = window.requestIdleCallback;
|
|
655
|
+
if (typeof ric === "function") {
|
|
656
|
+
ric(finish, { timeout });
|
|
657
|
+
} else {
|
|
658
|
+
requestAnimationFrame(finish);
|
|
659
|
+
setTimeout(finish, 50);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function loadFile(file, card, urlOverride) {
|
|
664
|
+
card.classList.remove("pending");
|
|
665
|
+
card.classList.add("loading");
|
|
666
|
+
if (lazyObserver)
|
|
667
|
+
lazyObserver.unobserve(card);
|
|
668
|
+
const indicator = card.querySelector(".loading-indicator");
|
|
669
|
+
if (indicator)
|
|
670
|
+
indicator.hidden = false;
|
|
671
|
+
const url = urlOverride || (card.dataset.manualMode === "full" ? file.load_url : file.preview_url || file.load_url);
|
|
672
|
+
const myGen = SERVER_GENERATION;
|
|
673
|
+
const myReq = ++CLIENT_REQ_SEQ;
|
|
674
|
+
card.dataset.reqId = String(myReq);
|
|
675
|
+
const retryStale = () => {
|
|
676
|
+
if (String(myReq) !== card.dataset.reqId)
|
|
677
|
+
return;
|
|
678
|
+
card.classList.remove("loading");
|
|
679
|
+
card.classList.add("pending");
|
|
680
|
+
if (indicator)
|
|
681
|
+
indicator.hidden = true;
|
|
682
|
+
const fresh = STATE.files.find((x) => x.path === card.dataset.path);
|
|
683
|
+
if (fresh && card.isConnected)
|
|
684
|
+
enqueueLoad(fresh, card, 0);
|
|
685
|
+
};
|
|
686
|
+
return trackLoad(fetch(url).then((r) => r.json())).then(async (data) => {
|
|
687
|
+
if (String(myReq) !== card.dataset.reqId)
|
|
688
|
+
return;
|
|
689
|
+
if (myGen !== SERVER_GENERATION) {
|
|
690
|
+
retryStale();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (data.generation && data.generation !== SERVER_GENERATION) {
|
|
694
|
+
retryStale();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
await nextIdle();
|
|
698
|
+
if (String(myReq) !== card.dataset.reqId)
|
|
699
|
+
return;
|
|
700
|
+
renderFile(file, data, card);
|
|
701
|
+
}).catch(() => {
|
|
702
|
+
if (String(myReq) !== card.dataset.reqId)
|
|
703
|
+
return;
|
|
704
|
+
card.classList.remove("loading");
|
|
705
|
+
card.classList.add("error");
|
|
706
|
+
const body = card.querySelector(".gdp-shell-body");
|
|
707
|
+
body.innerHTML = '<div class="gdp-error">failed to load — <button class="retry">retry</button></div>';
|
|
708
|
+
const btn = body.querySelector(".retry");
|
|
709
|
+
if (btn)
|
|
710
|
+
btn.addEventListener("click", () => {
|
|
711
|
+
card.classList.remove("error");
|
|
712
|
+
card.classList.add("pending");
|
|
713
|
+
body.innerHTML = "";
|
|
714
|
+
enqueueLoad(file, card, 1);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function mountDiff(card, file, data) {
|
|
719
|
+
const head = card.querySelector(".gdp-shell-header");
|
|
720
|
+
if (head)
|
|
721
|
+
head.style.display = "none";
|
|
722
|
+
const body = card.querySelector(".gdp-shell-body");
|
|
723
|
+
body.innerHTML = "";
|
|
724
|
+
if (!data.diff || !data.diff.trim()) {
|
|
725
|
+
body.innerHTML = '<div class="gdp-info">No content</div>';
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const layout = file.force_layout || STATE.layout;
|
|
729
|
+
const hljsRef = getHljs();
|
|
730
|
+
const ui = new Diff2HtmlUI(body, data.diff, {
|
|
731
|
+
drawFileList: false,
|
|
732
|
+
matching: "lines",
|
|
733
|
+
outputFormat: layout,
|
|
734
|
+
synchronisedScroll: true,
|
|
735
|
+
highlight: !!(STATE.syntaxHighlight && file.highlight && hljsRef),
|
|
736
|
+
fileListToggle: false
|
|
737
|
+
}, hljsRef);
|
|
738
|
+
ui.draw();
|
|
739
|
+
if (STATE.ignoreWs)
|
|
740
|
+
suppressWhitespaceOnlyInlineHighlights(body);
|
|
741
|
+
if (STATE.syntaxHighlight && file.highlight && hljsRef && typeof ui.highlightCode === "function")
|
|
742
|
+
ui.highlightCode();
|
|
743
|
+
enhanceMediaCard(file, card);
|
|
744
|
+
syncSideScrollCard(card);
|
|
745
|
+
appendStatSquaresToHeader(card, file);
|
|
746
|
+
setupHunkExpand(card, file);
|
|
747
|
+
}
|
|
748
|
+
function parseHunkHeader(text) {
|
|
749
|
+
const m = (text || "").match(/@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
750
|
+
if (!m)
|
|
751
|
+
return null;
|
|
752
|
+
return {
|
|
753
|
+
oldStart: +m[1],
|
|
754
|
+
oldCount: m[2] ? +m[2] : 1,
|
|
755
|
+
newStart: +m[3],
|
|
756
|
+
newCount: m[4] ? +m[4] : 1
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function nextNewLine(hunk) {
|
|
760
|
+
return hunk.newStart + hunk.newCount;
|
|
761
|
+
}
|
|
762
|
+
function nextOldLine(hunk) {
|
|
763
|
+
return hunk.oldStart + hunk.oldCount;
|
|
764
|
+
}
|
|
765
|
+
function setupHunkExpand(card, file) {
|
|
766
|
+
if (file.binary)
|
|
767
|
+
return;
|
|
768
|
+
if (file.media_kind)
|
|
769
|
+
return;
|
|
770
|
+
const infoRows = [];
|
|
771
|
+
const tables = card.querySelectorAll("table.d2h-diff-table");
|
|
772
|
+
if (tables.length === 0)
|
|
773
|
+
return;
|
|
774
|
+
const perTable = [];
|
|
775
|
+
tables.forEach((tbl) => {
|
|
776
|
+
const arr = [];
|
|
777
|
+
tbl.querySelectorAll("tr").forEach((tr) => {
|
|
778
|
+
const info = tr.querySelector("td.d2h-info:not(.d2h-code-linenumber):not(.d2h-code-side-linenumber)");
|
|
779
|
+
if (!info)
|
|
780
|
+
return;
|
|
781
|
+
const txt = (info.textContent || "").trim();
|
|
782
|
+
arr.push({ tr, info, hunk: parseHunkHeader(txt) });
|
|
783
|
+
});
|
|
784
|
+
perTable.push(arr);
|
|
785
|
+
});
|
|
786
|
+
const base = perTable.find((arr) => arr.some((x) => x.hunk)) || perTable[0] || [];
|
|
787
|
+
const usedTrs = new WeakSet;
|
|
788
|
+
base.forEach((baseItem) => {
|
|
789
|
+
const top = baseItem.tr.getBoundingClientRect().top;
|
|
790
|
+
const group = perTable.map((arr, tableIndex) => {
|
|
791
|
+
let best = null, bestD = Infinity;
|
|
792
|
+
for (const item of arr) {
|
|
793
|
+
if (usedTrs.has(item.tr))
|
|
794
|
+
continue;
|
|
795
|
+
const d = Math.abs(item.tr.getBoundingClientRect().top - top);
|
|
796
|
+
if (d < bestD) {
|
|
797
|
+
best = item;
|
|
798
|
+
bestD = d;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (!best || bestD >= 12)
|
|
802
|
+
return null;
|
|
803
|
+
usedTrs.add(best.tr);
|
|
804
|
+
return Object.assign({ sideIndex: tableIndex }, best);
|
|
805
|
+
}).filter(Boolean);
|
|
806
|
+
if (!group.length)
|
|
807
|
+
return;
|
|
808
|
+
const parsed = group.find((g) => g.hunk) || group[0];
|
|
809
|
+
if (!parsed.hunk)
|
|
810
|
+
return;
|
|
811
|
+
group.forEach((g) => g.tr.classList.add("gdp-hunk-row"));
|
|
812
|
+
infoRows.push({
|
|
813
|
+
tr: parsed.tr,
|
|
814
|
+
info: parsed.info,
|
|
815
|
+
hunk: parsed.hunk,
|
|
816
|
+
siblings: group,
|
|
817
|
+
prevHunkEndNew: 0,
|
|
818
|
+
prevHunkEndOld: 0
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
for (let i = 1;i < infoRows.length; i++) {
|
|
822
|
+
const prev = infoRows[i - 1].hunk;
|
|
823
|
+
infoRows[i].prevHunkEndNew = nextNewLine(prev);
|
|
824
|
+
infoRows[i].prevHunkEndOld = nextOldLine(prev);
|
|
825
|
+
}
|
|
826
|
+
const ref = STATE.to && STATE.to !== "worktree" ? STATE.to : "worktree";
|
|
827
|
+
const refPath = encodeURIComponent(file.path);
|
|
828
|
+
for (const item of infoRows) {
|
|
829
|
+
attachExpandControls(item, file, ref, refPath);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function attachExpandControls(item, file, ref, refPath) {
|
|
833
|
+
const { hunk, prevHunkEndNew, prevHunkEndOld } = item;
|
|
834
|
+
const fullGapStart = Math.max(1, prevHunkEndNew);
|
|
835
|
+
const fullGapEnd = hunk.newStart - 1;
|
|
836
|
+
if (fullGapStart > fullGapEnd) {
|
|
837
|
+
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
838
|
+
sib.tr.style.display = "none";
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const L = window.GdpExpandLogic;
|
|
843
|
+
if (item.topExpandedStart == null || item.bottomExpandedEnd == null) {
|
|
844
|
+
const init = L.initExpandState(prevHunkEndNew, hunk.newStart);
|
|
845
|
+
item.topExpandedStart = init.topExpandedStart;
|
|
846
|
+
item.bottomExpandedEnd = init.bottomExpandedEnd;
|
|
847
|
+
}
|
|
848
|
+
const gap = L.remainingGap({
|
|
849
|
+
topExpandedStart: item.topExpandedStart,
|
|
850
|
+
bottomExpandedEnd: item.bottomExpandedEnd
|
|
851
|
+
}, prevHunkEndNew);
|
|
852
|
+
if (!gap) {
|
|
853
|
+
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
854
|
+
sib.tr.style.display = "none";
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const remainingStart = gap.start;
|
|
859
|
+
const remainingEnd = gap.end;
|
|
860
|
+
const setBusy = (busy) => {
|
|
861
|
+
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
862
|
+
sib.tr.querySelectorAll(".gdp-expand-btn").forEach((b) => {
|
|
863
|
+
b.disabled = busy;
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
const fetchAndInsert = (start, end, dir) => {
|
|
868
|
+
if (start < 1)
|
|
869
|
+
start = 1;
|
|
870
|
+
if (end < start)
|
|
871
|
+
return;
|
|
872
|
+
setBusy(true);
|
|
873
|
+
const url = "/file_range?path=" + refPath + "&ref=" + encodeURIComponent(ref) + "&start=" + start + "&end=" + end;
|
|
874
|
+
trackLoad(fetch(url).then((r) => r.json())).then((data) => {
|
|
875
|
+
if (!data || !data.lines) {
|
|
876
|
+
setBusy(false);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const oldStartForGap = prevHunkEndOld + (start - prevHunkEndNew);
|
|
880
|
+
const card = item.tr.closest(".d2h-file-wrapper");
|
|
881
|
+
const sibs = item.siblings || [{ tr: item.tr, sideIndex: 0 }];
|
|
882
|
+
sibs.forEach((sib) => {
|
|
883
|
+
insertContextRows(sib.tr, data.lines, start, oldStartForGap, dir, sib.sideIndex || 0);
|
|
884
|
+
});
|
|
885
|
+
if (card)
|
|
886
|
+
highlightInsertedSpans(card, file);
|
|
887
|
+
if (dir === "after")
|
|
888
|
+
item.topExpandedStart = start;
|
|
889
|
+
else
|
|
890
|
+
item.bottomExpandedEnd = end;
|
|
891
|
+
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
892
|
+
const ln2 = sib.tr.querySelector(".d2h-code-linenumber.d2h-info, .d2h-code-side-linenumber.d2h-info");
|
|
893
|
+
const old = ln2 && ln2.querySelector(".gdp-expand-stack");
|
|
894
|
+
if (old)
|
|
895
|
+
old.remove();
|
|
896
|
+
}
|
|
897
|
+
attachExpandControls(item, file, ref, refPath);
|
|
898
|
+
}).catch(() => {
|
|
899
|
+
setBusy(false);
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
const STEP = 20;
|
|
903
|
+
const remainingSize = remainingEnd - remainingStart + 1;
|
|
904
|
+
const isFirst = prevHunkEndNew === 0;
|
|
905
|
+
const buildStack = () => {
|
|
906
|
+
const stack = document.createElement("div");
|
|
907
|
+
stack.className = "gdp-expand-stack";
|
|
908
|
+
const mkBtn = (path, title, fn) => {
|
|
909
|
+
const b = document.createElement("button");
|
|
910
|
+
b.className = "gdp-expand-btn";
|
|
911
|
+
b.title = title;
|
|
912
|
+
b.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">' + '<path fill="currentColor" d="' + path + '"/></svg>';
|
|
913
|
+
b.addEventListener("click", (e) => {
|
|
914
|
+
e.stopPropagation();
|
|
915
|
+
if (b.disabled)
|
|
916
|
+
return;
|
|
917
|
+
fn();
|
|
918
|
+
});
|
|
919
|
+
return b;
|
|
920
|
+
};
|
|
921
|
+
const upPath = "M8 3.5 3.75 7.75l1.06 1.06L7.25 6.37V13h1.5V6.37l2.44 2.44 1.06-1.06L8 3.5z";
|
|
922
|
+
const downPath = "M8 12.5 12.25 8.25l-1.06-1.06L8.75 9.63V3h-1.5v6.63L4.81 7.19 3.75 8.25 8 12.5z";
|
|
923
|
+
if (isFirst) {
|
|
924
|
+
stack.appendChild(mkBtn(upPath, "Show " + Math.min(STEP, remainingSize) + " more lines", () => fetchAndInsert(Math.max(remainingStart, remainingEnd - STEP + 1), remainingEnd, "after")));
|
|
925
|
+
} else {
|
|
926
|
+
stack.appendChild(mkBtn(upPath, "Show " + Math.min(STEP, remainingSize) + " more lines", () => fetchAndInsert(remainingStart, Math.min(remainingEnd, remainingStart + STEP - 1), "before")));
|
|
927
|
+
stack.appendChild(mkBtn(downPath, "Show " + Math.min(STEP, remainingSize) + " more lines", () => fetchAndInsert(Math.max(remainingStart, remainingEnd - STEP + 1), remainingEnd, "after")));
|
|
928
|
+
}
|
|
929
|
+
return stack;
|
|
930
|
+
};
|
|
931
|
+
const firstSib = item.siblings && item.siblings[0] || { tr: item.tr };
|
|
932
|
+
const ln = firstSib.tr.querySelector(".d2h-code-linenumber.d2h-info, .d2h-code-side-linenumber.d2h-info");
|
|
933
|
+
if (ln && !ln.querySelector(".gdp-expand-stack")) {
|
|
934
|
+
ln.appendChild(buildStack());
|
|
935
|
+
}
|
|
936
|
+
const syncHeight = () => {
|
|
937
|
+
const stack = firstSib.tr.querySelector(".gdp-expand-stack");
|
|
938
|
+
const targetH = stack ? Math.max(20, stack.getBoundingClientRect().height) : 20;
|
|
939
|
+
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
940
|
+
sib.tr.style.setProperty("height", targetH + "px", "important");
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
requestAnimationFrame(syncHeight);
|
|
944
|
+
setTimeout(syncHeight, 100);
|
|
945
|
+
}
|
|
946
|
+
function insertContextRows(targetTr, lines, newStart, oldStart, dir, sideIndex) {
|
|
947
|
+
const tbody = targetTr.parentElement;
|
|
948
|
+
if (!tbody)
|
|
949
|
+
return;
|
|
950
|
+
const anchor = dir === "after" ? targetTr.nextElementSibling : targetTr;
|
|
951
|
+
const isSplit = !!targetTr.querySelector("td.d2h-code-side-linenumber");
|
|
952
|
+
const frag = document.createDocumentFragment();
|
|
953
|
+
for (let i = 0;i < lines.length; i++) {
|
|
954
|
+
const tr = document.createElement("tr");
|
|
955
|
+
tr.className = "gdp-inserted-ctx";
|
|
956
|
+
if (dir)
|
|
957
|
+
tr.dataset.gdpDir = dir;
|
|
958
|
+
let lnHtml;
|
|
959
|
+
if (isSplit) {
|
|
960
|
+
const num = sideIndex === 0 ? oldStart + i : newStart + i;
|
|
961
|
+
lnHtml = '<td class="d2h-code-side-linenumber d2h-cntx">' + num + "</td>";
|
|
962
|
+
} else {
|
|
963
|
+
lnHtml = '<td class="d2h-code-linenumber d2h-cntx">' + '<div class="line-num1">' + (oldStart + i) + "</div>" + '<div class="line-num2">' + (newStart + i) + "</div>" + "</td>";
|
|
964
|
+
}
|
|
965
|
+
tr.innerHTML = lnHtml + '<td class="d2h-cntx">' + '<div class="' + (isSplit ? "d2h-code-side-line" : "d2h-code-line") + '">' + '<span class="d2h-code-line-prefix"> </span>' + '<span class="d2h-code-line-ctn">' + escapeHtmlText(lines[i]) + "</span>" + "</div>" + "</td>";
|
|
966
|
+
frag.appendChild(tr);
|
|
967
|
+
}
|
|
968
|
+
tbody.insertBefore(frag, anchor);
|
|
969
|
+
}
|
|
970
|
+
function escapeHtmlText(s) {
|
|
971
|
+
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
972
|
+
}
|
|
973
|
+
function appendStatSquaresToHeader(card, file) {
|
|
974
|
+
const header = card.querySelector(".d2h-file-header");
|
|
975
|
+
if (!header || header.querySelector(".gdp-stat-squares"))
|
|
976
|
+
return;
|
|
977
|
+
if (!header.querySelector(".gdp-stat-text")) {
|
|
978
|
+
const stats = document.createElement("span");
|
|
979
|
+
stats.className = "gdp-stat-text";
|
|
980
|
+
stats.innerHTML = '<span class="a">+' + (file.additions || 0) + "</span>" + '<span class="d">−' + (file.deletions || 0) + "</span>";
|
|
981
|
+
header.appendChild(stats);
|
|
982
|
+
}
|
|
983
|
+
const total = (file.additions || 0) + (file.deletions || 0);
|
|
984
|
+
const SEG = 5;
|
|
985
|
+
let aSeg, dSeg;
|
|
986
|
+
if (total === 0) {
|
|
987
|
+
aSeg = 0;
|
|
988
|
+
dSeg = 0;
|
|
989
|
+
} else {
|
|
990
|
+
aSeg = Math.round(file.additions / total * SEG);
|
|
991
|
+
dSeg = Math.max(0, SEG - aSeg);
|
|
992
|
+
if (file.additions > 0 && aSeg === 0)
|
|
993
|
+
aSeg = 1;
|
|
994
|
+
if (file.deletions > 0 && dSeg === 0)
|
|
995
|
+
dSeg = 1;
|
|
996
|
+
const over = aSeg + dSeg - SEG;
|
|
997
|
+
if (over > 0)
|
|
998
|
+
dSeg -= over;
|
|
999
|
+
}
|
|
1000
|
+
const wrap = document.createElement("span");
|
|
1001
|
+
wrap.className = "gdp-stat-squares";
|
|
1002
|
+
for (let i = 0;i < SEG; i++) {
|
|
1003
|
+
const box = document.createElement("span");
|
|
1004
|
+
if (i < aSeg)
|
|
1005
|
+
box.className = "sq add";
|
|
1006
|
+
else if (i < aSeg + dSeg)
|
|
1007
|
+
box.className = "sq del";
|
|
1008
|
+
else
|
|
1009
|
+
box.className = "sq nu";
|
|
1010
|
+
wrap.appendChild(box);
|
|
1011
|
+
}
|
|
1012
|
+
header.appendChild(wrap);
|
|
1013
|
+
}
|
|
1014
|
+
function renderFile(file, data, card) {
|
|
1015
|
+
card._diffData = data;
|
|
1016
|
+
card._file = file;
|
|
1017
|
+
card.classList.remove("loading", "pending");
|
|
1018
|
+
card.classList.add("loaded");
|
|
1019
|
+
card.style.minHeight = "";
|
|
1020
|
+
mountDiff(card, file, data);
|
|
1021
|
+
card.style.containIntrinsicSize = Math.max(card.offsetHeight, file.estimated_height_px || 200) + "px";
|
|
1022
|
+
if (data.truncated && data.mode === "preview") {
|
|
1023
|
+
addExpandHunksUI(file, data, card);
|
|
1024
|
+
}
|
|
1025
|
+
scheduleIdleHighlight(card, file);
|
|
1026
|
+
}
|
|
1027
|
+
function buildPreviewUrl(file, hunks) {
|
|
1028
|
+
const u = new URL(file.load_url, window.location.origin);
|
|
1029
|
+
u.searchParams.set("mode", "preview");
|
|
1030
|
+
u.searchParams.set("max_hunks", String(hunks));
|
|
1031
|
+
return u.pathname + u.search;
|
|
1032
|
+
}
|
|
1033
|
+
function addExpandHunksUI(file, data, card) {
|
|
1034
|
+
const total = data.hunk_count || 0;
|
|
1035
|
+
const rendered = data.rendered_hunk_count || 0;
|
|
1036
|
+
const remaining = total - rendered;
|
|
1037
|
+
if (remaining <= 0)
|
|
1038
|
+
return;
|
|
1039
|
+
const old = card.querySelector(".gdp-show-full-wrap");
|
|
1040
|
+
if (old)
|
|
1041
|
+
old.remove();
|
|
1042
|
+
const wrap = document.createElement("div");
|
|
1043
|
+
wrap.className = "gdp-show-full-wrap";
|
|
1044
|
+
const step = Math.min(10, remaining);
|
|
1045
|
+
const moreBtn = document.createElement("button");
|
|
1046
|
+
moreBtn.className = "gdp-show-full";
|
|
1047
|
+
moreBtn.textContent = "Show next " + step + " hunk" + (step === 1 ? "" : "s");
|
|
1048
|
+
moreBtn.addEventListener("click", () => loadMore(rendered + step, false));
|
|
1049
|
+
const allBtn = document.createElement("button");
|
|
1050
|
+
allBtn.className = "gdp-show-full secondary";
|
|
1051
|
+
allBtn.textContent = "Show all (" + remaining + " remaining)";
|
|
1052
|
+
allBtn.addEventListener("click", () => loadMore(total, true));
|
|
1053
|
+
const note = document.createElement("span");
|
|
1054
|
+
note.className = "gdp-hunk-note";
|
|
1055
|
+
note.textContent = rendered + " / " + total + " hunks shown";
|
|
1056
|
+
wrap.appendChild(note);
|
|
1057
|
+
wrap.appendChild(moreBtn);
|
|
1058
|
+
wrap.appendChild(allBtn);
|
|
1059
|
+
card.appendChild(wrap);
|
|
1060
|
+
function loadMore(count, full) {
|
|
1061
|
+
moreBtn.disabled = allBtn.disabled = true;
|
|
1062
|
+
moreBtn.textContent = "Loading…";
|
|
1063
|
+
const myGen = SERVER_GENERATION;
|
|
1064
|
+
const url = full ? file.load_url : buildPreviewUrl(file, count);
|
|
1065
|
+
trackLoad(fetch(url).then((r) => r.json())).then((next) => {
|
|
1066
|
+
if (myGen !== SERVER_GENERATION) {
|
|
1067
|
+
moreBtn.textContent = "Data changed — reload";
|
|
1068
|
+
moreBtn.disabled = allBtn.disabled = false;
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
wrap.remove();
|
|
1072
|
+
card._diffData = next;
|
|
1073
|
+
mountDiff(card, file, next);
|
|
1074
|
+
if (next.truncated || next.mode === "preview" && next.hunk_count > next.rendered_hunk_count) {
|
|
1075
|
+
addExpandHunksUI(file, next, card);
|
|
1076
|
+
}
|
|
1077
|
+
}).catch(() => {
|
|
1078
|
+
moreBtn.disabled = allBtn.disabled = false;
|
|
1079
|
+
moreBtn.textContent = "Failed — retry";
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const EXT_TO_LANG = {
|
|
1084
|
+
js: "javascript",
|
|
1085
|
+
mjs: "javascript",
|
|
1086
|
+
cjs: "javascript",
|
|
1087
|
+
ts: "typescript",
|
|
1088
|
+
tsx: "typescript",
|
|
1089
|
+
jsx: "javascript",
|
|
1090
|
+
py: "python",
|
|
1091
|
+
rb: "ruby",
|
|
1092
|
+
go: "go",
|
|
1093
|
+
rs: "rust",
|
|
1094
|
+
java: "java",
|
|
1095
|
+
kt: "kotlin",
|
|
1096
|
+
swift: "swift",
|
|
1097
|
+
c: "c",
|
|
1098
|
+
h: "c",
|
|
1099
|
+
cc: "cpp",
|
|
1100
|
+
cpp: "cpp",
|
|
1101
|
+
hpp: "cpp",
|
|
1102
|
+
cs: "csharp",
|
|
1103
|
+
php: "php",
|
|
1104
|
+
lua: "lua",
|
|
1105
|
+
sh: "bash",
|
|
1106
|
+
bash: "bash",
|
|
1107
|
+
zsh: "bash",
|
|
1108
|
+
fish: "bash",
|
|
1109
|
+
sql: "sql",
|
|
1110
|
+
json: "json",
|
|
1111
|
+
yaml: "yaml",
|
|
1112
|
+
yml: "yaml",
|
|
1113
|
+
toml: "toml",
|
|
1114
|
+
xml: "xml",
|
|
1115
|
+
html: "xml",
|
|
1116
|
+
vue: "xml",
|
|
1117
|
+
css: "css",
|
|
1118
|
+
scss: "scss",
|
|
1119
|
+
md: "markdown",
|
|
1120
|
+
dockerfile: "dockerfile"
|
|
1121
|
+
};
|
|
1122
|
+
function inferLang(path) {
|
|
1123
|
+
const m = path.match(/\.([^.]+)$/);
|
|
1124
|
+
if (!m)
|
|
1125
|
+
return null;
|
|
1126
|
+
return EXT_TO_LANG[m[1].toLowerCase()] || null;
|
|
1127
|
+
}
|
|
1128
|
+
function highlightInsertedSpans(card, file) {
|
|
1129
|
+
if (file.size_class === "huge")
|
|
1130
|
+
return;
|
|
1131
|
+
if (!STATE.syntaxHighlight)
|
|
1132
|
+
return;
|
|
1133
|
+
const hljsRef = getHljs();
|
|
1134
|
+
if (!hljsRef || !hljsRef.highlight)
|
|
1135
|
+
return;
|
|
1136
|
+
const lang = inferLang(file.path);
|
|
1137
|
+
if (!lang || !hljsRef.getLanguage || !hljsRef.getLanguage(lang))
|
|
1138
|
+
return;
|
|
1139
|
+
const spans = card.querySelectorAll("tr.gdp-inserted-ctx .d2h-code-line-ctn:not([data-gdp-hl])");
|
|
1140
|
+
spans.forEach((s) => {
|
|
1141
|
+
s.dataset.gdpHl = "1";
|
|
1142
|
+
const text = s.textContent || "";
|
|
1143
|
+
if (text.length === 0)
|
|
1144
|
+
return;
|
|
1145
|
+
try {
|
|
1146
|
+
s.innerHTML = hljsRef.highlight(text, { language: lang, ignoreIllegals: true }).value;
|
|
1147
|
+
if (!s.classList.contains("hljs"))
|
|
1148
|
+
s.classList.add("hljs");
|
|
1149
|
+
} catch (_) {}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function scheduleIdleHighlight(card, file) {
|
|
1153
|
+
if (file.highlight)
|
|
1154
|
+
return;
|
|
1155
|
+
if (file.size_class === "huge")
|
|
1156
|
+
return;
|
|
1157
|
+
if (!STATE.syntaxHighlight)
|
|
1158
|
+
return;
|
|
1159
|
+
if (!("requestIdleCallback" in window))
|
|
1160
|
+
return;
|
|
1161
|
+
const hljsRef = getHljs();
|
|
1162
|
+
if (!hljsRef || !hljsRef.highlight)
|
|
1163
|
+
return;
|
|
1164
|
+
const lang = inferLang(file.path);
|
|
1165
|
+
if (!lang || !hljsRef.getLanguage || !hljsRef.getLanguage(lang))
|
|
1166
|
+
return;
|
|
1167
|
+
const work = (deadline) => {
|
|
1168
|
+
const spans = card.querySelectorAll(".d2h-code-line-ctn:not([data-gdp-hl])");
|
|
1169
|
+
let i = 0;
|
|
1170
|
+
while (i < spans.length && deadline.timeRemaining() > 4) {
|
|
1171
|
+
const s = spans[i++];
|
|
1172
|
+
s.dataset.gdpHl = "1";
|
|
1173
|
+
const text = s.textContent || "";
|
|
1174
|
+
if (text.length === 0)
|
|
1175
|
+
continue;
|
|
1176
|
+
try {
|
|
1177
|
+
s.innerHTML = hljsRef.highlight(text, { language: lang, ignoreIllegals: true }).value;
|
|
1178
|
+
if (!s.classList.contains("hljs"))
|
|
1179
|
+
s.classList.add("hljs");
|
|
1180
|
+
} catch (_) {}
|
|
1181
|
+
}
|
|
1182
|
+
if (i < spans.length)
|
|
1183
|
+
requestIdleCallback(work, { timeout: 1500 });
|
|
1184
|
+
};
|
|
1185
|
+
requestIdleCallback(work, { timeout: 2000 });
|
|
1186
|
+
}
|
|
1187
|
+
function syncSideScrollCard(card) {
|
|
1188
|
+
card.querySelectorAll(".d2h-files-diff").forEach((group) => {
|
|
1189
|
+
const sides = group.querySelectorAll(".d2h-code-wrapper");
|
|
1190
|
+
if (sides.length !== 2)
|
|
1191
|
+
return;
|
|
1192
|
+
const [a, b] = sides;
|
|
1193
|
+
let syncing = false;
|
|
1194
|
+
const mirror = (src, dst) => {
|
|
1195
|
+
if (syncing)
|
|
1196
|
+
return;
|
|
1197
|
+
syncing = true;
|
|
1198
|
+
dst.scrollLeft = src.scrollLeft;
|
|
1199
|
+
requestAnimationFrame(() => {
|
|
1200
|
+
syncing = false;
|
|
1201
|
+
});
|
|
1202
|
+
};
|
|
1203
|
+
a.addEventListener("scroll", () => mirror(a, b), { passive: true });
|
|
1204
|
+
b.addEventListener("scroll", () => mirror(b, a), { passive: true });
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov)(\?.*)?$/i;
|
|
1208
|
+
const VIDEO_RE = /\.(mp4|webm|mov)$/i;
|
|
1209
|
+
function isMedia(p) {
|
|
1210
|
+
return MEDIA_RE.test(p);
|
|
1211
|
+
}
|
|
1212
|
+
function isVideo(p) {
|
|
1213
|
+
return VIDEO_RE.test(p);
|
|
1214
|
+
}
|
|
1215
|
+
function fileURL(path, ref) {
|
|
1216
|
+
return "/_file?path=" + encodeURIComponent(path) + "&ref=" + ref;
|
|
1217
|
+
}
|
|
1218
|
+
function mediaTag(path, ref) {
|
|
1219
|
+
const url = fileURL(path, ref);
|
|
1220
|
+
if (isVideo(path)) {
|
|
1221
|
+
return '<video src="' + url + '" controls preload="metadata"></video>';
|
|
1222
|
+
}
|
|
1223
|
+
return '<img src="' + url + '" alt="" loading="lazy">';
|
|
1224
|
+
}
|
|
1225
|
+
function enhanceMediaCard(file, card) {
|
|
1226
|
+
const path = file.path;
|
|
1227
|
+
if (!file.media_kind && !isMedia(path))
|
|
1228
|
+
return;
|
|
1229
|
+
const wrapper = card.querySelector(".d2h-file-wrapper");
|
|
1230
|
+
if (!wrapper)
|
|
1231
|
+
return;
|
|
1232
|
+
const body = wrapper.querySelector(".d2h-files-diff") || wrapper.querySelector(".d2h-file-diff");
|
|
1233
|
+
if (!body)
|
|
1234
|
+
return;
|
|
1235
|
+
const container = document.createElement("div");
|
|
1236
|
+
container.className = "gdp-media";
|
|
1237
|
+
let leftHTML, rightHTML;
|
|
1238
|
+
if (file.status === "A") {
|
|
1239
|
+
leftHTML = '<div class="media-empty">Not in HEAD</div>';
|
|
1240
|
+
rightHTML = mediaTag(path, "worktree");
|
|
1241
|
+
} else if (file.status === "D") {
|
|
1242
|
+
leftHTML = mediaTag(path, "HEAD");
|
|
1243
|
+
rightHTML = '<div class="media-empty">Deleted</div>';
|
|
1244
|
+
} else {
|
|
1245
|
+
leftHTML = mediaTag(path, "HEAD");
|
|
1246
|
+
rightHTML = mediaTag(path, "worktree");
|
|
1247
|
+
}
|
|
1248
|
+
container.innerHTML = '<div class="media-side"><div class="media-label del">Before</div>' + leftHTML + "</div>" + '<div class="media-side"><div class="media-label add">After</div>' + rightHTML + "</div>";
|
|
1249
|
+
body.replaceWith(container);
|
|
1250
|
+
}
|
|
1251
|
+
function setupScrollSpy() {
|
|
1252
|
+
const handler = () => {
|
|
1253
|
+
if (handler._raf)
|
|
1254
|
+
return;
|
|
1255
|
+
if (performance.now() < SUPPRESS_SPY_UNTIL)
|
|
1256
|
+
return;
|
|
1257
|
+
handler._raf = requestAnimationFrame(() => {
|
|
1258
|
+
handler._raf = null;
|
|
1259
|
+
if (performance.now() < SUPPRESS_SPY_UNTIL)
|
|
1260
|
+
return;
|
|
1261
|
+
const topbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--topbar-h")) || 56;
|
|
1262
|
+
const scanY = topbarH + 24;
|
|
1263
|
+
const cards = document.querySelectorAll(".gdp-file-shell");
|
|
1264
|
+
for (const w of cards) {
|
|
1265
|
+
const r = w.getBoundingClientRect();
|
|
1266
|
+
if (r.top <= scanY && r.bottom > scanY) {
|
|
1267
|
+
const text = w.dataset.path || "";
|
|
1268
|
+
let best = null, bestLen = 0;
|
|
1269
|
+
STATE.files.forEach((f) => {
|
|
1270
|
+
if ((text === f.path || text.endsWith(f.path)) && f.path.length > bestLen) {
|
|
1271
|
+
best = f.path;
|
|
1272
|
+
bestLen = f.path.length;
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
if (best) {
|
|
1276
|
+
markActive(best);
|
|
1277
|
+
const recentlyTouched = performance.now() - (window.__gdpSidebarTouchedAt || 0) < 1500;
|
|
1278
|
+
if (!recentlyTouched) {
|
|
1279
|
+
const li = document.querySelector('#filelist li[data-path="' + CSS.escape(best) + '"]');
|
|
1280
|
+
if (li) {
|
|
1281
|
+
const sb = document.querySelector("#sidebar");
|
|
1282
|
+
if (!sb)
|
|
1283
|
+
return;
|
|
1284
|
+
const lr = li.getBoundingClientRect();
|
|
1285
|
+
const sr = sb.getBoundingClientRect();
|
|
1286
|
+
if (lr.top < sr.top + 40 || lr.bottom > sr.bottom - 40) {
|
|
1287
|
+
li.scrollIntoView({ block: "nearest" });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
};
|
|
1297
|
+
if (window.__gdpScrollSpy)
|
|
1298
|
+
window.removeEventListener("scroll", window.__gdpScrollSpy);
|
|
1299
|
+
window.__gdpScrollSpy = handler;
|
|
1300
|
+
window.addEventListener("scroll", handler, { passive: true });
|
|
1301
|
+
handler(new Event("scroll"));
|
|
1302
|
+
}
|
|
1303
|
+
function collapseAll(force) {
|
|
1304
|
+
STATE.collapsed = typeof force === "boolean" ? force : !STATE.collapsed;
|
|
1305
|
+
document.querySelectorAll(".gdp-file-shell.loaded .d2h-file-wrapper").forEach((w) => {
|
|
1306
|
+
const body = w.querySelector(".d2h-files-diff, .d2h-file-diff");
|
|
1307
|
+
if (body)
|
|
1308
|
+
body.style.display = STATE.collapsed ? "none" : "";
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
$$(".sb-view-seg button").forEach((b) => {
|
|
1312
|
+
b.addEventListener("click", () => {
|
|
1313
|
+
STATE.sbView = b.dataset.view || "tree";
|
|
1314
|
+
localStorage.setItem("gdp:sbview", STATE.sbView);
|
|
1315
|
+
if (STATE.files && STATE.files.length)
|
|
1316
|
+
renderSidebar(STATE.files);
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
function applySidebarWidth(w) {
|
|
1320
|
+
const cw = Math.max(180, Math.min(900, w));
|
|
1321
|
+
document.documentElement.style.setProperty("--sidebar-w", cw + "px");
|
|
1322
|
+
STATE.sbWidth = cw;
|
|
1323
|
+
localStorage.setItem("gdp:sbwidth", String(cw));
|
|
1324
|
+
}
|
|
1325
|
+
applySidebarWidth(STATE.sbWidth);
|
|
1326
|
+
(function trackSidebarInteraction() {
|
|
1327
|
+
const sb = document.getElementById("sidebar");
|
|
1328
|
+
if (!sb)
|
|
1329
|
+
return;
|
|
1330
|
+
const mark = () => {
|
|
1331
|
+
window.__gdpSidebarTouchedAt = performance.now();
|
|
1332
|
+
};
|
|
1333
|
+
sb.addEventListener("wheel", mark, { passive: true });
|
|
1334
|
+
sb.addEventListener("mousedown", mark);
|
|
1335
|
+
sb.addEventListener("touchstart", mark, { passive: true });
|
|
1336
|
+
sb.addEventListener("scroll", mark, { passive: true });
|
|
1337
|
+
})();
|
|
1338
|
+
(function setupResizer() {
|
|
1339
|
+
const handle = $("#sidebar-resizer");
|
|
1340
|
+
if (!handle)
|
|
1341
|
+
return;
|
|
1342
|
+
const preview = document.createElement("div");
|
|
1343
|
+
preview.id = "sidebar-resize-preview";
|
|
1344
|
+
document.body.appendChild(preview);
|
|
1345
|
+
const MIN = 180, MAX = 900;
|
|
1346
|
+
const clamp = (w) => Math.max(MIN, Math.min(MAX, w));
|
|
1347
|
+
let dragging = false, startX = 0, startW = 0, currentW = 0;
|
|
1348
|
+
handle.addEventListener("mousedown", (e) => {
|
|
1349
|
+
dragging = true;
|
|
1350
|
+
startX = e.clientX;
|
|
1351
|
+
startW = STATE.sbWidth;
|
|
1352
|
+
currentW = startW;
|
|
1353
|
+
document.body.classList.add("gdp-resizing");
|
|
1354
|
+
preview.style.display = "block";
|
|
1355
|
+
preview.style.left = startW + "px";
|
|
1356
|
+
e.preventDefault();
|
|
1357
|
+
});
|
|
1358
|
+
window.addEventListener("mousemove", (e) => {
|
|
1359
|
+
if (!dragging)
|
|
1360
|
+
return;
|
|
1361
|
+
currentW = clamp(startW + (e.clientX - startX));
|
|
1362
|
+
preview.style.left = currentW + "px";
|
|
1363
|
+
});
|
|
1364
|
+
window.addEventListener("mouseup", () => {
|
|
1365
|
+
if (!dragging)
|
|
1366
|
+
return;
|
|
1367
|
+
dragging = false;
|
|
1368
|
+
preview.style.display = "none";
|
|
1369
|
+
document.body.classList.remove("gdp-resizing");
|
|
1370
|
+
applySidebarWidth(currentW);
|
|
1371
|
+
});
|
|
1372
|
+
handle.addEventListener("dblclick", () => applySidebarWidth(308));
|
|
1373
|
+
})();
|
|
1374
|
+
$$("#topbar .seg button").forEach((b) => {
|
|
1375
|
+
b.addEventListener("click", () => setLayout(b.dataset.layout || "side-by-side"));
|
|
1376
|
+
});
|
|
1377
|
+
$("#theme").addEventListener("click", () => {
|
|
1378
|
+
STATE.theme = STATE.theme === "dark" ? "light" : "dark";
|
|
1379
|
+
localStorage.setItem("gdp:theme", STATE.theme);
|
|
1380
|
+
applyTheme();
|
|
1381
|
+
});
|
|
1382
|
+
$("#collapse").addEventListener("click", () => collapseAll());
|
|
1383
|
+
function syncFilters(srcEl) {
|
|
1384
|
+
const v = srcEl.value;
|
|
1385
|
+
["#filter", "#sb-filter"].forEach((sel) => {
|
|
1386
|
+
const el = $(sel);
|
|
1387
|
+
if (el && el !== srcEl)
|
|
1388
|
+
el.value = v;
|
|
1389
|
+
});
|
|
1390
|
+
applyFilter();
|
|
1391
|
+
}
|
|
1392
|
+
$("#filter").addEventListener("input", (e) => syncFilters(e.target));
|
|
1393
|
+
const sbFilter = $("#sb-filter");
|
|
1394
|
+
if (sbFilter)
|
|
1395
|
+
sbFilter.addEventListener("input", (e) => syncFilters(e.target));
|
|
1396
|
+
document.addEventListener("keydown", (e) => {
|
|
1397
|
+
const targetEl = e.target;
|
|
1398
|
+
if (targetEl && (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA"))
|
|
1399
|
+
return;
|
|
1400
|
+
if (e.key === "/") {
|
|
1401
|
+
e.preventDefault();
|
|
1402
|
+
$("#filter").focus();
|
|
1403
|
+
} else if (e.key === "j" || e.key === "k") {
|
|
1404
|
+
const items = $$("#filelist li[data-path]:not(.hidden)");
|
|
1405
|
+
if (!items.length)
|
|
1406
|
+
return;
|
|
1407
|
+
let idx = items.findIndex((li) => li.classList.contains("active"));
|
|
1408
|
+
if (idx < 0)
|
|
1409
|
+
idx = 0;
|
|
1410
|
+
else
|
|
1411
|
+
idx = e.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
|
|
1412
|
+
const target = items[idx];
|
|
1413
|
+
if (target) {
|
|
1414
|
+
target.click();
|
|
1415
|
+
target.scrollIntoView({ block: "nearest" });
|
|
1416
|
+
}
|
|
1417
|
+
const nextIdx = e.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
|
|
1418
|
+
const nextItem = items[nextIdx];
|
|
1419
|
+
if (nextItem && nextItem !== target && nextItem.dataset.path)
|
|
1420
|
+
prefetchByPath(nextItem.dataset.path);
|
|
1421
|
+
} else if (e.key === "u")
|
|
1422
|
+
setLayout("line-by-line");
|
|
1423
|
+
else if (e.key === "s")
|
|
1424
|
+
setLayout("side-by-side");
|
|
1425
|
+
else if (e.key === "t")
|
|
1426
|
+
$("#theme").click();
|
|
1427
|
+
});
|
|
1428
|
+
applyTheme();
|
|
1429
|
+
setLayout(STATE.layout);
|
|
1430
|
+
function load(opts) {
|
|
1431
|
+
setStatus("refreshing");
|
|
1432
|
+
const params = new URLSearchParams;
|
|
1433
|
+
if (STATE.ignoreWs)
|
|
1434
|
+
params.set("ignore_ws", "1");
|
|
1435
|
+
if (STATE.from)
|
|
1436
|
+
params.set("from", STATE.from);
|
|
1437
|
+
if (STATE.to)
|
|
1438
|
+
params.set("to", STATE.to);
|
|
1439
|
+
if (opts && opts.nocache)
|
|
1440
|
+
params.set("nocache", "1");
|
|
1441
|
+
const url = "/diff.json" + (params.toString() ? "?" + params.toString() : "");
|
|
1442
|
+
return trackLoad(fetch(url).then((r) => r.json())).then((data) => {
|
|
1443
|
+
renderShell(data);
|
|
1444
|
+
setStatus("live");
|
|
1445
|
+
}).catch(() => setStatus("error"));
|
|
1446
|
+
}
|
|
1447
|
+
load();
|
|
1448
|
+
function syncRefInputs() {
|
|
1449
|
+
const fi = $("#ref-from"), ti = $("#ref-to");
|
|
1450
|
+
if (fi)
|
|
1451
|
+
fi.value = STATE.from;
|
|
1452
|
+
if (ti)
|
|
1453
|
+
ti.value = STATE.to;
|
|
1454
|
+
}
|
|
1455
|
+
function setRange(from, to) {
|
|
1456
|
+
STATE.from = from || "";
|
|
1457
|
+
STATE.to = to || "";
|
|
1458
|
+
localStorage.setItem("gdp:from", STATE.from);
|
|
1459
|
+
localStorage.setItem("gdp:to", STATE.to);
|
|
1460
|
+
syncRefInputs();
|
|
1461
|
+
load();
|
|
1462
|
+
}
|
|
1463
|
+
syncRefInputs();
|
|
1464
|
+
const REFS = { branches: [], tags: [], commits: [], current: "" };
|
|
1465
|
+
const popover = $("#ref-popover");
|
|
1466
|
+
const popBody = popover.querySelector(".rp-body");
|
|
1467
|
+
const popSearch = popover.querySelector(".rp-search");
|
|
1468
|
+
let popTarget = null;
|
|
1469
|
+
function fetchRefs() {
|
|
1470
|
+
return fetch("/_refs").then((r) => r.json()).then((refs) => {
|
|
1471
|
+
Object.assign(REFS, refs);
|
|
1472
|
+
}).catch(() => {});
|
|
1473
|
+
}
|
|
1474
|
+
fetchRefs();
|
|
1475
|
+
let popTab = "commits";
|
|
1476
|
+
function buildPopBody(query) {
|
|
1477
|
+
const q = (query || "").toLowerCase().trim();
|
|
1478
|
+
const m = (s) => !q || String(s).toLowerCase().includes(q);
|
|
1479
|
+
const html = [];
|
|
1480
|
+
if (popTab === "commits") {
|
|
1481
|
+
const commits = (REFS.commits || []).filter((c) => m(c));
|
|
1482
|
+
if (!commits.length) {
|
|
1483
|
+
html.push('<div class="rp-empty">no commits</div>');
|
|
1484
|
+
}
|
|
1485
|
+
for (const c of commits) {
|
|
1486
|
+
const [sha, subject, author, when] = c.split("\t");
|
|
1487
|
+
if (!sha)
|
|
1488
|
+
continue;
|
|
1489
|
+
html.push('<div class="rp-item-commit" data-val="' + escapeAttr(sha) + '">' + '<div class="row1">' + '<span class="sha">' + escapeHtml(sha) + "</span>" + '<span class="subject" title="' + escapeAttr(subject || "") + '">' + escapeHtml(subject || "") + "</span>" + "</div>" + '<div class="row2">' + '<span class="author">' + escapeHtml(author || "") + "</span>" + '<span class="when">' + escapeHtml(when || "") + "</span>" + "</div>" + "</div>");
|
|
1490
|
+
}
|
|
1491
|
+
} else if (popTab === "branches") {
|
|
1492
|
+
const branches = (REFS.branches || []).filter(m);
|
|
1493
|
+
if (!branches.length) {
|
|
1494
|
+
html.push('<div class="rp-empty">no branches</div>');
|
|
1495
|
+
}
|
|
1496
|
+
for (const b of branches) {
|
|
1497
|
+
const cur = b === REFS.current;
|
|
1498
|
+
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(b) + '">' + '<span class="name">' + escapeHtml(b) + "</span>" + (cur ? '<span class="badge cur">current</span>' : '<span class="badge">branch</span>') + "</div>");
|
|
1499
|
+
}
|
|
1500
|
+
} else if (popTab === "tags") {
|
|
1501
|
+
const tags = (REFS.tags || []).filter(m);
|
|
1502
|
+
if (!tags.length) {
|
|
1503
|
+
html.push('<div class="rp-empty">no tags</div>');
|
|
1504
|
+
}
|
|
1505
|
+
for (const t of tags) {
|
|
1506
|
+
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(t) + '">' + '<span class="name">' + escapeHtml(t) + "</span>" + '<span class="badge">tag</span>' + "</div>");
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
popBody.innerHTML = html.join("");
|
|
1510
|
+
highlightCurrentInPopover();
|
|
1511
|
+
}
|
|
1512
|
+
function highlightCurrentInPopover() {
|
|
1513
|
+
if (!popTarget)
|
|
1514
|
+
return;
|
|
1515
|
+
const cur = (popTarget.value || "").trim();
|
|
1516
|
+
if (!cur)
|
|
1517
|
+
return;
|
|
1518
|
+
const items = popBody.querySelectorAll("[data-val]");
|
|
1519
|
+
let match = null;
|
|
1520
|
+
items.forEach((it) => {
|
|
1521
|
+
if (it.dataset.val === cur)
|
|
1522
|
+
match = it;
|
|
1523
|
+
});
|
|
1524
|
+
if (match) {
|
|
1525
|
+
match.classList.add("current");
|
|
1526
|
+
const ph = popBody;
|
|
1527
|
+
const r = match.getBoundingClientRect();
|
|
1528
|
+
const pr = ph.getBoundingClientRect();
|
|
1529
|
+
if (r.top < pr.top || r.bottom > pr.bottom) {
|
|
1530
|
+
ph.scrollTop = match.offsetTop - ph.clientHeight / 2;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function escapeAttr(s) {
|
|
1535
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
1536
|
+
}
|
|
1537
|
+
function openPopover(input) {
|
|
1538
|
+
popTarget = input;
|
|
1539
|
+
popSearch.value = "";
|
|
1540
|
+
buildPopBody("");
|
|
1541
|
+
const cur = (input.value || "").trim();
|
|
1542
|
+
popover.querySelectorAll(".rp-chip").forEach((c) => {
|
|
1543
|
+
c.classList.toggle("current", c.dataset.val === cur);
|
|
1544
|
+
});
|
|
1545
|
+
popover.hidden = false;
|
|
1546
|
+
const r = input.getBoundingClientRect();
|
|
1547
|
+
popover.style.left = Math.max(8, r.left) + "px";
|
|
1548
|
+
popover.style.top = r.bottom + 4 + "px";
|
|
1549
|
+
setTimeout(() => popSearch.focus(), 0);
|
|
1550
|
+
}
|
|
1551
|
+
function closePopover() {
|
|
1552
|
+
popover.hidden = true;
|
|
1553
|
+
popTarget = null;
|
|
1554
|
+
}
|
|
1555
|
+
["#ref-from", "#ref-to"].forEach((sel) => {
|
|
1556
|
+
const el = $(sel);
|
|
1557
|
+
el.addEventListener("focus", () => openPopover(el));
|
|
1558
|
+
el.addEventListener("mousedown", (e) => {
|
|
1559
|
+
if (popover.hidden) {
|
|
1560
|
+
e.preventDefault();
|
|
1561
|
+
el.focus();
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
el.addEventListener("keydown", (e) => {
|
|
1565
|
+
if (e.key === "Enter") {
|
|
1566
|
+
e.preventDefault();
|
|
1567
|
+
closePopover();
|
|
1568
|
+
setRange($("#ref-from").value, $("#ref-to").value);
|
|
1569
|
+
} else if (e.key === "Escape") {
|
|
1570
|
+
closePopover();
|
|
1571
|
+
el.blur();
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
popSearch.addEventListener("input", () => buildPopBody(popSearch.value));
|
|
1576
|
+
popSearch.addEventListener("keydown", (e) => {
|
|
1577
|
+
if (e.key === "Escape") {
|
|
1578
|
+
closePopover();
|
|
1579
|
+
}
|
|
1580
|
+
if (e.key === "Enter") {
|
|
1581
|
+
const first = popBody.querySelector(".rp-item");
|
|
1582
|
+
if (first)
|
|
1583
|
+
first.click();
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
function handlePicked(val) {
|
|
1587
|
+
if (!popTarget || !val)
|
|
1588
|
+
return;
|
|
1589
|
+
popTarget.value = val;
|
|
1590
|
+
const targetWasFrom = popTarget.id === "ref-from";
|
|
1591
|
+
const otherEmpty = !$("#ref-to").value;
|
|
1592
|
+
closePopover();
|
|
1593
|
+
setRange($("#ref-from").value, $("#ref-to").value);
|
|
1594
|
+
if (targetWasFrom && otherEmpty) {
|
|
1595
|
+
const ti = $("#ref-to");
|
|
1596
|
+
setTimeout(() => ti.focus(), 0);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
popBody.addEventListener("click", (e) => {
|
|
1600
|
+
const item = e.target.closest(".rp-item-commit, .rp-item-ref");
|
|
1601
|
+
if (!item)
|
|
1602
|
+
return;
|
|
1603
|
+
handlePicked(item.dataset.val);
|
|
1604
|
+
});
|
|
1605
|
+
popover.querySelectorAll(".rp-tab").forEach((t) => {
|
|
1606
|
+
t.addEventListener("click", () => {
|
|
1607
|
+
popTab = t.dataset.tab || "commits";
|
|
1608
|
+
popover.querySelectorAll(".rp-tab").forEach((b) => b.classList.toggle("active", b === t));
|
|
1609
|
+
buildPopBody(popSearch.value);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
popover.querySelectorAll(".rp-chip").forEach((c) => {
|
|
1613
|
+
c.addEventListener("click", () => handlePicked(c.dataset.val));
|
|
1614
|
+
});
|
|
1615
|
+
document.addEventListener("mousedown", (e) => {
|
|
1616
|
+
if (popover.hidden)
|
|
1617
|
+
return;
|
|
1618
|
+
const target = e.target;
|
|
1619
|
+
if (popover.contains(target))
|
|
1620
|
+
return;
|
|
1621
|
+
if (target.id === "ref-from" || target.id === "ref-to")
|
|
1622
|
+
return;
|
|
1623
|
+
closePopover();
|
|
1624
|
+
});
|
|
1625
|
+
$("#ref-apply").addEventListener("click", () => setRange($("#ref-from").value, $("#ref-to").value));
|
|
1626
|
+
$("#ref-reset").addEventListener("click", () => setRange("HEAD", "worktree"));
|
|
1627
|
+
function applyIgnoreWs() {
|
|
1628
|
+
const btn = $("#ignore-ws");
|
|
1629
|
+
if (btn)
|
|
1630
|
+
btn.classList.toggle("active", STATE.ignoreWs);
|
|
1631
|
+
}
|
|
1632
|
+
applyIgnoreWs();
|
|
1633
|
+
$("#ignore-ws").addEventListener("click", () => {
|
|
1634
|
+
STATE.ignoreWs = !STATE.ignoreWs;
|
|
1635
|
+
localStorage.setItem("gdp:ignore-ws", STATE.ignoreWs ? "1" : "0");
|
|
1636
|
+
applyIgnoreWs();
|
|
1637
|
+
load();
|
|
1638
|
+
});
|
|
1639
|
+
function setSyntaxHighlight(on) {
|
|
1640
|
+
STATE.syntaxHighlight = on;
|
|
1641
|
+
localStorage.setItem("gdp:syntax-highlight", on ? "1" : "0");
|
|
1642
|
+
setHighlightButton(on && getHljs() ? "loaded" : "idle");
|
|
1643
|
+
if (on) {
|
|
1644
|
+
loadSyntaxHighlighter().then((hljsRef) => {
|
|
1645
|
+
if (!hljsRef)
|
|
1646
|
+
return;
|
|
1647
|
+
rerenderLoadedDiffs();
|
|
1648
|
+
});
|
|
1649
|
+
} else {
|
|
1650
|
+
rerenderLoadedDiffs();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
|
|
1654
|
+
$("#syntax-highlight").addEventListener("click", () => {
|
|
1655
|
+
setSyntaxHighlight(!STATE.syntaxHighlight);
|
|
1656
|
+
});
|
|
1657
|
+
if (STATE.syntaxHighlight)
|
|
1658
|
+
setSyntaxHighlight(true);
|
|
1659
|
+
$("#reload-prom").addEventListener("click", () => {
|
|
1660
|
+
const btn = $("#reload-prom");
|
|
1661
|
+
btn.classList.add("spinning");
|
|
1662
|
+
load().finally(() => {
|
|
1663
|
+
setTimeout(() => btn.classList.remove("spinning"), 200);
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
const AUTO_RELOAD_MS = 3000;
|
|
1667
|
+
let autoTimer = null;
|
|
1668
|
+
function setAutoReload(on) {
|
|
1669
|
+
STATE.autoReload = on;
|
|
1670
|
+
localStorage.setItem("gdp:auto-reload", on ? "1" : "0");
|
|
1671
|
+
const btn = $("#auto-reload");
|
|
1672
|
+
if (btn) {
|
|
1673
|
+
btn.classList.toggle("active", on);
|
|
1674
|
+
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
1675
|
+
}
|
|
1676
|
+
if (autoTimer) {
|
|
1677
|
+
clearInterval(autoTimer);
|
|
1678
|
+
autoTimer = null;
|
|
1679
|
+
}
|
|
1680
|
+
if (on)
|
|
1681
|
+
autoTimer = setInterval(() => {
|
|
1682
|
+
if (!document.hidden)
|
|
1683
|
+
load({ nocache: true });
|
|
1684
|
+
}, AUTO_RELOAD_MS);
|
|
1685
|
+
}
|
|
1686
|
+
setAutoReload(STATE.autoReload);
|
|
1687
|
+
$("#auto-reload").addEventListener("click", () => setAutoReload(!STATE.autoReload));
|
|
1688
|
+
window.addEventListener("storage", (e) => {
|
|
1689
|
+
if (e.key === "gdp:auto-reload")
|
|
1690
|
+
setAutoReload(e.newValue !== "0");
|
|
1691
|
+
if (e.key === "gdp:syntax-highlight")
|
|
1692
|
+
setSyntaxHighlight(e.newValue !== "0");
|
|
1693
|
+
});
|
|
1694
|
+
const TEST_RE = /(^|[/_.])(test|spec|__tests__)([/_.]|$)/i;
|
|
1695
|
+
function applyHideTests() {
|
|
1696
|
+
const btn = $("#hide-tests");
|
|
1697
|
+
if (btn)
|
|
1698
|
+
btn.classList.toggle("active", STATE.hideTests);
|
|
1699
|
+
document.querySelectorAll(".gdp-file-shell").forEach((card) => {
|
|
1700
|
+
const isTest = TEST_RE.test(card.dataset.path || "");
|
|
1701
|
+
card.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
|
|
1702
|
+
});
|
|
1703
|
+
document.querySelectorAll("#filelist li[data-path]").forEach((li) => {
|
|
1704
|
+
const isTest = TEST_RE.test(li.dataset.path || "");
|
|
1705
|
+
li.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
|
|
1706
|
+
});
|
|
1707
|
+
document.querySelectorAll("#filelist .tree-dir").forEach((dir) => {
|
|
1708
|
+
const childUl = dir.nextElementSibling;
|
|
1709
|
+
if (!childUl)
|
|
1710
|
+
return;
|
|
1711
|
+
const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden):not(.hidden-by-tests)");
|
|
1712
|
+
dir.classList.toggle("hidden-by-tests", STATE.hideTests && !anyVisible);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
applyHideTests();
|
|
1716
|
+
$("#hide-tests").addEventListener("click", () => {
|
|
1717
|
+
STATE.hideTests = !STATE.hideTests;
|
|
1718
|
+
localStorage.setItem("gdp:hide-tests", STATE.hideTests ? "1" : "0");
|
|
1719
|
+
applyHideTests();
|
|
1720
|
+
});
|
|
1721
|
+
let sseTimer = null;
|
|
1722
|
+
function scheduleSseLoad() {
|
|
1723
|
+
if (sseTimer)
|
|
1724
|
+
clearTimeout(sseTimer);
|
|
1725
|
+
sseTimer = setTimeout(() => {
|
|
1726
|
+
sseTimer = null;
|
|
1727
|
+
const savedScroll = window.scrollY;
|
|
1728
|
+
const savedActive = STATE.activeFile;
|
|
1729
|
+
load().then(() => {
|
|
1730
|
+
if (savedActive) {
|
|
1731
|
+
const card = document.querySelector(diffCardSelector(savedActive));
|
|
1732
|
+
if (card) {
|
|
1733
|
+
card.scrollIntoView({ block: "start" });
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
window.scrollTo(0, savedScroll);
|
|
1738
|
+
});
|
|
1739
|
+
}, 350);
|
|
1740
|
+
}
|
|
1741
|
+
const es = new EventSource("/events");
|
|
1742
|
+
es.addEventListener("update", () => scheduleSseLoad());
|
|
1743
|
+
es.addEventListener("reload", () => location.reload());
|
|
1744
|
+
es.addEventListener("error", () => setStatus("error"));
|
|
1745
|
+
es.addEventListener("open", () => setStatus("live"));
|
|
1746
|
+
let assetVersion = null;
|
|
1747
|
+
function pollAssetVersion() {
|
|
1748
|
+
fetch("/_asset_version").then((r) => r.ok ? r.json() : null).then((data) => {
|
|
1749
|
+
if (!data || !data.version)
|
|
1750
|
+
return;
|
|
1751
|
+
if (assetVersion == null) {
|
|
1752
|
+
assetVersion = data.version;
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (data.version !== assetVersion)
|
|
1756
|
+
location.reload();
|
|
1757
|
+
}).catch(() => {});
|
|
1758
|
+
}
|
|
1759
|
+
pollAssetVersion();
|
|
1760
|
+
setInterval(pollAssetVersion, 1500);
|
|
1761
|
+
})();
|
|
1762
|
+
})();
|