@youtyan/code-viewer 0.1.9 → 0.1.11
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/package.json +1 -1
- package/web/app.js +442 -70
- package/web/shiki.js +2 -1
- package/web/style.css +8 -0
- package/web-src/server/cache.ts +51 -0
- package/web-src/server/preview.ts +10 -7
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -6393,8 +6393,20 @@
|
|
|
6393
6393
|
const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
|
|
6394
6394
|
const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
|
|
6395
6395
|
let highlightLoadPromise = null;
|
|
6396
|
+
let sourceShikiLoadPromise = null;
|
|
6396
6397
|
let highlightConfigured = false;
|
|
6397
6398
|
let PROJECT_NAME = "";
|
|
6399
|
+
let REPO_SIDEBAR_REF = null;
|
|
6400
|
+
let REPO_SIDEBAR_LOAD_REF = null;
|
|
6401
|
+
let REPO_SIDEBAR_LOAD = null;
|
|
6402
|
+
function invalidateRepoSidebar() {
|
|
6403
|
+
REPO_SIDEBAR_REF = null;
|
|
6404
|
+
REPO_SIDEBAR_LOAD_REF = null;
|
|
6405
|
+
REPO_SIDEBAR_LOAD = null;
|
|
6406
|
+
}
|
|
6407
|
+
function isRepoSidebarReusable(ref) {
|
|
6408
|
+
return REPO_SIDEBAR_REF === (ref || "worktree") && isRepositorySidebarMode();
|
|
6409
|
+
}
|
|
6398
6410
|
const STATE = (() => {
|
|
6399
6411
|
const igRaw = localStorage.getItem("gdp:ignore-ws");
|
|
6400
6412
|
const fallbackRange = {
|
|
@@ -6487,6 +6499,100 @@
|
|
|
6487
6499
|
});
|
|
6488
6500
|
return highlightLoadPromise;
|
|
6489
6501
|
}
|
|
6502
|
+
const SOURCE_SHIKI_LANGS = Array.from(new Set([
|
|
6503
|
+
"bash",
|
|
6504
|
+
"bibtex",
|
|
6505
|
+
"c",
|
|
6506
|
+
"clojure",
|
|
6507
|
+
"cmake",
|
|
6508
|
+
"cpp",
|
|
6509
|
+
"csharp",
|
|
6510
|
+
"css",
|
|
6511
|
+
"dart",
|
|
6512
|
+
"diff",
|
|
6513
|
+
"dockerfile",
|
|
6514
|
+
"elixir",
|
|
6515
|
+
"erlang",
|
|
6516
|
+
"fortran",
|
|
6517
|
+
"go",
|
|
6518
|
+
"gradle",
|
|
6519
|
+
"graphql",
|
|
6520
|
+
"haskell",
|
|
6521
|
+
"html",
|
|
6522
|
+
"java",
|
|
6523
|
+
"javascript",
|
|
6524
|
+
"json",
|
|
6525
|
+
"julia",
|
|
6526
|
+
"kotlin",
|
|
6527
|
+
"lua",
|
|
6528
|
+
"make",
|
|
6529
|
+
"markdown",
|
|
6530
|
+
"nix",
|
|
6531
|
+
"ocaml",
|
|
6532
|
+
"perl",
|
|
6533
|
+
"php",
|
|
6534
|
+
"properties",
|
|
6535
|
+
"protobuf",
|
|
6536
|
+
"python",
|
|
6537
|
+
"r",
|
|
6538
|
+
"rst",
|
|
6539
|
+
"ruby",
|
|
6540
|
+
"rust",
|
|
6541
|
+
"scala",
|
|
6542
|
+
"scss",
|
|
6543
|
+
"sql",
|
|
6544
|
+
"swift",
|
|
6545
|
+
"terraform",
|
|
6546
|
+
"tex",
|
|
6547
|
+
"toml",
|
|
6548
|
+
"typescript",
|
|
6549
|
+
"vim",
|
|
6550
|
+
"vue",
|
|
6551
|
+
"xml",
|
|
6552
|
+
"yaml"
|
|
6553
|
+
]));
|
|
6554
|
+
const SOURCE_SHIKI_LANG_ALIASES = {
|
|
6555
|
+
makefile: "make",
|
|
6556
|
+
objectivec: "c",
|
|
6557
|
+
"objective-c": "c",
|
|
6558
|
+
"objective-cpp": "cpp",
|
|
6559
|
+
starlark: "python"
|
|
6560
|
+
};
|
|
6561
|
+
function normalizeSourceShikiLang(lang) {
|
|
6562
|
+
if (!lang)
|
|
6563
|
+
return null;
|
|
6564
|
+
return SOURCE_SHIKI_LANG_ALIASES[lang] || lang;
|
|
6565
|
+
}
|
|
6566
|
+
function loadSourceShikiHighlighter() {
|
|
6567
|
+
if (!sourceShikiLoadPromise) {
|
|
6568
|
+
sourceShikiLoadPromise = import("/shiki.js").then((mod) => {
|
|
6569
|
+
const typed = mod;
|
|
6570
|
+
const langs = typed.bundledLanguages ? SOURCE_SHIKI_LANGS.filter((lang) => !!typed.bundledLanguages?.[lang]) : SOURCE_SHIKI_LANGS;
|
|
6571
|
+
return typed.createHighlighter({
|
|
6572
|
+
themes: ["github-light", "github-dark"],
|
|
6573
|
+
langs
|
|
6574
|
+
});
|
|
6575
|
+
}).catch(() => null);
|
|
6576
|
+
}
|
|
6577
|
+
return sourceShikiLoadPromise;
|
|
6578
|
+
}
|
|
6579
|
+
function sourceShikiLines(textValue, lang, highlighter) {
|
|
6580
|
+
try {
|
|
6581
|
+
const html = highlighter.codeToHtml(textValue || " ", {
|
|
6582
|
+
lang,
|
|
6583
|
+
themes: { light: "github-light", dark: "github-dark" },
|
|
6584
|
+
defaultColor: false
|
|
6585
|
+
});
|
|
6586
|
+
const template = document.createElement("template");
|
|
6587
|
+
template.innerHTML = html;
|
|
6588
|
+
const renderedLines = Array.from(template.content.querySelectorAll(".line"));
|
|
6589
|
+
if (!renderedLines.length)
|
|
6590
|
+
return null;
|
|
6591
|
+
return renderedLines.map((line) => line.innerHTML || " ");
|
|
6592
|
+
} catch {
|
|
6593
|
+
return null;
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6490
6596
|
function rerenderLoadedDiffs() {
|
|
6491
6597
|
document.querySelectorAll(".gdp-file-shell.loaded").forEach((card) => {
|
|
6492
6598
|
const data = card._diffData;
|
|
@@ -6554,10 +6660,10 @@
|
|
|
6554
6660
|
}
|
|
6555
6661
|
function setFolderIcon(el, collapsed) {
|
|
6556
6662
|
const path = collapsed ? FOLDER_ICON_PATHS.closed : FOLDER_ICON_PATHS.open;
|
|
6557
|
-
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"
|
|
6663
|
+
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>';
|
|
6558
6664
|
}
|
|
6559
6665
|
function setChevronIcon(el) {
|
|
6560
|
-
el.innerHTML = '<svg class="octicon octicon-chevron-down" viewBox="0 0 12 12" width="12" height="12" fill="currentColor" aria-hidden="true"
|
|
6666
|
+
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>';
|
|
6561
6667
|
}
|
|
6562
6668
|
function iconSvg(className, paths) {
|
|
6563
6669
|
const pathList = Array.isArray(paths) ? paths : [paths];
|
|
@@ -6768,6 +6874,8 @@
|
|
|
6768
6874
|
ul.innerHTML = "";
|
|
6769
6875
|
ul.classList.toggle("tree", STATE.sbView === "tree");
|
|
6770
6876
|
STATE.files = files;
|
|
6877
|
+
if (!onFileClick)
|
|
6878
|
+
REPO_SIDEBAR_REF = null;
|
|
6771
6879
|
if (STATE.sbView === "tree") {
|
|
6772
6880
|
const root = buildTree(files);
|
|
6773
6881
|
renderTreeNode(root, 0, ul, onFileClick);
|
|
@@ -6827,7 +6935,7 @@
|
|
|
6827
6935
|
if (meta.totals) {
|
|
6828
6936
|
const t2 = document.createElement("span");
|
|
6829
6937
|
t2.className = "num";
|
|
6830
|
-
t2.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span>
|
|
6938
|
+
t2.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span> <span>" + meta.totals.files + " files</span>";
|
|
6831
6939
|
el.appendChild(t2);
|
|
6832
6940
|
}
|
|
6833
6941
|
const u2 = document.createElement("span");
|
|
@@ -7168,6 +7276,7 @@
|
|
|
7168
7276
|
});
|
|
7169
7277
|
if (!res.ok)
|
|
7170
7278
|
throw new Error(await res.text());
|
|
7279
|
+
invalidateRepoSidebar();
|
|
7171
7280
|
await loadRepo();
|
|
7172
7281
|
}
|
|
7173
7282
|
function createRepoUploadPanel(path) {
|
|
@@ -7282,8 +7391,8 @@
|
|
|
7282
7391
|
removeStandaloneSource();
|
|
7283
7392
|
$("#empty").classList.add("hidden");
|
|
7284
7393
|
$("#diff").replaceChildren();
|
|
7285
|
-
|
|
7286
|
-
|
|
7394
|
+
if (!isRepoSidebarReusable(meta.ref))
|
|
7395
|
+
$("#totals").textContent = "";
|
|
7287
7396
|
STATE.files = [];
|
|
7288
7397
|
LOAD_QUEUE.length = 0;
|
|
7289
7398
|
renderRepoBlobSidebar(meta.path || "", meta.ref);
|
|
@@ -7426,14 +7535,28 @@
|
|
|
7426
7535
|
}
|
|
7427
7536
|
function renderRepoBlobSidebar(currentPath, ref) {
|
|
7428
7537
|
syncRepoTargetInput(ref);
|
|
7538
|
+
const normalizedRef = ref || "worktree";
|
|
7539
|
+
if (isRepoSidebarReusable(normalizedRef)) {
|
|
7540
|
+
activateRepoSidebarPath(currentPath);
|
|
7541
|
+
return Promise.resolve();
|
|
7542
|
+
}
|
|
7543
|
+
if (REPO_SIDEBAR_LOAD && REPO_SIDEBAR_LOAD_REF === normalizedRef) {
|
|
7544
|
+
return REPO_SIDEBAR_LOAD.then(() => {
|
|
7545
|
+
activateRepoSidebarPath(currentPath);
|
|
7546
|
+
});
|
|
7547
|
+
}
|
|
7429
7548
|
const params = new URLSearchParams;
|
|
7430
|
-
params.set("ref",
|
|
7549
|
+
params.set("ref", normalizedRef);
|
|
7431
7550
|
params.set("recursive", "1");
|
|
7432
|
-
|
|
7551
|
+
REPO_SIDEBAR_LOAD_REF = normalizedRef;
|
|
7552
|
+
const load2 = trackLoad(fetch("/_tree?" + params.toString()).then((r2) => {
|
|
7433
7553
|
if (!r2.ok)
|
|
7434
7554
|
throw new Error("failed to load repository tree");
|
|
7435
7555
|
return r2.json();
|
|
7436
7556
|
})).then((meta) => {
|
|
7557
|
+
const activeRepoRef = repoFileTargetFromRoute() || (STATE.route.screen === "repo" ? STATE.route.ref : "");
|
|
7558
|
+
if ((activeRepoRef || "worktree") !== normalizedRef)
|
|
7559
|
+
return;
|
|
7437
7560
|
const files = meta.entries.map((entry, index) => ({
|
|
7438
7561
|
order: index + 1,
|
|
7439
7562
|
path: entry.path,
|
|
@@ -7443,19 +7566,31 @@
|
|
|
7443
7566
|
}));
|
|
7444
7567
|
renderSidebar(files, (file) => {
|
|
7445
7568
|
if (file.type === "tree") {
|
|
7446
|
-
setRoute(repoRoute(
|
|
7569
|
+
setRoute(repoRoute(normalizedRef, file.path));
|
|
7447
7570
|
loadRepo();
|
|
7448
7571
|
return;
|
|
7449
7572
|
}
|
|
7450
|
-
setRoute({ screen: "file", path: file.path, ref, view: "blob", range: currentRange() });
|
|
7451
|
-
renderStandaloneSource({ path: file.path, ref });
|
|
7573
|
+
setRoute({ screen: "file", path: file.path, ref: normalizedRef, view: "blob", range: currentRange() });
|
|
7574
|
+
renderStandaloneSource({ path: file.path, ref: normalizedRef });
|
|
7452
7575
|
});
|
|
7453
|
-
|
|
7454
|
-
|
|
7576
|
+
REPO_SIDEBAR_REF = normalizedRef;
|
|
7577
|
+
activateRepoSidebarPath(currentPath);
|
|
7455
7578
|
}).catch(() => {
|
|
7579
|
+
REPO_SIDEBAR_REF = null;
|
|
7456
7580
|
renderSidebar([], undefined);
|
|
7457
7581
|
$("#totals").textContent = "Cannot load tree";
|
|
7582
|
+
}).finally(() => {
|
|
7583
|
+
if (REPO_SIDEBAR_LOAD === load2) {
|
|
7584
|
+
REPO_SIDEBAR_LOAD_REF = null;
|
|
7585
|
+
REPO_SIDEBAR_LOAD = null;
|
|
7586
|
+
}
|
|
7458
7587
|
});
|
|
7588
|
+
REPO_SIDEBAR_LOAD = load2;
|
|
7589
|
+
return load2;
|
|
7590
|
+
}
|
|
7591
|
+
function activateRepoSidebarPath(currentPath) {
|
|
7592
|
+
markActive(currentPath);
|
|
7593
|
+
applyFilter();
|
|
7459
7594
|
}
|
|
7460
7595
|
function createPlaceholder(f2) {
|
|
7461
7596
|
const card = document.createElement("div");
|
|
@@ -7470,7 +7605,7 @@
|
|
|
7470
7605
|
}
|
|
7471
7606
|
const head = document.createElement("div");
|
|
7472
7607
|
head.className = "gdp-shell-header";
|
|
7473
|
-
head.innerHTML = '<span class="status-pill ' + escapeHtml2(f2.status || "M") + '">' + escapeHtml2(f2.status || "M") +
|
|
7608
|
+
head.innerHTML = '<span class="status-pill ' + escapeHtml2(f2.status || "M") + '">' + escapeHtml2(f2.status || "M") + '</span><span class="path">' + escapeHtml2(f2.display_path || f2.path) + '</span><span class="stats"><span class="a">+' + (f2.additions || 0) + "</span>" + '<span class="d">−' + (f2.deletions || 0) + '</span></span><span class="size-tag ' + escapeHtml2(f2.size_class || "") + '">' + escapeHtml2(f2.size_class || "") + "</span>" + '<span class="loading-indicator" hidden>loading…</span>';
|
|
7474
7609
|
card.appendChild(head);
|
|
7475
7610
|
const body = document.createElement("div");
|
|
7476
7611
|
body.className = "gdp-shell-body";
|
|
@@ -7908,7 +8043,7 @@
|
|
|
7908
8043
|
const button = document.createElement("button");
|
|
7909
8044
|
button.className = "gdp-expand-btn";
|
|
7910
8045
|
button.title = spec.title;
|
|
7911
|
-
button.innerHTML = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"
|
|
8046
|
+
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>';
|
|
7912
8047
|
button.addEventListener("click", (e2) => {
|
|
7913
8048
|
e2.stopPropagation();
|
|
7914
8049
|
if (button.disabled)
|
|
@@ -8006,9 +8141,9 @@
|
|
|
8006
8141
|
const num = sideIndex === 0 ? oldStart + i2 : newStart + i2;
|
|
8007
8142
|
lnHtml = '<td class="d2h-code-side-linenumber d2h-cntx">' + num + "</td>";
|
|
8008
8143
|
} else {
|
|
8009
|
-
lnHtml = '<td class="d2h-code-linenumber d2h-cntx"
|
|
8144
|
+
lnHtml = '<td class="d2h-code-linenumber d2h-cntx"><div class="line-num1">' + (oldStart + i2) + '</div><div class="line-num2">' + (newStart + i2) + "</div></td>";
|
|
8010
8145
|
}
|
|
8011
|
-
tr.innerHTML = lnHtml + '<td class="d2h-cntx"
|
|
8146
|
+
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[i2]) + "</span></div></td>";
|
|
8012
8147
|
frag.appendChild(tr);
|
|
8013
8148
|
}
|
|
8014
8149
|
tbody.insertBefore(frag, anchor);
|
|
@@ -8135,6 +8270,269 @@
|
|
|
8135
8270
|
function isPreviewableSource(path) {
|
|
8136
8271
|
return /\.(md|markdown|mdown|mkdn|mdx)$/i.test(path);
|
|
8137
8272
|
}
|
|
8273
|
+
const EXT_TO_LANG = {
|
|
8274
|
+
js: "javascript",
|
|
8275
|
+
mjs: "javascript",
|
|
8276
|
+
cjs: "javascript",
|
|
8277
|
+
ts: "typescript",
|
|
8278
|
+
tsx: "typescript",
|
|
8279
|
+
jsx: "javascript",
|
|
8280
|
+
py: "python",
|
|
8281
|
+
rb: "ruby",
|
|
8282
|
+
go: "go",
|
|
8283
|
+
rs: "rust",
|
|
8284
|
+
java: "java",
|
|
8285
|
+
kt: "kotlin",
|
|
8286
|
+
swift: "swift",
|
|
8287
|
+
c: "c",
|
|
8288
|
+
h: "c",
|
|
8289
|
+
cc: "cpp",
|
|
8290
|
+
cpp: "cpp",
|
|
8291
|
+
hpp: "cpp",
|
|
8292
|
+
cs: "csharp",
|
|
8293
|
+
php: "php",
|
|
8294
|
+
lua: "lua",
|
|
8295
|
+
sh: "bash",
|
|
8296
|
+
bash: "bash",
|
|
8297
|
+
zsh: "bash",
|
|
8298
|
+
fish: "bash",
|
|
8299
|
+
sql: "sql",
|
|
8300
|
+
json: "json",
|
|
8301
|
+
yaml: "yaml",
|
|
8302
|
+
yml: "yaml",
|
|
8303
|
+
toml: "toml",
|
|
8304
|
+
tf: "terraform",
|
|
8305
|
+
tfvars: "terraform",
|
|
8306
|
+
hcl: "terraform",
|
|
8307
|
+
xml: "xml",
|
|
8308
|
+
html: "xml",
|
|
8309
|
+
vue: "xml",
|
|
8310
|
+
css: "css",
|
|
8311
|
+
scss: "scss",
|
|
8312
|
+
md: "markdown",
|
|
8313
|
+
dockerfile: "dockerfile",
|
|
8314
|
+
proto: "protobuf",
|
|
8315
|
+
gradle: "gradle",
|
|
8316
|
+
properties: "properties",
|
|
8317
|
+
patch: "diff",
|
|
8318
|
+
diff: "diff",
|
|
8319
|
+
nix: "nix",
|
|
8320
|
+
cue: "cue",
|
|
8321
|
+
rego: "rego",
|
|
8322
|
+
bicep: "bicep",
|
|
8323
|
+
bazel: "starlark",
|
|
8324
|
+
bzl: "starlark",
|
|
8325
|
+
cmake: "cmake",
|
|
8326
|
+
groovy: "groovy",
|
|
8327
|
+
dart: "dart",
|
|
8328
|
+
scala: "scala",
|
|
8329
|
+
clj: "clojure",
|
|
8330
|
+
cljs: "clojure",
|
|
8331
|
+
cljc: "clojure",
|
|
8332
|
+
edn: "clojure",
|
|
8333
|
+
ex: "elixir",
|
|
8334
|
+
exs: "elixir",
|
|
8335
|
+
erl: "erlang",
|
|
8336
|
+
hrl: "erlang",
|
|
8337
|
+
hs: "haskell",
|
|
8338
|
+
lhs: "haskell",
|
|
8339
|
+
ml: "ocaml",
|
|
8340
|
+
mli: "ocaml",
|
|
8341
|
+
jl: "julia",
|
|
8342
|
+
r: "r",
|
|
8343
|
+
rmd: "r",
|
|
8344
|
+
pl: "perl",
|
|
8345
|
+
pm: "perl",
|
|
8346
|
+
tcl: "tcl",
|
|
8347
|
+
vim: "vim",
|
|
8348
|
+
f: "fortran",
|
|
8349
|
+
f90: "fortran",
|
|
8350
|
+
m: "objective-c",
|
|
8351
|
+
mm: "objective-cpp",
|
|
8352
|
+
tex: "tex",
|
|
8353
|
+
bib: "bibtex",
|
|
8354
|
+
rst: "rst"
|
|
8355
|
+
};
|
|
8356
|
+
const TEXT_SOURCE_EXTENSIONS = new Set([
|
|
8357
|
+
...Object.keys(EXT_TO_LANG),
|
|
8358
|
+
"txt",
|
|
8359
|
+
"md",
|
|
8360
|
+
"markdown",
|
|
8361
|
+
"mdown",
|
|
8362
|
+
"mkdn",
|
|
8363
|
+
"mdx",
|
|
8364
|
+
"json",
|
|
8365
|
+
"jsonc",
|
|
8366
|
+
"csv",
|
|
8367
|
+
"tsv",
|
|
8368
|
+
"yaml",
|
|
8369
|
+
"yml",
|
|
8370
|
+
"toml",
|
|
8371
|
+
"hcl",
|
|
8372
|
+
"tf",
|
|
8373
|
+
"tfvars",
|
|
8374
|
+
"tfstate",
|
|
8375
|
+
"xml",
|
|
8376
|
+
"html",
|
|
8377
|
+
"htm",
|
|
8378
|
+
"css",
|
|
8379
|
+
"scss",
|
|
8380
|
+
"sass",
|
|
8381
|
+
"less",
|
|
8382
|
+
"js",
|
|
8383
|
+
"jsx",
|
|
8384
|
+
"mjs",
|
|
8385
|
+
"cjs",
|
|
8386
|
+
"ts",
|
|
8387
|
+
"tsx",
|
|
8388
|
+
"mts",
|
|
8389
|
+
"cts",
|
|
8390
|
+
"vue",
|
|
8391
|
+
"svelte",
|
|
8392
|
+
"astro",
|
|
8393
|
+
"rs",
|
|
8394
|
+
"go",
|
|
8395
|
+
"py",
|
|
8396
|
+
"rb",
|
|
8397
|
+
"php",
|
|
8398
|
+
"java",
|
|
8399
|
+
"kt",
|
|
8400
|
+
"kts",
|
|
8401
|
+
"c",
|
|
8402
|
+
"cc",
|
|
8403
|
+
"cpp",
|
|
8404
|
+
"cxx",
|
|
8405
|
+
"h",
|
|
8406
|
+
"hpp",
|
|
8407
|
+
"cs",
|
|
8408
|
+
"swift",
|
|
8409
|
+
"sh",
|
|
8410
|
+
"bash",
|
|
8411
|
+
"zsh",
|
|
8412
|
+
"fish",
|
|
8413
|
+
"ps1",
|
|
8414
|
+
"sql",
|
|
8415
|
+
"graphql",
|
|
8416
|
+
"graphqls",
|
|
8417
|
+
"gql",
|
|
8418
|
+
"ini",
|
|
8419
|
+
"conf",
|
|
8420
|
+
"env",
|
|
8421
|
+
"properties",
|
|
8422
|
+
"gitignore",
|
|
8423
|
+
"dockerignore",
|
|
8424
|
+
"editorconfig",
|
|
8425
|
+
"lock",
|
|
8426
|
+
"log",
|
|
8427
|
+
"patch",
|
|
8428
|
+
"diff",
|
|
8429
|
+
"sum",
|
|
8430
|
+
"mk",
|
|
8431
|
+
"proto",
|
|
8432
|
+
"thrift",
|
|
8433
|
+
"prisma",
|
|
8434
|
+
"gradle",
|
|
8435
|
+
"cmake",
|
|
8436
|
+
"nix",
|
|
8437
|
+
"cue",
|
|
8438
|
+
"rego",
|
|
8439
|
+
"bicep",
|
|
8440
|
+
"bazel",
|
|
8441
|
+
"bzl",
|
|
8442
|
+
"dart",
|
|
8443
|
+
"scala",
|
|
8444
|
+
"clj",
|
|
8445
|
+
"cljs",
|
|
8446
|
+
"cljc",
|
|
8447
|
+
"edn",
|
|
8448
|
+
"ex",
|
|
8449
|
+
"exs",
|
|
8450
|
+
"erl",
|
|
8451
|
+
"hrl",
|
|
8452
|
+
"hs",
|
|
8453
|
+
"lhs",
|
|
8454
|
+
"ml",
|
|
8455
|
+
"mli",
|
|
8456
|
+
"jl",
|
|
8457
|
+
"r",
|
|
8458
|
+
"rmd",
|
|
8459
|
+
"pl",
|
|
8460
|
+
"pm",
|
|
8461
|
+
"tcl",
|
|
8462
|
+
"vim",
|
|
8463
|
+
"groovy",
|
|
8464
|
+
"f",
|
|
8465
|
+
"f90",
|
|
8466
|
+
"m",
|
|
8467
|
+
"mm",
|
|
8468
|
+
"pas",
|
|
8469
|
+
"tex",
|
|
8470
|
+
"bib",
|
|
8471
|
+
"rst",
|
|
8472
|
+
"adoc",
|
|
8473
|
+
"org",
|
|
8474
|
+
"ipynb",
|
|
8475
|
+
"ejs",
|
|
8476
|
+
"hbs",
|
|
8477
|
+
"mustache",
|
|
8478
|
+
"liquid",
|
|
8479
|
+
"pug"
|
|
8480
|
+
]);
|
|
8481
|
+
const TEXT_SOURCE_FILENAMES = new Set([
|
|
8482
|
+
"readme",
|
|
8483
|
+
"license",
|
|
8484
|
+
"copying",
|
|
8485
|
+
"authors",
|
|
8486
|
+
"contributors",
|
|
8487
|
+
"notice",
|
|
8488
|
+
"changelog",
|
|
8489
|
+
"todo",
|
|
8490
|
+
"manifest",
|
|
8491
|
+
"version",
|
|
8492
|
+
"codeowners",
|
|
8493
|
+
"go.mod",
|
|
8494
|
+
"build.bazel",
|
|
8495
|
+
"workspace.bazel",
|
|
8496
|
+
"module.bazel",
|
|
8497
|
+
"gemfile",
|
|
8498
|
+
"rakefile",
|
|
8499
|
+
"procfile",
|
|
8500
|
+
"brewfile",
|
|
8501
|
+
"gnumakefile",
|
|
8502
|
+
"bsdmakefile",
|
|
8503
|
+
".gitattributes",
|
|
8504
|
+
".gitmodules",
|
|
8505
|
+
".npmrc",
|
|
8506
|
+
".nvmrc",
|
|
8507
|
+
".yarnrc",
|
|
8508
|
+
".prettierrc",
|
|
8509
|
+
".eslintrc",
|
|
8510
|
+
".babelrc",
|
|
8511
|
+
".stylelintrc"
|
|
8512
|
+
]);
|
|
8513
|
+
const FILENAME_TO_LANG = {
|
|
8514
|
+
dockerfile: "dockerfile",
|
|
8515
|
+
makefile: "makefile",
|
|
8516
|
+
gnumakefile: "makefile",
|
|
8517
|
+
bsdmakefile: "makefile",
|
|
8518
|
+
"go.mod": "go",
|
|
8519
|
+
"build.bazel": "starlark",
|
|
8520
|
+
"workspace.bazel": "starlark",
|
|
8521
|
+
"module.bazel": "starlark"
|
|
8522
|
+
};
|
|
8523
|
+
function sourceFileName(path) {
|
|
8524
|
+
return (path.split("/").pop() || path).toLowerCase();
|
|
8525
|
+
}
|
|
8526
|
+
function sourceFileExtension(name) {
|
|
8527
|
+
const index = name.lastIndexOf(".");
|
|
8528
|
+
return index >= 0 ? name.slice(index + 1) : "";
|
|
8529
|
+
}
|
|
8530
|
+
function isDockerfileName(name) {
|
|
8531
|
+
return /^dockerfile(?:[.-].+)?$/i.test(name);
|
|
8532
|
+
}
|
|
8533
|
+
function isMakefileName(name) {
|
|
8534
|
+
return /^makefile(?:[.-].+)?$/i.test(name);
|
|
8535
|
+
}
|
|
8138
8536
|
function sourceDisplayKind(path) {
|
|
8139
8537
|
if (isVideo(path))
|
|
8140
8538
|
return "video";
|
|
@@ -8142,9 +8540,13 @@
|
|
|
8142
8540
|
return "image";
|
|
8143
8541
|
if (/\.pdf$/i.test(path))
|
|
8144
8542
|
return "pdf";
|
|
8145
|
-
|
|
8543
|
+
const name = sourceFileName(path);
|
|
8544
|
+
const ext = sourceFileExtension(name);
|
|
8545
|
+
if (TEXT_SOURCE_EXTENSIONS.has(ext))
|
|
8546
|
+
return "text";
|
|
8547
|
+
if (TEXT_SOURCE_FILENAMES.has(name))
|
|
8146
8548
|
return "text";
|
|
8147
|
-
if (
|
|
8549
|
+
if (isDockerfileName(name) || isMakefileName(name))
|
|
8148
8550
|
return "text";
|
|
8149
8551
|
return "unsupported";
|
|
8150
8552
|
}
|
|
@@ -8255,12 +8657,14 @@
|
|
|
8255
8657
|
header.textContent = target.path + " @ " + target.ref;
|
|
8256
8658
|
}
|
|
8257
8659
|
const lang = inferLang(target.path);
|
|
8258
|
-
const
|
|
8660
|
+
const usesVirtualSource = shouldVirtualizeSource(textValue, lines) && !isVirtualSourceDisabled();
|
|
8661
|
+
const hljsRef = STATE.syntaxHighlight && usesVirtualSource ? await loadSyntaxHighlighter() : null;
|
|
8662
|
+
const sourceShikiRef = STATE.syntaxHighlight && !usesVirtualSource ? await loadSourceShikiHighlighter() : null;
|
|
8259
8663
|
if (signal?.aborted)
|
|
8260
8664
|
return false;
|
|
8261
8665
|
const previewable = isPreviewableSource(target.path);
|
|
8262
8666
|
const tabsHost = card.querySelector(".gdp-file-detail-tabs");
|
|
8263
|
-
if (
|
|
8667
|
+
if (usesVirtualSource) {
|
|
8264
8668
|
const virtualCode = renderVirtualSource(target, textValue, lines, hljsRef, lang);
|
|
8265
8669
|
if (previewable) {
|
|
8266
8670
|
const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview");
|
|
@@ -8316,6 +8720,8 @@
|
|
|
8316
8720
|
const table2 = document.createElement("table");
|
|
8317
8721
|
table2.className = "gdp-source-table";
|
|
8318
8722
|
const tbody = document.createElement("tbody");
|
|
8723
|
+
const sourceShikiLang = normalizeSourceShikiLang(lang);
|
|
8724
|
+
const shikiLines = sourceShikiRef && sourceShikiLang ? sourceShikiLines(textValue, sourceShikiLang, sourceShikiRef) : null;
|
|
8319
8725
|
for (let index = 0;index < lines.length; index++) {
|
|
8320
8726
|
if (signal?.aborted)
|
|
8321
8727
|
return false;
|
|
@@ -8326,13 +8732,9 @@
|
|
|
8326
8732
|
num.textContent = String(index + 1);
|
|
8327
8733
|
const code2 = document.createElement("td");
|
|
8328
8734
|
code2.className = "gdp-source-line-code";
|
|
8329
|
-
if (
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
code2.classList.add("hljs");
|
|
8333
|
-
} catch {
|
|
8334
|
-
code2.textContent = line || " ";
|
|
8335
|
-
}
|
|
8735
|
+
if (shikiLines && shikiLines[index] != null) {
|
|
8736
|
+
code2.innerHTML = shikiLines[index] || " ";
|
|
8737
|
+
code2.classList.add("shiki");
|
|
8336
8738
|
} else {
|
|
8337
8739
|
code2.textContent = line || " ";
|
|
8338
8740
|
}
|
|
@@ -9005,46 +9407,15 @@
|
|
|
9005
9407
|
});
|
|
9006
9408
|
}
|
|
9007
9409
|
}
|
|
9008
|
-
const EXT_TO_LANG = {
|
|
9009
|
-
js: "javascript",
|
|
9010
|
-
mjs: "javascript",
|
|
9011
|
-
cjs: "javascript",
|
|
9012
|
-
ts: "typescript",
|
|
9013
|
-
tsx: "typescript",
|
|
9014
|
-
jsx: "javascript",
|
|
9015
|
-
py: "python",
|
|
9016
|
-
rb: "ruby",
|
|
9017
|
-
go: "go",
|
|
9018
|
-
rs: "rust",
|
|
9019
|
-
java: "java",
|
|
9020
|
-
kt: "kotlin",
|
|
9021
|
-
swift: "swift",
|
|
9022
|
-
c: "c",
|
|
9023
|
-
h: "c",
|
|
9024
|
-
cc: "cpp",
|
|
9025
|
-
cpp: "cpp",
|
|
9026
|
-
hpp: "cpp",
|
|
9027
|
-
cs: "csharp",
|
|
9028
|
-
php: "php",
|
|
9029
|
-
lua: "lua",
|
|
9030
|
-
sh: "bash",
|
|
9031
|
-
bash: "bash",
|
|
9032
|
-
zsh: "bash",
|
|
9033
|
-
fish: "bash",
|
|
9034
|
-
sql: "sql",
|
|
9035
|
-
json: "json",
|
|
9036
|
-
yaml: "yaml",
|
|
9037
|
-
yml: "yaml",
|
|
9038
|
-
toml: "toml",
|
|
9039
|
-
xml: "xml",
|
|
9040
|
-
html: "xml",
|
|
9041
|
-
vue: "xml",
|
|
9042
|
-
css: "css",
|
|
9043
|
-
scss: "scss",
|
|
9044
|
-
md: "markdown",
|
|
9045
|
-
dockerfile: "dockerfile"
|
|
9046
|
-
};
|
|
9047
9410
|
function inferLang(path) {
|
|
9411
|
+
const name = sourceFileName(path);
|
|
9412
|
+
const fileLang = FILENAME_TO_LANG[name];
|
|
9413
|
+
if (fileLang)
|
|
9414
|
+
return fileLang;
|
|
9415
|
+
if (isDockerfileName(name))
|
|
9416
|
+
return "dockerfile";
|
|
9417
|
+
if (isMakefileName(name))
|
|
9418
|
+
return "makefile";
|
|
9048
9419
|
const m = path.match(/\.([^.]+)$/);
|
|
9049
9420
|
if (!m)
|
|
9050
9421
|
return null;
|
|
@@ -9174,7 +9545,7 @@
|
|
|
9174
9545
|
leftHTML = mediaTag(path, "HEAD");
|
|
9175
9546
|
rightHTML = mediaTag(path, "worktree");
|
|
9176
9547
|
}
|
|
9177
|
-
container.innerHTML = '<div class="media-side"><div class="media-label del">Before</div>' + leftHTML +
|
|
9548
|
+
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>";
|
|
9178
9549
|
body.replaceWith(container);
|
|
9179
9550
|
}
|
|
9180
9551
|
function setupScrollSpy() {
|
|
@@ -9566,7 +9937,7 @@
|
|
|
9566
9937
|
const [sha, subject, author, when] = c2.split("\t");
|
|
9567
9938
|
if (!sha)
|
|
9568
9939
|
continue;
|
|
9569
|
-
html.push('<div class="rp-item-commit" data-val="' + escapeAttr(sha) + '"
|
|
9940
|
+
html.push('<div class="rp-item-commit" data-val="' + escapeAttr(sha) + '"><div class="row1"><span class="sha">' + escapeHtml2(sha) + '</span><span class="subject" title="' + escapeAttr(subject || "") + '">' + escapeHtml2(subject || "") + '</span></div><div class="row2"><span class="author">' + escapeHtml2(author || "") + '</span><span class="when">' + escapeHtml2(when || "") + "</span></div></div>");
|
|
9570
9941
|
}
|
|
9571
9942
|
} else if (popTab === "branches") {
|
|
9572
9943
|
const branches = (REFS.branches || []).filter(m);
|
|
@@ -9575,7 +9946,7 @@
|
|
|
9575
9946
|
}
|
|
9576
9947
|
for (const b2 of branches) {
|
|
9577
9948
|
const cur = b2 === REFS.current;
|
|
9578
|
-
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(b2) + '"
|
|
9949
|
+
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(b2) + '"><span class="name">' + escapeHtml2(b2) + "</span>" + (cur ? '<span class="badge cur">current</span>' : '<span class="badge">branch</span>') + "</div>");
|
|
9579
9950
|
}
|
|
9580
9951
|
} else if (popTab === "tags") {
|
|
9581
9952
|
const tags = (REFS.tags || []).filter(m);
|
|
@@ -9583,7 +9954,7 @@
|
|
|
9583
9954
|
html.push('<div class="rp-empty">no tags</div>');
|
|
9584
9955
|
}
|
|
9585
9956
|
for (const t2 of tags) {
|
|
9586
|
-
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(t2) + '"
|
|
9957
|
+
html.push('<div class="rp-item-ref" data-val="' + escapeAttr(t2) + '"><span class="name">' + escapeHtml2(t2) + '</span><span class="badge">tag</span></div>');
|
|
9587
9958
|
}
|
|
9588
9959
|
}
|
|
9589
9960
|
popBody.innerHTML = html.join("");
|
|
@@ -9826,6 +10197,7 @@
|
|
|
9826
10197
|
clearTimeout(sseTimer);
|
|
9827
10198
|
sseTimer = setTimeout(() => {
|
|
9828
10199
|
sseTimer = null;
|
|
10200
|
+
invalidateRepoSidebar();
|
|
9829
10201
|
const savedScroll = window.scrollY;
|
|
9830
10202
|
const savedActive = STATE.activeFile;
|
|
9831
10203
|
load().then(() => {
|
package/web/shiki.js
CHANGED
|
@@ -13178,5 +13178,6 @@ var createHighlighter = /* @__PURE__ */ createBundledHighlighter({
|
|
|
13178
13178
|
engine: () => (0, engine_oniguruma_exports.createOnigurumaEngine)(Promise.resolve().then(() => (init_wasm2(), exports_wasm2)))
|
|
13179
13179
|
});
|
|
13180
13180
|
export {
|
|
13181
|
-
createHighlighter
|
|
13181
|
+
createHighlighter,
|
|
13182
|
+
bundledLanguages
|
|
13182
13183
|
};
|
package/web/style.css
CHANGED
|
@@ -1660,6 +1660,14 @@ table.d2h-diff-table .d2h-code-line-prefix {
|
|
|
1660
1660
|
.gdp-source-line-code.hljs {
|
|
1661
1661
|
background: var(--bg);
|
|
1662
1662
|
}
|
|
1663
|
+
.gdp-source-line-code.shiki,
|
|
1664
|
+
.gdp-source-line-code.shiki span {
|
|
1665
|
+
color: var(--shiki-light) !important;
|
|
1666
|
+
}
|
|
1667
|
+
[data-theme="dark"] .gdp-source-line-code.shiki,
|
|
1668
|
+
[data-theme="dark"] .gdp-source-line-code.shiki span {
|
|
1669
|
+
color: var(--shiki-dark) !important;
|
|
1670
|
+
}
|
|
1663
1671
|
.gdp-source-virtual {
|
|
1664
1672
|
display: grid;
|
|
1665
1673
|
grid-template-rows: auto minmax(0, 1fr);
|
package/web-src/server/cache.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
1
4
|
// Short enough that a browser reload self-heals stale git data, while still
|
|
2
5
|
// coalescing bursts from one render pass.
|
|
3
6
|
export const CACHE_TTL_MS = 1500;
|
|
7
|
+
export const MAX_TIMED_CACHE_ENTRIES = 200;
|
|
4
8
|
|
|
5
9
|
export type TimedCacheEntry<T> = T & { storedAt: number };
|
|
6
10
|
|
|
@@ -11,3 +15,50 @@ export function cacheFresh<T>(
|
|
|
11
15
|
): cached is TimedCacheEntry<T> {
|
|
12
16
|
return !!cached && now - cached.storedAt <= ttlMs;
|
|
13
17
|
}
|
|
18
|
+
|
|
19
|
+
export function setTimedCacheEntry<T>(
|
|
20
|
+
cache: Map<string, TimedCacheEntry<T>>,
|
|
21
|
+
key: string,
|
|
22
|
+
value: T,
|
|
23
|
+
now = Date.now(),
|
|
24
|
+
maxEntries = MAX_TIMED_CACHE_ENTRIES,
|
|
25
|
+
): void {
|
|
26
|
+
cache.set(key, { ...value, storedAt: now });
|
|
27
|
+
while (cache.size > maxEntries) {
|
|
28
|
+
const oldest = cache.keys().next().value;
|
|
29
|
+
if (oldest === undefined) break;
|
|
30
|
+
cache.delete(oldest);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function worktreeFileSignature(path: string, cwd: string): string {
|
|
35
|
+
try {
|
|
36
|
+
const stats = lstatSync(join(cwd, path));
|
|
37
|
+
const inode = 'ino' in stats ? stats.ino : 0;
|
|
38
|
+
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
39
|
+
} catch {
|
|
40
|
+
return 'state:missing';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function fileDiffCacheKey(options: {
|
|
45
|
+
path: string;
|
|
46
|
+
oldPath?: string | null;
|
|
47
|
+
isUntracked: boolean;
|
|
48
|
+
range: { from?: string; to?: string };
|
|
49
|
+
extras: string[];
|
|
50
|
+
args: string[];
|
|
51
|
+
cwd: string;
|
|
52
|
+
}): string {
|
|
53
|
+
const worktreeTarget = options.range.from === 'worktree' || !options.range.to || options.range.to === 'worktree';
|
|
54
|
+
if (options.isUntracked && !worktreeTarget) {
|
|
55
|
+
throw new Error('untracked file diffs require a worktree range');
|
|
56
|
+
}
|
|
57
|
+
const signature = worktreeTarget
|
|
58
|
+
? `\0${worktreeFileSignature(options.path, options.cwd)}`
|
|
59
|
+
: '';
|
|
60
|
+
if (options.isUntracked) {
|
|
61
|
+
return `u\0${options.path}${signature}\0${options.extras.join('\0')}`;
|
|
62
|
+
}
|
|
63
|
+
return `t\0${options.path}\0${options.oldPath || ''}${signature}\0${[...options.extras, ...options.args].join('\0')}`;
|
|
64
|
+
}
|
|
@@ -4,7 +4,7 @@ import { closeSync, constants, existsSync, openSync, readFileSync, realpathSync,
|
|
|
4
4
|
import { basename, dirname, extname, join, normalize, relative } from 'node:path';
|
|
5
5
|
import { APP_ENTRY_PATHS, SPA_PATHS } from '../routes';
|
|
6
6
|
import type { DiffMeta, FileDiffResponse, FileMeta, FileRangeResponse, RepoTreeResponse } from '../types';
|
|
7
|
-
import { cacheFresh, type TimedCacheEntry } from './cache';
|
|
7
|
+
import { cacheFresh, fileDiffCacheKey, setTimedCacheEntry, type TimedCacheEntry } from './cache';
|
|
8
8
|
import { startDevAssetReload } from './dev-assets';
|
|
9
9
|
import * as git from './git';
|
|
10
10
|
import { isSameWorktreeRange } from './range';
|
|
@@ -273,14 +273,14 @@ function handleDiffJson(url: URL) {
|
|
|
273
273
|
fileCache.clear();
|
|
274
274
|
}
|
|
275
275
|
const body = JSON.stringify(payload);
|
|
276
|
-
metaCache
|
|
276
|
+
setTimedCacheEntry(metaCache, key, { body, sig });
|
|
277
277
|
return new Response(body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
278
278
|
}
|
|
279
279
|
const cached = metaCache.get(key);
|
|
280
280
|
if (cacheFresh(cached)) return new Response(cached.body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
281
281
|
const payload = computePayload(extras, range);
|
|
282
282
|
const body = JSON.stringify(payload);
|
|
283
|
-
metaCache
|
|
283
|
+
setTimedCacheEntry(metaCache, key, { body, sig: JSON.stringify({ ...payload, generation: undefined }) });
|
|
284
284
|
return new Response(body, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
285
285
|
}
|
|
286
286
|
|
|
@@ -420,9 +420,12 @@ function handleFileDiff(url: URL) {
|
|
|
420
420
|
}
|
|
421
421
|
const { args } = buildRangeArgs(range);
|
|
422
422
|
const oldPath = url.searchParams.get('old_path');
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
let cacheKey: string;
|
|
424
|
+
try {
|
|
425
|
+
cacheKey = fileDiffCacheKey({ path, oldPath, isUntracked, range, extras, args, cwd });
|
|
426
|
+
} catch {
|
|
427
|
+
return text('invalid diff range', 400);
|
|
428
|
+
}
|
|
426
429
|
const cached = fileCache.get(cacheKey);
|
|
427
430
|
let diffText: string;
|
|
428
431
|
let errText = '';
|
|
@@ -436,7 +439,7 @@ function handleFileDiff(url: URL) {
|
|
|
436
439
|
diffText = res.stdout || '';
|
|
437
440
|
if (res.code !== 0) errText = res.stderr;
|
|
438
441
|
}
|
|
439
|
-
fileCache
|
|
442
|
+
setTimedCacheEntry(fileCache, cacheKey, { diffText });
|
|
440
443
|
}
|
|
441
444
|
const mode = url.searchParams.get('mode') || 'full';
|
|
442
445
|
const truncated = mode === 'preview'
|