@youtyan/code-viewer 0.1.10 → 0.1.12

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/web/app.js CHANGED
@@ -131,6 +131,164 @@
131
131
  };
132
132
  }
133
133
 
134
+ // web-src/fuzzy-search.ts
135
+ function basenameStart(path) {
136
+ const slash = path.lastIndexOf("/");
137
+ return slash < 0 ? 0 : slash + 1;
138
+ }
139
+ function isBoundary(path, index) {
140
+ if (index <= 0)
141
+ return true;
142
+ const prev = path[index - 1];
143
+ return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
144
+ }
145
+ function toRanges(indices) {
146
+ const ranges = [];
147
+ for (const index of indices) {
148
+ const last = ranges[ranges.length - 1];
149
+ if (last && last.end === index) {
150
+ last.end = index + 1;
151
+ } else {
152
+ ranges.push({ start: index, end: index + 1 });
153
+ }
154
+ }
155
+ return ranges;
156
+ }
157
+ function fuzzyMatchPath(query, path) {
158
+ const q = query.trim().toLowerCase();
159
+ if (!q)
160
+ return { score: 0, ranges: [] };
161
+ const lowerPath = path.toLowerCase();
162
+ const baseStart = basenameStart(path);
163
+ const indices = [];
164
+ let from = 0;
165
+ let score = 0;
166
+ for (const ch of q) {
167
+ const index = lowerPath.indexOf(ch, from);
168
+ if (index < 0)
169
+ return null;
170
+ indices.push(index);
171
+ score += 10;
172
+ if (index >= baseStart)
173
+ score += 8;
174
+ if (isBoundary(path, index))
175
+ score += 6;
176
+ const prev = indices[indices.length - 2];
177
+ if (prev != null && prev + 1 === index)
178
+ score += 12;
179
+ from = index + 1;
180
+ }
181
+ const first = indices[0] || 0;
182
+ score -= Math.min(first, 40);
183
+ if (indices[0] >= baseStart)
184
+ score += 20;
185
+ const basename = lowerPath.slice(baseStart);
186
+ if (basename.startsWith(q))
187
+ score += 30;
188
+ if (basename === q || basename.startsWith(q + "."))
189
+ score += 25;
190
+ if (lowerPath.endsWith(q))
191
+ score += 15;
192
+ return { score, ranges: toRanges(indices) };
193
+ }
194
+ function rankFuzzyPaths(query, items) {
195
+ return items.map((item) => {
196
+ const match = fuzzyMatchPath(query, item.path);
197
+ return match ? { item, score: match.score, ranges: match.ranges } : null;
198
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
199
+ }
200
+ function isGlobPathQuery(query) {
201
+ return /[*?]/.test(query.trim());
202
+ }
203
+ function escapeRegexChar(ch) {
204
+ return /[\\^$+?.()|{}]/.test(ch) ? "\\" + ch : ch;
205
+ }
206
+ function globToRegExp(query) {
207
+ const pattern = query.trim();
208
+ if (!pattern)
209
+ return null;
210
+ let source = "^";
211
+ for (let i = 0;i < pattern.length; i++) {
212
+ const ch = pattern[i];
213
+ if (ch === "*") {
214
+ if (pattern[i + 1] === "*") {
215
+ source += ".*";
216
+ i++;
217
+ } else {
218
+ source += "[^/]*";
219
+ }
220
+ } else if (ch === "?") {
221
+ source += "[^/]";
222
+ } else if (ch === "[") {
223
+ const close = pattern.indexOf("]", i + 1);
224
+ if (close < 0) {
225
+ source += "\\[";
226
+ } else {
227
+ const body = pattern.slice(i + 1, close).replace(/\\/g, "\\\\");
228
+ source += "[" + body + "]";
229
+ i = close;
230
+ }
231
+ } else {
232
+ source += escapeRegexChar(ch);
233
+ }
234
+ }
235
+ source += "$";
236
+ try {
237
+ return new RegExp(source, "i");
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+ function globMatchPath(query, path) {
243
+ const regex = globToRegExp(query);
244
+ const baseStart = basenameStart(path);
245
+ const basename = path.slice(baseStart);
246
+ if (!regex || !regex.test(path) && (query.includes("/") || !regex.test(basename)))
247
+ return null;
248
+ const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
249
+ const ranges = [];
250
+ const lowerPath = path.toLowerCase();
251
+ for (const part of literal) {
252
+ const start = lowerPath.indexOf(part.toLowerCase());
253
+ if (start >= 0)
254
+ ranges.push({ start, end: start + part.length });
255
+ }
256
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
257
+ const mergedRanges = [];
258
+ for (const range of ranges) {
259
+ const last = mergedRanges[mergedRanges.length - 1];
260
+ if (last && last.end >= range.start) {
261
+ last.end = Math.max(last.end, range.end);
262
+ } else {
263
+ mergedRanges.push({ ...range });
264
+ }
265
+ }
266
+ const score = 1000 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(query.replace(/^\*+/, "").toLowerCase()) ? 50 : 0);
267
+ return { score, ranges: mergedRanges };
268
+ }
269
+ function rankPathMatches(query, items) {
270
+ if (isGlobPathQuery(query)) {
271
+ return items.map((item) => {
272
+ const match = globMatchPath(query, item.path);
273
+ return match ? { item, score: match.score, ranges: match.ranges, mode: "glob" } : null;
274
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
275
+ }
276
+ return rankFuzzyPaths(query, items).map((item) => ({ ...item, mode: "fuzzy" }));
277
+ }
278
+
279
+ // web-src/search-palette.ts
280
+ var PALETTE_RESULT_LIMIT = 50;
281
+ function limitPaletteResults(items) {
282
+ return items.slice(0, PALETTE_RESULT_LIMIT);
283
+ }
284
+ function movePaletteSelection(index, count, direction) {
285
+ if (count <= 0)
286
+ return -1;
287
+ if (index < 0)
288
+ return direction > 0 ? 0 : count - 1;
289
+ return (index + direction + count) % count;
290
+ }
291
+
134
292
  // web-src/catch-up.ts
135
293
  function shouldCatchUpDiff(route) {
136
294
  return route.screen !== "repo" && !(route.screen === "file" && route.view === "blob");
@@ -160,6 +318,24 @@
160
318
  to: raw.slice(sep + 2) || fallback.to
161
319
  };
162
320
  }
321
+ function parseLineTarget(value) {
322
+ const raw = value || "";
323
+ const range = /^(\d+)-(\d+)$/.exec(raw);
324
+ if (range) {
325
+ const a = Number(range[1]);
326
+ const b = Number(range[2]);
327
+ const start = Math.min(a, b);
328
+ const end = Math.max(a, b);
329
+ if (start > 0)
330
+ return { start, end };
331
+ return;
332
+ }
333
+ const line = Number(raw);
334
+ return Number.isInteger(line) && line > 0 ? line : undefined;
335
+ }
336
+ function formatLineTarget(line) {
337
+ return typeof line === "number" ? String(line) : line.start + "-" + line.end;
338
+ }
163
339
  function parseRoute(pathname, search, fallbackRange) {
164
340
  const params = new URLSearchParams(search);
165
341
  const legacyRange = parseLegacyRange(params.get("range"), fallbackRange);
@@ -178,14 +354,20 @@
178
354
  };
179
355
  case "/todif":
180
356
  case "/todiff":
181
- return { screen: "diff", range };
357
+ return {
358
+ screen: "diff",
359
+ range,
360
+ ...params.get("path") ? { path: params.get("path") || "" } : {},
361
+ ...parseLineTarget(params.get("line")) ? { line: parseLineTarget(params.get("line")) } : {}
362
+ };
182
363
  case "/file": {
183
364
  const path = params.get("path") || "";
184
365
  const target = params.get("target") || "";
185
366
  const ref = target || params.get("ref") || "worktree";
367
+ const line = parseLineTarget(params.get("line"));
186
368
  if (!path)
187
369
  return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
188
- return { screen: "file", path, ref, range, view: target ? "blob" : "detail" };
370
+ return { screen: "file", path, ref, range, view: target ? "blob" : "detail", ...line ? { line } : {} };
189
371
  }
190
372
  default:
191
373
  return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
@@ -204,11 +386,11 @@
204
386
  }
205
387
  case "file":
206
388
  if (route.view === "blob") {
207
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree");
389
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
208
390
  }
209
- return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
391
+ return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
210
392
  case "diff":
211
- return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
393
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? "&path=" + encodeURIComponent(route.path) : "") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
212
394
  case "unknown":
213
395
  return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
214
396
  default:
@@ -6393,6 +6575,7 @@
6393
6575
  const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
6394
6576
  const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
6395
6577
  let highlightLoadPromise = null;
6578
+ let sourceShikiLoadPromise = null;
6396
6579
  let highlightConfigured = false;
6397
6580
  let PROJECT_NAME = "";
6398
6581
  let REPO_SIDEBAR_REF = null;
@@ -6498,6 +6681,100 @@
6498
6681
  });
