@youtyan/code-viewer 0.1.11 → 0.1.13

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,309 @@
131
131
  };
132
132
  }
133
133
 
134
+ // web-src/focus-scope.ts
135
+ function isEditableKeyTarget(target) {
136
+ if (!target)
137
+ return false;
138
+ const tag = target.tagName;
139
+ return tag === "INPUT" || tag === "TEXTAREA" || target.closest('[contenteditable="true"]') != null;
140
+ }
141
+ function keymapScope(target) {
142
+ if (target?.closest("#content"))
143
+ return "main";
144
+ if (target?.closest("#sidebar"))
145
+ return "sidebar";
146
+ return "global";
147
+ }
148
+ function prepareKeyboardPanels(doc = document) {
149
+ const sidebar = doc.querySelector("#sidebar");
150
+ const content = doc.querySelector("#content");
151
+ if (sidebar)
152
+ sidebar.tabIndex = -1;
153
+ if (content)
154
+ content.tabIndex = -1;
155
+ }
156
+ function getPanelFocusScope(doc = document) {
157
+ const scope = doc.body?.dataset.focusScope;
158
+ return scope === "sidebar" || scope === "main" ? scope : null;
159
+ }
160
+ function setPanelFocusScope(scope, doc = document) {
161
+ if (!doc.body)
162
+ return;
163
+ if (scope)
164
+ doc.body.dataset.focusScope = scope;
165
+ else
166
+ delete doc.body.dataset.focusScope;
167
+ }
168
+ function restorePanelFocusScope(scope, doc = document) {
169
+ if (scope === "sidebar")
170
+ focusSidebarPanel(doc);
171
+ else if (scope === "main")
172
+ focusMainPanel(doc);
173
+ else
174
+ setPanelFocusScope(null, doc);
175
+ }
176
+ function focusSidebarPanel(doc = document) {
177
+ const active = doc.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
178
+ const sidebar = doc.querySelector("#sidebar");
179
+ (active || sidebar)?.focus({ preventScroll: true });
180
+ setPanelFocusScope("sidebar", doc);
181
+ }
182
+ function focusMainPanel(doc = document) {
183
+ doc.querySelector("#content")?.focus({ preventScroll: true });
184
+ setPanelFocusScope("main", doc);
185
+ }
186
+ function findMainScrollTarget(doc = document) {
187
+ const active = doc.activeElement;
188
+ const activeScroller = active?.closest("#content .gdp-source-virtual-scroller");
189
+ if (activeScroller && activeScroller.offsetParent !== null)
190
+ return activeScroller;
191
+ const sourceScroller = doc.querySelector("#content .gdp-source-virtual-scroller");
192
+ if (sourceScroller && sourceScroller.offsetParent !== null)
193
+ return sourceScroller;
194
+ const content = doc.querySelector("#content");
195
+ if (!content || content.offsetParent === null)
196
+ return null;
197
+ const isScrollable = (item) => {
198
+ if (item.offsetParent === null)
199
+ return false;
200
+ const style = doc.defaultView?.getComputedStyle(item);
201
+ return !!style && /(auto|scroll)/.test(style.overflowY) && item.scrollHeight > item.clientHeight;
202
+ };
203
+ const preferred = Array.from(content.querySelectorAll(".gdp-source-viewer, .gdp-markdown-layout, .gdp-markdown-preview, .d2h-files-diff, .d2h-file-diff"));
204
+ const scrollable = preferred.find(isScrollable) || (isScrollable(content) ? content : null) || Array.from(content.querySelectorAll("*")).find(isScrollable);
205
+ return scrollable || doc.scrollingElement;
206
+ }
207
+
208
+ // web-src/fuzzy-search.ts
209
+ function basenameStart(path) {
210
+ const slash = path.lastIndexOf("/");
211
+ return slash < 0 ? 0 : slash + 1;
212
+ }
213
+ function isBoundary(path, index) {
214
+ if (index <= 0)
215
+ return true;
216
+ const prev = path[index - 1];
217
+ return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
218
+ }
219
+ function toRanges(indices) {
220
+ const ranges = [];
221
+ for (const index of indices) {
222
+ const last = ranges[ranges.length - 1];
223
+ if (last && last.end === index) {
224
+ last.end = index + 1;
225
+ } else {
226
+ ranges.push({ start: index, end: index + 1 });
227
+ }
228
+ }
229
+ return ranges;
230
+ }
231
+ function fuzzyMatchPath(query, path) {
232
+ const q = query.trim().toLowerCase();
233
+ if (!q)
234
+ return { score: 0, ranges: [] };
235
+ const lowerPath = path.toLowerCase();
236
+ const baseStart = basenameStart(path);
237
+ const indices = [];
238
+ let from = 0;
239
+ let score = 0;
240
+ for (const ch of q) {
241
+ const index = lowerPath.indexOf(ch, from);
242
+ if (index < 0)
243
+ return null;
244
+ indices.push(index);
245
+ score += 10;
246
+ if (index >= baseStart)
247
+ score += 8;
248
+ if (isBoundary(path, index))
249
+ score += 6;
250
+ const prev = indices[indices.length - 2];
251
+ if (prev != null && prev + 1 === index)
252
+ score += 12;
253
+ from = index + 1;
254
+ }
255
+ const first = indices[0] || 0;
256
+ score -= Math.min(first, 40);
257
+ if (indices[0] >= baseStart)
258
+ score += 20;
259
+ const basename = lowerPath.slice(baseStart);
260
+ if (basename.startsWith(q))
261
+ score += 30;
262
+ if (basename === q || basename.startsWith(q + "."))
263
+ score += 25;
264
+ if (lowerPath.endsWith(q))
265
+ score += 15;
266
+ return { score, ranges: toRanges(indices) };
267
+ }
268
+ function rankFuzzyPaths(query, items) {
269
+ return items.map((item) => {
270
+ const match = fuzzyMatchPath(query, item.path);
271
+ return match ? { item, score: match.score, ranges: match.ranges } : null;
272
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
273
+ }
274
+ function isGlobPathQuery(query) {
275
+ return /[*?]/.test(query.trim());
276
+ }
277
+ function escapeRegexChar(ch) {
278
+ return /[\\^$+?.()|{}]/.test(ch) ? "\\" + ch : ch;
279
+ }
280
+ function globToRegExp(query) {
281
+ const pattern = query.trim();
282
+ if (!pattern)
283
+ return null;
284
+ let source = "^";
285
+ for (let i = 0;i < pattern.length; i++) {
286
+ const ch = pattern[i];
287
+ if (ch === "*") {
288
+ if (pattern[i + 1] === "*") {
289
+ source += ".*";
290
+ i++;
291
+ } else {
292
+ source += "[^/]*";
293
+ }
294
+ } else if (ch === "?") {
295
+ source += "[^/]";
296
+ } else if (ch === "[") {
297
+ const close = pattern.indexOf("]", i + 1);
298
+ if (close < 0) {
299
+ source += "\\[";
300
+ } else {
301
+ const body = pattern.slice(i + 1, close).replace(/\\/g, "\\\\");
302
+ source += "[" + body + "]";
303
+ i = close;
304
+ }
305
+ } else {
306
+ source += escapeRegexChar(ch);
307
+ }
308
+ }
309
+ source += "$";
310
+ try {
311
+ return new RegExp(source, "i");
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+ function globMatchPath(query, path) {
317
+ const regex = globToRegExp(query);
318
+ const baseStart = basenameStart(path);
319
+ const basename = path.slice(baseStart);
320
+ if (!regex || !regex.test(path) && (query.includes("/") || !regex.test(basename)))
321
+ return null;
322
+ const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
323
+ const ranges = [];
324
+ const lowerPath = path.toLowerCase();
325
+ for (const part of literal) {
326
+ const start = lowerPath.indexOf(part.toLowerCase());
327
+ if (start >= 0)
328
+ ranges.push({ start, end: start + part.length });
329
+ }
330
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
331
+ const mergedRanges = [];
332
+ for (const range of ranges) {
333
+ const last = mergedRanges[mergedRanges.length - 1];
334
+ if (last && last.end >= range.start) {
335
+ last.end = Math.max(last.end, range.end);
336
+ } else {
337
+ mergedRanges.push({ ...range });
338
+ }
339
+ }
340
+ const score = 1000 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(query.replace(/^\*+/, "").toLowerCase()) ? 50 : 0);
341
+ return { score, ranges: mergedRanges };
342
+ }
343
+ function rankPathMatches(query, items) {
344
+ if (isGlobPathQuery(query)) {
345
+ return items.map((item) => {
346
+ const match = globMatchPath(query, item.path);
347
+ return match ? { item, score: match.score, ranges: match.ranges, mode: "glob" } : null;
348
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
349
+ }
350
+ return rankFuzzyPaths(query, items).map((item) => ({ ...item, mode: "fuzzy" }));
351
+ }
352
+
353
+ // web-src/keymap.ts
354
+ var DEFAULT_KEY_BINDINGS = [
355
+ { action: "open-file-palette", key: "k", ctrl: true, allowEditable: true, allowPaletteOpen: true },
356
+ { action: "open-file-palette", key: "k", meta: true, allowEditable: true, allowPaletteOpen: true },
357
+ { action: "open-grep-palette", key: "g", ctrl: true, allowEditable: true, allowPaletteOpen: true },
358
+ { action: "open-grep-palette", key: "g", meta: true, allowEditable: true, allowPaletteOpen: true },
359
+ { action: "focus-file-filter", key: "/" },
360
+ { action: "focus-sidebar", key: "h", ctrl: true },
361
+ { action: "focus-main", key: "l", ctrl: true },
362
+ { action: "cancel-source-load", key: "escape", requires: { lightboxClosed: true } },
363
+ { action: "open-sidebar-item", key: "enter", scope: "sidebar" },
364
+ { action: "open-sidebar-item", key: "enter", scope: "global" },
365
+ { action: "sidebar-next", key: "j", scope: "sidebar" },
366
+ { action: "sidebar-next", key: "j", scope: "global" },
367
+ { action: "sidebar-previous", key: "k", scope: "sidebar" },
368
+ { action: "sidebar-previous", key: "k", scope: "global" },
369
+ { action: "sidebar-page-down", key: "d", scope: "sidebar", ctrl: true },
370
+ { action: "sidebar-page-down", key: "d", scope: "global", ctrl: true },
371
+ { action: "sidebar-page-up", key: "u", scope: "sidebar", ctrl: true },
372
+ { action: "sidebar-page-up", key: "u", scope: "global", ctrl: true },
373
+ { action: "sidebar-expand", key: "l", scope: "sidebar" },
374
+ { action: "sidebar-expand", key: "l", scope: "global" },
375
+ { action: "sidebar-collapse", key: "h", scope: "sidebar" },
376
+ { action: "sidebar-collapse", key: "h", scope: "global" },
377
+ { action: "scroll-main-down", key: "j", scope: "main" },
378
+ { action: "scroll-main-up", key: "k", scope: "main" },
379
+ { action: "scroll-main-page-down", key: "d", scope: "main", ctrl: true },
380
+ { action: "scroll-main-page-up", key: "u", scope: "main", ctrl: true },
381
+ { action: "tab-preview", key: "p", scope: "main", pendingG: true },
382
+ { action: "tab-code", key: "c", scope: "main", pendingG: true },
383
+ { action: "goto-top", key: "g", pendingG: true },
384
+ { action: "goto-bottom", key: "g", shift: true, pendingG: true },
385
+ { action: "goto-bottom", key: "g", shift: true },
386
+ { action: "start-g-sequence", key: "g", scope: "sidebar" },
387
+ { action: "start-g-sequence", key: "g", scope: "main" },
388
+ { action: "layout-unified", key: "u" },
389
+ { action: "layout-split", key: "s" },
390
+ { action: "toggle-theme", key: "t" }
391
+ ];
392
+ function resolveKeymapAction(event, context) {
393
+ const key = event.key.toLowerCase();
394
+ if (context.composing)
395
+ return null;
396
+ for (const binding of DEFAULT_KEY_BINDINGS) {
397
+ if (binding.key !== key)
398
+ continue;
399
+ if (binding.requires?.lightboxClosed && context.lightboxOpen)
400
+ continue;
401
+ if (binding.scope && binding.scope !== context.scope)
402
+ continue;
403
+ if (!!binding.pendingG !== !!context.pendingG)
404
+ continue;
405
+ if (context.paletteOpen && !binding.allowPaletteOpen)
406
+ continue;
407
+ if (context.editable && !binding.allowEditable)
408
+ continue;
409
+ if (!!binding.ctrl !== !!event.ctrlKey)
410
+ continue;
411
+ if (!!binding.meta !== !!event.metaKey)
412
+ continue;
413
+ if (!!binding.alt !== !!event.altKey)
414
+ continue;
415
+ if (!!binding.shift !== !!event.shiftKey)
416
+ continue;
417
+ if (!binding.ctrl && !binding.meta && !binding.alt && !binding.shift && (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey))
418
+ continue;
419
+ return binding.action;
420
+ }
421
+ return null;
422
+ }
423
+
424
+ // web-src/search-palette.ts
425
+ var PALETTE_RESULT_LIMIT = 50;
426
+ function limitPaletteResults(items) {
427
+ return items.slice(0, PALETTE_RESULT_LIMIT);
428
+ }
429
+ function movePaletteSelection(index, count, direction) {
430
+ if (count <= 0)
431
+ return -1;
432
+ if (index < 0)
433
+ return direction > 0 ? 0 : count - 1;
434
+ return (index + direction + count) % count;
435
+ }
436
+
134
437
  // web-src/catch-up.ts
135
438
  function shouldCatchUpDiff(route) {
136
439
  return route.screen !== "repo" && !(route.screen === "file" && route.view === "blob");
@@ -160,6 +463,24 @@
160
463
  to: raw.slice(sep + 2) || fallback.to
161
464
  };
162
465
  }
466
+ function parseLineTarget(value) {
467
+ const raw = value || "";
468
+ const range = /^(\d+)-(\d+)$/.exec(raw);
469
+ if (range) {
470
+ const a = Number(range[1]);
471
+ const b = Number(range[2]);
472
+ const start = Math.min(a, b);
473
+ const end = Math.max(a, b);
474
+ if (start > 0)
475
+ return { start, end };
476
+ return;
477
+ }
478
+ const line = Number(raw);
479
+ return Number.isInteger(line) && line > 0 ? line : undefined;
480
+ }
481
+ function formatLineTarget(line) {
482
+ return typeof line === "number" ? String(line) : line.start + "-" + line.end;
483
+ }
163
484
  function parseRoute(pathname, search, fallbackRange) {
164
485
  const params = new URLSearchParams(search);
165
486
  const legacyRange = parseLegacyRange(params.get("range"), fallbackRange);
@@ -178,15 +499,28 @@
178
499
  };
179
500
  case "/todif":
180
501
  case "/todiff":
181
- return { screen: "diff", range };
502
+ return {
503
+ screen: "diff",
504
+ range,
505
+ ...params.get("path") ? { path: params.get("path") || "" } : {},
506
+ ...parseLineTarget(params.get("line")) ? { line: parseLineTarget(params.get("line")) } : {}
507
+ };
182
508
  case "/file": {
183
509
  const path = params.get("path") || "";
184
510
  const target = params.get("target") || "";
185
511
  const ref = target || params.get("ref") || "worktree";
512
+ const line = parseLineTarget(params.get("line"));
186
513
  if (!path)
187
514
  return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
188
- return { screen: "file", path, ref, range, view: target ? "blob" : "detail" };
515
+ return { screen: "file", path, ref, range, view: target ? "blob" : "detail", ...line ? { line } : {} };
189
516
  }
517
+ case "/help":
518
+ return {
519
+ screen: "help",
520
+ range,
521
+ lang: params.get("lang") || "en",
522
+ section: params.get("section") || "keybindings"
523
+ };
190
524
  default:
191
525
  return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
192
526
  }
@@ -204,11 +538,20 @@
204
538
  }
205
539
  case "file":
206
540
  if (route.view === "blob") {
207
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree");
541
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
208
542
  }
209
- return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
543
+ 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
544
  case "diff":
211
- return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
545
+ 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)) : "");
546
+ case "help": {
547
+ const params = new URLSearchParams;
548
+ if (route.lang && route.lang !== "en")
549
+ params.set("lang", route.lang);
550
+ if (route.section && route.section !== "keybindings")
551
+ params.set("section", route.section);
552
+ const qs = params.toString();
553
+ return "/help" + (qs ? "?" + qs : "");
554
+ }
212
555
  case "unknown":
