@youtyan/code-viewer 0.1.0 → 0.1.1
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/README.md +12 -5
- package/package.json +4 -3
- package/web/app.js +1393 -90
- package/web/favicon.png +0 -0
- package/web/index.html +36 -26
- package/web/style.css +737 -59
- package/web-src/server/dev.ts +95 -0
- package/web-src/server/git.ts +104 -3
- package/web-src/server/preview.ts +88 -6
- package/web-src/server/range.ts +8 -0
- package/web-src/server/runtime.d.ts +6 -1
- package/web-src/types.ts +18 -0
package/web/app.js
CHANGED
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
function mapNewToOld(newLine, prevHunkEndNew, prevHunkEndOld) {
|
|
34
34
|
return prevHunkEndOld + (newLine - prevHunkEndNew);
|
|
35
35
|
}
|
|
36
|
+
function trailingClickRange(hunkEndNew, step) {
|
|
37
|
+
return { start: hunkEndNew, end: hunkEndNew + step - 1 };
|
|
38
|
+
}
|
|
39
|
+
function applyTrailingResult(state, receivedCount, step) {
|
|
40
|
+
return {
|
|
41
|
+
newStart: state.newStart + receivedCount,
|
|
42
|
+
oldStart: state.oldStart + receivedCount,
|
|
43
|
+
eof: receivedCount === 0 || receivedCount < step
|
|
44
|
+
};
|
|
45
|
+
}
|
|
36
46
|
var GdpExpandLogic = {
|
|
37
47
|
initExpandState,
|
|
38
48
|
remainingGap,
|
|
@@ -41,9 +51,137 @@
|
|
|
41
51
|
downClickRange,
|
|
42
52
|
applyUp,
|
|
43
53
|
applyDown,
|
|
44
|
-
mapNewToOld
|
|
54
|
+
mapNewToOld,
|
|
55
|
+
trailingClickRange,
|
|
56
|
+
applyTrailingResult
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
// web-src/file-navigation.ts
|
|
60
|
+
function nextVisibleFileIndex(currentIndex, itemCount, direction) {
|
|
61
|
+
if (itemCount <= 0)
|
|
62
|
+
return -1;
|
|
63
|
+
if (currentIndex < 0)
|
|
64
|
+
return direction > 0 ? 0 : itemCount - 1;
|
|
65
|
+
return Math.max(0, Math.min(itemCount - 1, currentIndex + direction));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// web-src/file-path-copy.ts
|
|
69
|
+
function filePathClipboardText(path) {
|
|
70
|
+
return path || "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// web-src/file-filter.ts
|
|
74
|
+
function normalizeFileFilterQuery(value) {
|
|
75
|
+
return (value || "").toLowerCase().trim();
|
|
76
|
+
}
|
|
77
|
+
function parseSlashRegex(query) {
|
|
78
|
+
if (!query.startsWith("/") || query.length < 2)
|
|
79
|
+
return null;
|
|
80
|
+
const lastSlash = query.lastIndexOf("/");
|
|
81
|
+
if (lastSlash <= 0)
|
|
82
|
+
return null;
|
|
83
|
+
return {
|
|
84
|
+
source: query.slice(1, lastSlash),
|
|
85
|
+
flags: query.slice(lastSlash + 1)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function compileFileFilter(value) {
|
|
89
|
+
const raw = (value || "").trim();
|
|
90
|
+
if (!raw)
|
|
91
|
+
return { kind: "empty", match: () => true };
|
|
92
|
+
const slashRegex = parseSlashRegex(raw);
|
|
93
|
+
if (slashRegex) {
|
|
94
|
+
try {
|
|
95
|
+
const regex = new RegExp(slashRegex.source, slashRegex.flags);
|
|
96
|
+
return { kind: "regex", match: (path) => regex.test(path) };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
kind: "invalid",
|
|
100
|
+
match: () => false,
|
|
101
|
+
error: error instanceof Error ? error.message : String(error)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const q = normalizeFileFilterQuery(raw.startsWith("/") ? raw.slice(1) : raw);
|
|
106
|
+
return {
|
|
107
|
+
kind: "substring",
|
|
108
|
+
match: (path) => path.toLowerCase().includes(q)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// web-src/routes.ts
|
|
113
|
+
function assertNever(value) {
|
|
114
|
+
throw new Error("unhandled route: " + JSON.stringify(value));
|
|
115
|
+
}
|
|
116
|
+
function parseLegacyRange(value, fallback) {
|
|
117
|
+
const raw = value || "";
|
|
118
|
+
const sep = raw.indexOf("..");
|
|
119
|
+
if (sep < 0)
|
|
120
|
+
return fallback;
|
|
121
|
+
return {
|
|
122
|
+
from: raw.slice(0, sep) || fallback.from,
|
|
123
|
+
to: raw.slice(sep + 2) || fallback.to
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseRoute(pathname, search, fallbackRange) {
|
|
127
|
+
const params = new URLSearchParams(search);
|
|
128
|
+
const legacyRange = parseLegacyRange(params.get("range"), fallbackRange);
|
|
129
|
+
const range = {
|
|
130
|
+
from: params.get("from") || legacyRange.from,
|
|
131
|
+
to: params.get("to") || legacyRange.to
|
|
132
|
+
};
|
|
133
|
+
switch (pathname) {
|
|
134
|
+
case "/":
|
|
135
|
+
case "/index.html":
|
|
136
|
+
return {
|
|
137
|
+
screen: "repo",
|
|
138
|
+
ref: params.get("ref") || params.get("target") || "worktree",
|
|
139
|
+
path: params.get("path") || "",
|
|
140
|
+
range
|
|
141
|
+
};
|
|
142
|
+
case "/todif":
|
|
143
|
+
case "/todiff":
|
|
144
|
+
return { screen: "diff", range };
|
|
145
|
+
case "/file": {
|
|
146
|
+
const path = params.get("path") || "";
|
|
147
|
+
const target = params.get("target") || "";
|
|
148
|
+
const ref = target || params.get("ref") || "worktree";
|
|
149
|
+
if (!path)
|
|
150
|
+
return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
|
|
151
|
+
return { screen: "file", path, ref, range, view: target ? "blob" : "detail" };
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function buildRoute(route) {
|
|
158
|
+
switch (route.screen) {
|
|
159
|
+
case "repo": {
|
|
160
|
+
const params = new URLSearchParams;
|
|
161
|
+
if (route.ref && route.ref !== "worktree")
|
|
162
|
+
params.set("ref", route.ref);
|
|
163
|
+
if (route.path)
|
|
164
|
+
params.set("path", route.path);
|
|
165
|
+
const qs = params.toString();
|
|
166
|
+
return "/" + (qs ? "?" + qs : "");
|
|
167
|
+
}
|
|
168
|
+
case "file":
|
|
169
|
+
if (route.view === "blob") {
|
|
170
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree");
|
|
171
|
+
}
|
|
172
|
+
return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
173
|
+
case "diff":
|
|
174
|
+
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
175
|
+
case "unknown":
|
|
176
|
+
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
177
|
+
default:
|
|
178
|
+
return assertNever(route);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function buildRawFileUrl(target) {
|
|
182
|
+
return "/_file?path=" + encodeURIComponent(target.path) + "&ref=" + encodeURIComponent(target.ref || "worktree");
|
|
183
|
+
}
|
|
184
|
+
|
|
47
185
|
// web-src/ws-highlight.ts
|
|
48
186
|
function isWhitespaceOnlyInlineHighlight(text) {
|
|
49
187
|
return !!text && !/\S/.test(text);
|
|
@@ -62,14 +200,35 @@
|
|
|
62
200
|
// web-src/app.ts
|
|
63
201
|
window.GdpExpandLogic = GdpExpandLogic;
|
|
64
202
|
(() => {
|
|
203
|
+
const FOLDER_ICON_PATHS = {
|
|
204
|
+
closed: "M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z",
|
|
205
|
+
open: "M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"
|
|
206
|
+
};
|
|
207
|
+
const CHEVRON_DOWN_12_PATH = "M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1.3-.3.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0 .3.3.3.8 0 1.1l-3.2 3.2c-.2.2-.4.3-.6.3Z";
|
|
208
|
+
const CHEVRON_DOWN_16_PATH = "M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z";
|
|
209
|
+
const COPY_16_PATHS = [
|
|
210
|
+
"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z",
|
|
211
|
+
"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
|
|
212
|
+
];
|
|
213
|
+
const FILE_16_PATH = "M2 1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 12.25 16h-8.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 8 4.25V1.5Zm5.75.062V4.25c0 .138.112.25.25.25h2.688Z";
|
|
214
|
+
const UNFOLD_16_PATH = "m8.177.677 2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25a.75.75 0 0 1-1.5 0V4H5.104a.25.25 0 0 1-.177-.427L7.823.677a.25.25 0 0 1 .354 0ZM7.25 10.75a.75.75 0 0 1 1.5 0V12h2.146a.25.25 0 0 1 .177.427l-2.896 2.896a.25.25 0 0 1-.354 0l-2.896-2.896A.25.25 0 0 1 5.104 12H7.25v-1.25Zm-5-2a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z";
|
|
215
|
+
const FOLD_16_PATH = "M10.896 2H8.75V.75a.75.75 0 0 0-1.5 0V2H5.104a.25.25 0 0 0-.177.427l2.896 2.896a.25.25 0 0 0 .354 0l2.896-2.896A.25.25 0 0 0 10.896 2ZM8.75 15.25a.75.75 0 0 1-1.5 0V14H5.104a.25.25 0 0 1-.177-.427l2.896-2.896a.25.25 0 0 1 .354 0l2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25Zm-6.5-6.5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z";
|
|
65
216
|
const $ = (sel) => document.querySelector(sel);
|
|
66
217
|
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
67
218
|
const diffCardSelector = (path) => '.gdp-file-shell[data-path="' + (window.CSS && CSS.escape ? CSS.escape(path) : path) + '"]';
|
|
68
219
|
const HIGHLIGHT_SRC = "/vendor/highlight.js/highlight.min.js";
|
|
220
|
+
const DEFAULT_RANGE = { from: "HEAD", to: "worktree" };
|
|
69
221
|
let highlightLoadPromise = null;
|
|
70
222
|
let highlightConfigured = false;
|
|
223
|
+
let PROJECT_NAME = "";
|
|
71
224
|
const STATE = (() => {
|
|
72
225
|
const igRaw = localStorage.getItem("gdp:ignore-ws");
|
|
226
|
+
const fallbackRange = {
|
|
227
|
+
from: localStorage.getItem("gdp:from") || DEFAULT_RANGE.from,
|
|
228
|
+
to: localStorage.getItem("gdp:to") || DEFAULT_RANGE.to
|
|
229
|
+
};
|
|
230
|
+
const parsedRoute = parseRoute(window.location.pathname, window.location.search, fallbackRange);
|
|
231
|
+
const route = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
|
|
73
232
|
return {
|
|
74
233
|
layout: localStorage.getItem("gdp:layout") || "side-by-side",
|
|
75
234
|
theme: localStorage.getItem("gdp:theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"),
|
|
@@ -77,14 +236,17 @@
|
|
|
77
236
|
sbWidth: parseInt(localStorage.getItem("gdp:sbwidth")) || 308,
|
|
78
237
|
collapsedDirs: new Set(JSON.parse(localStorage.getItem("gdp:collapsed-dirs") || "[]")),
|
|
79
238
|
ignoreWs: igRaw === null ? true : igRaw === "1",
|
|
80
|
-
from:
|
|
81
|
-
to:
|
|
239
|
+
from: route.range.from,
|
|
240
|
+
to: route.range.to,
|
|
82
241
|
collapsed: false,
|
|
83
242
|
files: [],
|
|
84
243
|
activeFile: null,
|
|
85
244
|
autoReload: localStorage.getItem("gdp:auto-reload") !== "0",
|
|
86
245
|
hideTests: localStorage.getItem("gdp:hide-tests") === "1",
|
|
87
|
-
syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0"
|
|
246
|
+
syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0",
|
|
247
|
+
viewedFiles: new Set(JSON.parse(localStorage.getItem("gdp:viewed-files") || "[]")),
|
|
248
|
+
route,
|
|
249
|
+
repoRef: route.screen === "repo" ? route.ref : "worktree"
|
|
88
250
|
};
|
|
89
251
|
})();
|
|
90
252
|
function setStatus(s) {
|
|
@@ -191,6 +353,35 @@
|
|
|
191
353
|
span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
|
|
192
354
|
return span;
|
|
193
355
|
}
|
|
356
|
+
function persistViewedFiles() {
|
|
357
|
+
localStorage.setItem("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles]));
|
|
358
|
+
}
|
|
359
|
+
function setFileViewed(path, viewed) {
|
|
360
|
+
if (viewed)
|
|
361
|
+
STATE.viewedFiles.add(path);
|
|
362
|
+
else
|
|
363
|
+
STATE.viewedFiles.delete(path);
|
|
364
|
+
persistViewedFiles();
|
|
365
|
+
applyViewedState();
|
|
366
|
+
}
|
|
367
|
+
function setFolderIcon(el, collapsed) {
|
|
368
|
+
const path = collapsed ? FOLDER_ICON_PATHS.closed : FOLDER_ICON_PATHS.open;
|
|
369
|
+
el.innerHTML = '<svg class="octicon octicon-file-directory-' + (collapsed ? "fill" : "open-fill") + '" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">' + '<path fill="currentColor" d="' + path + '"></path></svg>';
|
|
370
|
+
}
|
|
371
|
+
function setChevronIcon(el) {
|
|
372
|
+
el.innerHTML = '<svg class="octicon octicon-chevron-down" viewBox="0 0 12 12" width="12" height="12" fill="currentColor" aria-hidden="true">' + '<path fill="currentColor" d="' + CHEVRON_DOWN_12_PATH + '"></path></svg>';
|
|
373
|
+
}
|
|
374
|
+
function iconSvg(className, paths) {
|
|
375
|
+
const pathList = Array.isArray(paths) ? paths : [paths];
|
|
376
|
+
return '<svg class="octicon ' + className + '" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true">' + pathList.map((path) => '<path fill="currentColor" d="' + path + '"></path>').join("") + "</svg>";
|
|
377
|
+
}
|
|
378
|
+
function setUnfoldButtonState(button, expanded) {
|
|
379
|
+
if (!button)
|
|
380
|
+
return;
|
|
381
|
+
button.setAttribute("aria-pressed", expanded ? "true" : "false");
|
|
382
|
+
button.title = expanded ? "Collapse expanded lines" : "Expand all lines";
|
|
383
|
+
button.innerHTML = expanded ? iconSvg("octicon-fold", FOLD_16_PATH) : iconSvg("octicon-unfold", UNFOLD_16_PATH);
|
|
384
|
+
}
|
|
194
385
|
function buildTree(files) {
|
|
195
386
|
const root = { name: "", dirs: {}, files: [], path: "", minOrder: Infinity };
|
|
196
387
|
for (const f of files) {
|
|
@@ -226,7 +417,7 @@
|
|
|
226
417
|
Object.values(root.dirs).forEach(compress);
|
|
227
418
|
return root;
|
|
228
419
|
}
|
|
229
|
-
function renderTreeNode(node, depth, ul) {
|
|
420
|
+
function renderTreeNode(node, depth, ul, onFileClick) {
|
|
230
421
|
const items = [];
|
|
231
422
|
for (const k of Object.keys(node.dirs)) {
|
|
232
423
|
const d = node.dirs[k];
|
|
@@ -245,7 +436,7 @@
|
|
|
245
436
|
li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
|
|
246
437
|
const chev = document.createElement("span");
|
|
247
438
|
chev.className = "chev";
|
|
248
|
-
chev
|
|
439
|
+
setChevronIcon(chev);
|
|
249
440
|
li.appendChild(chev);
|
|
250
441
|
const dirIcon = document.createElement("span");
|
|
251
442
|
dirIcon.className = "dir-icon";
|
|
@@ -259,12 +450,12 @@
|
|
|
259
450
|
if (collapsed)
|
|
260
451
|
li.classList.add("collapsed");
|
|
261
452
|
const updateIcon = () => {
|
|
262
|
-
dirIcon
|
|
453
|
+
setFolderIcon(dirIcon, li.classList.contains("collapsed"));
|
|
263
454
|
};
|
|
264
455
|
updateIcon();
|
|
265
456
|
const childUl = document.createElement("ul");
|
|
266
457
|
childUl.className = "tree-children";
|
|
267
|
-
renderTreeNode(dir, depth + 1, childUl);
|
|
458
|
+
renderTreeNode(dir, depth + 1, childUl, onFileClick);
|
|
268
459
|
li.addEventListener("click", (e) => {
|
|
269
460
|
e.stopPropagation();
|
|
270
461
|
li.classList.toggle("collapsed");
|
|
@@ -282,45 +473,76 @@
|
|
|
282
473
|
const li = document.createElement("li");
|
|
283
474
|
li.className = "tree-file";
|
|
284
475
|
li.dataset.path = f.path;
|
|
476
|
+
li.classList.toggle("viewed", STATE.viewedFiles.has(f.path));
|
|
285
477
|
li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
|
|
286
|
-
|
|
478
|
+
const spacer = document.createElement("span");
|
|
479
|
+
spacer.className = "chev-spacer";
|
|
480
|
+
li.appendChild(spacer);
|
|
481
|
+
if (f.status) {
|
|
482
|
+
li.appendChild(fileBadge(f.status));
|
|
483
|
+
} else {
|
|
484
|
+
const icon = document.createElement("span");
|
|
485
|
+
icon.className = "d2h-icon-wrapper";
|
|
486
|
+
icon.innerHTML = fileEntryIcon();
|
|
487
|
+
li.appendChild(icon);
|
|
488
|
+
}
|
|
287
489
|
const name = document.createElement("span");
|
|
288
490
|
name.className = "name";
|
|
289
491
|
name.textContent = f.path.split("/").pop();
|
|
290
492
|
name.title = f.path;
|
|
291
493
|
li.appendChild(name);
|
|
292
|
-
li.addEventListener("click", () =>
|
|
293
|
-
|
|
494
|
+
li.addEventListener("click", () => {
|
|
495
|
+
if (onFileClick)
|
|
496
|
+
onFileClick(f);
|
|
497
|
+
else
|
|
498
|
+
scrollToFile(f.path);
|
|
499
|
+
});
|
|
500
|
+
if (!onFileClick)
|
|
501
|
+
li.addEventListener("mouseenter", () => prefetchByPath(f.path), { passive: true });
|
|
294
502
|
ul.appendChild(li);
|
|
295
503
|
}
|
|
296
504
|
}
|
|
297
505
|
}
|
|
298
|
-
function renderFlat(files, ul) {
|
|
506
|
+
function renderFlat(files, ul, onFileClick) {
|
|
299
507
|
files.forEach((f, i) => {
|
|
300
508
|
const li = document.createElement("li");
|
|
301
509
|
li.dataset.index = String(i);
|
|
302
510
|
li.dataset.path = f.path;
|
|
303
|
-
li.
|
|
511
|
+
li.classList.toggle("viewed", STATE.viewedFiles.has(f.path));
|
|
512
|
+
if (f.status) {
|
|
513
|
+
li.appendChild(fileBadge(f.status));
|
|
514
|
+
} else {
|
|
515
|
+
const icon = document.createElement("span");
|
|
516
|
+
icon.className = "d2h-icon-wrapper";
|
|
517
|
+
icon.innerHTML = fileEntryIcon();
|
|
518
|
+
li.appendChild(icon);
|
|
519
|
+
}
|
|
304
520
|
const name = document.createElement("span");
|
|
305
521
|
name.className = "name";
|
|
306
522
|
name.textContent = f.path;
|
|
307
523
|
name.title = f.path;
|
|
308
524
|
li.appendChild(name);
|
|
309
|
-
li.addEventListener("click", () =>
|
|
310
|
-
|
|
525
|
+
li.addEventListener("click", () => {
|
|
526
|
+
if (onFileClick)
|
|
527
|
+
onFileClick(f);
|
|
528
|
+
else
|
|
529
|
+
scrollToFile(f.path);
|
|
530
|
+
});
|
|
531
|
+
if (!onFileClick)
|
|
532
|
+
li.addEventListener("mouseenter", () => prefetchByPath(f.path), { passive: true });
|
|
311
533
|
ul.appendChild(li);
|
|
312
534
|
});
|
|
313
535
|
}
|
|
314
|
-
function renderSidebar(files) {
|
|
536
|
+
function renderSidebar(files, onFileClick) {
|
|
315
537
|
const ul = $("#filelist");
|
|
316
538
|
ul.innerHTML = "";
|
|
317
539
|
ul.classList.toggle("tree", STATE.sbView === "tree");
|
|
318
540
|
STATE.files = files;
|
|
319
541
|
if (STATE.sbView === "tree") {
|
|
320
542
|
const root = buildTree(files);
|
|
321
|
-
renderTreeNode(root, 0, ul);
|
|
543
|
+
renderTreeNode(root, 0, ul, onFileClick);
|
|
322
544
|
} else {
|
|
323
|
-
renderFlat(files, ul);
|
|
545
|
+
renderFlat(files, ul, onFileClick);
|
|
324
546
|
}
|
|
325
547
|
$("#totals").textContent = files.length ? files.length + " file" + (files.length === 1 ? "" : "s") : "";
|
|
326
548
|
$$(".sb-view-seg button").forEach((b) => {
|
|
@@ -330,20 +552,23 @@
|
|
|
330
552
|
markActive(STATE.activeFile);
|
|
331
553
|
applyFilter();
|
|
332
554
|
}
|
|
555
|
+
function syncRepoTargetInput(ref) {
|
|
556
|
+
const input = document.querySelector("#repo-target");
|
|
557
|
+
const wrap = document.querySelector("#repo-target-wrap");
|
|
558
|
+
if (!input || !wrap)
|
|
559
|
+
return;
|
|
560
|
+
input.value = ref || "worktree";
|
|
561
|
+
wrap.hidden = !(STATE.route.screen === "file" && STATE.route.view === "blob");
|
|
562
|
+
}
|
|
333
563
|
function renderMeta(meta) {
|
|
334
564
|
const el = $("#meta");
|
|
335
565
|
if (!meta) {
|
|
336
566
|
el.textContent = "";
|
|
337
567
|
return;
|
|
338
568
|
}
|
|
569
|
+
PROJECT_NAME = meta.project || PROJECT_NAME;
|
|
339
570
|
document.title = (meta.project ? meta.project + " - " : "") + "git diff preview";
|
|
340
571
|
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
572
|
if (meta.branch) {
|
|
348
573
|
const b = document.createElement("span");
|
|
349
574
|
b.className = "ref";
|
|
@@ -397,19 +622,46 @@
|
|
|
397
622
|
li.classList.toggle("active", li.dataset.path === path);
|
|
398
623
|
});
|
|
399
624
|
}
|
|
625
|
+
function applyViewedState() {
|
|
626
|
+
$$("#filelist li[data-path]").forEach((li) => {
|
|
627
|
+
const path = li.dataset.path || "";
|
|
628
|
+
li.classList.toggle("viewed", STATE.viewedFiles.has(path));
|
|
629
|
+
});
|
|
630
|
+
$$(".gdp-file-shell[data-path]").forEach((card) => {
|
|
631
|
+
const path = card.dataset.path || "";
|
|
632
|
+
const viewed = STATE.viewedFiles.has(path);
|
|
633
|
+
card.classList.toggle("viewed", viewed);
|
|
634
|
+
card.querySelectorAll(".d2h-file-collapse-input").forEach((checkbox) => {
|
|
635
|
+
checkbox.checked = viewed;
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
400
639
|
function applyFilter() {
|
|
401
|
-
const
|
|
640
|
+
const input = $("#sb-filter");
|
|
641
|
+
const filter = compileFileFilter(input.value);
|
|
642
|
+
const invalid = filter.kind === "invalid";
|
|
643
|
+
input.toggleAttribute("aria-invalid", invalid);
|
|
644
|
+
input.title = invalid ? filter.error || "invalid regular expression" : "";
|
|
645
|
+
const matches = invalid ? () => true : filter.match;
|
|
402
646
|
$$("#filelist li[data-path]").forEach((li) => {
|
|
403
|
-
const match =
|
|
647
|
+
const match = matches(li.dataset.path || "");
|
|
404
648
|
li.classList.toggle("hidden", !match);
|
|
405
649
|
});
|
|
650
|
+
document.querySelectorAll(".gdp-file-shell").forEach((card) => {
|
|
651
|
+
const match = matches(card.dataset.path || "");
|
|
652
|
+
card.classList.toggle("hidden-by-filter", !match);
|
|
653
|
+
});
|
|
654
|
+
updateTreeDirVisibility();
|
|
655
|
+
if (typeof applyViewedState === "function")
|
|
656
|
+
applyViewedState();
|
|
657
|
+
}
|
|
658
|
+
function updateTreeDirVisibility() {
|
|
406
659
|
$$("#filelist .tree-dir").forEach((dir) => {
|
|
407
660
|
const childUl = dir.nextElementSibling;
|
|
408
661
|
if (!childUl || !childUl.classList.contains("tree-children"))
|
|
409
662
|
return;
|
|
410
|
-
const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden)");
|
|
411
|
-
|
|
412
|
-
dir.classList.toggle("hidden", !(fullVisible || anyVisible));
|
|
663
|
+
const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden):not(.hidden-by-tests)");
|
|
664
|
+
dir.classList.toggle("hidden", !anyVisible);
|
|
413
665
|
});
|
|
414
666
|
}
|
|
415
667
|
let SERVER_GENERATION = 0;
|
|
@@ -418,6 +670,7 @@
|
|
|
418
670
|
let ACTIVE_LOADS = 0;
|
|
419
671
|
const MAX_PARALLEL = 2;
|
|
420
672
|
let lazyObserver = null;
|
|
673
|
+
let SOURCE_REQ_SEQ = 0;
|
|
421
674
|
let IN_FLIGHT = 0;
|
|
422
675
|
function updateLoadBar() {
|
|
423
676
|
const el = $("#load-bar");
|
|
@@ -442,6 +695,65 @@
|
|
|
442
695
|
function escapeHtml(s) {
|
|
443
696
|
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
444
697
|
}
|
|
698
|
+
function sourceTargetsEqual(a, b) {
|
|
699
|
+
return !!a && !!b && a.path === b.path && a.ref === b.ref;
|
|
700
|
+
}
|
|
701
|
+
function fileSourceTarget(file) {
|
|
702
|
+
if ((file.status || "").startsWith("D")) {
|
|
703
|
+
return { path: file.old_path || file.path, ref: STATE.from || "HEAD" };
|
|
704
|
+
}
|
|
705
|
+
const ref = STATE.to && STATE.to !== "worktree" ? STATE.to : "worktree";
|
|
706
|
+
return { path: file.path, ref };
|
|
707
|
+
}
|
|
708
|
+
function currentRange() {
|
|
709
|
+
return { from: STATE.from || DEFAULT_RANGE.from, to: STATE.to || DEFAULT_RANGE.to };
|
|
710
|
+
}
|
|
711
|
+
function sourceTargetFromRoute() {
|
|
712
|
+
return STATE.route.screen === "file" ? { path: STATE.route.path, ref: STATE.route.ref } : null;
|
|
713
|
+
}
|
|
714
|
+
function repoFileTargetFromRoute() {
|
|
715
|
+
return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
|
|
716
|
+
}
|
|
717
|
+
function setRoute(route, replace = false) {
|
|
718
|
+
const nextRoute = route.screen === "unknown" ? { screen: "diff", range: route.range } : route;
|
|
719
|
+
STATE.route = nextRoute;
|
|
720
|
+
STATE.from = nextRoute.range.from;
|
|
721
|
+
STATE.to = nextRoute.range.to;
|
|
722
|
+
if (nextRoute.screen === "repo" || nextRoute.screen === "file" && nextRoute.view === "blob") {
|
|
723
|
+
STATE.repoRef = nextRoute.ref || "worktree";
|
|
724
|
+
}
|
|
725
|
+
const url = buildRoute(nextRoute);
|
|
726
|
+
const state = nextRoute.screen === "file" ? { screen: "file", path: nextRoute.path, ref: nextRoute.ref, view: nextRoute.view || "detail" } : { view: nextRoute.screen };
|
|
727
|
+
if (replace)
|
|
728
|
+
history.replaceState(state, "", url);
|
|
729
|
+
else
|
|
730
|
+
history.pushState(state, "", url);
|
|
731
|
+
syncHeaderMenu();
|
|
732
|
+
}
|
|
733
|
+
function setPageMode() {
|
|
734
|
+
document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
|
|
735
|
+
document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
|
|
736
|
+
document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
|
|
737
|
+
syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
|
|
738
|
+
}
|
|
739
|
+
function syncHeaderMenu() {
|
|
740
|
+
document.querySelectorAll(".app-menu-item").forEach((link) => {
|
|
741
|
+
const fileRouteOwner = STATE.route.screen === "file" && STATE.route.view === "blob" ? "repo" : "diff";
|
|
742
|
+
const active = link.dataset.route === STATE.route.screen || STATE.route.screen === "file" && link.dataset.route === fileRouteOwner;
|
|
743
|
+
link.classList.toggle("active", active);
|
|
744
|
+
link.setAttribute("aria-current", active ? "page" : "false");
|
|
745
|
+
if (link.dataset.route === "repo") {
|
|
746
|
+
link.href = buildRoute({ screen: "repo", ref: STATE.repoRef || "worktree", path: "", range: currentRange() });
|
|
747
|
+
}
|
|
748
|
+
if (link.dataset.route === "diff") {
|
|
749
|
+
link.href = buildRoute({ screen: "diff", range: currentRange() });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function removeStandaloneSource() {
|
|
754
|
+
document.querySelectorAll(".gdp-standalone-source").forEach((el) => el.remove());
|
|
755
|
+
document.querySelectorAll(".gdp-repo-blob-layout").forEach((el) => el.remove());
|
|
756
|
+
}
|
|
445
757
|
function renderShell(meta) {
|
|
446
758
|
const newFiles = meta.files || [];
|
|
447
759
|
STATE.files = newFiles;
|
|
@@ -506,9 +818,229 @@
|
|
|
506
818
|
}
|
|
507
819
|
setupLazyObserver();
|
|
508
820
|
enqueueInitialLoads();
|
|
821
|
+
applySourceRouteToShell();
|
|
509
822
|
setupScrollSpy();
|
|
510
823
|
if (typeof applyHideTests === "function")
|
|
511
824
|
applyHideTests();
|
|
825
|
+
applyFilter();
|
|
826
|
+
applyViewedState();
|
|
827
|
+
}
|
|
828
|
+
function fileEntryIcon() {
|
|
829
|
+
return iconSvg("octicon-file", FILE_16_PATH);
|
|
830
|
+
}
|
|
831
|
+
function repoRoute(ref, path) {
|
|
832
|
+
return { screen: "repo", ref: ref || "worktree", path, range: currentRange() };
|
|
833
|
+
}
|
|
834
|
+
function wireRepoTargetPicker(input, onPick) {
|
|
835
|
+
input.addEventListener("focus", () => openPopover(input));
|
|
836
|
+
input.addEventListener("click", (e) => {
|
|
837
|
+
e.stopPropagation();
|
|
838
|
+
openPopover(input);
|
|
839
|
+
});
|
|
840
|
+
input.addEventListener("mousedown", (e) => {
|
|
841
|
+
if (popover.hidden) {
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
input.focus();
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
input.addEventListener("keydown", (e) => {
|
|
847
|
+
if (e.key === "Enter") {
|
|
848
|
+
e.preventDefault();
|
|
849
|
+
closePopover();
|
|
850
|
+
} else if (e.key === "Escape") {
|
|
851
|
+
closePopover();
|
|
852
|
+
input.blur();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
input.addEventListener("change", () => onPick(input.value || "worktree"));
|
|
856
|
+
}
|
|
857
|
+
function createRepoBreadcrumb(target, path) {
|
|
858
|
+
const nav = document.createElement("nav");
|
|
859
|
+
nav.className = "gdp-file-breadcrumb gdp-repo-breadcrumb";
|
|
860
|
+
const root = document.createElement("button");
|
|
861
|
+
root.type = "button";
|
|
862
|
+
root.className = path ? "gdp-file-breadcrumb-part" : "gdp-file-breadcrumb-current";
|
|
863
|
+
root.textContent = PROJECT_NAME || "repository";
|
|
864
|
+
root.addEventListener("click", () => {
|
|
865
|
+
setRoute(repoRoute(target, ""));
|
|
866
|
+
loadRepo();
|
|
867
|
+
});
|
|
868
|
+
nav.appendChild(root);
|
|
869
|
+
const parts = path ? path.split("/") : [];
|
|
870
|
+
parts.forEach((part, index) => {
|
|
871
|
+
const sep = document.createElement("span");
|
|
872
|
+
sep.className = "gdp-file-breadcrumb-sep";
|
|
873
|
+
sep.textContent = "/";
|
|
874
|
+
nav.appendChild(sep);
|
|
875
|
+
const currentPath = parts.slice(0, index + 1).join("/");
|
|
876
|
+
const button = document.createElement("button");
|
|
877
|
+
button.type = "button";
|
|
878
|
+
button.className = index === parts.length - 1 ? "gdp-file-breadcrumb-current" : "gdp-file-breadcrumb-part";
|
|
879
|
+
button.textContent = part;
|
|
880
|
+
button.disabled = index === parts.length - 1;
|
|
881
|
+
button.addEventListener("click", () => {
|
|
882
|
+
setRoute(repoRoute(target, currentPath));
|
|
883
|
+
loadRepo();
|
|
884
|
+
});
|
|
885
|
+
nav.appendChild(button);
|
|
886
|
+
});
|
|
887
|
+
return nav;
|
|
888
|
+
}
|
|
889
|
+
function renderRepo(meta) {
|
|
890
|
+
PROJECT_NAME = meta.project || PROJECT_NAME;
|
|
891
|
+
setPageMode();
|
|
892
|
+
removeStandaloneSource();
|
|
893
|
+
$("#empty").classList.add("hidden");
|
|
894
|
+
$("#diff").replaceChildren();
|
|
895
|
+
$("#filelist").replaceChildren();
|
|
896
|
+
$("#totals").textContent = "";
|
|
897
|
+
STATE.files = [];
|
|
898
|
+
LOAD_QUEUE.length = 0;
|
|
899
|
+
const target = $("#diff");
|
|
900
|
+
const shell = document.createElement("section");
|
|
901
|
+
shell.className = "gdp-repo-shell";
|
|
902
|
+
const targetPicker = document.createElement("input");
|
|
903
|
+
targetPicker.className = "ref-input gdp-repo-target";
|
|
904
|
+
targetPicker.id = "repo-ref";
|
|
905
|
+
targetPicker.readOnly = true;
|
|
906
|
+
targetPicker.autocomplete = "off";
|
|
907
|
+
targetPicker.value = meta.ref || "worktree";
|
|
908
|
+
targetPicker.placeholder = "ref...";
|
|
909
|
+
targetPicker.title = "repository ref";
|
|
910
|
+
wireRepoTargetPicker(targetPicker, (ref) => {
|
|
911
|
+
setRoute(repoRoute(ref, ""));
|
|
912
|
+
loadRepo();
|
|
913
|
+
});
|
|
914
|
+
const toolbar = document.createElement("div");
|
|
915
|
+
toolbar.className = "gdp-file-detail-header gdp-repo-toolbar";
|
|
916
|
+
toolbar.append(createRepoBreadcrumb(meta.ref, meta.path || ""), targetPicker);
|
|
917
|
+
shell.appendChild(toolbar);
|
|
918
|
+
const listCard = document.createElement("section");
|
|
919
|
+
listCard.className = "gdp-file-shell loaded gdp-repo-list-shell";
|
|
920
|
+
const listWrapper = document.createElement("div");
|
|
921
|
+
listWrapper.className = "d2h-file-wrapper";
|
|
922
|
+
const listHeader = document.createElement("div");
|
|
923
|
+
listHeader.className = "d2h-file-header";
|
|
924
|
+
const listName = document.createElement("div");
|
|
925
|
+
listName.className = "d2h-file-name-wrapper";
|
|
926
|
+
const listIcon = document.createElement("span");
|
|
927
|
+
listIcon.className = "dir-icon";
|
|
928
|
+
setFolderIcon(listIcon, false);
|
|
929
|
+
const listTitle = document.createElement("span");
|
|
930
|
+
listTitle.className = "d2h-file-name";
|
|
931
|
+
listTitle.textContent = meta.path || meta.project || "Files";
|
|
932
|
+
listName.append(listIcon, listTitle);
|
|
933
|
+
listHeader.appendChild(listName);
|
|
934
|
+
listWrapper.appendChild(listHeader);
|
|
935
|
+
const list = document.createElement("div");
|
|
936
|
+
list.className = "gdp-source-viewer gdp-repo-file-list";
|
|
937
|
+
if (meta.path) {
|
|
938
|
+
const parent = meta.path.split("/").slice(0, -1).join("/");
|
|
939
|
+
const row = document.createElement("button");
|
|
940
|
+
row.type = "button";
|
|
941
|
+
row.className = "gdp-repo-row parent";
|
|
942
|
+
const parentIcon = document.createElement("span");
|
|
943
|
+
parentIcon.className = "dir-icon";
|
|
944
|
+
setFolderIcon(parentIcon, false);
|
|
945
|
+
const parentName = document.createElement("span");
|
|
946
|
+
parentName.className = "name";
|
|
947
|
+
parentName.textContent = "..";
|
|
948
|
+
const parentKind = document.createElement("span");
|
|
949
|
+
parentKind.className = "kind";
|
|
950
|
+
parentKind.textContent = "parent";
|
|
951
|
+
row.append(parentIcon, parentName, parentKind);
|
|
952
|
+
row.addEventListener("click", () => {
|
|
953
|
+
setRoute(repoRoute(meta.ref, parent));
|
|
954
|
+
loadRepo();
|
|
955
|
+
});
|
|
956
|
+
list.appendChild(row);
|
|
957
|
+
}
|
|
958
|
+
meta.entries.forEach((entry) => {
|
|
959
|
+
const row = document.createElement("button");
|
|
960
|
+
row.type = "button";
|
|
961
|
+
row.className = "gdp-repo-row " + entry.type;
|
|
962
|
+
const icon = document.createElement("span");
|
|
963
|
+
icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
|
|
964
|
+
if (entry.type === "tree")
|
|
965
|
+
setFolderIcon(icon, true);
|
|
966
|
+
else
|
|
967
|
+
icon.innerHTML = fileEntryIcon();
|
|
968
|
+
const name = document.createElement("span");
|
|
969
|
+
name.className = "name";
|
|
970
|
+
name.textContent = entry.name;
|
|
971
|
+
const kind = document.createElement("span");
|
|
972
|
+
kind.className = "kind";
|
|
973
|
+
kind.textContent = entry.type === "tree" ? "directory" : entry.type === "commit" ? "submodule" : "file";
|
|
974
|
+
row.append(icon, name, kind);
|
|
975
|
+
row.addEventListener("click", () => {
|
|
976
|
+
if (entry.type === "tree") {
|
|
977
|
+
setRoute(repoRoute(meta.ref, entry.path));
|
|
978
|
+
loadRepo();
|
|
979
|
+
} else if (entry.type === "blob") {
|
|
980
|
+
setRoute({ screen: "file", path: entry.path, ref: meta.ref, view: "blob", range: currentRange() });
|
|
981
|
+
renderStandaloneSource({ path: entry.path, ref: meta.ref });
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
list.appendChild(row);
|
|
985
|
+
});
|
|
986
|
+
if (!meta.entries.length) {
|
|
987
|
+
const empty = document.createElement("div");
|
|
988
|
+
empty.className = "gdp-repo-empty";
|
|
989
|
+
empty.textContent = "No files in this directory.";
|
|
990
|
+
list.appendChild(empty);
|
|
991
|
+
}
|
|
992
|
+
listWrapper.appendChild(list);
|
|
993
|
+
listCard.appendChild(listWrapper);
|
|
994
|
+
shell.appendChild(listCard);
|
|
995
|
+
if (meta.readme && meta.readme.text) {
|
|
996
|
+
const readme = document.createElement("section");
|
|
997
|
+
readme.className = "gdp-file-shell loaded gdp-repo-readme";
|
|
998
|
+
const wrapper = document.createElement("div");
|
|
999
|
+
wrapper.className = "d2h-file-wrapper";
|
|
1000
|
+
const readmeHeader = document.createElement("div");
|
|
1001
|
+
readmeHeader.className = "d2h-file-header";
|
|
1002
|
+
const nameWrapper = document.createElement("div");
|
|
1003
|
+
nameWrapper.className = "d2h-file-name-wrapper";
|
|
1004
|
+
const icon = document.createElement("span");
|
|
1005
|
+
icon.className = "d2h-icon-wrapper";
|
|
1006
|
+
icon.innerHTML = iconSvg("octicon-file", FILE_16_PATH);
|
|
1007
|
+
const name = document.createElement("span");
|
|
1008
|
+
name.className = "d2h-file-name";
|
|
1009
|
+
name.textContent = meta.readme.path;
|
|
1010
|
+
nameWrapper.append(icon, name);
|
|
1011
|
+
readmeHeader.appendChild(nameWrapper);
|
|
1012
|
+
wrapper.appendChild(readmeHeader);
|
|
1013
|
+
wrapper.appendChild(renderMarkdownPreview(meta.readme.text, { path: meta.readme.path, ref: meta.ref }, getHljs()));
|
|
1014
|
+
readme.appendChild(wrapper);
|
|
1015
|
+
shell.appendChild(readme);
|
|
1016
|
+
}
|
|
1017
|
+
target.appendChild(shell);
|
|
1018
|
+
}
|
|
1019
|
+
function renderRepoBlobSidebar(currentPath, ref) {
|
|
1020
|
+
syncRepoTargetInput(ref);
|
|
1021
|
+
const params = new URLSearchParams;
|
|
1022
|
+
params.set("ref", ref || "worktree");
|
|
1023
|
+
params.set("recursive", "1");
|
|
1024
|
+
return trackLoad(fetch("/_tree?" + params.toString()).then((r) => {
|
|
1025
|
+
if (!r.ok)
|
|
1026
|
+
throw new Error("failed to load repository tree");
|
|
1027
|
+
return r.json();
|
|
1028
|
+
})).then((meta) => {
|
|
1029
|
+
const files = meta.entries.filter((entry) => entry.type !== "tree").map((entry, index) => ({
|
|
1030
|
+
order: index + 1,
|
|
1031
|
+
path: entry.path,
|
|
1032
|
+
display_path: entry.path
|
|
1033
|
+
}));
|
|
1034
|
+
renderSidebar(files, (file) => {
|
|
1035
|
+
setRoute({ screen: "file", path: file.path, ref, view: "blob", range: currentRange() });
|
|
1036
|
+
renderStandaloneSource({ path: file.path, ref });
|
|
1037
|
+
});
|
|
1038
|
+
markActive(currentPath);
|
|
1039
|
+
applyFilter();
|
|
1040
|
+
}).catch(() => {
|
|
1041
|
+
renderSidebar([], undefined);
|
|
1042
|
+
$("#totals").textContent = "Cannot load tree";
|
|
1043
|
+
});
|
|
512
1044
|
}
|
|
513
1045
|
function createPlaceholder(f) {
|
|
514
1046
|
const card = document.createElement("div");
|
|
@@ -517,6 +1049,7 @@
|
|
|
517
1049
|
card.dataset.key = f.key || f.path;
|
|
518
1050
|
card.dataset.sizeClass = f.size_class || "small";
|
|
519
1051
|
card.dataset.status = f.status || "M";
|
|
1052
|
+
card.classList.toggle("viewed", STATE.viewedFiles.has(f.path));
|
|
520
1053
|
if (f.estimated_height_px) {
|
|
521
1054
|
card.style.minHeight = f.estimated_height_px + "px";
|
|
522
1055
|
}
|
|
@@ -733,7 +1266,8 @@
|
|
|
733
1266
|
outputFormat: layout,
|
|
734
1267
|
synchronisedScroll: true,
|
|
735
1268
|
highlight: !!(STATE.syntaxHighlight && file.highlight && hljsRef),
|
|
736
|
-
fileListToggle: false
|
|
1269
|
+
fileListToggle: false,
|
|
1270
|
+
fileContentToggle: false
|
|
737
1271
|
}, hljsRef);
|
|
738
1272
|
ui.draw();
|
|
739
1273
|
if (STATE.ignoreWs)
|
|
@@ -889,8 +1423,8 @@
|
|
|
889
1423
|
else
|
|
890
1424
|
item.bottomExpandedEnd = end;
|
|
891
1425
|
for (const sib of item.siblings || [{ tr: item.tr }]) {
|
|
892
|
-
const
|
|
893
|
-
const old =
|
|
1426
|
+
const ln = sib.tr.querySelector(".d2h-code-linenumber.d2h-info, .d2h-code-side-linenumber.d2h-info");
|
|
1427
|
+
const old = ln && ln.querySelector(".gdp-expand-stack");
|
|
894
1428
|
if (old)
|
|
895
1429
|
old.remove();
|
|
896
1430
|
}
|
|
@@ -903,46 +1437,131 @@
|
|
|
903
1437
|
const remainingSize = remainingEnd - remainingStart + 1;
|
|
904
1438
|
const isFirst = prevHunkEndNew === 0;
|
|
905
1439
|
const buildStack = () => {
|
|
906
|
-
const
|
|
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";
|
|
1440
|
+
const buttons = [];
|
|
923
1441
|
if (isFirst) {
|
|
924
|
-
|
|
1442
|
+
buttons.push({
|
|
1443
|
+
direction: "up",
|
|
1444
|
+
title: "Show " + Math.min(STEP, remainingSize) + " more lines",
|
|
1445
|
+
onClick: () => fetchAndInsert(Math.max(remainingStart, remainingEnd - STEP + 1), remainingEnd, "after")
|
|
1446
|
+
});
|
|
925
1447
|
} else {
|
|
926
|
-
|
|
927
|
-
|
|
1448
|
+
buttons.push({
|
|
1449
|
+
direction: "up",
|
|
1450
|
+
title: "Show " + Math.min(STEP, remainingSize) + " more lines",
|
|
1451
|
+
onClick: () => fetchAndInsert(remainingStart, Math.min(remainingEnd, remainingStart + STEP - 1), "before")
|
|
1452
|
+
});
|
|
1453
|
+
buttons.push({
|
|
1454
|
+
direction: "down",
|
|
1455
|
+
title: "Show " + Math.min(STEP, remainingSize) + " more lines",
|
|
1456
|
+
onClick: () => fetchAndInsert(Math.max(remainingStart, remainingEnd - STEP + 1), remainingEnd, "after")
|
|
1457
|
+
});
|
|
928
1458
|
}
|
|
929
|
-
return
|
|
1459
|
+
return createExpandStack(buttons);
|
|
930
1460
|
};
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
ln.
|
|
1461
|
+
const siblings = item.siblings || [{ tr: item.tr }];
|
|
1462
|
+
siblings.forEach((sib) => {
|
|
1463
|
+
const ln = sib.tr.querySelector(".d2h-code-linenumber.d2h-info, .d2h-code-side-linenumber.d2h-info");
|
|
1464
|
+
if (ln && !ln.querySelector(".gdp-expand-stack")) {
|
|
1465
|
+
ln.appendChild(buildStack());
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
const firstSib = siblings[0];
|
|
1469
|
+
if (firstSib) {
|
|
1470
|
+
syncExpandRowHeights(siblings.map((sib) => sib.tr), firstSib.tr);
|
|
935
1471
|
}
|
|
1472
|
+
}
|
|
1473
|
+
const EXPAND_ICON_PATHS = {
|
|
1474
|
+
up: "M8 3.5 3.75 7.75l1.06 1.06L7.25 6.37V13h1.5V6.37l2.44 2.44 1.06-1.06L8 3.5z",
|
|
1475
|
+
down: "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"
|
|
1476
|
+
};
|
|
1477
|
+
function createExpandStack(buttons) {
|
|
1478
|
+
const stack = document.createElement("div");
|
|
1479
|
+
stack.className = "gdp-expand-stack";
|
|
1480
|
+
buttons.forEach((spec) => {
|
|
1481
|
+
const button = document.createElement("button");
|
|
1482
|
+
button.className = "gdp-expand-btn";
|
|
1483
|
+
button.title = spec.title;
|
|
1484
|
+
button.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">' + '<path fill="currentColor" d="' + EXPAND_ICON_PATHS[spec.direction] + '"/></svg>';
|
|
1485
|
+
button.addEventListener("click", (e) => {
|
|
1486
|
+
e.stopPropagation();
|
|
1487
|
+
if (button.disabled)
|
|
1488
|
+
return;
|
|
1489
|
+
spec.onClick();
|
|
1490
|
+
});
|
|
1491
|
+
stack.appendChild(button);
|
|
1492
|
+
});
|
|
1493
|
+
return stack;
|
|
1494
|
+
}
|
|
1495
|
+
function syncExpandRowHeights(rows, stackRow) {
|
|
936
1496
|
const syncHeight = () => {
|
|
937
|
-
const stack =
|
|
1497
|
+
const stack = stackRow.querySelector(".gdp-expand-stack");
|
|
938
1498
|
const targetH = stack ? Math.max(20, stack.getBoundingClientRect().height) : 20;
|
|
939
|
-
|
|
940
|
-
sib.tr.style.setProperty("height", targetH + "px", "important");
|
|
941
|
-
}
|
|
1499
|
+
rows.forEach((row) => row.style.setProperty("height", targetH + "px", "important"));
|
|
942
1500
|
};
|
|
943
1501
|
requestAnimationFrame(syncHeight);
|
|
944
1502
|
setTimeout(syncHeight, 100);
|
|
945
1503
|
}
|
|
1504
|
+
function attachTrailingExpandControls(item, file, ref, refPath) {
|
|
1505
|
+
const STEP = 20;
|
|
1506
|
+
let nextNewStart = nextNewLine(item.hunk);
|
|
1507
|
+
let nextOldStart = nextOldLine(item.hunk);
|
|
1508
|
+
const rows = (item.siblings || [{ tr: item.tr, sideIndex: 0 }]).map((sib) => {
|
|
1509
|
+
const tbody = sib.tr.parentElement;
|
|
1510
|
+
if (!tbody)
|
|
1511
|
+
return null;
|
|
1512
|
+
const isSplit = !!sib.tr.querySelector("td.d2h-code-side-linenumber");
|
|
1513
|
+
const tr = document.createElement("tr");
|
|
1514
|
+
tr.className = "gdp-hunk-row gdp-trailing-expand-row";
|
|
1515
|
+
const ln = document.createElement("td");
|
|
1516
|
+
ln.className = isSplit ? "d2h-code-side-linenumber d2h-info" : "d2h-code-linenumber d2h-info";
|
|
1517
|
+
const info = document.createElement("td");
|
|
1518
|
+
info.className = "d2h-info";
|
|
1519
|
+
const spacer = document.createElement("div");
|
|
1520
|
+
spacer.className = isSplit ? "d2h-code-side-line" : "d2h-code-line";
|
|
1521
|
+
info.appendChild(spacer);
|
|
1522
|
+
tr.appendChild(ln);
|
|
1523
|
+
tr.appendChild(info);
|
|
1524
|
+
tbody.appendChild(tr);
|
|
1525
|
+
return { tr, ln, sideIndex: sib.sideIndex || 0 };
|
|
1526
|
+
}).filter(Boolean);
|
|
1527
|
+
if (!rows.length)
|
|
1528
|
+
return;
|
|
1529
|
+
const setBusy = (busy) => {
|
|
1530
|
+
rows.forEach((row) => row.ln.querySelectorAll(".gdp-expand-btn").forEach((btn) => {
|
|
1531
|
+
btn.disabled = busy;
|
|
1532
|
+
}));
|
|
1533
|
+
};
|
|
1534
|
+
const fetchAndInsert = () => {
|
|
1535
|
+
const range = window.GdpExpandLogic.trailingClickRange(nextNewStart, STEP);
|
|
1536
|
+
setBusy(true);
|
|
1537
|
+
const url = "/file_range?path=" + refPath + "&ref=" + encodeURIComponent(ref) + "&start=" + range.start + "&end=" + range.end;
|
|
1538
|
+
trackLoad(fetch(url).then((r) => r.json())).then((data) => {
|
|
1539
|
+
const lines = data && data.lines || [];
|
|
1540
|
+
if (!lines.length) {
|
|
1541
|
+
rows.forEach((row) => row.tr.remove());
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const card = item.tr.closest(".d2h-file-wrapper");
|
|
1545
|
+
rows.forEach((row) => insertContextRows(row.tr, lines, range.start, nextOldStart, "before", row.sideIndex));
|
|
1546
|
+
const next = window.GdpExpandLogic.applyTrailingResult({ newStart: nextNewStart, oldStart: nextOldStart }, lines.length, STEP);
|
|
1547
|
+
nextNewStart = next.newStart;
|
|
1548
|
+
nextOldStart = next.oldStart;
|
|
1549
|
+
if (card)
|
|
1550
|
+
highlightInsertedSpans(card, file);
|
|
1551
|
+
if (next.eof) {
|
|
1552
|
+
rows.forEach((row) => row.tr.remove());
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
setBusy(false);
|
|
1556
|
+
}).catch(() => {
|
|
1557
|
+
setBusy(false);
|
|
1558
|
+
});
|
|
1559
|
+
};
|
|
1560
|
+
rows.forEach((row) => {
|
|
1561
|
+
row.ln.appendChild(createExpandStack([{ direction: "down", title: "Show more lines", onClick: fetchAndInsert }]));
|
|
1562
|
+
});
|
|
1563
|
+
syncExpandRowHeights(rows.map((row) => row.tr), rows[0].tr);
|
|
1564
|
+
}
|
|
946
1565
|
function insertContextRows(targetTr, lines, newStart, oldStart, dir, sideIndex) {
|
|
947
1566
|
const tbody = targetTr.parentElement;
|
|
948
1567
|
if (!tbody)
|
|
@@ -970,10 +1589,556 @@
|
|
|
970
1589
|
function escapeHtmlText(s) {
|
|
971
1590
|
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
972
1591
|
}
|
|
1592
|
+
function setFileCollapsed(card, collapsed) {
|
|
1593
|
+
card.classList.toggle("gdp-file-collapsed", collapsed);
|
|
1594
|
+
card.querySelectorAll(".d2h-files-diff, .d2h-file-diff, .gdp-source-viewer, .gdp-media").forEach((body) => {
|
|
1595
|
+
body.classList.toggle("d2h-d-none", collapsed);
|
|
1596
|
+
});
|
|
1597
|
+
const button = card.querySelector(".gdp-file-toggle");
|
|
1598
|
+
if (button) {
|
|
1599
|
+
button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
1600
|
+
button.title = collapsed ? "Expand file" : "Collapse file";
|
|
1601
|
+
}
|
|
1602
|
+
const unfold = card.querySelector(".gdp-file-unfold");
|
|
1603
|
+
if (unfold)
|
|
1604
|
+
unfold.disabled = collapsed;
|
|
1605
|
+
const viewFile = card.querySelector(".gdp-view-file");
|
|
1606
|
+
if (viewFile)
|
|
1607
|
+
viewFile.disabled = collapsed;
|
|
1608
|
+
}
|
|
1609
|
+
function setViewFileButtonState(button, sourceMode) {
|
|
1610
|
+
if (!button)
|
|
1611
|
+
return;
|
|
1612
|
+
button.classList.add("gdp-btn", "gdp-btn-sm");
|
|
1613
|
+
button.textContent = sourceMode ? "View Diff" : "View File";
|
|
1614
|
+
button.setAttribute("aria-pressed", sourceMode ? "true" : "false");
|
|
1615
|
+
button.title = sourceMode ? "View diff" : "View file";
|
|
1616
|
+
}
|
|
1617
|
+
function renderSourceLoading(card, target) {
|
|
1618
|
+
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
1619
|
+
const view = document.createElement("div");
|
|
1620
|
+
view.className = "gdp-source-viewer loading";
|
|
1621
|
+
view.textContent = "Loading " + target.path + " at " + target.ref + "...";
|
|
1622
|
+
if (body)
|
|
1623
|
+
body.replaceWith(view);
|
|
1624
|
+
else
|
|
1625
|
+
card.appendChild(view);
|
|
1626
|
+
}
|
|
1627
|
+
function renderSourceError(card, target, message) {
|
|
1628
|
+
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
1629
|
+
const view = document.createElement("div");
|
|
1630
|
+
view.className = "gdp-source-viewer error";
|
|
1631
|
+
view.textContent = message || "Cannot load " + target.path + " at " + target.ref;
|
|
1632
|
+
if (body)
|
|
1633
|
+
body.replaceWith(view);
|
|
1634
|
+
else
|
|
1635
|
+
card.appendChild(view);
|
|
1636
|
+
}
|
|
1637
|
+
function isPreviewableSource(path) {
|
|
1638
|
+
return /\.(md|markdown|mdown|mkdn|mdx)$/i.test(path);
|
|
1639
|
+
}
|
|
1640
|
+
function appendInlineMarkdown(parent, text) {
|
|
1641
|
+
const parts = text.split(/(`[^`]+`)/g);
|
|
1642
|
+
parts.forEach((part) => {
|
|
1643
|
+
if (part.startsWith("`") && part.endsWith("`") && part.length > 1) {
|
|
1644
|
+
const code = document.createElement("code");
|
|
1645
|
+
code.textContent = part.slice(1, -1);
|
|
1646
|
+
parent.appendChild(code);
|
|
1647
|
+
} else {
|
|
1648
|
+
parent.appendChild(document.createTextNode(part));
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
function appendMarkdownParagraph(markdown, lines) {
|
|
1653
|
+
if (!lines.length)
|
|
1654
|
+
return;
|
|
1655
|
+
const p = document.createElement("p");
|
|
1656
|
+
appendInlineMarkdown(p, lines.join(" ").trim());
|
|
1657
|
+
markdown.appendChild(p);
|
|
1658
|
+
}
|
|
1659
|
+
function renderMarkdownPreview(textValue, target, hljsRef) {
|
|
1660
|
+
const markdown = document.createElement("div");
|
|
1661
|
+
markdown.className = "gdp-markdown-preview markdown-body";
|
|
1662
|
+
const lines = textValue.replace(/\r\n/g, `
|
|
1663
|
+
`).replace(/\r/g, `
|
|
1664
|
+
`).split(`
|
|
1665
|
+
`);
|
|
1666
|
+
let paragraph = [];
|
|
1667
|
+
let list = null;
|
|
1668
|
+
const flushParagraph = () => {
|
|
1669
|
+
appendMarkdownParagraph(markdown, paragraph);
|
|
1670
|
+
paragraph = [];
|
|
1671
|
+
};
|
|
1672
|
+
const flushList = () => {
|
|
1673
|
+
list = null;
|
|
1674
|
+
};
|
|
1675
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1676
|
+
const line = lines[i];
|
|
1677
|
+
const fence = line.match(/^```(\S*)\s*$/);
|
|
1678
|
+
if (fence) {
|
|
1679
|
+
flushParagraph();
|
|
1680
|
+
flushList();
|
|
1681
|
+
const codeLines = [];
|
|
1682
|
+
i++;
|
|
1683
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i])) {
|
|
1684
|
+
codeLines.push(lines[i]);
|
|
1685
|
+
i++;
|
|
1686
|
+
}
|
|
1687
|
+
const pre = document.createElement("pre");
|
|
1688
|
+
const code = document.createElement("code");
|
|
1689
|
+
const lang = fence[1] || inferLang(target.path) || "";
|
|
1690
|
+
const raw = codeLines.join(`
|
|
1691
|
+
`);
|
|
1692
|
+
if (hljsRef && hljsRef.highlight && lang && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
|
|
1693
|
+
try {
|
|
1694
|
+
code.innerHTML = hljsRef.highlight(raw, { language: lang, ignoreIllegals: true }).value;
|
|
1695
|
+
code.classList.add("hljs");
|
|
1696
|
+
} catch {
|
|
1697
|
+
code.textContent = raw;
|
|
1698
|
+
}
|
|
1699
|
+
} else {
|
|
1700
|
+
code.textContent = raw;
|
|
1701
|
+
}
|
|
1702
|
+
pre.appendChild(code);
|
|
1703
|
+
markdown.appendChild(pre);
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (!line.trim()) {
|
|
1707
|
+
flushParagraph();
|
|
1708
|
+
flushList();
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1712
|
+
if (heading) {
|
|
1713
|
+
flushParagraph();
|
|
1714
|
+
flushList();
|
|
1715
|
+
const level = String(Math.min(heading[1].length, 6));
|
|
1716
|
+
const h = document.createElement("h" + level);
|
|
1717
|
+
appendInlineMarkdown(h, heading[2]);
|
|
1718
|
+
markdown.appendChild(h);
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (/^\s*---+\s*$/.test(line)) {
|
|
1722
|
+
flushParagraph();
|
|
1723
|
+
flushList();
|
|
1724
|
+
markdown.appendChild(document.createElement("hr"));
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const bullet = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
1728
|
+
const ordered = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
1729
|
+
if (bullet || ordered) {
|
|
1730
|
+
flushParagraph();
|
|
1731
|
+
const tag = ordered ? "ol" : "ul";
|
|
1732
|
+
if (!list || list.tagName.toLowerCase() !== tag) {
|
|
1733
|
+
list = document.createElement(tag);
|
|
1734
|
+
markdown.appendChild(list);
|
|
1735
|
+
}
|
|
1736
|
+
const li = document.createElement("li");
|
|
1737
|
+
appendInlineMarkdown(li, (bullet || ordered)[1]);
|
|
1738
|
+
list.appendChild(li);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
1742
|
+
if (quote) {
|
|
1743
|
+
flushParagraph();
|
|
1744
|
+
flushList();
|
|
1745
|
+
const blockquote = document.createElement("blockquote");
|
|
1746
|
+
appendInlineMarkdown(blockquote, quote[1]);
|
|
1747
|
+
markdown.appendChild(blockquote);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
flushList();
|
|
1751
|
+
paragraph.push(line);
|
|
1752
|
+
}
|
|
1753
|
+
flushParagraph();
|
|
1754
|
+
return markdown;
|
|
1755
|
+
}
|
|
1756
|
+
async function renderSourceText(card, target, textValue) {
|
|
1757
|
+
const lines = textValue.length ? textValue.replace(/\r\n/g, `
|
|
1758
|
+
`).replace(/\r/g, `
|
|
1759
|
+
`).split(`
|
|
1760
|
+
`) : [""];
|
|
1761
|
+
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
1762
|
+
const isStandalone = card.classList.contains("gdp-standalone-source");
|
|
1763
|
+
const view = document.createElement("div");
|
|
1764
|
+
view.className = "gdp-source-viewer";
|
|
1765
|
+
const header = isStandalone ? null : document.createElement("div");
|
|
1766
|
+
if (header) {
|
|
1767
|
+
header.className = "gdp-source-meta";
|
|
1768
|
+
header.textContent = target.path + " @ " + target.ref;
|
|
1769
|
+
}
|
|
1770
|
+
const table = document.createElement("table");
|
|
1771
|
+
table.className = "gdp-source-table";
|
|
1772
|
+
const tbody = document.createElement("tbody");
|
|
1773
|
+
const hljsRef = await loadSyntaxHighlighter();
|
|
1774
|
+
const lang = inferLang(target.path);
|
|
1775
|
+
lines.forEach((line, index) => {
|
|
1776
|
+
const tr = document.createElement("tr");
|
|
1777
|
+
const num = document.createElement("td");
|
|
1778
|
+
num.className = "gdp-source-line-number";
|
|
1779
|
+
num.textContent = String(index + 1);
|
|
1780
|
+
const code = document.createElement("td");
|
|
1781
|
+
code.className = "gdp-source-line-code";
|
|
1782
|
+
if (hljsRef && hljsRef.highlight && lang && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
|
|
1783
|
+
try {
|
|
1784
|
+
code.innerHTML = hljsRef.highlight(line || " ", { language: lang, ignoreIllegals: true }).value;
|
|
1785
|
+
code.classList.add("hljs");
|
|
1786
|
+
} catch {
|
|
1787
|
+
code.textContent = line || " ";
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
code.textContent = line || " ";
|
|
1791
|
+
}
|
|
1792
|
+
tr.appendChild(num);
|
|
1793
|
+
tr.appendChild(code);
|
|
1794
|
+
tbody.appendChild(tr);
|
|
1795
|
+
});
|
|
1796
|
+
table.appendChild(tbody);
|
|
1797
|
+
if (isPreviewableSource(target.path)) {
|
|
1798
|
+
const tabsHost = card.querySelector(".gdp-file-detail-tabs");
|
|
1799
|
+
const tabs = document.createElement("div");
|
|
1800
|
+
tabs.className = "gdp-source-tabs";
|
|
1801
|
+
const previewButton = document.createElement("button");
|
|
1802
|
+
previewButton.type = "button";
|
|
1803
|
+
previewButton.className = "active";
|
|
1804
|
+
previewButton.textContent = "Preview";
|
|
1805
|
+
const codeButton = document.createElement("button");
|
|
1806
|
+
codeButton.type = "button";
|
|
1807
|
+
codeButton.textContent = "Code";
|
|
1808
|
+
const preview = renderMarkdownPreview(textValue, target, hljsRef);
|
|
1809
|
+
table.hidden = true;
|
|
1810
|
+
previewButton.addEventListener("click", () => {
|
|
1811
|
+
previewButton.classList.add("active");
|
|
1812
|
+
codeButton.classList.remove("active");
|
|
1813
|
+
preview.hidden = false;
|
|
1814
|
+
table.hidden = true;
|
|
1815
|
+
});
|
|
1816
|
+
codeButton.addEventListener("click", () => {
|
|
1817
|
+
codeButton.classList.add("active");
|
|
1818
|
+
previewButton.classList.remove("active");
|
|
1819
|
+
preview.hidden = true;
|
|
1820
|
+
table.hidden = false;
|
|
1821
|
+
});
|
|
1822
|
+
tabs.appendChild(previewButton);
|
|
1823
|
+
tabs.appendChild(codeButton);
|
|
1824
|
+
if (header)
|
|
1825
|
+
view.appendChild(header);
|
|
1826
|
+
if (tabsHost) {
|
|
1827
|
+
tabsHost.hidden = false;
|
|
1828
|
+
tabsHost.replaceChildren(tabs);
|
|
1829
|
+
}
|
|
1830
|
+
view.appendChild(preview);
|
|
1831
|
+
view.appendChild(table);
|
|
1832
|
+
if (body)
|
|
1833
|
+
body.replaceWith(view);
|
|
1834
|
+
else
|
|
1835
|
+
card.appendChild(view);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (header)
|
|
1839
|
+
view.appendChild(header);
|
|
1840
|
+
view.appendChild(table);
|
|
1841
|
+
if (body)
|
|
1842
|
+
body.replaceWith(view);
|
|
1843
|
+
else
|
|
1844
|
+
card.appendChild(view);
|
|
1845
|
+
}
|
|
1846
|
+
function renderSourceMedia(card, target, mediaKind) {
|
|
1847
|
+
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
1848
|
+
const isStandalone = card.classList.contains("gdp-standalone-source");
|
|
1849
|
+
const view = document.createElement("div");
|
|
1850
|
+
view.className = "gdp-source-viewer media";
|
|
1851
|
+
if (!isStandalone) {
|
|
1852
|
+
const meta = document.createElement("div");
|
|
1853
|
+
meta.className = "gdp-source-meta";
|
|
1854
|
+
meta.textContent = target.path + " @ " + target.ref;
|
|
1855
|
+
view.appendChild(meta);
|
|
1856
|
+
}
|
|
1857
|
+
const url = buildRawFileUrl(target);
|
|
1858
|
+
if (mediaKind === "video") {
|
|
1859
|
+
const video = document.createElement("video");
|
|
1860
|
+
video.src = url;
|
|
1861
|
+
video.controls = true;
|
|
1862
|
+
video.preload = "metadata";
|
|
1863
|
+
view.appendChild(video);
|
|
1864
|
+
} else {
|
|
1865
|
+
const img = document.createElement("img");
|
|
1866
|
+
img.src = url;
|
|
1867
|
+
img.alt = "";
|
|
1868
|
+
view.appendChild(img);
|
|
1869
|
+
}
|
|
1870
|
+
if (body)
|
|
1871
|
+
body.replaceWith(view);
|
|
1872
|
+
else
|
|
1873
|
+
card.appendChild(view);
|
|
1874
|
+
}
|
|
1875
|
+
function renderSourceBinary(card, target) {
|
|
1876
|
+
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
1877
|
+
const isStandalone = card.classList.contains("gdp-standalone-source");
|
|
1878
|
+
const view = document.createElement("div");
|
|
1879
|
+
view.className = "gdp-source-viewer binary";
|
|
1880
|
+
const link = document.createElement("a");
|
|
1881
|
+
link.href = buildRawFileUrl(target);
|
|
1882
|
+
link.textContent = "Open raw file";
|
|
1883
|
+
link.target = "_blank";
|
|
1884
|
+
link.rel = "noreferrer";
|
|
1885
|
+
if (!isStandalone) {
|
|
1886
|
+
const meta = document.createElement("div");
|
|
1887
|
+
meta.className = "gdp-source-meta";
|
|
1888
|
+
meta.textContent = target.path + " @ " + target.ref;
|
|
1889
|
+
view.appendChild(meta);
|
|
1890
|
+
}
|
|
1891
|
+
view.appendChild(link);
|
|
1892
|
+
if (body)
|
|
1893
|
+
body.replaceWith(view);
|
|
1894
|
+
else
|
|
1895
|
+
card.appendChild(view);
|
|
1896
|
+
}
|
|
1897
|
+
function createFileBreadcrumb(path) {
|
|
1898
|
+
const nav = document.createElement("nav");
|
|
1899
|
+
nav.className = "gdp-file-breadcrumb";
|
|
1900
|
+
nav.setAttribute("aria-label", "File path");
|
|
1901
|
+
const parts = path.split("/").filter(Boolean);
|
|
1902
|
+
const allParts = PROJECT_NAME ? [PROJECT_NAME, ...parts] : parts;
|
|
1903
|
+
allParts.forEach((part, index) => {
|
|
1904
|
+
if (index > 0) {
|
|
1905
|
+
const sep = document.createElement("span");
|
|
1906
|
+
sep.className = "gdp-file-breadcrumb-sep";
|
|
1907
|
+
sep.textContent = "/";
|
|
1908
|
+
nav.appendChild(sep);
|
|
1909
|
+
}
|
|
1910
|
+
const crumb = document.createElement("span");
|
|
1911
|
+
crumb.className = index === allParts.length - 1 ? "gdp-file-breadcrumb-current" : "gdp-file-breadcrumb-part";
|
|
1912
|
+
crumb.textContent = part;
|
|
1913
|
+
nav.appendChild(crumb);
|
|
1914
|
+
});
|
|
1915
|
+
if (!allParts.length) {
|
|
1916
|
+
const crumb = document.createElement("span");
|
|
1917
|
+
crumb.className = "gdp-file-breadcrumb-current";
|
|
1918
|
+
crumb.textContent = path;
|
|
1919
|
+
nav.appendChild(crumb);
|
|
1920
|
+
}
|
|
1921
|
+
return nav;
|
|
1922
|
+
}
|
|
1923
|
+
async function renderStandaloneSource(target) {
|
|
1924
|
+
const req = ++SOURCE_REQ_SEQ;
|
|
1925
|
+
const root = $("#diff");
|
|
1926
|
+
const repoTarget = repoFileTargetFromRoute();
|
|
1927
|
+
setPageMode();
|
|
1928
|
+
removeStandaloneSource();
|
|
1929
|
+
document.querySelectorAll(".gdp-repo-blob-layout").forEach((el) => el.remove());
|
|
1930
|
+
const card = document.createElement("article");
|
|
1931
|
+
card.className = "gdp-file-shell loaded gdp-standalone-source gdp-source-mode";
|
|
1932
|
+
card.dataset.path = target.path;
|
|
1933
|
+
const wrapper = document.createElement("div");
|
|
1934
|
+
wrapper.className = "gdp-file-detail-wrapper";
|
|
1935
|
+
const sticky = document.createElement("div");
|
|
1936
|
+
sticky.className = "gdp-file-detail-sticky";
|
|
1937
|
+
const header = document.createElement("div");
|
|
1938
|
+
header.className = "gdp-file-detail-header";
|
|
1939
|
+
const name = document.createElement("div");
|
|
1940
|
+
name.className = "gdp-file-detail-path";
|
|
1941
|
+
name.appendChild(createFileBreadcrumb(target.path));
|
|
1942
|
+
const copy = document.createElement("button");
|
|
1943
|
+
copy.type = "button";
|
|
1944
|
+
copy.className = "gdp-file-header-icon gdp-copy-path";
|
|
1945
|
+
copy.title = "copy file path";
|
|
1946
|
+
copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
|
|
1947
|
+
copy.addEventListener("click", async (e) => {
|
|
1948
|
+
e.stopPropagation();
|
|
1949
|
+
try {
|
|
1950
|
+
await navigator.clipboard.writeText(target.path);
|
|
1951
|
+
copy.classList.add("copied");
|
|
1952
|
+
setTimeout(() => {
|
|
1953
|
+
copy.classList.remove("copied");
|
|
1954
|
+
}, 1200);
|
|
1955
|
+
} catch {
|
|
1956
|
+
copy.classList.add("failed");
|
|
1957
|
+
setTimeout(() => {
|
|
1958
|
+
copy.classList.remove("failed");
|
|
1959
|
+
}, 1200);
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
name.appendChild(copy);
|
|
1963
|
+
header.appendChild(name);
|
|
1964
|
+
if (!repoTarget) {
|
|
1965
|
+
const back = document.createElement("button");
|
|
1966
|
+
back.type = "button";
|
|
1967
|
+
back.className = "gdp-view-file gdp-btn gdp-btn-sm";
|
|
1968
|
+
setViewFileButtonState(back, true);
|
|
1969
|
+
back.addEventListener("click", () => {
|
|
1970
|
+
setRoute({ screen: "diff", range: currentRange() });
|
|
1971
|
+
setPageMode();
|
|
1972
|
+
removeStandaloneSource();
|
|
1973
|
+
});
|
|
1974
|
+
header.appendChild(back);
|
|
1975
|
+
}
|
|
1976
|
+
sticky.appendChild(header);
|
|
1977
|
+
const tabsHost = document.createElement("div");
|
|
1978
|
+
tabsHost.className = "gdp-file-detail-tabs";
|
|
1979
|
+
tabsHost.hidden = true;
|
|
1980
|
+
sticky.appendChild(tabsHost);
|
|
1981
|
+
wrapper.appendChild(sticky);
|
|
1982
|
+
const detailBody = document.createElement("div");
|
|
1983
|
+
detailBody.className = "gdp-file-detail-body";
|
|
1984
|
+
wrapper.appendChild(detailBody);
|
|
1985
|
+
card.appendChild(wrapper);
|
|
1986
|
+
if (repoTarget) {
|
|
1987
|
+
const layout = document.createElement("div");
|
|
1988
|
+
layout.className = "gdp-repo-blob-layout";
|
|
1989
|
+
renderRepoBlobSidebar(target.path, repoTarget);
|
|
1990
|
+
layout.appendChild(card);
|
|
1991
|
+
root.replaceChildren(layout);
|
|
1992
|
+
} else {
|
|
1993
|
+
root.prepend(card);
|
|
1994
|
+
}
|
|
1995
|
+
renderSourceLoading(card, target);
|
|
1996
|
+
try {
|
|
1997
|
+
const mediaKind = isVideo(target.path) ? "video" : isMedia(target.path) ? "image" : null;
|
|
1998
|
+
if (mediaKind === "image" || mediaKind === "video") {
|
|
1999
|
+
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
2000
|
+
return;
|
|
2001
|
+
renderSourceMedia(card, target, mediaKind);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
const response = await trackLoad(fetch(buildRawFileUrl(target)));
|
|
2005
|
+
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
2006
|
+
return;
|
|
2007
|
+
if (!response.ok) {
|
|
2008
|
+
renderSourceError(card, target, "Cannot load " + target.path + " at " + target.ref);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const textValue = await response.text();
|
|
2012
|
+
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
2013
|
+
return;
|
|
2014
|
+
await renderSourceText(card, target, textValue);
|
|
2015
|
+
} catch {
|
|
2016
|
+
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
2017
|
+
return;
|
|
2018
|
+
renderSourceError(card, target, "Cannot load " + target.path + " at " + target.ref);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function applySourceRouteToShell() {
|
|
2022
|
+
const target = sourceTargetFromRoute();
|
|
2023
|
+
setPageMode();
|
|
2024
|
+
if (!target) {
|
|
2025
|
+
removeStandaloneSource();
|
|
2026
|
+
document.querySelectorAll(".gdp-view-file").forEach((button) => {
|
|
2027
|
+
setViewFileButtonState(button, false);
|
|
2028
|
+
});
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
renderStandaloneSource(target);
|
|
2032
|
+
}
|
|
2033
|
+
async function expandAllFileContext(card, file) {
|
|
2034
|
+
if (card.classList.contains("gdp-context-expanded")) {
|
|
2035
|
+
const data = card._diffData;
|
|
2036
|
+
if (!data)
|
|
2037
|
+
return;
|
|
2038
|
+
card.classList.remove("gdp-context-expanded");
|
|
2039
|
+
mountDiff(card, file, data);
|
|
2040
|
+
if (data.truncated && data.mode === "preview")
|
|
2041
|
+
addExpandHunksUI(file, data, card);
|
|
2042
|
+
scheduleIdleHighlight(card, file);
|
|
2043
|
+
setUnfoldButtonState(card.querySelector(".gdp-file-unfold"), false);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (card._diffData && (card._diffData.truncated || card._diffData.mode === "preview")) {
|
|
2047
|
+
await loadFile(file, card, file.load_url);
|
|
2048
|
+
card.classList.add("gdp-context-expanded");
|
|
2049
|
+
setUnfoldButtonState(card.querySelector(".gdp-file-unfold"), true);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const button = card.querySelector(".gdp-file-unfold");
|
|
2053
|
+
if (button)
|
|
2054
|
+
button.disabled = true;
|
|
2055
|
+
try {
|
|
2056
|
+
for (let i = 0;i < 200; i++) {
|
|
2057
|
+
const next = card.querySelector(".gdp-expand-btn:not(:disabled)");
|
|
2058
|
+
if (!next)
|
|
2059
|
+
break;
|
|
2060
|
+
next.click();
|
|
2061
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
2062
|
+
}
|
|
2063
|
+
card.classList.add("gdp-context-expanded");
|
|
2064
|
+
setUnfoldButtonState(button || null, true);
|
|
2065
|
+
} finally {
|
|
2066
|
+
if (button)
|
|
2067
|
+
button.disabled = false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
973
2070
|
function appendStatSquaresToHeader(card, file) {
|
|
974
2071
|
const header = card.querySelector(".d2h-file-header");
|
|
975
|
-
if (!header
|
|
2072
|
+
if (!header)
|
|
976
2073
|
return;
|
|
2074
|
+
if (!header.querySelector(".gdp-file-toggle")) {
|
|
2075
|
+
const toggle = document.createElement("button");
|
|
2076
|
+
toggle.type = "button";
|
|
2077
|
+
toggle.className = "gdp-file-header-icon gdp-file-toggle";
|
|
2078
|
+
toggle.title = "Collapse file";
|
|
2079
|
+
toggle.setAttribute("aria-expanded", "true");
|
|
2080
|
+
toggle.innerHTML = iconSvg("octicon-chevron-down", CHEVRON_DOWN_16_PATH);
|
|
2081
|
+
toggle.addEventListener("click", (e) => {
|
|
2082
|
+
e.stopPropagation();
|
|
2083
|
+
setFileCollapsed(card, !card.classList.contains("gdp-file-collapsed"));
|
|
2084
|
+
});
|
|
2085
|
+
header.insertBefore(toggle, header.firstChild);
|
|
2086
|
+
}
|
|
2087
|
+
header.querySelectorAll(".d2h-file-collapse-input").forEach((checkbox) => {
|
|
2088
|
+
checkbox.checked = STATE.viewedFiles.has(file.path);
|
|
2089
|
+
if (checkbox.dataset.gdpBound !== "1") {
|
|
2090
|
+
checkbox.dataset.gdpBound = "1";
|
|
2091
|
+
checkbox.addEventListener("change", () => setFileViewed(file.path, checkbox.checked));
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
if (!header.querySelector(".gdp-copy-path")) {
|
|
2095
|
+
const nameWrapper = header.querySelector(".d2h-file-name-wrapper");
|
|
2096
|
+
const copy = document.createElement("button");
|
|
2097
|
+
copy.type = "button";
|
|
2098
|
+
copy.className = "gdp-file-header-icon gdp-copy-path";
|
|
2099
|
+
copy.title = "copy file path";
|
|
2100
|
+
copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
|
|
2101
|
+
copy.addEventListener("click", async (e) => {
|
|
2102
|
+
e.stopPropagation();
|
|
2103
|
+
const path = filePathClipboardText(file.path);
|
|
2104
|
+
if (!path)
|
|
2105
|
+
return;
|
|
2106
|
+
try {
|
|
2107
|
+
await navigator.clipboard.writeText(path);
|
|
2108
|
+
copy.classList.add("copied");
|
|
2109
|
+
setTimeout(() => {
|
|
2110
|
+
copy.classList.remove("copied");
|
|
2111
|
+
}, 1200);
|
|
2112
|
+
} catch {
|
|
2113
|
+
copy.classList.add("failed");
|
|
2114
|
+
setTimeout(() => {
|
|
2115
|
+
copy.classList.remove("failed");
|
|
2116
|
+
}, 1200);
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
const statusTag = nameWrapper ? nameWrapper.querySelector(".d2h-tag") : null;
|
|
2120
|
+
if (statusTag)
|
|
2121
|
+
statusTag.insertAdjacentElement("afterend", copy);
|
|
2122
|
+
else if (nameWrapper)
|
|
2123
|
+
nameWrapper.insertAdjacentElement("beforeend", copy);
|
|
2124
|
+
else
|
|
2125
|
+
header.insertBefore(copy, header.firstChild);
|
|
2126
|
+
}
|
|
2127
|
+
if (!header.querySelector(".gdp-file-unfold")) {
|
|
2128
|
+
const unfold = document.createElement("button");
|
|
2129
|
+
unfold.type = "button";
|
|
2130
|
+
unfold.className = "gdp-file-header-icon gdp-file-unfold";
|
|
2131
|
+
setUnfoldButtonState(unfold, card.classList.contains("gdp-context-expanded"));
|
|
2132
|
+
unfold.addEventListener("click", (e) => {
|
|
2133
|
+
e.stopPropagation();
|
|
2134
|
+
expandAllFileContext(card, file);
|
|
2135
|
+
});
|
|
2136
|
+
const copy = header.querySelector(".gdp-copy-path");
|
|
2137
|
+
if (copy)
|
|
2138
|
+
copy.insertAdjacentElement("afterend", unfold);
|
|
2139
|
+
else
|
|
2140
|
+
header.appendChild(unfold);
|
|
2141
|
+
}
|
|
977
2142
|
if (!header.querySelector(".gdp-stat-text")) {
|
|
978
2143
|
const stats = document.createElement("span");
|
|
979
2144
|
stats.className = "gdp-stat-text";
|
|
@@ -1010,6 +2175,21 @@
|
|
|
1010
2175
|
wrap.appendChild(box);
|
|
1011
2176
|
}
|
|
1012
2177
|
header.appendChild(wrap);
|
|
2178
|
+
if (!header.querySelector(".gdp-view-file")) {
|
|
2179
|
+
const viewFile = document.createElement("button");
|
|
2180
|
+
viewFile.type = "button";
|
|
2181
|
+
viewFile.className = "gdp-view-file gdp-btn gdp-btn-sm";
|
|
2182
|
+
setViewFileButtonState(viewFile, false);
|
|
2183
|
+
viewFile.addEventListener("click", (e) => {
|
|
2184
|
+
e.stopPropagation();
|
|
2185
|
+
const target = fileSourceTarget(file);
|
|
2186
|
+
setRoute({ screen: "file", path: target.path, ref: target.ref, range: currentRange() });
|
|
2187
|
+
applySourceRouteToShell();
|
|
2188
|
+
});
|
|
2189
|
+
header.appendChild(viewFile);
|
|
2190
|
+
} else {
|
|
2191
|
+
setViewFileButtonState(header.querySelector(".gdp-view-file"), false);
|
|
2192
|
+
}
|
|
1013
2193
|
}
|
|
1014
2194
|
function renderFile(file, data, card) {
|
|
1015
2195
|
card._diffData = data;
|
|
@@ -1379,29 +2559,72 @@
|
|
|
1379
2559
|
localStorage.setItem("gdp:theme", STATE.theme);
|
|
1380
2560
|
applyTheme();
|
|
1381
2561
|
});
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
2562
|
+
function visibleFileItems() {
|
|
2563
|
+
return $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
|
|
2564
|
+
}
|
|
2565
|
+
function moveActiveVisibleFile(direction) {
|
|
2566
|
+
const items = visibleFileItems();
|
|
2567
|
+
if (!items.length)
|
|
2568
|
+
return;
|
|
2569
|
+
const current = items.findIndex((li) => li.classList.contains("active"));
|
|
2570
|
+
const idx = nextVisibleFileIndex(current, items.length, direction);
|
|
2571
|
+
const target = items[idx];
|
|
2572
|
+
if (!target)
|
|
2573
|
+
return;
|
|
2574
|
+
if (target.dataset.path)
|
|
2575
|
+
markActive(target.dataset.path);
|
|
2576
|
+
target.scrollIntoView({ block: "nearest" });
|
|
2577
|
+
if (target.dataset.path)
|
|
2578
|
+
prefetchByPath(target.dataset.path);
|
|
2579
|
+
}
|
|
2580
|
+
function jumpToActiveOrFirstFilteredFile() {
|
|
2581
|
+
const items = visibleFileItems();
|
|
2582
|
+
const active = items.find((li) => li.classList.contains("active"));
|
|
2583
|
+
const target = active || items[0];
|
|
2584
|
+
if (target) {
|
|
2585
|
+
target.click();
|
|
2586
|
+
$("#sb-filter").blur();
|
|
2587
|
+
}
|
|
1391
2588
|
}
|
|
1392
|
-
$("#filter").addEventListener("input", (e) => syncFilters(e.target));
|
|
1393
2589
|
const sbFilter = $("#sb-filter");
|
|
1394
|
-
if (sbFilter)
|
|
1395
|
-
sbFilter.addEventListener("input", (
|
|
2590
|
+
if (sbFilter) {
|
|
2591
|
+
sbFilter.addEventListener("input", () => applyFilter());
|
|
2592
|
+
sbFilter.addEventListener("keydown", (e) => {
|
|
2593
|
+
if (e.key === "Enter") {
|
|
2594
|
+
e.preventDefault();
|
|
2595
|
+
jumpToActiveOrFirstFilteredFile();
|
|
2596
|
+
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
2597
|
+
e.preventDefault();
|
|
2598
|
+
moveActiveVisibleFile(e.key === "ArrowDown" ? 1 : -1);
|
|
2599
|
+
} else if (e.key === "Escape") {
|
|
2600
|
+
if (sbFilter.value) {
|
|
2601
|
+
sbFilter.value = "";
|
|
2602
|
+
applyFilter();
|
|
2603
|
+
} else {
|
|
2604
|
+
sbFilter.blur();
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
function focusFileFilter() {
|
|
2610
|
+
const input = $("#sb-filter");
|
|
2611
|
+
input.focus();
|
|
2612
|
+
input.select();
|
|
2613
|
+
}
|
|
1396
2614
|
document.addEventListener("keydown", (e) => {
|
|
2615
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
2616
|
+
e.preventDefault();
|
|
2617
|
+
focusFileFilter();
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
1397
2620
|
const targetEl = e.target;
|
|
1398
2621
|
if (targetEl && (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA"))
|
|
1399
2622
|
return;
|
|
1400
2623
|
if (e.key === "/") {
|
|
1401
2624
|
e.preventDefault();
|
|
1402
|
-
|
|
2625
|
+
focusFileFilter();
|
|
1403
2626
|
} else if (e.key === "j" || e.key === "k") {
|
|
1404
|
-
const items =
|
|
2627
|
+
const items = visibleFileItems();
|
|
1405
2628
|
if (!items.length)
|
|
1406
2629
|
return;
|
|
1407
2630
|
let idx = items.findIndex((li) => li.classList.contains("active"));
|
|
@@ -1427,7 +2650,31 @@
|
|
|
1427
2650
|
});
|
|
1428
2651
|
applyTheme();
|
|
1429
2652
|
setLayout(STATE.layout);
|
|
2653
|
+
setPageMode();
|
|
2654
|
+
if (window.location.pathname === "/") {
|
|
2655
|
+
setRoute(STATE.route, true);
|
|
2656
|
+
}
|
|
2657
|
+
function loadRepo() {
|
|
2658
|
+
if (STATE.route.screen !== "repo")
|
|
2659
|
+
return Promise.resolve();
|
|
2660
|
+
setStatus("refreshing");
|
|
2661
|
+
const params = new URLSearchParams;
|
|
2662
|
+
params.set("ref", STATE.route.ref || "worktree");
|
|
2663
|
+
if (STATE.route.path)
|
|
2664
|
+
params.set("path", STATE.route.path);
|
|
2665
|
+
return trackLoad(fetch("/_tree?" + params.toString()).then((r) => {
|
|
2666
|
+
if (!r.ok)
|
|
2667
|
+
throw new Error("failed to load repository tree");
|
|
2668
|
+
return r.json();
|
|
2669
|
+
})).then((data) => {
|
|
2670
|
+
renderRepo(data);
|
|
2671
|
+
setStatus("live");
|
|
2672
|
+
syncHeaderMenu();
|
|
2673
|
+
}).catch(() => setStatus("error"));
|
|
2674
|
+
}
|
|
1430
2675
|
function load(opts) {
|
|
2676
|
+
if (STATE.route.screen === "repo")
|
|
2677
|
+
return loadRepo();
|
|
1431
2678
|
setStatus("refreshing");
|
|
1432
2679
|
const params = new URLSearchParams;
|
|
1433
2680
|
if (STATE.ignoreWs)
|
|
@@ -1444,7 +2691,10 @@
|
|
|
1444
2691
|
setStatus("live");
|
|
1445
2692
|
}).catch(() => setStatus("error"));
|
|
1446
2693
|
}
|
|
1447
|
-
|
|
2694
|
+
if (STATE.route.screen === "repo")
|
|
2695
|
+
loadRepo();
|
|
2696
|
+
else
|
|
2697
|
+
load();
|
|
1448
2698
|
function syncRefInputs() {
|
|
1449
2699
|
const fi = $("#ref-from"), ti = $("#ref-to");
|
|
1450
2700
|
if (fi)
|
|
@@ -1458,9 +2708,16 @@
|
|
|
1458
2708
|
localStorage.setItem("gdp:from", STATE.from);
|
|
1459
2709
|
localStorage.setItem("gdp:to", STATE.to);
|
|
1460
2710
|
syncRefInputs();
|
|
2711
|
+
const range = currentRange();
|
|
2712
|
+
if (STATE.route.screen === "file") {
|
|
2713
|
+
setRoute({ screen: "file", path: STATE.route.path, ref: STATE.route.ref, range }, true);
|
|
2714
|
+
} else {
|
|
2715
|
+
setRoute({ screen: "diff", range }, true);
|
|
2716
|
+
}
|
|
1461
2717
|
load();
|
|
1462
2718
|
}
|
|
1463
2719
|
syncRefInputs();
|
|
2720
|
+
syncHeaderMenu();
|
|
1464
2721
|
const REFS = { branches: [], tags: [], commits: [], current: "" };
|
|
1465
2722
|
const popover = $("#ref-popover");
|
|
1466
2723
|
const popBody = popover.querySelector(".rp-body");
|
|
@@ -1544,7 +2801,8 @@
|
|
|
1544
2801
|
});
|
|
1545
2802
|
popover.hidden = false;
|
|
1546
2803
|
const r = input.getBoundingClientRect();
|
|
1547
|
-
|
|
2804
|
+
const popWidth = Math.min(560, Math.floor(window.innerWidth * 0.9));
|
|
2805
|
+
popover.style.left = Math.max(8, Math.min(r.left, window.innerWidth - popWidth - 8)) + "px";
|
|
1548
2806
|
popover.style.top = r.bottom + 4 + "px";
|
|
1549
2807
|
setTimeout(() => popSearch.focus(), 0);
|
|
1550
2808
|
}
|
|
@@ -1561,17 +2819,31 @@
|
|
|
1561
2819
|
el.focus();
|
|
1562
2820
|
}
|
|
1563
2821
|
});
|
|
2822
|
+
el.addEventListener("click", (e) => {
|
|
2823
|
+
e.stopPropagation();
|
|
2824
|
+
openPopover(el);
|
|
2825
|
+
});
|
|
1564
2826
|
el.addEventListener("keydown", (e) => {
|
|
1565
2827
|
if (e.key === "Enter") {
|
|
1566
2828
|
e.preventDefault();
|
|
1567
2829
|
closePopover();
|
|
1568
|
-
setRange($("#ref-from").value, $("#ref-to").value);
|
|
1569
2830
|
} else if (e.key === "Escape") {
|
|
1570
2831
|
closePopover();
|
|
1571
2832
|
el.blur();
|
|
1572
2833
|
}
|
|
1573
2834
|
});
|
|
1574
2835
|
});
|
|
2836
|
+
wireRepoTargetPicker($("#repo-target"), (ref) => {
|
|
2837
|
+
if (STATE.route.screen !== "file")
|
|
2838
|
+
return;
|
|
2839
|
+
setRoute({ screen: "file", path: STATE.route.path, ref, view: "blob", range: currentRange() });
|
|
2840
|
+
renderStandaloneSource({ path: STATE.route.path, ref });
|
|
2841
|
+
});
|
|
2842
|
+
document.addEventListener("focusin", (e) => {
|
|
2843
|
+
const el = e.target;
|
|
2844
|
+
if (el instanceof HTMLInputElement && (el.id === "repo-ref" || el.id === "repo-target"))
|
|
2845
|
+
openPopover(el);
|
|
2846
|
+
});
|
|
1575
2847
|
popSearch.addEventListener("input", () => buildPopBody(popSearch.value));
|
|
1576
2848
|
popSearch.addEventListener("keydown", (e) => {
|
|
1577
2849
|
if (e.key === "Escape") {
|
|
@@ -1586,8 +2858,19 @@
|
|
|
1586
2858
|
function handlePicked(val) {
|
|
1587
2859
|
if (!popTarget || !val)
|
|
1588
2860
|
return;
|
|
1589
|
-
|
|
1590
|
-
|
|
2861
|
+
const pickedTarget = popTarget;
|
|
2862
|
+
pickedTarget.value = val;
|
|
2863
|
+
if (pickedTarget.id === "repo-ref") {
|
|
2864
|
+
closePopover();
|
|
2865
|
+
pickedTarget.dispatchEvent(new Event("change"));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
if (pickedTarget.id === "repo-target") {
|
|
2869
|
+
closePopover();
|
|
2870
|
+
pickedTarget.dispatchEvent(new Event("change"));
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
const targetWasFrom = pickedTarget.id === "ref-from";
|
|
1591
2874
|
const otherEmpty = !$("#ref-to").value;
|
|
1592
2875
|
closePopover();
|
|
1593
2876
|
setRange($("#ref-from").value, $("#ref-to").value);
|
|
@@ -1618,12 +2901,36 @@
|
|
|
1618
2901
|
const target = e.target;
|
|
1619
2902
|
if (popover.contains(target))
|
|
1620
2903
|
return;
|
|
1621
|
-
if (target.id === "ref-from" || target.id === "ref-to")
|
|
2904
|
+
if (target.id === "ref-from" || target.id === "ref-to" || target.id === "repo-ref" || target.id === "repo-target")
|
|
1622
2905
|
return;
|
|
1623
2906
|
closePopover();
|
|
1624
2907
|
});
|
|
1625
|
-
$("#ref-apply").addEventListener("click", () => setRange($("#ref-from").value, $("#ref-to").value));
|
|
1626
2908
|
$("#ref-reset").addEventListener("click", () => setRange("HEAD", "worktree"));
|
|
2909
|
+
window.addEventListener("popstate", () => {
|
|
2910
|
+
const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
|
|
2911
|
+
STATE.route = parsedRoute.screen === "unknown" ? { screen: "diff", range: parsedRoute.range } : parsedRoute;
|
|
2912
|
+
STATE.from = STATE.route.range.from;
|
|
2913
|
+
STATE.to = STATE.route.range.to;
|
|
2914
|
+
if (STATE.route.screen === "repo")
|
|
2915
|
+
STATE.repoRef = STATE.route.ref || "worktree";
|
|
2916
|
+
syncRefInputs();
|
|
2917
|
+
syncHeaderMenu();
|
|
2918
|
+
if (STATE.route.screen === "repo") {
|
|
2919
|
+
SOURCE_REQ_SEQ++;
|
|
2920
|
+
setPageMode();
|
|
2921
|
+
removeStandaloneSource();
|
|
2922
|
+
loadRepo();
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (STATE.route.screen !== "file") {
|
|
2926
|
+
SOURCE_REQ_SEQ++;
|
|
2927
|
+
setPageMode();
|
|
2928
|
+
removeStandaloneSource();
|
|
2929
|
+
load();
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
applySourceRouteToShell();
|
|
2933
|
+
});
|
|
1627
2934
|
function applyIgnoreWs() {
|
|
1628
2935
|
const btn = $("#ignore-ws");
|
|
1629
2936
|
if (btn)
|
|
@@ -1704,13 +3011,9 @@
|
|
|
1704
3011
|
const isTest = TEST_RE.test(li.dataset.path || "");
|
|
1705
3012
|
li.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
|
|
1706
3013
|
});
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
-
});
|
|
3014
|
+
updateTreeDirVisibility();
|
|
3015
|
+
if (typeof applyViewedState === "function")
|
|
3016
|
+
applyViewedState();
|
|
1714
3017
|
}
|
|
1715
3018
|
applyHideTests();
|
|
1716
3019
|
$("#hide-tests").addEventListener("click", () => {
|