@youtyan/code-viewer 0.1.18 → 0.1.19

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.
@@ -82,7 +82,13 @@ function startDevAssetReload(options) {
82
82
  }
83
83
 
84
84
  // web-src/server/git.ts
85
- import { existsSync, lstatSync as lstatSync2, readdirSync, readFileSync } from "node:fs";
85
+ import {
86
+ existsSync,
87
+ lstatSync as lstatSync2,
88
+ readdirSync,
89
+ readFileSync
90
+ } from "node:fs";
91
+ import { join as join2 } from "node:path";
86
92
 
87
93
  // web-src/server/runtime.ts
88
94
  import { spawn, spawnSync } from "node:child_process";
@@ -235,9 +241,11 @@ async function writeWebResponse(res, response) {
235
241
  }
236
242
 
237
243
  // web-src/server/git.ts
238
- import { join as join2 } from "node:path";
239
244
  var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
240
245
  var WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
246
+ var DEFAULT_REF_COMMIT_LIMIT = 100;
247
+ var MAX_REF_COMMIT_LIMIT = 500;
248
+ var COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI";
241
249
  var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
242
250
  "node_modules",
243
251
  ".venv",
@@ -291,11 +299,19 @@ function catFileBlobStream(oid, cwd) {
291
299
  }
292
300
  function objectSize(ref, path, cwd) {
293
301
  const res = run(["git", "cat-file", "-s", `${ref}:${path}`], cwd);
294
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
302
+ return {
303
+ code: res.code,
304
+ size: Number(res.stdout.trim()) || 0,
305
+ stderr: res.stderr
306
+ };
295
307
  }
296
308
  function objectByteSize(oid, cwd) {
297
309
  const res = run(["git", "cat-file", "-s", oid], cwd);
298
- return { code: res.code, size: Number(res.stdout.trim()) || 0, stderr: res.stderr };
310
+ return {
311
+ code: res.code,
312
+ size: Number(res.stdout.trim()) || 0,
313
+ stderr: res.stderr
314
+ };
299
315
  }