213
556
  return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
214
557
  default:
@@ -6002,7 +6345,29 @@
6002
6345
  return markdown;
6003
6346
  }
6004
6347
  function renderMarkdownHtml(textValue, target, highlighter, signal) {
6005
- return createMarkdownIt(target, highlighter, signal).render(textValue);
6348
+ const md = createMarkdownIt(target, highlighter, signal);
6349
+ const frontmatter = splitYamlFrontmatter(textValue);
6350
+ if (!frontmatter)
6351
+ return md.render(textValue);
6352
+ return '<div class="gdp-markdown-frontmatter" data-gdp-frontmatter="yaml">' + md.render("```yaml\n" + frontmatter.yaml + "\n```\n") + "</div>" + md.render(frontmatter.body);
6353
+ }
6354
+ function splitYamlFrontmatter(textValue) {
6355
+ if (!textValue.startsWith(`---
6356
+ `) && !textValue.startsWith(`---\r
6357
+ `))
6358
+ return null;
6359
+ const newline2 = textValue.startsWith(`---\r
6360
+ `) ? `\r
6361
+ ` : `
6362
+ `;
6363
+ const start = 3 + newline2.length;
6364
+ const closing = textValue.indexOf(newline2 + "---" + newline2, start);
6365
+ if (closing < 0)
6366
+ return null;
6367
+ return {
6368
+ yaml: textValue.slice(start, closing),
6369
+ body: textValue.slice(closing + newline2.length + 3 + newline2.length)
6370
+ };
6006
6371
  }