6499
6682
  return highlightLoadPromise;
6500
6683
  }
6684
+ const SOURCE_SHIKI_LANGS = Array.from(new Set([
6685
+ "bash",
6686
+ "bibtex",
6687
+ "c",
6688
+ "clojure",
6689
+ "cmake",
6690
+ "cpp",
6691
+ "csharp",
6692
+ "css",
6693
+ "dart",
6694
+ "diff",
6695
+ "dockerfile",
6696
+ "elixir",
6697
+ "erlang",
6698
+ "fortran",
6699
+ "go",
6700
+ "gradle",
6701
+ "graphql",
6702
+ "haskell",
6703
+ "html",
6704
+ "java",
6705
+ "javascript",
6706
+ "json",
6707
+ "julia",
6708
+ "kotlin",
6709
+ "lua",
6710
+ "make",
6711
+ "markdown",
6712
+ "nix",
6713
+ "ocaml",
6714
+ "perl",
6715
+ "php",
6716
+ "properties",
6717
+ "protobuf",
6718
+ "python",
6719
+ "r",
6720
+ "rst",
6721
+ "ruby",
6722
+ "rust",
6723
+ "scala",
6724
+ "scss",
6725
+ "sql",
6726
+ "swift",
6727
+ "terraform",
6728
+ "tex",
6729
+ "toml",
6730
+ "typescript",
6731
+ "vim",
6732
+ "vue",
6733
+ "xml",
6734
+ "yaml"
6735
+ ]));
6736
+ const SOURCE_SHIKI_LANG_ALIASES = {
6737
+ makefile: "make",
6738
+ objectivec: "c",
6739
+ "objective-c": "c",
6740
+ "objective-cpp": "cpp",
6741
+ starlark: "python"
6742
+ };
6743
+ function normalizeSourceShikiLang(lang) {
6744
+ if (!lang)
6745
+ return null;
6746
+ return SOURCE_SHIKI_LANG_ALIASES[lang] || lang;
6747
+ }
6748
+ function loadSourceShikiHighlighter() {
6749
+ if (!sourceShikiLoadPromise) {
6750
+ sourceShikiLoadPromise = import("/shiki.js").then((mod) => {
6751
+ const typed = mod;
6752
+ const langs = typed.bundledLanguages ? SOURCE_SHIKI_LANGS.filter((lang) => !!typed.bundledLanguages?.[lang]) : SOURCE_SHIKI_LANGS;
6753
+ return typed.createHighlighter({
6754
+ themes: ["github-light", "github-dark"],
6755
+ langs
6756
+ });
6757
+ }).catch(() => null);
6758
+ }
6759
+ return sourceShikiLoadPromise;
6760
+ }
6761
+ function sourceShikiLines(textValue, lang, highlighter) {
6762
+ try {
6763
+ const html = highlighter.codeToHtml(textValue || " ", {
6764
+ lang,
6765
+ themes: { light: "github-light", dark: "github-dark" },
6766
+ defaultColor: false
6767
+ });
6768
+ const template = document.createElement("template");
6769
+ template.innerHTML = html;
6770
+ const renderedLines = Array.from(template.content.querySelectorAll(".line"));
6771
+ if (!renderedLines.length)
6772
+ return null;
6773
+ return renderedLines.map((line) => line.innerHTML || " ");
6774
+ } catch {
6775
+ return null;
6776
+ }
6777
+ }
6501
6778
  function rerenderLoadedDiffs() {
6502
6779
  document.querySelectorAll(".gdp-file-shell.loaded").forEach((card) => {
6503
6780
  const data = card._diffData;
@@ -6565,10 +6842,10 @@
6565
6842
  }
6566
6843
  function setFolderIcon(el, collapsed) {
6567
6844
  const path = collapsed ? FOLDER_ICON_PATHS.closed : FOLDER_ICON_PATHS.open;
6568
- 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>';
6845
+ 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>';
6569
6846
  }
6570
6847
  function setChevronIcon(el) {
6571
- 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>';
6848
+ 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>';
6572
6849
  }
6573
6850
  function iconSvg(className, paths) {
6574
6851
  const pathList = Array.isArray(paths) ? paths : [paths];
@@ -6840,7 +7117,7 @@
6840
7117
  if (meta.totals) {
6841
7118
  const t2 = document.createElement("span");
6842
7119
  t2.className = "num";
6843
- t2.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span> " + "<span>" + meta.totals.files + " files</span>";
7120
+ t2.innerHTML = '<span class="add">+' + meta.totals.additions + "</span> " + '<span class="del">−' + meta.totals.deletions + "</span> <span>" + meta.totals.files + " files</span>";
6844
7121
  el.appendChild(t2);
6845
7122
  }
6846
7123
  const u2 = document.createElement("span");
@@ -6859,7 +7136,41 @@
6859
7136
  return;
6860
7137
  enqueueLoad(f2, card, 5);
6861
7138
  }
6862
- function scrollToFile(path) {
7139
+ function clearDiffLineFocus() {
7140
+ document.querySelectorAll(".gdp-diff-line-target").forEach((row) => {
7141
+ row.classList.remove("gdp-diff-line-target");
7142
+ });
7143
+ }
7144
+ function diffRowLineNumber(row) {
7145
+ const newLine = row.querySelector(".line-num2, td.d2h-code-side-linenumber");
7146
+ const raw = (newLine?.textContent || "").trim();
7147
+ const line = Number(raw);
7148
+ return Number.isInteger(line) && line > 0 ? line : null;
7149
+ }
7150
+ function focusDiffLine(card, line) {
7151
+ const start = lineTargetStart(line);
7152
+ if (!start)
7153
+ return false;
7154
+ const rows = Array.from(card.querySelectorAll("table.d2h-diff-table tr"));
7155
+ const row = rows.find((candidate) => diffRowLineNumber(candidate) === start);
7156
+ if (!row)
7157
+ return false;
7158
+ clearDiffLineFocus();
7159
+ row.classList.add("gdp-diff-line-target");
7160
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
7161
+ return true;
7162
+ }
7163
+ function applyDiffRouteFocus(card) {
7164
+ if (STATE.route.screen !== "diff" || !STATE.route.path || !STATE.route.line)
7165
+ return false;
7166
+ if (card && card.dataset.path !== STATE.route.path)
7167
+ return false;
7168
+ const targetCard = card || document.querySelector(diffCardSelector(STATE.route.path));
7169
+ if (!targetCard)
7170
+ return false;
7171
+ return focusDiffLine(targetCard, STATE.route.line);
7172
+ }
7173
+ function scrollToFile(path, line) {
6863
7174
  const card = document.querySelector(diffCardSelector(path));
6864
7175
  if (!card)
6865
7176
  return;
@@ -6875,7 +7186,9 @@
6875
7186
  if (f2)
6876
7187
  enqueueLoad(f2, card, 10);
6877
7188
  }
6878
- card.scrollIntoView({ behavior: "smooth", block: "start" });
7189
+ if (!line || !focusDiffLine(card, line)) {
7190
+ card.scrollIntoView({ behavior: "smooth", block: "start" });
7191
+ }
6879
7192
  }
6880
7193
  function markActive(path) {
6881
7194
  STATE.activeFile = path;
@@ -7510,7 +7823,7 @@
7510
7823
  }
7511
7824
  const head = document.createElement("div");
7512
7825
  head.className = "gdp-shell-header";
7513
- 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>';
7826
+ 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>';
7514
7827
  card.appendChild(head);
7515
7828
  const body = document.createElement("div");
7516
7829
  body.className = "gdp-shell-body";
@@ -7948,7 +8261,7 @@
7948
8261
  const button = document.createElement("button");
7949
8262
  button.className = "gdp-expand-btn";
7950
8263
  button.title = spec.title;
7951
- 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>';
8264
+ 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>';
7952
8265
  button.addEventListener("click", (e2) => {
7953
8266
  e2.stopPropagation();
7954
8267
  if (button.disabled)
@@ -8046,9 +8359,9 @@
8046
8359
  const num = sideIndex === 0 ? oldStart + i2 : newStart + i2;
8047
8360
  lnHtml = '<td class="d2h-code-side-linenumber d2h-cntx">' + num + "</td>";
8048
8361
  } else {
8049
- lnHtml = '<td class="d2h-code-linenumber d2h-cntx">' + '<div class="line-num1">' + (oldStart + i2) + "</div>" + '<div class="line-num2">' + (newStart + i2) + "</div>" + "</td>";
8362
+ lnHtml = '<td class="d2h-code-linenumber d2h-cntx"><div class="line-num1">' + (oldStart + i2) + '</div><div class="line-num2">' + (newStart + i2) + "</div></td>";
8050
8363
  }
8051
- tr.innerHTML = lnHtml + '<td class="d2h-cntx">' + '<div class="' + (isSplit ? "d2h-code-side-line" : "d2h-code-line") + '">' + '<span class="d2h-code-line-prefix">&nbsp;</span>' + '<span class="d2h-code-line-ctn">' + escapeHtmlText(lines[i2]) + "</span>" + "</div>" + "</td>";
8364
+ tr.innerHTML = lnHtml + '<td class="d2h-cntx"><div class="' + (isSplit ? "d2h-code-side-line" : "d2h-code-line") + '"><span class="d2h-code-line-prefix">&nbsp;</span><span class="d2h-code-line-ctn">' + escapeHtmlText(lines[i2]) + "</span></div></td>";
8052
8365
  frag.appendChild(tr);
8053
8366
  }
8054
8367
  tbody.insertBefore(frag, anchor);
@@ -8562,12 +8875,14 @@
8562
8875
  header.textContent = target.path + " @ " + target.ref;
8563
8876
  }
8564
8877
  const lang = inferLang(target.path);
8565
- const hljsRef = STATE.syntaxHighlight ? await loadSyntaxHighlighter() : null;
8878
+ const usesVirtualSource = shouldVirtualizeSource(textValue, lines) && !isVirtualSourceDisabled();
8879
+ const hljsRef = STATE.syntaxHighlight && usesVirtualSource ? await loadSyntaxHighlighter() : null;
8880
+ const sourceShikiRef = STATE.syntaxHighlight && !usesVirtualSource ? await loadSourceShikiHighlighter() : null;
8566
8881
  if (signal?.aborted)
8567
8882
  return false;
8568
8883
  const previewable = isPreviewableSource(target.path);
8569
8884
  const tabsHost = card.querySelector(".gdp-file-detail-tabs");
8570
- if (shouldVirtualizeSource(textValue, lines) && !isVirtualSourceDisabled()) {
8885
+ if (usesVirtualSource) {
8571
8886
  const virtualCode = renderVirtualSource(target, textValue, lines, hljsRef, lang);
8572
8887
  if (previewable) {
8573
8888
  const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview");
@@ -8623,23 +8938,24 @@
8623
8938
  const table2 = document.createElement("table");
8624
8939
  table2.className = "gdp-source-table";
8625
8940
  const tbody = document.createElement("tbody");
8941
+ const sourceShikiLang = normalizeSourceShikiLang(lang);
8942
+ const shikiLines = sourceShikiRef && sourceShikiLang ? sourceShikiLines(textValue, sourceShikiLang, sourceShikiRef) : null;
8626
8943
  for (let index = 0;index < lines.length; index++) {
8627
8944
  if (signal?.aborted)
8628
8945
  return false;
8629
8946
  const line = lines[index];
8630
8947
  const tr = document.createElement("tr");
8948
+ tr.dataset.line = String(index + 1);
8949
+ tr.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
8631
8950
  const num = document.createElement("td");
8632
8951
  num.className = "gdp-source-line-number";
8633
8952
  num.textContent = String(index + 1);
8953
+ bindSourceLineNumber(num, card, target, index + 1);
8634
8954
  const code2 = document.createElement("td");
8635
8955
  code2.className = "gdp-source-line-code";
8636
- if (hljsRef && hljsRef.highlight && lang && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
8637
- try {
8638
- code2.innerHTML = hljsRef.highlight(line || " ", { language: lang, ignoreIllegals: true }).value;
8639
- code2.classList.add("hljs");
8640
- } catch {
8641
- code2.textContent = line || " ";
8642
- }
8956
+ if (shikiLines && shikiLines[index] != null) {
8957
+ code2.innerHTML = shikiLines[index] || " ";
8958
+ code2.classList.add("shiki");
8643
8959
  } else {
8644
8960
  code2.textContent = line || " ";
8645
8961
  }
@@ -8726,6 +9042,69 @@
8726
9042
  url.searchParams.delete("virtual");
8727
9043
  return url.pathname + url.search;
8728
9044
  }
9045
+ function currentSourceLineTarget(target) {
9046
+ const routeTarget = sourceTargetFromRoute();
9047
+ return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : undefined;
9048
+ }
9049
+ function lineTargetStart(line) {
9050
+ if (!line)
9051
+ return;
9052
+ return typeof line === "number" ? line : line.start;
9053
+ }
9054
+ function lineInSourceTarget(lineNumber, target) {
9055
+ if (!target)
9056
+ return false;
9057
+ if (typeof target === "number")
9058
+ return lineNumber === target;
9059
+ return lineNumber >= target.start && lineNumber <= target.end;
9060
+ }
9061
+ let SOURCE_LINE_DRAG = null;
9062
+ function normalizeSourceLineSelection(start, end) {
9063
+ const a2 = Math.max(1, Math.floor(start));
9064
+ const b2 = Math.max(1, Math.floor(end));
9065
+ const from = Math.min(a2, b2);
9066
+ const to = Math.max(a2, b2);
9067
+ return from === to ? from : { start: from, end: to };
9068
+ }
9069
+ function setSourceLineRoute(target, line) {
9070
+ if (STATE.route.screen !== "file")
9071
+ return;
9072
+ setRoute({
9073
+ screen: "file",
9074
+ path: target.path,
9075
+ ref: target.ref,
9076
+ view: STATE.route.view,
9077
+ range: currentRange(),
9078
+ line
9079
+ }, true);
9080
+ }
9081
+ function syncRenderedSourceLineHighlights(card, target) {
9082
+ const lineTarget = currentSourceLineTarget(target);
9083
+ card.querySelectorAll("[data-line]").forEach((row) => {
9084
+ const line = Number(row.dataset.line || "0");
9085
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(line, lineTarget));
9086
+ });
9087
+ }
9088
+ function updateSourceLineSelection(card, target, start, end) {
9089
+ setSourceLineRoute(target, normalizeSourceLineSelection(start, end));
9090
+ syncRenderedSourceLineHighlights(card, target);
9091
+ }
9092
+ function beginSourceLineSelection(event, card, target, line) {
9093
+ event.preventDefault();
9094
+ SOURCE_LINE_DRAG = { target, start: line };
9095
+ updateSourceLineSelection(card, target, line, line);
9096
+ }
9097
+ function bindSourceLineNumber(num, card, target, line) {
9098
+ num.addEventListener("mousedown", (e2) => beginSourceLineSelection(e2, card, target, line));
9099
+ num.addEventListener("mouseenter", () => {
9100
+ if (!SOURCE_LINE_DRAG || !sourceTargetsEqual(SOURCE_LINE_DRAG.target, target))
9101
+ return;
9102
+ updateSourceLineSelection(card, target, SOURCE_LINE_DRAG.start, line);
9103
+ });
9104
+ }
9105
+ document.addEventListener("mouseup", () => {
9106
+ SOURCE_LINE_DRAG = null;
9107
+ });
8729
9108
  function renderVirtualSource(target, textValue, lines, hljsRef, lang) {
8730
9109
  const wrap = document.createElement("div");
8731
9110
  wrap.className = "gdp-source-virtual";
@@ -8764,7 +9143,8 @@
8764
9143
  full.title = "Render every line without virtualization. This can be slow for large files.";
8765
9144
  full.addEventListener("click", (e2) => {
8766
9145
  e2.preventDefault();
8767
- history.pushState(null, "", full.href);
9146
+ const url = new URL(full.href, window.location.origin);
9147
+ setRoute(parseRoute(url.pathname, url.search, currentRange()), true);
8768
9148
  renderStandaloneSource(target);
8769
9149
  });
8770
9150
  actions.append(copy, full);
@@ -8798,9 +9178,12 @@
8798
9178
  for (let index = start;index < end; index++) {
8799
9179
  const row = document.createElement("div");
8800
9180
  row.className = "gdp-source-virtual-row";
9181
+ row.dataset.line = String(index + 1);
9182
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
8801
9183
  const num = document.createElement("span");
8802
9184
  num.className = "gdp-source-virtual-line-number";
8803
9185
  num.textContent = String(index + 1);
9186
+ bindSourceLineNumber(num, wrap, target, index + 1);
8804
9187
  const code2 = document.createElement("span");
8805
9188
  code2.className = "gdp-source-virtual-line-code";
8806
9189
  const line = lines[index] ?? "";
@@ -9048,6 +9431,7 @@
9048
9431
  return;
9049
9432
  if (!rendered)
9050
9433
  return;
9434
+ scrollStandaloneSourceLine(card, lineTargetStart(STATE.route.screen === "file" ? STATE.route.line : undefined));
9051
9435
  finishSourceLoad(req);
9052
9436
  }
9053
9437
  } catch (err) {
@@ -9061,6 +9445,19 @@
9061
9445
  renderSourceError(card, target, "Cannot load " + target.path + " at " + target.ref);
9062
9446
  }
9063
9447
  }
9448
+ function scrollStandaloneSourceLine(card, line) {
9449
+ if (!line || line < 1)
9450
+ return;
9451
+ const virtualScroller = card.querySelector(".gdp-source-virtual-scroller");
9452
+ if (virtualScroller) {
9453
+ const centeredOffset = virtualScroller.clientHeight / 2 - VIRTUAL_SOURCE_ROW_HEIGHT / 2;
9454
+ virtualScroller.scrollTop = Math.max(0, (line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - Math.max(0, centeredOffset));
9455
+ return;
9456
+ }
9457
+ const row = card.querySelector('.gdp-source-table tr[data-line="' + String(line) + '"]');
9458
+ if (row)
9459
+ row.scrollIntoView({ block: "center" });
9460
+ }
9064
9461
  function applySourceRouteToShell() {
9065
9462
  const target = sourceTargetFromRoute();
9066
9463
  setPageMode();
@@ -9249,6 +9646,7 @@
9249
9646
  card.classList.add("loaded");
9250
9647
  card.style.minHeight = "";
9251
9648
  mountDiff(card, file, data);
9649
+ applyDiffRouteFocus(card);
9252
9650
  card.style.containIntrinsicSize = Math.max(card.offsetHeight, file.estimated_height_px || 200) + "px";
9253
9651
  applyViewedToCard(card, STATE.viewedFiles.has(file.path), true);
9254
9652
  if (data.truncated && data.mode === "preview") {
@@ -9450,7 +9848,7 @@
9450
9848
  leftHTML = mediaTag(path, "HEAD");
9451
9849
  rightHTML = mediaTag(path, "worktree");
9452
9850
  }
9453
- 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>";
9851
+ 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>";
9454
9852
  body.replaceWith(container);
9455
9853
  }
9456
9854
  function setupScrollSpy() {
@@ -9680,10 +10078,409 @@
9680
10078
  input.focus();
9681
10079
  input.select();
9682
10080
  }
10081
+ let PALETTE = null;
10082
+ const REPO_FILE_CACHE = new Map;
10083
+ function paletteSource() {
10084
+ if (STATE.route.screen === "diff")
10085
+ return "diff";
10086
+ if (STATE.route.screen === "file" && STATE.route.view !== "blob")
10087
+ return "diff";
10088
+ return "repo";
10089
+ }
10090
+ function paletteRef(source) {
10091
+ if (source === "diff")
10092
+ return STATE.to && STATE.to !== "worktree" ? STATE.to : "worktree";
10093
+ if (STATE.route.screen === "repo")
10094
+ return STATE.route.ref || "worktree";
10095
+ if (STATE.route.screen === "file")
10096
+ return STATE.route.ref || "worktree";
10097
+ return STATE.repoRef || "worktree";
10098
+ }
10099
+ function closeSearchPalette() {
10100
+ if (!PALETTE)
10101
+ return;
10102
+ PALETTE.controller?.abort();
10103
+ if (PALETTE.debounce)
10104
+ window.clearTimeout(PALETTE.debounce);
10105
+ PALETTE.root.remove();
10106
+ PALETTE = null;
10107
+ }
10108
+ function createPalette(mode) {
10109
+ closeSearchPalette();
10110
+ const root = document.createElement("div");
10111
+ root.className = "gdp-palette-backdrop";
10112
+ const dialog = document.createElement("div");
10113
+ dialog.className = "gdp-palette";
10114
+ dialog.setAttribute("role", "dialog");
10115
+ dialog.setAttribute("aria-modal", "true");
10116
+ const label = document.createElement("div");
10117
+ label.className = "gdp-palette-label";
10118
+ label.textContent = mode === "file" ? "Files" : "Grep";
10119
+ const input = document.createElement("input");
10120
+ input.className = "gdp-palette-input";
10121
+ input.type = "search";
10122
+ input.autocomplete = "off";
10123
+ input.spellcheck = false;
10124
+ input.placeholder = mode === "file" ? "Search files" : "Search text";
10125
+ input.setAttribute("role", "combobox");
10126
+ input.setAttribute("aria-expanded", "true");
10127
+ input.setAttribute("aria-controls", "gdp-palette-list");
10128
+ const status = document.createElement("div");
10129
+ status.className = "gdp-palette-status";
10130
+ const controls = document.createElement("div");
10131
+ controls.className = "gdp-palette-controls";
10132
+ const list2 = document.createElement("div");
10133
+ list2.id = "gdp-palette-list";
10134
+ list2.className = "gdp-palette-list";
10135
+ list2.setAttribute("role", "listbox");
10136
+ dialog.append(label, input, controls, status, list2);
10137
+ root.appendChild(dialog);
10138
+ document.body.appendChild(root);
10139
+ const state = {
10140
+ root,
10141
+ input,
10142
+ controls,
10143
+ list: list2,
10144
+ status,
10145
+ mode,
10146
+ grepRegex: false,
10147
+ selected: -1,
10148
+ items: [],
10149
+ composing: false,
10150
+ diffSnapshot: [...STATE.files]
10151
+ };
10152
+ PALETTE = state;
10153
+ root.addEventListener("mousedown", (e2) => {
10154
+ if (e2.target === root)
10155
+ closeSearchPalette();
10156
+ });
10157
+ input.addEventListener("compositionstart", () => {
10158
+ state.composing = true;
10159
+ });
10160
+ input.addEventListener("compositionend", () => {
10161
+ state.composing = false;
10162
+ });
10163
+ input.addEventListener("input", () => updatePaletteResults(state));
10164
+ input.addEventListener("keydown", (e2) => handlePaletteKeydown(e2, state));
10165
+ input.focus();
10166
+ updatePaletteResults(state);
10167
+ return state;
10168
+ }
10169
+ function renderPaletteControls(state) {
10170
+ state.controls.innerHTML = "";
10171
+ if (state.mode === "file") {
10172
+ const hint2 = document.createElement("span");
10173
+ hint2.className = "gdp-palette-mode-hint";
10174
+ hint2.textContent = isGlobPathQuery(state.input.value) ? "Glob: * ? []" : "Fuzzy path search";
10175
+ state.controls.appendChild(hint2);
10176
+ return;
10177
+ }
10178
+ const plain = document.createElement("button");
10179
+ plain.type = "button";
10180
+ plain.className = "gdp-palette-mode-button";
10181
+ plain.setAttribute("aria-pressed", String(!state.grepRegex));
10182
+ plain.textContent = "Plain";
10183
+ plain.addEventListener("mousedown", (e2) => {
10184
+ e2.preventDefault();
10185
+ state.grepRegex = false;
10186
+ renderPaletteControls(state);
10187
+ updatePaletteResults(state);
10188
+ state.input.focus();
10189
+ });
10190
+ const regex = document.createElement("button");
10191
+ regex.type = "button";
10192
+ regex.className = "gdp-palette-mode-button";
10193
+ regex.setAttribute("aria-pressed", String(state.grepRegex));
10194
+ regex.textContent = ".* Regex";
10195
+ regex.title = "Alt+R";
10196
+ regex.addEventListener("mousedown", (e2) => {
10197
+ e2.preventDefault();
10198
+ state.grepRegex = true;
10199
+ renderPaletteControls(state);
10200
+ updatePaletteResults(state);
10201
+ state.input.focus();
10202
+ });
10203
+ const hint = document.createElement("span");
10204
+ hint.className = "gdp-palette-mode-hint";
10205
+ hint.textContent = "Alt+R toggles regex";
10206
+ state.controls.append(plain, regex, hint);
10207
+ }
10208
+ function regexQueryIsValid(query) {
10209
+ try {
10210
+ new RegExp(query);
10211
+ return true;
10212
+ } catch {
10213
+ return false;
10214
+ }
10215
+ }
10216
+ function appendHighlightedPath(parent, path, ranges) {
10217
+ let cursor = 0;
10218
+ for (const range of ranges) {
10219
+ if (range.start > cursor)
10220
+ parent.appendChild(document.createTextNode(path.slice(cursor, range.start)));
10221
+ const mark = document.createElement("mark");
10222
+ mark.textContent = path.slice(range.start, range.end);
10223
+ parent.appendChild(mark);
10224
+ cursor = range.end;
10225
+ }
10226
+ if (cursor < path.length)
10227
+ parent.appendChild(document.createTextNode(path.slice(cursor)));
10228
+ }
10229
+ function renderPalette(state) {
10230
+ state.list.innerHTML = "";
10231
+ state.items.forEach((item, index) => {
10232
+ const row = document.createElement("button");
10233
+ row.type = "button";
10234
+ row.id = "gdp-palette-item-" + index;
10235
+ row.className = "gdp-palette-row";
10236
+ row.setAttribute("role", "option");
10237
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10238
+ const title = document.createElement("span");
10239
+ title.className = "gdp-palette-row-title";
10240
+ const detail = document.createElement("span");
10241
+ detail.className = "gdp-palette-row-detail";
10242
+ if (item.kind === "file") {
10243
+ title.textContent = item.path.split("/").pop() || item.path;
10244
+ appendHighlightedPath(detail, item.displayPath, item.ranges);
10245
+ if (item.old_path && item.displayPath !== item.old_path) {
10246
+ detail.appendChild(document.createTextNode(" " + item.old_path));
10247
+ }
10248
+ } else {
10249
+ title.textContent = item.path + ":" + item.line;
10250
+ detail.textContent = item.preview;
10251
+ }
10252
+ row.append(title, detail);
10253
+ row.addEventListener("mouseenter", () => {
10254
+ state.selected = index;
10255
+ syncPaletteSelection(state);
10256
+ });
10257
+ row.addEventListener("mousedown", (e2) => {
10258
+ e2.preventDefault();
10259
+ state.selected = index;
10260
+ selectPaletteItem(state);
10261
+ });
10262
+ state.list.appendChild(row);
10263
+ });
10264
+ syncPaletteSelection(state);
10265
+ }
10266
+ function syncPaletteSelection(state) {
10267
+ state.input.setAttribute("aria-activedescendant", state.selected >= 0 ? "gdp-palette-item-" + state.selected : "");
10268
+ state.list.querySelectorAll(".gdp-palette-row").forEach((row, index) => {
10269
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10270
+ if (index === state.selected)
10271
+ row.scrollIntoView({ block: "nearest" });
10272
+ });
10273
+ }
10274
+ async function repoPaletteFiles(ref) {
10275
+ const cached = REPO_FILE_CACHE.get(ref);
10276
+ if (cached && cached.generation === SERVER_GENERATION)
10277
+ return cached;
10278
+ const params = new URLSearchParams;
10279
+ params.set("ref", ref);
10280
+ const res = await trackLoad(fetch("/_files?" + params.toString()).then((r2) => {
10281
+ if (!r2.ok)
10282
+ throw new Error("failed to load files");
10283
+ return r2.json();
10284
+ }));
10285
+ REPO_FILE_CACHE.set(ref, res);
10286
+ return res;
10287
+ }
10288
+ function diffFilePaletteItems(state, query) {
10289
+ const matchPath = isGlobPathQuery(query) ? globMatchPath : fuzzyMatchPath;
10290
+ const candidates = state.diffSnapshot.map((file) => {
10291
+ const current = matchPath(query, file.path);
10292
+ const old = file.old_path ? matchPath(query, file.old_path) : null;
10293
+ const best = old && (!current || old.score > current.score) ? { match: old, displayPath: file.old_path || file.path } : current ? { match: current, displayPath: file.path } : null;
10294
+ return best ? { file, ...best } : null;
10295
+ }).filter((item) => item !== null).sort((a2, b2) => b2.match.score - a2.match.score || a2.file.path.localeCompare(b2.file.path));
10296
+ return limitPaletteResults(candidates).map((candidate) => ({
10297
+ kind: "file",
10298
+ path: candidate.file.path,
10299
+ old_path: candidate.file.old_path,
10300
+ displayPath: candidate.displayPath,
10301
+ ref: paletteRef("diff"),
10302
+ targetPath: fileSourceTarget(candidate.file).path,
10303
+ targetRef: fileSourceTarget(candidate.file).ref,
10304
+ source: "diff",
10305
+ ranges: candidate.match.ranges
10306
+ }));
10307
+ }
10308
+ async function updateFilePalette(state, query) {
10309
+ renderPaletteControls(state);
10310
+ const source = paletteSource();
10311
+ if (!query.trim()) {
10312
+ const base2 = source === "diff" ? state.diffSnapshot.map((file) => {
10313
+ const target = fileSourceTarget(file);
10314
+ return { kind: "file", path: file.path, old_path: file.old_path, displayPath: file.path, ref: paletteRef(source), targetPath: target.path, targetRef: target.ref, source, ranges: [] };
10315
+ }) : [];
10316
+ state.items = limitPaletteResults(base2);
10317
+ state.selected = state.items.length ? 0 : -1;
10318
+ state.status.textContent = source === "diff" ? state.diffSnapshot.length + " diff files" : "Type to search repository files";
10319
+ renderPalette(state);
10320
+ return;
10321
+ }
10322
+ if (source === "diff") {
10323
+ state.items = diffFilePaletteItems(state, query);
10324
+ } else {
10325
+ state.status.textContent = "Loading files...";
10326
+ const ref = paletteRef(source);
10327
+ const response = await repoPaletteFiles(ref);
10328
+ if (PALETTE !== state || state.input.value !== query)
10329
+ return;
10330
+ state.items = limitPaletteResults(rankPathMatches(query, response.files)).map((match2) => ({
10331
+ kind: "file",
10332
+ path: match2.item.path,
10333
+ displayPath: match2.item.path,
10334
+ ref,
10335
+ source,
10336
+ ranges: match2.ranges
10337
+ }));
10338
+ }
10339
+ state.selected = state.items.length ? 0 : -1;
10340
+ state.status.textContent = state.items.length ? state.items.length + " results" : "No results";
10341
+ renderPalette(state);
10342
+ }
10343
+ function updateGrepPalette(state, query) {
10344
+ renderPaletteControls(state);
10345
+ state.controller?.abort();
10346
+ if (state.debounce)
10347
+ window.clearTimeout(state.debounce);
10348
+ if (!query.trim()) {
10349
+ state.items = [];
10350
+ state.selected = -1;
10351
+ state.status.textContent = "Type to grep";
10352
+ renderPalette(state);
10353
+ return;
10354
+ }
10355
+ if (state.grepRegex && !regexQueryIsValid(query)) {
10356
+ state.controller?.abort();
10357
+ state.items = [];
10358
+ state.selected = -1;
10359
+ state.status.textContent = "Invalid regular expression";
10360
+ renderPalette(state);
10361
+ return;
10362
+ }
10363
+ state.status.textContent = "Searching...";
10364
+ state.debounce = window.setTimeout(() => {
10365
+ const source = paletteSource();
10366
+ const ref = paletteRef(source);
10367
+ const params = new URLSearchParams;
10368
+ params.set("ref", ref);
10369
+ params.set("q", query);
10370
+ params.set("max", "200");
10371
+ if (state.grepRegex)
10372
+ params.set("regex", "1");
10373
+ if (source === "diff") {
10374
+ for (const file of state.diffSnapshot)
10375
+ params.append("path", file.path);
10376
+ }
10377
+ const controller = new AbortController;
10378
+ state.controller = controller;
10379
+ trackLoad(fetch("/_grep?" + params.toString(), { signal: controller.signal }).then((r2) => {
10380
+ if (!r2.ok)
10381
+ throw new Error("grep failed");
10382
+ return r2.json();
10383
+ })).then((response) => {
10384
+ if (PALETTE !== state || controller.signal.aborted)
10385
+ return;
10386
+ state.items = limitPaletteResults(response.matches.map((match2) => ({
10387
+ kind: "grep",
10388
+ path: match2.path,
10389
+ line: match2.line,
10390
+ column: match2.column,
10391
+ preview: match2.preview,
10392
+ ref,
10393
+ source
10394
+ })));
10395
+ state.selected = state.items.length ? 0 : -1;
10396
+ state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
10397
+ renderPalette(state);
10398
+ }).catch((err) => {
10399
+ if (isAbortError(err))
10400
+ return;
10401
+ state.status.textContent = "Search failed";
10402
+ });
10403
+ }, 80);
10404
+ }
10405
+ function updatePaletteResults(state) {
10406
+ const query = state.input.value;
10407
+ if (state.mode === "file") {
10408
+ updateFilePalette(state, query).catch(() => {
10409
+ state.status.textContent = "Search failed";
10410
+ });
10411
+ } else {
10412
+ updateGrepPalette(state, query);
10413
+ }
10414
+ }
10415
+ function selectPaletteItem(state) {
10416
+ const item = state.items[state.selected];
10417
+ if (!item)
10418
+ return;
10419
+ closeSearchPalette();
10420
+ if (item.kind === "file") {
10421
+ if (item.source === "diff") {
10422
+ if (STATE.route.screen === "file") {
10423
+ setRoute({ screen: "file", path: item.targetPath || item.path, ref: item.targetRef || item.ref, range: currentRange() });
10424
+ applySourceRouteToShell();
10425
+ } else {
10426
+ scrollToFile(item.path);
10427
+ }
10428
+ } else {
10429
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", range: currentRange() });
10430
+ renderStandaloneSource({ path: item.path, ref: item.ref });
10431
+ }
10432
+ return;
10433
+ }
10434
+ if (item.source === "diff") {
10435
+ setRoute({ screen: "diff", range: currentRange(), path: item.path, line: item.line });
10436
+ scrollToFile(item.path, item.line);
10437
+ } else {
10438
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", line: item.line, range: currentRange() });
10439
+ renderStandaloneSource({ path: item.path, ref: item.ref });
10440
+ }
10441
+ }
10442
+ function handlePaletteKeydown(e2, state) {
10443
+ if (e2.key === "Escape") {
10444
+ e2.preventDefault();
10445
+ closeSearchPalette();
10446
+ return;
10447
+ }
10448
+ if (e2.key === "Enter") {
10449
+ if (state.composing)
10450
+ return;
10451
+ e2.preventDefault();
10452
+ selectPaletteItem(state);
10453
+ return;
10454
+ }
10455
+ if (state.mode === "grep" && e2.altKey && e2.key.toLowerCase() === "r") {
10456
+ e2.preventDefault();
10457
+ state.grepRegex = !state.grepRegex;
10458
+ updatePaletteResults(state);
10459
+ return;
10460
+ }
10461
+ const direction = e2.key === "ArrowDown" || e2.ctrlKey && e2.key.toLowerCase() === "n" ? 1 : e2.key === "ArrowUp" || e2.ctrlKey && e2.key.toLowerCase() === "p" ? -1 : 0;
10462
+ if (direction) {
10463
+ e2.preventDefault();
10464
+ state.selected = movePaletteSelection(state.selected, state.items.length, direction);
10465
+ syncPaletteSelection(state);
10466
+ }
10467
+ }
10468
+ function openSearchPalette(mode) {
10469
+ createPalette(mode);
10470
+ }
9683
10471
  document.addEventListener("keydown", (e2) => {
9684
10472
  if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "k") {
9685
10473
  e2.preventDefault();
9686
- focusFileFilter();
10474
+ if (PALETTE?.mode === "file")
10475
+ return;
10476
+ openSearchPalette("file");
10477
+ return;
10478
+ }
10479
+ if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "g") {
10480
+ e2.preventDefault();
10481
+ if (PALETTE?.mode === "grep")
10482
+ return;
10483
+ openSearchPalette("grep");
9687
10484
  return;
9688
10485
  }
9689
10486
  const targetEl = e2.target;
@@ -9842,7 +10639,7 @@
9842
10639
  const [sha, subject, author, when] = c2.split("\t");
9843
10640
  if (!sha)
9844
10641
  continue;
9845
- 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>");
10642
+ 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>");
9846
10643
  }
9847
10644
  } else if (popTab === "branches") {
9848
10645
  const branches = (REFS.branches || []).filter(m);
@@ -9851,7 +10648,7 @@
9851
10648
  }
9852
10649
  for (const b2 of branches) {
9853
10650
  const cur = b2 === REFS.current;
9854
- 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>");
10651
+ 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>");
9855
10652
  }
9856
10653
  } else if (popTab === "tags") {
9857
10654
  const tags = (REFS.tags || []).filter(m);
@@ -9859,7 +10656,7 @@
9859
10656
  html.push('<div class="rp-empty">no tags</div>');
9860
10657
  }
9861
10658
  for (const t2 of tags) {
9862
- html.push('<div class="rp-item-ref" data-val="' + escapeAttr(t2) + '">' + '<span class="name">' + escapeHtml2(t2) + "</span>" + '<span class="badge">tag</span>' + "</div>");
10659
+ html.push('<div class="rp-item-ref" data-val="' + escapeAttr(t2) + '"><span class="name">' + escapeHtml2(t2) + '</span><span class="badge">tag</span></div>');
9863
10660
  }
9864
10661
  }
9865
10662
  popBody.innerHTML = html.join("");