300
316
  function objectId(ref, path, cwd) {
301
317
  const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
@@ -316,7 +332,12 @@ function verifyTreeRef(ref, cwd) {
316
332
  return res.code === 0;
317
333
  }
318
334
  function refs(cwd) {
319
- const out = { branches: [], tags: [], commits: [], current: "" };
335
+ const out = {
336
+ branches: [],
337
+ tags: [],
338
+ commits: [],
339
+ current: ""
340
+ };
320
341
  const branches = run([
321
342
  "git",
322
343
  "for-each-ref",
@@ -329,17 +350,104 @@ function refs(cwd) {
329
350
  out.branches = branches.stdout.split(`
330
351
  `).filter((line) => line && line !== "origin/HEAD");
331
352
  }
332
- const tags = run(["git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", "refs/tags"], cwd);
353
+ const tags = run([
354
+ "git",
355
+ "for-each-ref",
356
+ "--sort=-creatordate",
357
+ "--format=%(refname:short)",
358
+ "refs/tags"
359
+ ], cwd);
333
360
  if (tags.code === 0)
334
361
  out.tags = tags.stdout.split(`
335
362
  `).filter(Boolean);
336
- const commits = run(["git", "log", "-50", "--format=%h\t%s\t%an\t%ar"], cwd);
337
- if (commits.code === 0)
338
- out.commits = commits.stdout.split(`
339
- `).filter(Boolean);
363
+ out.commits = refCommits(cwd, "", DEFAULT_REF_COMMIT_LIMIT);
340
364
  out.current = currentBranch(cwd) || "";
341
365
  return out;
342
366
  }
367
+ function clampCommitLimit(max) {
368
+ return Math.max(1, Math.min(max, MAX_REF_COMMIT_LIMIT));
369
+ }
370
+ function parseCommitLog(stdout) {
371
+ const parts = stdout.split("\x00");
372
+ const commits = [];
373
+ for (let index = 0;index < parts.length; ) {
374
+ if (!parts[index]) {
375
+ index++;
376
+ continue;
377
+ }
378
+ const sha = parts[index++] || "";
379
+ const subject = parts[index++] || "";
380
+ const author = parts[index++] || "";
381
+ const when = parts[index++] || "";
382
+ if (sha)
383
+ commits.push({ sha, subject, author, when });
384
+ }
385
+ return commits;
386
+ }
387
+ function commitLogArgs(limit) {
388
+ return [
389
+ "git",
390
+ "log",
391
+ "--all",
392
+ "-z",
393
+ `--max-count=${limit}`,
394
+ `--format=${COMMIT_FORMAT}`
395
+ ];
396
+ }
397
+ function mergeCommitResults(limit, ...groups) {
398
+ const seen = new Set;
399
+ const merged = [];
400
+ for (const commits of groups) {
401
+ for (const commit of commits) {
402
+ if (!commit.sha || seen.has(commit.sha))
403
+ continue;
404
+ seen.add(commit.sha);
405
+ merged.push(commit);
406
+ if (merged.length >= limit)
407
+ return merged;
408
+ }
409
+ }
410
+ return merged;
411
+ }
412
+ function runCommitLog(cwd, args) {
413
+ const commits = run(args, cwd);
414
+ return commits.code === 0 ? parseCommitLog(commits.stdout) : [];
415
+ }
416
+ function refCommits(cwd, query = "", max = DEFAULT_REF_COMMIT_LIMIT) {
417
+ const limit = clampCommitLimit(max);
418
+ const trimmed = query.trim().slice(0, 200).replace(/\0/g, "");
419
+ const hashMatches = [];
420
+ if (/^[0-9a-f]{4,40}$/i.test(trimmed)) {
421
+ const verified = run(["git", "rev-parse", "--verify", `${trimmed}^{commit}`], cwd);
422
+ const single = run([
423
+ "git",
424
+ "log",
425
+ "-z",
426
+ "-1",
427
+ `--format=${COMMIT_FORMAT}`,
428
+ verified.code === 0 && verified.stdout.trim() ? verified.stdout.trim() : trimmed
429
+ ], cwd);
430
+ if (single.code === 0 && single.stdout.trim()) {
431
+ hashMatches.push(...parseCommitLog(single.stdout));
432
+ }
433
+ }
434
+ if (!trimmed) {
435
+ return runCommitLog(cwd, commitLogArgs(limit));
436
+ }
437
+ const subjectMatches = runCommitLog(cwd, [
438
+ ...commitLogArgs(limit),
439
+ "--regexp-ignore-case",
440
+ "--fixed-strings",
441
+ `--grep=${trimmed}`
442
+ ]);
443
+ const authorMatches = runCommitLog(cwd, [
444
+ ...commitLogArgs(limit),
445
+ "--regexp-ignore-case",
446
+ "--fixed-strings",
447
+ `--author=${trimmed}`
448
+ ]);
449
+ return mergeCommitResults(limit, hashMatches, subjectMatches, authorMatches);
450
+ }
343
451
  function nameStatus(args, cwd) {
344
452
  const res = run([
345
453
  "git",
@@ -366,7 +474,12 @@ function nameStatus(args, cwd) {
366
474
  const oldPath = parts[i++] || "";
367
475
  const path = parts[i++] || "";
368
476
  if (path)
369
- files.push({ status: kind, old_path: oldPath, path, similarity: Number(status.slice(1)) || undefined });
477
+ files.push({
478
+ status: kind,
479
+ old_path: oldPath,
480
+ path,
481
+ similarity: Number(status.slice(1)) || undefined
482
+ });
370
483
  } else {
371
484
  const path = parts[i++] || "";
372
485
  if (path)
@@ -512,7 +625,11 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
512
625
  continue;
513
626
  walk(full, entryPath, depth + 1);
514
627
  } else if (entry.isFile() || entry.isSymbolicLink()) {
515
- if (!pushRecursiveEntry({ name: entry.name, path: entryPath, type: "blob" }))
628
+ if (!pushRecursiveEntry({
629
+ name: entry.name,
630
+ path: entryPath,
631
+ type: "blob"
632
+ }))
516
633
  return;
517
634
  }
518
635
  }
@@ -545,7 +662,11 @@ function gitTreeEntries(ref, path, cwd, recursive) {
545
662
  if (!match)
546
663
  return null;
547
664
  const entryPath = match[2];
548
- return { name: entryPath.split("/").pop() || entryPath, path: entryPath, type: match[1] };
665
+ return {
666
+ name: entryPath.split("/").pop() || entryPath,
667
+ path: entryPath,
668
+ type: match[1]
669
+ };
549
670
  }).filter((entry) => !!entry);
550
671
  if (recursive)
551
672
  entries.sort((a, b) => a.path.localeCompare(b.path));
@@ -566,7 +687,11 @@ function worktreeFiles(cwd) {
566
687
  function listTree(ref, path, cwd, options = {}) {
567
688
  const base = normalizeTreePath(path);
568
689
  if (ref === "worktree") {
569
- return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames), stderr: "" };
690
+ return {
691
+ code: 0,
692
+ entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
693
+ stderr: ""
694
+ };
570
695
  }
571
696
  const direct = gitTreeEntries(ref, base, cwd, false);
572
697
  if (direct.code !== 0 || !options.recursive)
@@ -574,7 +699,11 @@ function listTree(ref, path, cwd, options = {}) {
574
699
  const recursive = gitTreeEntries(ref, base, cwd, true);
575
700
  if (recursive.code !== 0)
576
701
  return recursive;
577
- return { code: 0, entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries), stderr: "" };
702
+ return {
703
+ code: 0,
704
+ entries: combineDirectAndRecursiveFiles(direct.entries, recursive.entries),
705
+ stderr: ""
706
+ };
578
707
  }
579
708
  function untrackedMeta(cwd) {
580
709
  return untracked(cwd).map((path) => {
@@ -589,7 +718,14 @@ function untrackedMeta(cwd) {
589
718
  lines = data.toString("utf8").split(`
590
719
  `).length - 1;
591
720
  }
592
- return { path, status: "A", additions: binary ? 0 : lines, deletions: 0, binary, untracked: true };
721
+ return {
722
+ path,
723
+ status: "A",
724
+ additions: binary ? 0 : lines,
725
+ deletions: 0,
726
+ binary,
727
+ untracked: true
728
+ };
593
729
  });
594
730
  }
595
731
  function fileMeta(args, cwd, includeUntracked = false) {
@@ -990,7 +1126,12 @@ function buildFileSearchList(ref, generation, entries) {
990
1126
  }
991
1127
  function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
992
1128
  const safePaths = paths.length ? paths : ["."];
993
- const omitGlobs = omitDirNames.flatMap((name) => ["--glob", `!${name}/**`, "--glob", `!**/${name}/**`]);
1129
+ const omitGlobs = omitDirNames.flatMap((name) => [
1130
+ "--glob",
1131
+ `!${name}/**`,
1132
+ "--glob",
1133
+ `!**/${name}/**`
1134
+ ]);
994
1135
  const args = [
995
1136
  "rg",
996
1137
  "--no-config",
@@ -1029,7 +1170,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
1029
1170
  const preview = parsed[4];
1030
1171
  if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
1031
1172
  continue;
1032
- matches.push({ path, line: lineNo, column, preview: preview.slice(0, 500) });
1173
+ matches.push({
1174
+ path,
1175
+ line: lineNo,
1176
+ column,
1177
+ preview: preview.slice(0, 500)
1178
+ });
1033
1179
  }
1034
1180
  return matches;
1035
1181
  }
@@ -1728,6 +1874,12 @@ function handleGrep(url) {
1728
1874
  return text("invalid target", 400);
1729
1875
  return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
1730
1876
  }
1877
+ function handleRefCommits(url) {
1878
+ const query = url.searchParams.get("q") || "";
1879
+ const parsedMax = Number(url.searchParams.get("max") || "");
1880
+ const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
1881
+ return json({ commits: refCommits(cwd, query, max) });
1882
+ }
1731
1883
  function handleFileDiff(url) {
1732
1884
  const path = url.searchParams.get("path") || "";
1733
1885
  if (!safePath(path))
@@ -2300,6 +2452,8 @@ var server = await startServer({
2300
2452
  return handleFiles(url);
2301
2453
  if (url.pathname === "/_grep")
2302
2454
  return handleGrep(url);
2455
+ if (url.pathname === "/_commits")
2456
+ return handleRefCommits(url);
2303
2457
  if (url.pathname === "/file_diff")
2304
2458
  return handleFileDiff(url);
2305
2459
  if (url.pathname === "/file_range")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "check": "bun run typecheck",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "check:bundle": "bun build --target=browser --format=iife --outfile=/tmp/code-viewer-app.js web-src/app.ts && cmp /tmp/code-viewer-app.js web/app.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-mermaid.js web-src/mermaid-entry.ts && cmp /tmp/code-viewer-mermaid.js web/mermaid.js && bun build --target=browser --format=esm --outfile=/tmp/code-viewer-shiki.js web-src/shiki-entry.ts && cmp /tmp/code-viewer-shiki.js web/shiki.js",
35
- "check:format": "biome format biome.jsonc package.json web-src/app.ts web-src/server/preview.ts web-src/test/source-fixture.ts web-src/test/source-fixture.test.ts web-src/test/view-file-button.test.ts web-src/test/open-path.test.ts web-src/test/markdown-preview.test.ts web-src/test/search-server.test.ts web-src/test/upload-files.test.ts web-src/test/git-truncate.test.ts web-src/test/asset-version-removal.test.ts web-src/test/sidebar-folder-icon.test.ts web-src/test/mark-viewed-button.test.ts",
35
+ "check:format": "biome format .",
36
36
  "dev": "bun run web-src/server/dev.ts",
37
37
  "preview": "bun run web-src/server/dev.ts",
38
38
  "preview:raw": "bun run web-src/server/preview.ts",
package/web/app.js CHANGED
@@ -359,22 +359,58 @@
359
359
  if (isGlobPathQuery(query)) {
360
360
  return items.map((item) => {
361
361
  const match = globMatchPath(query, item.path);
362
- return match ? { item, score: match.score, ranges: match.ranges, mode: "glob" } : null;
362
+ return match ? {
363
+ item,
364
+ score: match.score,
365
+ ranges: match.ranges,
366
+ mode: "glob"
367
+ } : null;
363
368
  }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
364
369
  }
365
- return rankFuzzyPaths(query, items).map((item) => ({ ...item, mode: "fuzzy" }));
370
+ return rankFuzzyPaths(query, items).map((item) => ({
371
+ ...item,
372
+ mode: "fuzzy"
373
+ }));
366
374
  }
367
375
 
368
376
  // web-src/keymap.ts
369
377
  var DEFAULT_KEY_BINDINGS = [
370
- { action: "open-file-palette", key: "k", ctrl: true, allowEditable: true, allowPaletteOpen: true },
371
- { action: "open-file-palette", key: "k", meta: true, allowEditable: true, allowPaletteOpen: true },
372
- { action: "open-grep-palette", key: "g", ctrl: true, allowEditable: true, allowPaletteOpen: true },
373
- { action: "open-grep-palette", key: "g", meta: true, allowEditable: true, allowPaletteOpen: true },
378
+ {
379
+ action: "open-file-palette",
380
+ key: "k",
381
+ ctrl: true,
382
+ allowEditable: true,
383
+ allowPaletteOpen: true
384
+ },
385
+ {
386
+ action: "open-file-palette",
387
+ key: "k",
388
+ meta: true,
389
+ allowEditable: true,
390
+ allowPaletteOpen: true
391
+ },
392
+ {
393
+ action: "open-grep-palette",
394
+ key: "g",
395
+ ctrl: true,
396
+ allowEditable: true,
397
+ allowPaletteOpen: true
398
+ },
399
+ {
400
+ action: "open-grep-palette",
401
+ key: "g",
402
+ meta: true,
403
+ allowEditable: true,
404
+ allowPaletteOpen: true
405
+ },
374
406
  { action: "focus-file-filter", key: "/" },
375
407
  { action: "focus-sidebar", key: "h", ctrl: true },
376
408
  { action: "focus-main", key: "l", ctrl: true },
377
- { action: "cancel-source-load", key: "escape", requires: { lightboxClosed: true } },
409
+ {
410
+ action: "cancel-source-load",
411
+ key: "escape",
412
+ requires: { lightboxClosed: true }
413
+ },
378
414
  { action: "open-sidebar-item", key: "enter", scope: "sidebar" },
379
415
  { action: "open-sidebar-item", key: "enter", scope: "global" },
380
416
  { action: "sidebar-next", key: "j", scope: "sidebar" },
@@ -399,12 +435,37 @@
399
435
  { action: "scroll-main-page-up", key: "pageup", scope: "global" },
400
436
  { action: "scroll-main-page-down", key: "pagedown", scope: "sidebar" },
401
437
  { action: "scroll-main-page-up", key: "pageup", scope: "sidebar" },
402
- { action: "scroll-main-page-down", key: "arrowdown", scope: "main", ctrl: true },
438
+ {
439
+ action: "scroll-main-page-down",
440
+ key: "arrowdown",
441
+ scope: "main",
442
+ ctrl: true
443
+ },
403
444
  { action: "scroll-main-page-up", key: "arrowup", scope: "main", ctrl: true },
404
- { action: "scroll-main-page-down", key: "arrowdown", scope: "global", ctrl: true },
405
- { action: "scroll-main-page-up", key: "arrowup", scope: "global", ctrl: true },
406
- { action: "scroll-main-page-down", key: "arrowdown", scope: "sidebar", ctrl: true },
407
- { action: "scroll-main-page-up", key: "arrowup", scope: "sidebar", ctrl: true },
445
+ {
446
+ action: "scroll-main-page-down",
447
+ key: "arrowdown",
448
+ scope: "global",
449
+ ctrl: true
450
+ },
451
+ {
452
+ action: "scroll-main-page-up",
453
+ key: "arrowup",
454
+ scope: "global",
455
+ ctrl: true
456
+ },
457
+ {
458
+ action: "scroll-main-page-down",
459
+ key: "arrowdown",
460
+ scope: "sidebar",
461
+ ctrl: true
462
+ },
463
+ {
464
+ action: "scroll-main-page-up",
465
+ key: "arrowup",
466
+ scope: "sidebar",
467
+ ctrl: true
468
+ },
408
469
  { action: "tab-preview", key: "p", scope: "main", pendingG: true },
409
470
  { action: "tab-code", key: "c", scope: "main", pendingG: true },
410
471
  { action: "goto-top", key: "g", pendingG: true },
@@ -6091,8 +6152,21 @@
6091
6152
  const ref = target || params.get("ref") || "worktree";
6092
6153
  const line = parseLineTarget(params.get("line"));
6093
6154
  if (!path)
6094
- return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
6095
- return { screen: "file", path, ref, range, view: target ? "blob" : "detail", ...line ? { line } : {} };
6155
+ return {
6156
+ screen: "unknown",
6157
+ reason: "missing-path",
6158
+ rawPathname: pathname,
6159
+ rawSearch: search,
6160
+ range
6161
+ };
6162
+ return {
6163
+ screen: "file",
6164
+ path,
6165
+ ref,
6166
+ range,
6167
+ view: target ? "blob" : "detail",
6168
+ ...line ? { line } : {}
6169
+ };
6096
6170
  }
6097
6171
  case "/help":
6098
6172
  return {
@@ -6102,7 +6176,13 @@
6102
6176
  section: params.get("section") || "keybindings"
6103
6177
  };
6104
6178
  default:
6105
- return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
6179
+ return {
6180
+ screen: "unknown",
6181
+ reason: "unknown-pathname",
6182
+ rawPathname: pathname,
6183
+ rawSearch: search,
6184
+ range
6185
+ };
6106
6186
  }
6107
6187
  }
6108
6188
  function buildRoute(route) {
@@ -6218,7 +6298,10 @@
6218
6298
  }
6219
6299
  function resolveRepoRelative(currentPath, requestedPath) {
6220
6300
  const base2 = currentPath.split("/").slice(0, -1);
6221
- const parts = [...requestedPath.startsWith("/") ? [] : base2, ...requestedPath.split("/")].filter((part) => part && part !== ".");
6301
+ const parts = [
6302
+ ...requestedPath.startsWith("/") ? [] : base2,
6303
+ ...requestedPath.split("/")
6304
+ ].filter((part) => part && part !== ".");
6222
6305
  const resolved = [];
6223
6306
  for (const part of parts) {
6224
6307
  if (part === "..") {
@@ -6440,7 +6523,10 @@
6440
6523
  const toc = root.querySelector(".gdp-markdown-toc");
6441
6524
  if (!toc)
6442
6525
  return;
6443
- const entries = Array.from(toc.querySelectorAll("a[data-target]")).map((link2) => ({ link: link2, target: root.querySelector("#" + CSS.escape(link2.dataset.target || "")) })).filter((entry) => !!entry.target);
6526
+ const entries = Array.from(toc.querySelectorAll("a[data-target]")).map((link2) => ({
6527
+ link: link2,
6528
+ target: root.querySelector("#" + CSS.escape(link2.dataset.target || ""))
6529
+ })).filter((entry) => !!entry.target);
6444
6530
  if (!entries.length)
6445
6531
  return;
6446
6532
  toc.addEventListener("click", (e2) => {
@@ -6485,7 +6571,10 @@
6485
6571
  if (!raf)
6486
6572
  raf = requestAnimationFrame(update);
6487
6573
  };
6488
- window.addEventListener("scroll", schedule, { passive: true, signal: controller.signal });
6574
+ window.addEventListener("scroll", schedule, {
6575
+ passive: true,
6576
+ signal: controller.signal
6577
+ });
6489
6578
  window.addEventListener("resize", schedule, { signal: controller.signal });
6490
6579
  setTimeout(() => {
6491
6580
  if (!root.isConnected)
@@ -6537,7 +6626,11 @@
6537
6626
  const typed = mod;
6538
6627
  const mermaid = typed.default;
6539
6628
  if (!mermaidInitialized) {
6540
- mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: "default" });
6629
+ mermaid.initialize({
6630
+ startOnLoad: false,
6631
+ securityLevel: "strict",
6632
+ theme: "default"
6633
+ });
6541
6634
  mermaidInitialized = true;
6542
6635
  }
6543
6636
  return mermaid;
@@ -12438,20 +12531,58 @@
12438
12531
  }
12439
12532
  fetchRefs();
12440
12533
  let popTab = "commits";
12534
+ let commitSearchTimer = null;
12535
+ let commitSearchSeq = 0;
12536
+ let commitSearchAbort = null;
12537
+ let commitSearchLoading = false;
12538
+ function fetchCommitRefs(query) {
12539
+ const seq = ++commitSearchSeq;
12540
+ if (commitSearchAbort)
12541
+ commitSearchAbort.abort();
12542
+ commitSearchAbort = new AbortController;
12543
+ const url = "/_commits?max=100&q=" + encodeURIComponent((query || "").trim());
12544
+ return fetch(url, { signal: commitSearchAbort.signal }).then((r2) => r2.json()).then((refs) => {
12545
+ if (seq !== commitSearchSeq)
12546
+ return;
12547
+ commitSearchLoading = false;
12548
+ REFS.commits = refs.commits || [];
12549
+ if (!popover.hidden && popTab === "commits") {
12550
+ buildPopBody(popSearch.value);
12551
+ }
12552
+ }).catch(() => {
12553
+ if (seq === commitSearchSeq)
12554
+ commitSearchLoading = false;
12555
+ });
12556
+ }
12557
+ function scheduleCommitSearch(query) {
12558
+ if (commitSearchTimer)
12559
+ clearTimeout(commitSearchTimer);
12560
+ commitSearchLoading = true;
12561
+ commitSearchTimer = setTimeout(() => {
12562
+ commitSearchTimer = null;
12563
+ fetchCommitRefs(query);
12564
+ }, 150);
12565
+ }
12441
12566
  function buildPopBody(query) {
12442
12567
  const q = (query || "").toLowerCase().trim();
12443
12568
  const m = (s2) => !q || String(s2).toLowerCase().includes(q);
12444
12569
  const html = [];
12445
12570
  if (popTab === "commits") {
12446
- const commits = (REFS.commits || []).filter((c2) => m(c2));
12571
+ if (commitSearchLoading) {
12572
+ html.push('<div class="rp-empty">loading commits...</div>');
12573
+ popBody.innerHTML = html.join("");
12574
+ highlightCurrentInPopover();
12575
+ return;
12576
+ }
12577
+ const commits = (REFS.commits || []).filter((commit) => m(`${commit.sha} ${commit.subject} ${commit.author}`));
12447
12578
  if (!commits.length) {
12448
12579
  html.push('<div class="rp-empty">no commits</div>');
12449
12580
  }
12450
- for (const c2 of commits) {
12451
- const [sha, subject, author, when] = c2.split("\t");
12452
- if (!sha)
12581
+ for (const commit of commits) {
12582
+ if (!commit.sha)
12453
12583
  continue;
12454
- 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>");
12584
+ const shortSha = commit.sha.slice(0, 7);
12585
+ html.push('<div class="rp-item-commit" data-val="' + escapeAttr(commit.sha) + '"><div class="row1"><span class="sha">' + escapeHtml2(shortSha) + '</span><span class="subject" title="' + escapeAttr(commit.subject || "") + '">' + escapeHtml2(commit.subject || "") + '</span></div><div class="row2"><span class="author">' + escapeHtml2(commit.author || "") + '</span><span class="when">' + escapeHtml2(commit.when || "") + "</span></div></div>");
12455
12586
  }
12456
12587
  } else if (popTab === "branches") {
12457
12588
  const branches = (REFS.branches || []).filter(m);
@@ -12502,6 +12633,8 @@
12502
12633
  function openPopover(input) {
12503
12634
  popTarget = input;
12504
12635
  popSearch.value = "";
12636
+ if (popTab === "commits")
12637
+ scheduleCommitSearch("");
12505
12638
  buildPopBody("");
12506
12639
  const cur = (input.value || "").trim();
12507
12640
  popover.querySelectorAll(".rp-chip").forEach((c2) => {
@@ -12539,13 +12672,17 @@
12539
12672
  });
12540
12673
  renderStandaloneSource({ path: STATE.route.path, ref });
12541
12674
  });
12542
- popSearch.addEventListener("input", () => buildPopBody(popSearch.value));
12675
+ popSearch.addEventListener("input", () => {
12676
+ if (popTab === "commits")
12677
+ scheduleCommitSearch(popSearch.value);
12678
+ buildPopBody(popSearch.value);
12679
+ });
12543
12680
  popSearch.addEventListener("keydown", (e2) => {
12544
12681
  if (e2.key === "Escape") {
12545
12682
  closePopover();
12546
12683
  }
12547
12684
  if (e2.key === "Enter") {
12548
- const first = popBody.querySelector(".rp-item");
12685
+ const first = popBody.querySelector(".rp-item-commit, .rp-item-ref");
12549
12686
  if (first)
12550
12687
  first.click();
12551
12688
  }
@@ -12568,6 +12705,8 @@
12568
12705
  t2.addEventListener("click", () => {
12569
12706
  popTab = t2.dataset.tab || "commits";
12570
12707
  popover.querySelectorAll(".rp-tab").forEach((b2) => b2.classList.toggle("active", b2 === t2));
12708
+ if (popTab === "commits")
12709
+ scheduleCommitSearch(popSearch.value);
12571
12710
  buildPopBody(popSearch.value);
12572
12711
  });
12573
12712
  });