6007
6372
  async function loadMarkdownHighlighter() {
6008
6373
  if (!shikiPromise) {
@@ -6399,6 +6764,177 @@
6399
6764
  let REPO_SIDEBAR_REF = null;
6400
6765
  let REPO_SIDEBAR_LOAD_REF = null;
6401
6766
  let REPO_SIDEBAR_LOAD = null;
6767
+ let PENDING_G_SCOPE = null;
6768
+ let PENDING_G_UNTIL = 0;
6769
+ let SOURCE_CURSOR = null;
6770
+ const SOURCE_CURSOR_TOTALS = new Map;
6771
+ const HELP_LANGUAGES = ["en", "ja"];
6772
+ const HELP_SECTIONS = ["keybindings"];
6773
+ const HELP_CONTENT = {
6774
+ en: {
6775
+ languageLabel: "Language",
6776
+ title: "Help",
6777
+ sections: {
6778
+ keybindings: {
6779
+ nav: "Keybindings",
6780
+ title: "Keyboard Shortcuts",
6781
+ intro: "Use these shortcuts to move between panels and navigate files without leaving the keyboard.",
6782
+ groups: [
6783
+ { title: "Global", rows: [["Ctrl+K", "Open file palette"], ["Ctrl+G", "Open grep palette"], ["/", "Focus file filter"], ["t", "Toggle theme"]] },
6784
+ { title: "Panels", rows: [["Ctrl+H", "Focus sidebar"], ["Ctrl+L", "Focus main panel"]] },
6785
+ { title: "Sidebar", rows: [["j / k", "Move selection down / up"], ["Ctrl+D / Ctrl+U", "Move selection by half a page"], ["gg / Shift+G", "Move to top / bottom"], ["Enter", "Open selected item"], ["h / l", "Collapse / expand directory"]] },
6786
+ { title: "Main Panel", rows: [["j / k", "Move code cursor down / up"], ["Ctrl+D / Ctrl+U", "Move code cursor by half a page"], ["gg / Shift+G", "Move code cursor to top / bottom"], ["gp / gc", "Switch to Preview / Code tab"]] }
6787
+ ]
6788
+ }
6789
+ }
6790
+ },
6791
+ ja: {
6792
+ languageLabel: "言語",
6793
+ title: "ヘルプ",
6794
+ sections: {
6795
+ keybindings: {
6796
+ nav: "キーバインド",
6797
+ title: "キーバインド",
6798
+ intro: "キーボードだけでパネル移動、ファイル選択、スクロールを行うためのショートカットです。",
6799
+ groups: [
6800
+ { title: "グローバル", rows: [["Ctrl+K", "ファイルパレットを開く"], ["Ctrl+G", "grep パレットを開く"], ["/", "ファイルフィルターへフォーカス"], ["t", "テーマ切り替え"]] },
6801
+ { title: "パネル", rows: [["Ctrl+H", "サイドバーへフォーカス"], ["Ctrl+L", "メインパネルへフォーカス"]] },
6802
+ { title: "サイドバー", rows: [["j / k", "選択を下 / 上へ移動"], ["Ctrl+D / Ctrl+U", "半ページ分選択を移動"], ["gg / Shift+G", "先頭 / 末尾へ移動"], ["Enter", "選択項目を開く"], ["h / l", "ディレクトリを閉じる / 開く"]] },
6803
+ { title: "メインパネル", rows: [["j / k", "コードカーソルを下 / 上へ移動"], ["Ctrl+D / Ctrl+U", "コードカーソルを半ページ分移動"], ["gg / Shift+G", "コードカーソルを先頭 / 末尾へ移動"], ["gp / gc", "Preview / Code タブへ切り替え"]] }
6804
+ ]
6805
+ }
6806
+ }
6807
+ }
6808
+ };
6809
+ function sourceLineScrollAmount() {
6810
+ const virtualRow = Array.from(document.querySelectorAll("#content .gdp-source-virtual-row")).find((item) => item.offsetParent !== null);
6811
+ if (virtualRow)
6812
+ return virtualRow.getBoundingClientRect().height || VIRTUAL_SOURCE_ROW_HEIGHT;
6813
+ const sourceRow = Array.from(document.querySelectorAll("#content .gdp-source-table tr")).find((item) => item.offsetParent !== null);
6814
+ if (sourceRow)
6815
+ return sourceRow.getBoundingClientRect().height || 20;
6816
+ const preview = document.querySelector("#content .gdp-markdown-preview:not([hidden])");
6817
+ const lineHeight = Number.parseFloat(getComputedStyle(preview || document.body).lineHeight);
6818
+ return Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 20;
6819
+ }
6820
+ function hasVisibleSourceCodeSurface() {
6821
+ return Array.from(document.querySelectorAll("#content .gdp-source-virtual-scroller, #content .gdp-source-table")).some((item) => item.offsetParent !== null);
6822
+ }
6823
+ function sourceCursorKey(target) {
6824
+ return target.ref + "\x00" + target.path;
6825
+ }
6826
+ function sourceCursorMatches(target, line) {
6827
+ return !!SOURCE_CURSOR && sourceTargetsEqual(SOURCE_CURSOR.target, target) && SOURCE_CURSOR.line === line;
6828
+ }
6829
+ function syncSourceCursorRows(target) {
6830
+ document.querySelectorAll("#content [data-line]").forEach((row) => {
6831
+ const line = Number(row.dataset.line || "0");
6832
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, line));
6833
+ });
6834
+ }
6835
+ function visibleSourceLineFallback() {
6836
+ const scroller = findMainScrollTarget();
6837
+ if (scroller)
6838
+ return Math.max(1, Math.floor(scroller.scrollTop / VIRTUAL_SOURCE_ROW_HEIGHT) + 1);
6839
+ const rows = $$("#content .gdp-source-table tr[data-line]");
6840
+ const contentTop = document.querySelector("#content")?.getBoundingClientRect().top ?? 0;
6841
+ const row = rows.find((item) => item.getBoundingClientRect().bottom >= Math.max(0, contentTop));
6842
+ return Math.max(1, Number(row?.dataset.line || "1"));
6843
+ }
6844
+ function ensureSourceCursor(target) {
6845
+ if (SOURCE_CURSOR && sourceTargetsEqual(SOURCE_CURSOR.target, target))
6846
+ return SOURCE_CURSOR;
6847
+ const routeLine = lineTargetStart(currentSourceLineTarget(target));
6848
+ SOURCE_CURSOR = { target, line: routeLine || visibleSourceLineFallback() };
6849
+ syncSourceCursorRows(target);
6850
+ return SOURCE_CURSOR;
6851
+ }
6852
+ function resetSourceCursorForTarget(target, totalLines) {
6853
+ const routeLine = lineTargetStart(currentSourceLineTarget(target));
6854
+ SOURCE_CURSOR = { target, line: Math.max(1, Math.min(totalLines, routeLine || 1)) };
6855
+ }
6856
+ function scrollSourceCursorIntoView(cursor, edge = "nearest") {
6857
+ const scroller = findMainScrollTarget();
6858
+ if (scroller) {
6859
+ const top = (cursor.line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT;
6860
+ const bottom = top + VIRTUAL_SOURCE_ROW_HEIGHT;
6861
+ const before = scroller.scrollTop;
6862
+ if (edge === "center")
6863
+ scroller.scrollTop = Math.max(0, top - Math.round(scroller.clientHeight / 2));
6864
+ else if (top < scroller.scrollTop)
6865
+ scroller.scrollTop = top;
6866
+ else if (bottom > scroller.scrollTop + scroller.clientHeight)
6867
+ scroller.scrollTop = bottom - scroller.clientHeight;
6868
+ if (scroller.scrollTop !== before)
6869
+ scroller.dispatchEvent(new Event("scroll"));
6870
+ scroller.__gdpRenderVirtualSource?.();
6871
+ syncSourceCursorRows(cursor.target);
6872
+ return;
6873
+ }
6874
+ document.querySelector('#content [data-line="' + cursor.line + '"]')?.scrollIntoView({ block: edge });
6875
+ }
6876
+ function moveSourceCursor(direction, unit, edge) {
6877
+ if (!hasVisibleSourceCodeSurface())
6878
+ return false;
6879
+ const target = sourceTargetFromRoute();
6880
+ if (!target)
6881
+ return false;
6882
+ const total = SOURCE_CURSOR_TOTALS.get(sourceCursorKey(target));
6883
+ if (!total)
6884
+ return false;
6885
+ const cursor = ensureSourceCursor(target);
6886
+ if (unit === "edge") {
6887
+ cursor.line = edge === "bottom" ? total : 1;
6888
+ syncSourceCursorRows(target);
6889
+ scrollSourceCursorIntoView(cursor, "center");
6890
+ return true;
6891
+ }
6892
+ const pageRows = Math.max(1, Math.floor((findMainScrollTarget()?.clientHeight || window.innerHeight) * 0.55 / (sourceLineScrollAmount() || VIRTUAL_SOURCE_ROW_HEIGHT)));
6893
+ const delta = unit === "page" ? pageRows : 1;
6894
+ cursor.line = Math.max(1, Math.min(total, cursor.line + direction * delta));
6895
+ syncSourceCursorRows(target);
6896
+ scrollSourceCursorIntoView(cursor);
6897
+ return true;
6898
+ }
6899
+ function scrollMainPanel(direction, repeated = false, unit = "line") {
6900
+ if (moveSourceCursor(direction, unit))
6901
+ return;
6902
+ const target = findMainScrollTarget();
6903
+ const viewportHeight = target?.clientHeight || document.scrollingElement?.clientHeight || window.innerHeight;
6904
+ const top = direction * (unit === "line" ? Math.round(sourceLineScrollAmount() || 32) : Math.round(viewportHeight * 0.55));
6905
+ const behavior = repeated ? "auto" : "smooth";
6906
+ if (target)
6907
+ target.scrollBy({ top, behavior });
6908
+ else
6909
+ window.scrollBy({ top, behavior });
6910
+ }
6911
+ function scrollMainToEdge(edge) {
6912
+ if (moveSourceCursor(edge === "bottom" ? 1 : -1, "edge", edge))
6913
+ return;
6914
+ const target = findMainScrollTarget();
6915
+ if (target) {
6916
+ target.scrollTo({ top: edge === "top" ? 0 : target.scrollHeight, behavior: "auto" });
6917
+ return;
6918
+ }
6919
+ const top = edge === "top" ? 0 : Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
6920
+ window.scrollTo({ top, behavior: "auto" });
6921
+ }
6922
+ function switchSourceTab(tab) {
6923
+ const tabs = document.querySelector("#content .gdp-source-tabs");
6924
+ if (!tabs)
6925
+ return false;
6926
+ const button = tabs.querySelector('button[data-source-tab="' + tab + '"]');
6927
+ if (!button || button.hidden || button.disabled)
6928
+ return false;
6929
+ button.click();
6930
+ focusMainPanel();
6931
+ return true;
6932
+ }
6933
+ function isFocusableClickTarget(target) {
6934
+ if (!(target instanceof Element))
6935
+ return false;
6936
+ return !!target.closest('a, button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]');
6937
+ }
6402
6938
  function invalidateRepoSidebar() {
6403
6939
  REPO_SIDEBAR_REF = null;
6404
6940
  REPO_SIDEBAR_LOAD_REF = null;
@@ -6741,6 +7277,7 @@
6741
7277
  const dir = item.dir;
6742
7278
  const li = document.createElement("li");
6743
7279
  li.className = "tree-dir";
7280
+ li.tabIndex = -1;
6744
7281
  li.dataset.dirpath = dir.path;
6745
7282
  if (dir.explicit)
6746
7283
  li.dataset.explicit = "true";
@@ -6798,6 +7335,7 @@
6798
7335
  li.addEventListener("click", (e2) => {
6799
7336
  e2.stopPropagation();
6800
7337
  onFileClick({ path: dir.path, display_path: dir.path, type: "tree", children_omitted: dir.children_omitted });
7338
+ focusSidebarPanel();
6801
7339
  });
6802
7340
  } else {
6803
7341
  li.addEventListener("click", toggleDir);
@@ -6808,6 +7346,7 @@
6808
7346
  const f2 = item.file;
6809
7347
  const li = document.createElement("li");
6810
7348
  li.className = "tree-file";
7349
+ li.tabIndex = -1;
6811
7350
  li.dataset.path = f2.path;
6812
7351
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
6813
7352
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
@@ -6832,6 +7371,7 @@
6832
7371
  onFileClick(f2);
6833
7372
  else
6834
7373
  scrollToFile(f2.path);
7374
+ focusSidebarPanel();
6835
7375
  });
6836
7376
  if (!onFileClick)
6837
7377
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -6842,6 +7382,7 @@
6842
7382
  function renderFlat(files, ul, onFileClick) {
6843
7383
  files.forEach((f2, i2) => {
6844
7384
  const li = document.createElement("li");
7385
+ li.tabIndex = -1;
6845
7386
  li.dataset.index = String(i2);
6846
7387
  li.dataset.path = f2.path;
6847
7388
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
@@ -6863,6 +7404,7 @@
6863
7404
  onFileClick(f2);
6864
7405
  else
6865
7406
  scrollToFile(f2.path);
7407
+ focusSidebarPanel();
6866
7408
  });
6867
7409
  if (!onFileClick)
6868
7410
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -6954,7 +7496,41 @@
6954
7496
  return;
6955
7497
  enqueueLoad(f2, card, 5);
6956
7498
  }
6957
- function scrollToFile(path) {
7499
+ function clearDiffLineFocus() {
7500
+ document.querySelectorAll(".gdp-diff-line-target").forEach((row) => {
7501
+ row.classList.remove("gdp-diff-line-target");
7502
+ });
7503
+ }
7504
+ function diffRowLineNumber(row) {
7505
+ const newLine = row.querySelector(".line-num2, td.d2h-code-side-linenumber");
7506
+ const raw = (newLine?.textContent || "").trim();
7507
+ const line = Number(raw);
7508
+ return Number.isInteger(line) && line > 0 ? line : null;
7509
+ }
7510
+ function focusDiffLine(card, line) {
7511
+ const start = lineTargetStart(line);
7512
+ if (!start)
7513
+ return false;
7514
+ const rows = Array.from(card.querySelectorAll("table.d2h-diff-table tr"));
7515
+ const row = rows.find((candidate) => diffRowLineNumber(candidate) === start);
7516
+ if (!row)
7517
+ return false;
7518
+ clearDiffLineFocus();
7519
+ row.classList.add("gdp-diff-line-target");
7520
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
7521
+ return true;
7522
+ }
7523
+ function applyDiffRouteFocus(card) {
7524
+ if (STATE.route.screen !== "diff" || !STATE.route.path || !STATE.route.line)
7525
+ return false;
7526
+ if (card && card.dataset.path !== STATE.route.path)
7527
+ return false;
7528
+ const targetCard = card || document.querySelector(diffCardSelector(STATE.route.path));
7529
+ if (!targetCard)
7530
+ return false;
7531
+ return focusDiffLine(targetCard, STATE.route.line);
7532
+ }
7533
+ function scrollToFile(path, line) {
6958
7534
  const card = document.querySelector(diffCardSelector(path));
6959
7535
  if (!card)
6960
7536
  return;
@@ -6970,7 +7546,9 @@
6970
7546
  if (f2)
6971
7547
  enqueueLoad(f2, card, 10);
6972
7548
  }
6973
- card.scrollIntoView({ behavior: "smooth", block: "start" });
7549
+ if (!line || !focusDiffLine(card, line)) {
7550
+ card.scrollIntoView({ behavior: "smooth", block: "start" });
7551
+ }
6974
7552
  }
6975
7553
  function markActive(path) {
6976
7554
  STATE.activeFile = path;
@@ -7095,6 +7673,12 @@
7095
7673
  function repoFileTargetFromRoute() {
7096
7674
  return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
7097
7675
  }
7676
+ function helpLanguageFromRoute() {
7677
+ return STATE.route.screen === "help" && HELP_LANGUAGES.includes(STATE.route.lang) ? STATE.route.lang : "en";
7678
+ }
7679
+ function helpSectionFromRoute() {
7680
+ return STATE.route.screen === "help" && HELP_SECTIONS.includes(STATE.route.section) ? STATE.route.section : "keybindings";
7681
+ }
7098
7682
  function setRoute(route, replace2 = false) {
7099
7683
  const nextRoute = route.screen === "unknown" ? { screen: "diff", range: route.range } : route;
7100
7684
  STATE.route = nextRoute;
@@ -7115,6 +7699,7 @@
7115
7699
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
7116
7700
  document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
7117
7701
  document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
7702
+ document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
7118
7703
  syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
7119
7704
  }
7120
7705
  function syncHeaderMenu() {
@@ -7129,12 +7714,104 @@
7129
7714
  if (link2.dataset.route === "diff") {
7130
7715
  link2.href = buildRoute({ screen: "diff", range: currentRange() });
7131
7716
  }
7717
+ if (link2.dataset.route === "help") {
7718
+ link2.href = buildRoute({ screen: "help", lang: helpLanguageFromRoute(), section: helpSectionFromRoute(), range: currentRange() });
7719
+ }
7132
7720
  });
7133
7721
  }
7134
7722
  function removeStandaloneSource() {
7135
7723
  document.querySelectorAll(".gdp-standalone-source").forEach((el) => el.remove());
7136
7724
  document.querySelectorAll(".gdp-repo-blob-layout").forEach((el) => el.remove());
7137
7725
  }
7726
+ function renderHelpPage() {
7727
+ cancelActiveSourceLoad("navigation");
7728
+ removeStandaloneSource();
7729
+ LOAD_QUEUE.length = 0;
7730
+ const target = $("#diff");
7731
+ const empty = $("#empty");
7732
+ empty.classList.add("hidden");
7733
+ $("#meta").textContent = "";
7734
+ $("#totals").textContent = "";
7735
+ $("#filelist").textContent = "";
7736
+ const lang = helpLanguageFromRoute();
7737
+ const section = helpSectionFromRoute();
7738
+ const content = HELP_CONTENT[lang];
7739
+ const sectionContent = content.sections[section];
7740
+ const shell = document.createElement("section");
7741
+ shell.className = "gdp-help-shell";
7742
+ const header = document.createElement("header");
7743
+ header.className = "gdp-help-header";
7744
+ const title = document.createElement("h1");
7745
+ title.textContent = content.title;
7746
+ const langSelect = document.createElement("select");
7747
+ langSelect.className = "gdp-help-language";
7748
+ langSelect.setAttribute("aria-label", content.languageLabel);
7749
+ HELP_LANGUAGES.forEach((optionLang) => {
7750
+ const option = document.createElement("option");
7751
+ option.value = optionLang;
7752
+ option.textContent = optionLang.toUpperCase();
7753
+ option.selected = optionLang === lang;
7754
+ langSelect.appendChild(option);
7755
+ });
7756
+ langSelect.addEventListener("change", () => {
7757
+ setRoute({ screen: "help", lang: langSelect.value, section, range: currentRange() });
7758
+ setPageMode();
7759
+ renderHelpPage();
7760
+ syncHeaderMenu();
7761
+ });
7762
+ header.append(title, langSelect);
7763
+ const layout = document.createElement("div");
7764
+ layout.className = "gdp-help-layout";
7765
+ const helpNav = document.createElement("nav");
7766
+ helpNav.className = "gdp-help-nav";
7767
+ HELP_SECTIONS.forEach((helpSection) => {
7768
+ const button = document.createElement("button");
7769
+ button.type = "button";
7770
+ button.className = helpSection === section ? "active" : "";
7771
+ button.textContent = content.sections[helpSection].nav;
7772
+ button.addEventListener("click", () => {
7773
+ setRoute({ screen: "help", lang, section: helpSection, range: currentRange() });
7774
+ renderHelpPage();
7775
+ syncHeaderMenu();
7776
+ });
7777
+ helpNav.appendChild(button);
7778
+ });
7779
+ const article = document.createElement("article");
7780
+ article.className = "gdp-help-content";
7781
+ const h2 = document.createElement("h2");
7782
+ h2.textContent = sectionContent.title;
7783
+ const intro = document.createElement("p");
7784
+ intro.textContent = sectionContent.intro;
7785
+ article.append(h2, intro);
7786
+ sectionContent.groups.forEach((group) => {
7787
+ const groupSection = document.createElement("section");
7788
+ groupSection.className = "gdp-help-group";
7789
+ const groupTitle = document.createElement("h3");
7790
+ groupTitle.textContent = group.title;
7791
+ const table2 = document.createElement("table");
7792
+ group.rows.forEach(([keys, description]) => {
7793
+ const tr = document.createElement("tr");
7794
+ const keyCell = document.createElement("th");
7795
+ keyCell.scope = "row";
7796
+ keys.split(" / ").forEach((key, index) => {
7797
+ if (index > 0)
7798
+ keyCell.append(" / ");
7799
+ const kbd = document.createElement("kbd");
7800
+ kbd.textContent = key;
7801
+ keyCell.appendChild(kbd);
7802
+ });
7803
+ const desc = document.createElement("td");
7804
+ desc.textContent = description;
7805
+ tr.append(keyCell, desc);
7806
+ table2.appendChild(tr);
7807
+ });
7808
+ groupSection.append(groupTitle, table2);
7809
+ article.appendChild(groupSection);
7810
+ });
7811
+ layout.append(helpNav, article);
7812
+ shell.append(header, layout);
7813
+ target.replaceChildren(shell);
7814
+ }
7138
7815
  function renderShell(meta) {
7139
7816
  const newFiles = meta.files || [];
7140
7817
  STATE.files = newFiles;
@@ -8624,11 +9301,35 @@
8624
9301
  });
8625
9302
  return info;
8626
9303
  }
8627
- function createSourceTabs(active) {
9304
+ function createSourceCopyButton(textValue) {
9305
+ const copy = document.createElement("button");
9306
+ copy.type = "button";
9307
+ copy.className = "gdp-file-header-icon gdp-copy-source";
9308
+ copy.title = "Copy source";
9309
+ copy.setAttribute("aria-label", "Copy source");
9310
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
9311
+ copy.addEventListener("click", async () => {
9312
+ try {
9313
+ await navigator.clipboard.writeText(textValue);
9314
+ copy.classList.add("copied");
9315
+ setTimeout(() => {
9316
+ copy.classList.remove("copied");
9317
+ }, 1200);
9318
+ } catch {
9319
+ copy.classList.add("failed");
9320
+ setTimeout(() => {
9321
+ copy.classList.remove("failed");
9322
+ }, 1200);
9323
+ }
9324
+ });
9325
+ return copy;
9326
+ }
9327
+ function createSourceTabs(active, textValue) {
8628
9328
  const tabs = document.createElement("div");
8629
9329
  tabs.className = "gdp-source-tabs";
8630
9330
  const codeButton = document.createElement("button");
8631
9331
  codeButton.type = "button";
9332
+ codeButton.dataset.sourceTab = "code";
8632
9333
  codeButton.textContent = "Code";
8633
9334
  codeButton.classList.toggle("active", active === "code");
8634
9335
  tabs.appendChild(codeButton);
@@ -8636,10 +9337,13 @@
8636
9337
  if (active === "preview") {
8637
9338
  previewButton = document.createElement("button");
8638
9339
  previewButton.type = "button";
9340
+ previewButton.dataset.sourceTab = "preview";
8639
9341
  previewButton.className = "active";
8640
9342
  previewButton.textContent = "Preview";
8641
9343
  tabs.prepend(previewButton);
8642
9344
  }
9345
+ if (textValue != null)
9346
+ tabs.appendChild(createSourceCopyButton(textValue));
8643
9347
  return { tabs, codeButton, previewButton };
8644
9348
  }
8645
9349
  async function renderSourceText(card, target, textValue, signal) {
@@ -8647,6 +9351,8 @@
8647
9351
  `).replace(/\r/g, `
8648
9352
  `).split(`
8649
9353
  `) : [""];
9354
+ SOURCE_CURSOR_TOTALS.set(sourceCursorKey(target), lines.length);
9355
+ resetSourceCursorForTarget(target, lines.length);
8650
9356
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
8651
9357
  const isStandalone = card.classList.contains("gdp-standalone-source");
8652
9358
  const view = document.createElement("div");
@@ -8667,7 +9373,7 @@
8667
9373
  if (usesVirtualSource) {
8668
9374
  const virtualCode = renderVirtualSource(target, textValue, lines, hljsRef, lang);
8669
9375
  if (previewable) {
8670
- const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview");
9376
+ const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview", textValue);
8671
9377
  if (tabsHost) {
8672
9378
  tabsHost.hidden = false;
8673
9379
  tabsHost.replaceChildren(tabs2);
@@ -8727,9 +9433,13 @@
8727
9433
  return false;
8728
9434
  const line = lines[index];
8729
9435
  const tr = document.createElement("tr");
9436
+ tr.dataset.line = String(index + 1);
9437
+ tr.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9438
+ tr.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
8730
9439
  const num = document.createElement("td");
8731
9440
  num.className = "gdp-source-line-number";
8732
9441
  num.textContent = String(index + 1);
9442
+ bindSourceLineNumber(num, card, target, index + 1);
8733
9443
  const code2 = document.createElement("td");
8734
9444
  code2.className = "gdp-source-line-code";
8735
9445
  if (shikiLines && shikiLines[index] != null) {
@@ -8748,7 +9458,7 @@
8748
9458
  }
8749
9459
  }
8750
9460
  table2.appendChild(tbody);
8751
- const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code");
9461
+ const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code", textValue);
8752
9462
  if (tabsHost) {
8753
9463
  tabsHost.hidden = false;
8754
9464
  tabsHost.replaceChildren(tabs);
@@ -8821,6 +9531,69 @@
8821
9531
  url.searchParams.delete("virtual");
8822
9532
  return url.pathname + url.search;
8823
9533
  }
9534
+ function currentSourceLineTarget(target) {
9535
+ const routeTarget = sourceTargetFromRoute();
9536
+ return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : undefined;
9537
+ }
9538
+ function lineTargetStart(line) {
9539
+ if (!line)
9540
+ return;
9541
+ return typeof line === "number" ? line : line.start;
9542
+ }
9543
+ function lineInSourceTarget(lineNumber, target) {
9544
+ if (!target)
9545
+ return false;
9546
+ if (typeof target === "number")
9547
+ return lineNumber === target;
9548
+ return lineNumber >= target.start && lineNumber <= target.end;
9549
+ }
9550
+ let SOURCE_LINE_DRAG = null;
9551
+ function normalizeSourceLineSelection(start, end) {
9552
+ const a2 = Math.max(1, Math.floor(start));
9553
+ const b2 = Math.max(1, Math.floor(end));
9554
+ const from = Math.min(a2, b2);
9555
+ const to = Math.max(a2, b2);
9556
+ return from === to ? from : { start: from, end: to };
9557
+ }
9558
+ function setSourceLineRoute(target, line) {
9559
+ if (STATE.route.screen !== "file")
9560
+ return;
9561
+ setRoute({
9562
+ screen: "file",
9563
+ path: target.path,
9564
+ ref: target.ref,
9565
+ view: STATE.route.view,
9566
+ range: currentRange(),
9567
+ line
9568
+ }, true);
9569
+ }
9570
+ function syncRenderedSourceLineHighlights(card, target) {
9571
+ const lineTarget = currentSourceLineTarget(target);
9572
+ card.querySelectorAll("[data-line]").forEach((row) => {
9573
+ const line = Number(row.dataset.line || "0");
9574
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(line, lineTarget));
9575
+ });
9576
+ }
9577
+ function updateSourceLineSelection(card, target, start, end) {
9578
+ setSourceLineRoute(target, normalizeSourceLineSelection(start, end));
9579
+ syncRenderedSourceLineHighlights(card, target);
9580
+ }
9581
+ function beginSourceLineSelection(event, card, target, line) {
9582
+ event.preventDefault();
9583
+ SOURCE_LINE_DRAG = { target, start: line };
9584
+ updateSourceLineSelection(card, target, line, line);
9585
+ }
9586
+ function bindSourceLineNumber(num, card, target, line) {
9587
+ num.addEventListener("mousedown", (e2) => beginSourceLineSelection(e2, card, target, line));
9588
+ num.addEventListener("mouseenter", () => {
9589
+ if (!SOURCE_LINE_DRAG || !sourceTargetsEqual(SOURCE_LINE_DRAG.target, target))
9590
+ return;
9591
+ updateSourceLineSelection(card, target, SOURCE_LINE_DRAG.start, line);
9592
+ });
9593
+ }
9594
+ document.addEventListener("mouseup", () => {
9595
+ SOURCE_LINE_DRAG = null;
9596
+ });
8824
9597
  function renderVirtualSource(target, textValue, lines, hljsRef, lang) {
8825
9598
  const wrap = document.createElement("div");
8826
9599
  wrap.className = "gdp-source-virtual";
@@ -8836,19 +9609,21 @@
8836
9609
  actions.className = "gdp-source-virtual-actions";
8837
9610
  const copy = document.createElement("button");
8838
9611
  copy.type = "button";
8839
- copy.className = "gdp-source-virtual-action";
8840
- copy.textContent = "Copy all";
9612
+ copy.className = "gdp-file-header-icon gdp-copy-source gdp-source-virtual-copy";
9613
+ copy.title = "Copy source";
9614
+ copy.setAttribute("aria-label", "Copy source");
9615
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
8841
9616
  copy.addEventListener("click", async () => {
8842
9617
  try {
8843
9618
  await navigator.clipboard.writeText(textValue);
8844
- copy.textContent = "Copied";
9619
+ copy.classList.add("copied");
8845
9620
  setTimeout(() => {
8846
- copy.textContent = "Copy all";
9621
+ copy.classList.remove("copied");
8847
9622
  }, 1200);
8848
9623
  } catch {
8849
- copy.textContent = "Copy failed";
9624
+ copy.classList.add("failed");
8850
9625
  setTimeout(() => {
8851
- copy.textContent = "Copy all";
9626
+ copy.classList.remove("failed");
8852
9627
  }, 1600);
8853
9628
  }
8854
9629
  });
@@ -8859,7 +9634,8 @@
8859
9634
  full.title = "Render every line without virtualization. This can be slow for large files.";
8860
9635
  full.addEventListener("click", (e2) => {
8861
9636
  e2.preventDefault();
8862
- history.pushState(null, "", full.href);
9637
+ const url = new URL(full.href, window.location.origin);
9638
+ setRoute(parseRoute(url.pathname, url.search, currentRange()), true);
8863
9639
  renderStandaloneSource(target);
8864
9640
  });
8865
9641
  actions.append(copy, full);
@@ -8893,9 +9669,13 @@
8893
9669
  for (let index = start;index < end; index++) {
8894
9670
  const row = document.createElement("div");
8895
9671
  row.className = "gdp-source-virtual-row";
9672
+ row.dataset.line = String(index + 1);
9673
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9674
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
8896
9675
  const num = document.createElement("span");
8897
9676
  num.className = "gdp-source-virtual-line-number";
8898
9677
  num.textContent = String(index + 1);
9678
+ bindSourceLineNumber(num, wrap, target, index + 1);
8899
9679
  const code2 = document.createElement("span");
8900
9680
  code2.className = "gdp-source-virtual-line-code";
8901
9681
  const line = lines[index] ?? "";
@@ -8918,6 +9698,7 @@
8918
9698
  if (!raf)
8919
9699
  raf = requestAnimationFrame(render);
8920
9700
  };
9701
+ scroller.__gdpRenderVirtualSource = render;
8921
9702
  scroller.addEventListener("scroll", schedule, { passive: true });
8922
9703
  let resizeObserver = null;
8923
9704
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
@@ -9143,6 +9924,7 @@
9143
9924
  return;
9144
9925
  if (!rendered)
9145
9926
  return;
9927
+ scrollStandaloneSourceLine(card, lineTargetStart(STATE.route.screen === "file" ? STATE.route.line : undefined));
9146
9928
  finishSourceLoad(req);
9147
9929
  }
9148
9930
  } catch (err) {
@@ -9156,6 +9938,19 @@
9156
9938
  renderSourceError(card, target, "Cannot load " + target.path + " at " + target.ref);
9157
9939
  }
9158
9940
  }
9941
+ function scrollStandaloneSourceLine(card, line) {
9942
+ if (!line || line < 1)
9943
+ return;
9944
+ const virtualScroller = card.querySelector(".gdp-source-virtual-scroller");
9945
+ if (virtualScroller) {
9946
+ const centeredOffset = virtualScroller.clientHeight / 2 - VIRTUAL_SOURCE_ROW_HEIGHT / 2;
9947
+ virtualScroller.scrollTop = Math.max(0, (line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - Math.max(0, centeredOffset));
9948
+ return;
9949
+ }
9950
+ const row = card.querySelector('.gdp-source-table tr[data-line="' + String(line) + '"]');
9951
+ if (row)
9952
+ row.scrollIntoView({ block: "center" });
9953
+ }
9159
9954
  function applySourceRouteToShell() {
9160
9955
  const target = sourceTargetFromRoute();
9161
9956
  setPageMode();
@@ -9344,6 +10139,7 @@
9344
10139
  card.classList.add("loaded");
9345
10140
  card.style.minHeight = "";
9346
10141
  mountDiff(card, file, data);
10142
+ applyDiffRouteFocus(card);
9347
10143
  card.style.containIntrinsicSize = Math.max(card.offsetHeight, file.estimated_height_px || 200) + "px";
9348
10144
  applyViewedToCard(card, STATE.viewedFiles.has(file.path), true);
9349
10145
  if (data.truncated && data.mode === "preview") {
@@ -9619,6 +10415,15 @@
9619
10415
  });
9620
10416
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
9621
10417
  $("#sb-collapse-all").addEventListener("click", () => setAllSidebarDirsCollapsed(true));
10418
+ prepareKeyboardPanels();
10419
+ const contentPanel = document.querySelector("#content");
10420
+ contentPanel?.addEventListener("focusin", () => setPanelFocusScope("main"));
10421
+ contentPanel?.addEventListener("mousedown", (event) => {
10422
+ if (isFocusableClickTarget(event.target))
10423
+ setPanelFocusScope("main");
10424
+ else
10425
+ focusMainPanel();
10426
+ });
9622
10427
  function applySidebarWidth(w) {
9623
10428
  const cw = Math.max(180, Math.min(900, w));
9624
10429
  document.documentElement.style.setProperty("--sidebar-w", cw + "px");
@@ -9637,6 +10442,13 @@
9637
10442
  sb.addEventListener("mousedown", mark);
9638
10443
  sb.addEventListener("touchstart", mark, { passive: true });
9639
10444
  sb.addEventListener("scroll", mark, { passive: true });
10445
+ sb.addEventListener("focusin", () => setPanelFocusScope("sidebar"));
10446
+ sb.addEventListener("mousedown", (event) => {
10447
+ if (isFocusableClickTarget(event.target))
10448
+ setPanelFocusScope("sidebar");
10449
+ else
10450
+ focusSidebarPanel();
10451
+ });
9640
10452
  })();
9641
10453
  (function setupResizer() {
9642
10454
  const handle = $("#sidebar-resizer");
@@ -9699,6 +10511,32 @@
9699
10511
  function visibleSidebarItems() {
9700
10512
  return $$("#filelist li[data-path], #filelist .tree-dir[data-dirpath]").filter(isSidebarRowVisible);
9701
10513
  }
10514
+ function scrollSidebarItemIntoView(item, block2 = "nearest") {
10515
+ const sidebar = document.querySelector("#sidebar");
10516
+ if (!sidebar) {
10517
+ item.scrollIntoView({ block: block2 });
10518
+ return;
10519
+ }
10520
+ const sidebarRect = sidebar.getBoundingClientRect();
10521
+ const itemRect = item.getBoundingClientRect();
10522
+ const stickyBottom = Math.max(sidebarRect.top, document.querySelector(".sb-head")?.getBoundingClientRect().bottom || sidebarRect.top, document.querySelector(".sb-filter-wrap")?.getBoundingClientRect().bottom || sidebarRect.top);
10523
+ const topPadding = Math.max(8, stickyBottom - sidebarRect.top + 8);
10524
+ const bottomPadding = 14;
10525
+ const visibleTop = sidebarRect.top + topPadding;
10526
+ const visibleBottom = sidebarRect.bottom - bottomPadding;
10527
+ if (block2 === "start") {
10528
+ sidebar.scrollTop += itemRect.top - visibleTop;
10529
+ return;
10530
+ }
10531
+ if (block2 === "end") {
10532
+ sidebar.scrollTop += itemRect.bottom - visibleBottom;
10533
+ return;
10534
+ }
10535
+ if (itemRect.top < visibleTop)
10536
+ sidebar.scrollTop += itemRect.top - visibleTop;
10537
+ else if (itemRect.bottom > visibleBottom)
10538
+ sidebar.scrollTop += itemRect.bottom - visibleBottom;
10539
+ }
9702
10540
  function isRepositorySidebarMode() {
9703
10541
  return document.body.classList.contains("gdp-repo-page") || document.body.classList.contains("gdp-repo-blob-page");
9704
10542
  }
@@ -9714,7 +10552,44 @@
9714
10552
  const path = target.dataset.path || target.dataset.dirpath;
9715
10553
  if (path)
9716
10554
  markActive(path);
9717
- target.scrollIntoView({ block: "nearest" });
10555
+ scrollSidebarItemIntoView(target);
10556
+ if (target.dataset.path)
10557
+ prefetchByPath(target.dataset.path);
10558
+ }
10559
+ function moveActiveSidebarPage(direction) {
10560
+ const items = visibleSidebarItems();
10561
+ if (!items.length)
10562
+ return;
10563
+ const repoSidebar = isRepositorySidebarMode();
10564
+ const sidebar = document.querySelector("#sidebar");
10565
+ const sample = items.find((item) => item.getBoundingClientRect().height > 0);
10566
+ const rowHeight = sample ? sample.getBoundingClientRect().height : 28;
10567
+ const halfPageRows = Math.max(1, Math.floor((sidebar?.clientHeight || window.innerHeight) / 2 / rowHeight));
10568
+ const current = items.findIndex((li) => li.classList.contains("active"));
10569
+ const start = current < 0 ? 0 : current;
10570
+ const idx = Math.max(0, Math.min(items.length - 1, start + direction * halfPageRows));
10571
+ const target = items[idx];
10572
+ const path = target.dataset.path || target.dataset.dirpath;
10573
+ if (!repoSidebar && target.dataset.path)
10574
+ target.click();
10575
+ else if (path)
10576
+ markActive(path);
10577
+ scrollSidebarItemIntoView(target);
10578
+ if (target.dataset.path)
10579
+ prefetchByPath(target.dataset.path);
10580
+ }
10581
+ function moveActiveSidebarToEdge(edge) {
10582
+ const items = visibleSidebarItems();
10583
+ const repoSidebar = isRepositorySidebarMode();
10584
+ const target = edge === "top" ? items[0] : items[items.length - 1];
10585
+ if (!target)
10586
+ return;
10587
+ const path = target.dataset.path || target.dataset.dirpath;
10588
+ if (!repoSidebar && target.dataset.path)
10589
+ target.click();
10590
+ else if (path)
10591
+ markActive(path);
10592
+ scrollSidebarItemIntoView(target, edge === "top" ? "start" : "end");
9718
10593
  if (target.dataset.path)
9719
10594
  prefetchByPath(target.dataset.path);
9720
10595
  }
@@ -9775,69 +10650,535 @@
9775
10650
  input.focus();
9776
10651
  input.select();
9777
10652
  }
9778
- document.addEventListener("keydown", (e2) => {
9779
- if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "k") {
9780
- e2.preventDefault();
9781
- focusFileFilter();
10653
+ let PALETTE = null;
10654
+ const REPO_FILE_CACHE = new Map;
10655
+ function paletteSource() {
10656
+ if (STATE.route.screen === "diff")
10657
+ return "diff";
10658
+ if (STATE.route.screen === "file" && STATE.route.view !== "blob")
10659
+ return "diff";
10660
+ return "repo";
10661
+ }
10662
+ function paletteRef(source) {
10663
+ if (source === "diff")
10664
+ return STATE.to && STATE.to !== "worktree" ? STATE.to : "worktree";
10665
+ if (STATE.route.screen === "repo")
10666
+ return STATE.route.ref || "worktree";
10667
+ if (STATE.route.screen === "file")
10668
+ return STATE.route.ref || "worktree";
10669
+ return STATE.repoRef || "worktree";
10670
+ }
10671
+ function closeSearchPalette() {
10672
+ if (!PALETTE)
9782
10673
  return;
9783
- }
9784
- const targetEl = e2.target;
9785
- if (targetEl && (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA"))
10674
+ const previousFocusScope = PALETTE.previousFocusScope;
10675
+ PALETTE.controller?.abort();
10676
+ if (PALETTE.debounce)
10677
+ window.clearTimeout(PALETTE.debounce);
10678
+ PALETTE.root.remove();
10679
+ PALETTE = null;
10680
+ restorePanelFocusScope(previousFocusScope);
10681
+ }
10682
+ function createPalette(mode) {
10683
+ const previousFocusScope = PALETTE ? PALETTE.previousFocusScope : getPanelFocusScope();
10684
+ closeSearchPalette();
10685
+ const root = document.createElement("div");
10686
+ root.className = "gdp-palette-backdrop";
10687
+ const dialog = document.createElement("div");
10688
+ dialog.className = "gdp-palette";
10689
+ dialog.setAttribute("role", "dialog");
10690
+ dialog.setAttribute("aria-modal", "true");
10691
+ const label = document.createElement("div");
10692
+ label.className = "gdp-palette-label";
10693
+ label.textContent = mode === "file" ? "Files" : "Grep";
10694
+ const input = document.createElement("input");
10695
+ input.className = "gdp-palette-input";
10696
+ input.type = "search";
10697
+ input.autocomplete = "off";
10698
+ input.spellcheck = false;
10699
+ input.placeholder = mode === "file" ? "Search files" : "Search text";
10700
+ input.setAttribute("role", "combobox");
10701
+ input.setAttribute("aria-expanded", "true");
10702
+ input.setAttribute("aria-controls", "gdp-palette-list");
10703
+ const status = document.createElement("div");
10704
+ status.className = "gdp-palette-status";
10705
+ const controls = document.createElement("div");
10706
+ controls.className = "gdp-palette-controls";
10707
+ const list2 = document.createElement("div");
10708
+ list2.id = "gdp-palette-list";
10709
+ list2.className = "gdp-palette-list";
10710
+ list2.setAttribute("role", "listbox");
10711
+ dialog.append(label, input, controls, status, list2);
10712
+ root.appendChild(dialog);
10713
+ document.body.appendChild(root);
10714
+ const state = {
10715
+ root,
10716
+ input,
10717
+ controls,
10718
+ list: list2,
10719
+ status,
10720
+ mode,
10721
+ grepRegex: false,
10722
+ selected: -1,
10723
+ items: [],
10724
+ composing: false,
10725
+ diffSnapshot: [...STATE.files],
10726
+ previousFocusScope
10727
+ };
10728
+ PALETTE = state;
10729
+ setPanelFocusScope(null);
10730
+ root.addEventListener("mousedown", (e2) => {
10731
+ if (e2.target === root)
10732
+ closeSearchPalette();
10733
+ });
10734
+ input.addEventListener("compositionstart", () => {
10735
+ state.composing = true;
10736
+ });
10737
+ input.addEventListener("compositionend", () => {
10738
+ state.composing = false;
10739
+ });
10740
+ input.addEventListener("input", () => updatePaletteResults(state));
10741
+ input.addEventListener("keydown", (e2) => handlePaletteKeydown(e2, state));
10742
+ input.focus();
10743
+ updatePaletteResults(state);
10744
+ return state;
10745
+ }
10746
+ function renderPaletteControls(state) {
10747
+ state.controls.innerHTML = "";
10748
+ if (state.mode === "file") {
10749
+ const hint2 = document.createElement("span");
10750
+ hint2.className = "gdp-palette-mode-hint";
10751
+ hint2.textContent = isGlobPathQuery(state.input.value) ? "Glob: * ? []" : "Fuzzy path search";
10752
+ state.controls.appendChild(hint2);
9786
10753
  return;
9787
- if (e2.key === "Escape" && !document.querySelector(".mkdp-lightbox")) {
9788
- if (cancelActiveSourceLoad("esc")) {
10754
+ }
10755
+ const plain = document.createElement("button");
10756
+ plain.type = "button";
10757
+ plain.className = "gdp-palette-mode-button";
10758
+ plain.setAttribute("aria-pressed", String(!state.grepRegex));
10759
+ plain.textContent = "Plain";
10760
+ plain.addEventListener("mousedown", (e2) => {
10761
+ e2.preventDefault();
10762
+ state.grepRegex = false;
10763
+ renderPaletteControls(state);
10764
+ updatePaletteResults(state);
10765
+ state.input.focus();
10766
+ });
10767
+ const regex = document.createElement("button");
10768
+ regex.type = "button";
10769
+ regex.className = "gdp-palette-mode-button";
10770
+ regex.setAttribute("aria-pressed", String(state.grepRegex));
10771
+ regex.textContent = ".* Regex";
10772
+ regex.title = "Alt+R";
10773
+ regex.addEventListener("mousedown", (e2) => {
10774
+ e2.preventDefault();
10775
+ state.grepRegex = true;
10776
+ renderPaletteControls(state);
10777
+ updatePaletteResults(state);
10778
+ state.input.focus();
10779
+ });
10780
+ const hint = document.createElement("span");
10781
+ hint.className = "gdp-palette-mode-hint";
10782
+ hint.textContent = "Alt+R toggles regex";
10783
+ state.controls.append(plain, regex, hint);
10784
+ }
10785
+ function regexQueryIsValid(query) {
10786
+ try {
10787
+ new RegExp(query);
10788
+ return true;
10789
+ } catch {
10790
+ return false;
10791
+ }
10792
+ }
10793
+ function appendHighlightedPath(parent, path, ranges) {
10794
+ let cursor = 0;
10795
+ for (const range of ranges) {
10796
+ if (range.start > cursor)
10797
+ parent.appendChild(document.createTextNode(path.slice(cursor, range.start)));
10798
+ const mark = document.createElement("mark");
10799
+ mark.textContent = path.slice(range.start, range.end);
10800
+ parent.appendChild(mark);
10801
+ cursor = range.end;
10802
+ }
10803
+ if (cursor < path.length)
10804
+ parent.appendChild(document.createTextNode(path.slice(cursor)));
10805
+ }
10806
+ function renderPalette(state) {
10807
+ state.list.innerHTML = "";
10808
+ state.items.forEach((item, index) => {
10809
+ const row = document.createElement("button");
10810
+ row.type = "button";
10811
+ row.id = "gdp-palette-item-" + index;
10812
+ row.className = "gdp-palette-row";
10813
+ row.setAttribute("role", "option");
10814
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10815
+ const title = document.createElement("span");
10816
+ title.className = "gdp-palette-row-title";
10817
+ const detail = document.createElement("span");
10818
+ detail.className = "gdp-palette-row-detail";
10819
+ if (item.kind === "file") {
10820
+ title.textContent = item.path.split("/").pop() || item.path;
10821
+ appendHighlightedPath(detail, item.displayPath, item.ranges);
10822
+ if (item.old_path && item.displayPath !== item.old_path) {
10823
+ detail.appendChild(document.createTextNode(" " + item.old_path));
10824
+ }
10825
+ } else {
10826
+ title.textContent = item.path + ":" + item.line;
10827
+ detail.textContent = item.preview;
10828
+ }
10829
+ row.append(title, detail);
10830
+ row.addEventListener("mouseenter", () => {
10831
+ state.selected = index;
10832
+ syncPaletteSelection(state);
10833
+ });
10834
+ row.addEventListener("mousedown", (e2) => {
9789
10835
  e2.preventDefault();
10836
+ state.selected = index;
10837
+ selectPaletteItem(state);
10838
+ });
10839
+ state.list.appendChild(row);
10840
+ });
10841
+ syncPaletteSelection(state);
10842
+ }
10843
+ function syncPaletteSelection(state) {
10844
+ state.input.setAttribute("aria-activedescendant", state.selected >= 0 ? "gdp-palette-item-" + state.selected : "");
10845
+ state.list.querySelectorAll(".gdp-palette-row").forEach((row, index) => {
10846
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10847
+ if (index === state.selected)
10848
+ row.scrollIntoView({ block: "nearest" });
10849
+ });
10850
+ }
10851
+ async function repoPaletteFiles(ref) {
10852
+ const cached = REPO_FILE_CACHE.get(ref);
10853
+ if (cached && cached.generation === SERVER_GENERATION)
10854
+ return cached;
10855
+ const params = new URLSearchParams;
10856
+ params.set("ref", ref);
10857
+ const res = await trackLoad(fetch("/_files?" + params.toString()).then((r2) => {
10858
+ if (!r2.ok)
10859
+ throw new Error("failed to load files");
10860
+ return r2.json();
10861
+ }));
10862
+ REPO_FILE_CACHE.set(ref, res);
10863
+ return res;
10864
+ }
10865
+ function diffFilePaletteItems(state, query) {
10866
+ const matchPath = isGlobPathQuery(query) ? globMatchPath : fuzzyMatchPath;
10867
+ const candidates = state.diffSnapshot.map((file) => {
10868
+ const current = matchPath(query, file.path);
10869
+ const old = file.old_path ? matchPath(query, file.old_path) : null;
10870
+ const best = old && (!current || old.score > current.score) ? { match: old, displayPath: file.old_path || file.path } : current ? { match: current, displayPath: file.path } : null;
10871
+ return best ? { file, ...best } : null;
10872
+ }).filter((item) => item !== null).sort((a2, b2) => b2.match.score - a2.match.score || a2.file.path.localeCompare(b2.file.path));
10873
+ return limitPaletteResults(candidates).map((candidate) => ({
10874
+ kind: "file",
10875
+ path: candidate.file.path,
10876
+ old_path: candidate.file.old_path,
10877
+ displayPath: candidate.displayPath,
10878
+ ref: paletteRef("diff"),
10879
+ targetPath: fileSourceTarget(candidate.file).path,
10880
+ targetRef: fileSourceTarget(candidate.file).ref,
10881
+ source: "diff",
10882
+ ranges: candidate.match.ranges
10883
+ }));
10884
+ }
10885
+ async function updateFilePalette(state, query) {
10886
+ renderPaletteControls(state);
10887
+ const source = paletteSource();
10888
+ if (!query.trim()) {
10889
+ const base2 = source === "diff" ? state.diffSnapshot.map((file) => {
10890
+ const target = fileSourceTarget(file);
10891
+ 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: [] };
10892
+ }) : [];
10893
+ state.items = limitPaletteResults(base2);
10894
+ state.selected = state.items.length ? 0 : -1;
10895
+ state.status.textContent = source === "diff" ? state.diffSnapshot.length + " diff files" : "Type to search repository files";
10896
+ renderPalette(state);
10897
+ return;
10898
+ }
10899
+ if (source === "diff") {
10900
+ state.items = diffFilePaletteItems(state, query);
10901
+ } else {
10902
+ state.status.textContent = "Loading files...";
10903
+ const ref = paletteRef(source);
10904
+ const response = await repoPaletteFiles(ref);
10905
+ if (PALETTE !== state || state.input.value !== query)
9790
10906
  return;
10907
+ state.items = limitPaletteResults(rankPathMatches(query, response.files)).map((match2) => ({
10908
+ kind: "file",
10909
+ path: match2.item.path,
10910
+ displayPath: match2.item.path,
10911
+ ref,
10912
+ source,
10913
+ ranges: match2.ranges
10914
+ }));
10915
+ }
10916
+ state.selected = state.items.length ? 0 : -1;
10917
+ state.status.textContent = state.items.length ? state.items.length + " results" : "No results";
10918
+ renderPalette(state);
10919
+ }
10920
+ function updateGrepPalette(state, query) {
10921
+ renderPaletteControls(state);
10922
+ state.controller?.abort();
10923
+ if (state.debounce)
10924
+ window.clearTimeout(state.debounce);
10925
+ if (!query.trim()) {
10926
+ state.items = [];
10927
+ state.selected = -1;
10928
+ state.status.textContent = "Type to grep";
10929
+ renderPalette(state);
10930
+ return;
10931
+ }
10932
+ if (state.grepRegex && !regexQueryIsValid(query)) {
10933
+ state.controller?.abort();
10934
+ state.items = [];
10935
+ state.selected = -1;
10936
+ state.status.textContent = "Invalid regular expression";
10937
+ renderPalette(state);
10938
+ return;
10939
+ }
10940
+ state.status.textContent = "Searching...";
10941
+ state.debounce = window.setTimeout(() => {
10942
+ const source = paletteSource();
10943
+ const ref = paletteRef(source);
10944
+ const params = new URLSearchParams;
10945
+ params.set("ref", ref);
10946
+ params.set("q", query);
10947
+ params.set("max", "200");
10948
+ if (state.grepRegex)
10949
+ params.set("regex", "1");
10950
+ if (source === "diff") {
10951
+ for (const file of state.diffSnapshot)
10952
+ params.append("path", file.path);
10953
+ }
10954
+ const controller = new AbortController;
10955
+ state.controller = controller;
10956
+ trackLoad(fetch("/_grep?" + params.toString(), { signal: controller.signal }).then((r2) => {
10957
+ if (!r2.ok)
10958
+ throw new Error("grep failed");
10959
+ return r2.json();
10960
+ })).then((response) => {
10961
+ if (PALETTE !== state || controller.signal.aborted)
10962
+ return;
10963
+ state.items = limitPaletteResults(response.matches.map((match2) => ({
10964
+ kind: "grep",
10965
+ path: match2.path,
10966
+ line: match2.line,
10967
+ column: match2.column,
10968
+ preview: match2.preview,
10969
+ ref,
10970
+ source
10971
+ })));
10972
+ state.selected = state.items.length ? 0 : -1;
10973
+ state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
10974
+ renderPalette(state);
10975
+ }).catch((err) => {
10976
+ if (isAbortError(err))
10977
+ return;
10978
+ state.status.textContent = "Search failed";
10979
+ });
10980
+ }, 80);
10981
+ }
10982
+ function updatePaletteResults(state) {
10983
+ const query = state.input.value;
10984
+ if (state.mode === "file") {
10985
+ updateFilePalette(state, query).catch(() => {
10986
+ state.status.textContent = "Search failed";
10987
+ });
10988
+ } else {
10989
+ updateGrepPalette(state, query);
10990
+ }
10991
+ }
10992
+ function selectPaletteItem(state) {
10993
+ const item = state.items[state.selected];
10994
+ if (!item)
10995
+ return;
10996
+ closeSearchPalette();
10997
+ if (item.kind === "file") {
10998
+ if (item.source === "diff") {
10999
+ if (STATE.route.screen === "file") {
11000
+ setRoute({ screen: "file", path: item.targetPath || item.path, ref: item.targetRef || item.ref, range: currentRange() });
11001
+ applySourceRouteToShell();
11002
+ } else {
11003
+ scrollToFile(item.path);
11004
+ }
11005
+ } else {
11006
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", range: currentRange() });
11007
+ renderStandaloneSource({ path: item.path, ref: item.ref });
9791
11008
  }
11009
+ return;
11010
+ }
11011
+ if (item.source === "diff") {
11012
+ setRoute({ screen: "diff", range: currentRange(), path: item.path, line: item.line });
11013
+ scrollToFile(item.path, item.line);
11014
+ } else {
11015
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", line: item.line, range: currentRange() });
11016
+ renderStandaloneSource({ path: item.path, ref: item.ref });
9792
11017
  }
9793
- if (e2.key === "/") {
11018
+ }
11019
+ function handlePaletteKeydown(e2, state) {
11020
+ if (e2.key === "Escape") {
9794
11021
  e2.preventDefault();
9795
- focusFileFilter();
9796
- } else if (e2.key === "Enter") {
9797
- if (isRepositorySidebarMode()) {
9798
- e2.preventDefault();
9799
- openActiveSidebarItem();
9800
- }
9801
- } else if (e2.key === "j" || e2.key === "k") {
11022
+ closeSearchPalette();
11023
+ return;
11024
+ }
11025
+ if (e2.key === "Enter") {
11026
+ if (state.composing)
11027
+ return;
11028
+ e2.preventDefault();
11029
+ selectPaletteItem(state);
11030
+ return;
11031
+ }
11032
+ if (state.mode === "grep" && e2.altKey && e2.key.toLowerCase() === "r") {
9802
11033
  e2.preventDefault();
11034
+ state.grepRegex = !state.grepRegex;
11035
+ updatePaletteResults(state);
11036
+ return;
11037
+ }
11038
+ const direction = e2.key === "ArrowDown" || e2.ctrlKey && e2.key.toLowerCase() === "n" ? 1 : e2.key === "ArrowUp" || e2.ctrlKey && e2.key.toLowerCase() === "p" ? -1 : 0;
11039
+ if (direction) {
11040
+ e2.preventDefault();
11041
+ state.selected = movePaletteSelection(state.selected, state.items.length, direction);
11042
+ syncPaletteSelection(state);
11043
+ }
11044
+ }
11045
+ function openSearchPalette(mode) {
11046
+ createPalette(mode);
11047
+ }
11048
+ function dispatchKeymapAction(action, scope, repeated = false) {
11049
+ if (action !== "start-g-sequence") {
11050
+ PENDING_G_SCOPE = null;
11051
+ PENDING_G_UNTIL = 0;
11052
+ }
11053
+ if (action === "open-file-palette") {
11054
+ if (PALETTE?.mode !== "file")
11055
+ openSearchPalette("file");
11056
+ return true;
11057
+ }
11058
+ if (action === "open-grep-palette") {
11059
+ if (PALETTE?.mode !== "grep")
11060
+ openSearchPalette("grep");
11061
+ return true;
11062
+ }
11063
+ if (action === "focus-file-filter") {
11064
+ focusFileFilter();
11065
+ return true;
11066
+ }
11067
+ if (action === "focus-sidebar") {
11068
+ focusSidebarPanel();
11069
+ return true;
11070
+ }
11071
+ if (action === "focus-main") {
11072
+ focusMainPanel();
11073
+ return true;
11074
+ }
11075
+ if (action === "cancel-source-load") {
11076
+ cancelActiveSourceLoad("esc");
11077
+ return true;
11078
+ }
11079
+ if (action === "open-sidebar-item") {
11080
+ if (!isRepositorySidebarMode())
11081
+ return false;
11082
+ openActiveSidebarItem();
11083
+ focusMainPanel();
11084
+ return true;
11085
+ }
11086
+ if (action === "sidebar-next" || action === "sidebar-previous") {
9803
11087
  const repoSidebar = isRepositorySidebarMode();
9804
11088
  const items = repoSidebar ? visibleSidebarItems() : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
9805
11089
  if (!items.length)
9806
- return;
11090
+ return true;
9807
11091
  let idx = items.findIndex((li) => li.classList.contains("active"));
9808
11092
  if (idx < 0)
9809
11093
  idx = 0;
9810
11094
  else
9811
- idx = e2.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
11095
+ idx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
9812
11096
  const target = items[idx];
9813
11097
  const path = target?.dataset.path || target?.dataset.dirpath;
9814
11098
  if (!repoSidebar && target) {
9815
11099
  target.click();
9816
- target.scrollIntoView({ block: "nearest" });
11100
+ scrollSidebarItemIntoView(target);
9817
11101
  } else if (path) {
9818
11102
  markActive(path);
9819
- target.scrollIntoView({ block: "nearest" });
11103
+ scrollSidebarItemIntoView(target);
9820
11104
  }
9821
- const nextIdx = e2.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
11105
+ const nextIdx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
9822
11106
  const nextItem = items[nextIdx];
9823
11107
  if (nextItem && nextItem !== target && nextItem.dataset.path)
9824
11108
  prefetchByPath(nextItem.dataset.path);
9825
- } else if (e2.key === "l") {
9826
- if (isRepositorySidebarMode()) {
9827
- e2.preventDefault();
9828
- toggleActiveSidebarDirectoryCollapsed();
9829
- }
9830
- } else if (e2.key === "h") {
9831
- if (isRepositorySidebarMode()) {
9832
- e2.preventDefault();
9833
- setActiveSidebarDirectoryCollapsed(true);
9834
- }
9835
- } else if (e2.key === "u")
11109
+ return true;
11110
+ }
11111
+ if (action === "sidebar-page-down" || action === "sidebar-page-up") {
11112
+ moveActiveSidebarPage(action === "sidebar-page-down" ? 1 : -1);
11113
+ return true;
11114
+ }
11115
+ if (action === "sidebar-expand") {
11116
+ if (!isRepositorySidebarMode())
11117
+ return false;
11118
+ toggleActiveSidebarDirectoryCollapsed();
11119
+ return true;
11120
+ }
11121
+ if (action === "sidebar-collapse") {
11122
+ if (!isRepositorySidebarMode())
11123
+ return false;
11124
+ setActiveSidebarDirectoryCollapsed(true);
11125
+ return true;
11126
+ }
11127
+ if (action === "scroll-main-down" || action === "scroll-main-up") {
11128
+ scrollMainPanel(action === "scroll-main-down" ? 1 : -1, repeated);
11129
+ return true;
11130
+ }
11131
+ if (action === "scroll-main-page-down" || action === "scroll-main-page-up") {
11132
+ scrollMainPanel(action === "scroll-main-page-down" ? 1 : -1, repeated, "page");
11133
+ return true;
11134
+ }
11135
+ if (action === "tab-preview" || action === "tab-code") {
11136
+ return switchSourceTab(action === "tab-preview" ? "preview" : "code");
11137
+ }
11138
+ if (action === "start-g-sequence") {
11139
+ PENDING_G_SCOPE = scope;
11140
+ PENDING_G_UNTIL = performance.now() + 900;
11141
+ return true;
11142
+ }
11143
+ if (action === "goto-top" || action === "goto-bottom") {
11144
+ const edge = action === "goto-top" ? "top" : "bottom";
11145
+ if (scope === "main")
11146
+ scrollMainToEdge(edge);
11147
+ else if (scope === "sidebar")
11148
+ moveActiveSidebarToEdge(edge);
11149
+ else
11150
+ window.scrollTo({ top: edge === "top" ? 0 : Math.max(document.documentElement.scrollHeight, document.body.scrollHeight), behavior: "auto" });
11151
+ return true;
11152
+ }
11153
+ if (action === "layout-unified") {
9836
11154
  setLayout("line-by-line");
9837
- else if (e2.key === "s")
11155
+ return true;
11156
+ }
11157
+ if (action === "layout-split") {
9838
11158
  setLayout("side-by-side");
9839
- else if (e2.key === "t")
11159
+ return true;
11160
+ }
11161
+ if (action === "toggle-theme") {
9840
11162
  $("#theme").click();
11163
+ return true;
11164
+ }
11165
+ return false;
11166
+ }
11167
+ document.addEventListener("keydown", (e2) => {
11168
+ const targetEl = e2.target;
11169
+ const scope = keymapScope(targetEl);
11170
+ const action = resolveKeymapAction(e2, {
11171
+ scope,
11172
+ editable: isEditableKeyTarget(targetEl),
11173
+ composing: e2.isComposing,
11174
+ paletteOpen: !!PALETTE,
11175
+ pendingG: PENDING_G_SCOPE === scope && performance.now() <= PENDING_G_UNTIL,
11176
+ lightboxOpen: !!document.querySelector(".mkdp-lightbox")
11177
+ });
11178
+ if (!action)
11179
+ return;
11180
+ if (dispatchKeymapAction(action, scope, e2.repeat))
11181
+ e2.preventDefault();
9841
11182
  });
9842
11183
  applyTheme();
9843
11184
  setLayout(STATE.layout);
@@ -9864,6 +11205,12 @@
9864
11205
  }).catch(() => setStatus("error"));
9865
11206
  }
9866
11207
  function load(options = {}) {
11208
+ if (STATE.route.screen === "help") {
11209
+ setStatus("live");
11210
+ renderHelpPage();
11211
+ syncHeaderMenu();
11212
+ return Promise.resolve();
11213
+ }
9867
11214
  if (STATE.route.screen === "repo")
9868
11215
  return loadRepo();
9869
11216
  setStatus("refreshing");
@@ -9882,7 +11229,10 @@
9882
11229
  setStatus("live");
9883
11230
  }).catch(() => setStatus("error"));
9884
11231
  }
9885
- if (STATE.route.screen === "repo")
11232
+ if (STATE.route.screen === "help") {
11233
+ setStatus("live");
11234
+ renderHelpPage();
11235
+ } else if (STATE.route.screen === "repo")
9886
11236
  loadRepo();
9887
11237
  else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
9888
11238
  setStatus("live");
@@ -9905,10 +11255,13 @@
9905
11255
  const range = currentRange();
9906
11256
  if (STATE.route.screen === "file") {
9907
11257
  setRoute({ screen: "file", path: STATE.route.path, ref: STATE.route.ref, range }, true);
11258
+ } else if (STATE.route.screen === "help") {
11259
+ setRoute({ screen: "help", lang: helpLanguageFromRoute(), section: helpSectionFromRoute(), range }, true);
11260
+ renderHelpPage();
9908
11261
  } else {
9909
11262
  setRoute({ screen: "diff", range }, true);
11263
+ load();
9910
11264
  }
9911
- load();
9912
11265
  }
9913
11266
  syncRefInputs();
9914
11267
  syncHeaderMenu();
@@ -10109,6 +11462,13 @@
10109
11462
  STATE.repoRef = STATE.route.ref || "worktree";
10110
11463
  syncRefInputs();
10111
11464
  syncHeaderMenu();
11465
+ if (STATE.route.screen === "help") {
11466
+ cancelActiveSourceLoad("navigation");
11467
+ setPageMode();
11468
+ renderHelpPage();
11469
+ setStatus("live");
11470
+ return;
11471
+ }
10112
11472
  if (STATE.route.screen === "repo") {
10113
11473
  cancelActiveSourceLoad("navigation");
10114
11474
  setPageMode();