@youtyan/code-viewer 0.1.12 → 0.1.14

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,80 @@
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
+
134
208
  // web-src/fuzzy-search.ts
135
209
  function basenameStart(path) {
136
210
  const slash = path.lastIndexOf("/");
@@ -276,6 +350,77 @@
276
350
  return rankFuzzyPaths(query, items).map((item) => ({ ...item, mode: "fuzzy" }));
277
351
  }
278
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
+
279
424
  // web-src/search-palette.ts
280
425
  var PALETTE_RESULT_LIMIT = 50;
281
426
  function limitPaletteResults(items) {
@@ -369,6 +514,13 @@
369
514
  return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
370
515
  return { screen: "file", path, ref, range, view: target ? "blob" : "detail", ...line ? { line } : {} };
371
516
  }
517
+ case "/help":
518
+ return {
519
+ screen: "help",
520
+ range,
521
+ lang: params.get("lang") || "en",
522
+ section: params.get("section") || "keybindings"
523
+ };
372
524
  default:
373
525
  return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
374
526
  }
@@ -391,6 +543,15 @@
391
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)) : "");
392
544
  case "diff":
393
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
+ }
394
555
  case "unknown":
395
556
  return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
396
557
  default:
@@ -6184,7 +6345,29 @@
6184
6345
  return markdown;
6185
6346
  }
6186
6347
  function renderMarkdownHtml(textValue, target, highlighter, signal) {
6187
- 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
+ };
6188
6371
  }
6189
6372
  async function loadMarkdownHighlighter() {
6190
6373
  if (!shikiPromise) {
@@ -6581,6 +6764,179 @@
6581
6764
  let REPO_SIDEBAR_REF = null;
6582
6765
  let REPO_SIDEBAR_LOAD_REF = null;
6583
6766
  let REPO_SIDEBAR_LOAD = null;
6767
+ let SIDEBAR_FILES = [];
6768
+ let SIDEBAR_ON_FILE_CLICK;
6769
+ let PENDING_G_SCOPE = null;
6770
+ let PENDING_G_UNTIL = 0;
6771
+ let SOURCE_CURSOR = null;
6772
+ const SOURCE_CURSOR_TOTALS = new Map;
6773
+ const HELP_LANGUAGES = ["en", "ja"];
6774
+ const HELP_SECTIONS = ["keybindings"];
6775
+ const HELP_CONTENT = {
6776
+ en: {
6777
+ languageLabel: "Language",
6778
+ title: "Help",
6779
+ sections: {
6780
+ keybindings: {
6781
+ nav: "Keybindings",
6782
+ title: "Keyboard Shortcuts",
6783
+ intro: "Use these shortcuts to move between panels and navigate files without leaving the keyboard.",
6784
+ groups: [
6785
+ { title: "Global", rows: [["Ctrl+K", "Open file palette"], ["Ctrl+G", "Open grep palette"], ["/", "Focus file filter"], ["t", "Toggle theme"]] },
6786
+ { title: "Panels", rows: [["Ctrl+H", "Focus sidebar"], ["Ctrl+L", "Focus main panel"]] },
6787
+ { 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"]] },
6788
+ { 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"]] }
6789
+ ]
6790
+ }
6791
+ }
6792
+ },
6793
+ ja: {
6794
+ languageLabel: "言語",
6795
+ title: "ヘルプ",
6796
+ sections: {
6797
+ keybindings: {
6798
+ nav: "キーバインド",
6799
+ title: "キーバインド",
6800
+ intro: "キーボードだけでパネル移動、ファイル選択、スクロールを行うためのショートカットです。",
6801
+ groups: [
6802
+ { title: "グローバル", rows: [["Ctrl+K", "ファイルパレットを開く"], ["Ctrl+G", "grep パレットを開く"], ["/", "ファイルフィルターへフォーカス"], ["t", "テーマ切り替え"]] },
6803
+ { title: "パネル", rows: [["Ctrl+H", "サイドバーへフォーカス"], ["Ctrl+L", "メインパネルへフォーカス"]] },
6804
+ { title: "サイドバー", rows: [["j / k", "選択を下 / 上へ移動"], ["Ctrl+D / Ctrl+U", "半ページ分選択を移動"], ["gg / Shift+G", "先頭 / 末尾へ移動"], ["Enter", "選択項目を開く"], ["h / l", "ディレクトリを閉じる / 開く"]] },
6805
+ { title: "メインパネル", rows: [["j / k", "コードカーソルを下 / 上へ移動"], ["Ctrl+D / Ctrl+U", "コードカーソルを半ページ分移動"], ["gg / Shift+G", "コードカーソルを先頭 / 末尾へ移動"], ["gp / gc", "Preview / Code タブへ切り替え"]] }
6806
+ ]
6807
+ }
6808
+ }
6809
+ }
6810
+ };
6811
+ function sourceLineScrollAmount() {
6812
+ const virtualRow = Array.from(document.querySelectorAll("#content .gdp-source-virtual-row")).find((item) => item.offsetParent !== null);
6813
+ if (virtualRow)
6814
+ return virtualRow.getBoundingClientRect().height || VIRTUAL_SOURCE_ROW_HEIGHT;
6815
+ const sourceRow = Array.from(document.querySelectorAll("#content .gdp-source-table tr")).find((item) => item.offsetParent !== null);
6816
+ if (sourceRow)
6817
+ return sourceRow.getBoundingClientRect().height || 20;
6818
+ const preview = document.querySelector("#content .gdp-markdown-preview:not([hidden])");
6819
+ const lineHeight = Number.parseFloat(getComputedStyle(preview || document.body).lineHeight);
6820
+ return Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 20;
6821
+ }
6822
+ function hasVisibleSourceCodeSurface() {
6823
+ return Array.from(document.querySelectorAll("#content .gdp-source-virtual-scroller, #content .gdp-source-table")).some((item) => item.offsetParent !== null);
6824
+ }
6825
+ function sourceCursorKey(target) {
6826
+ return target.ref + "\x00" + target.path;
6827
+ }
6828
+ function sourceCursorMatches(target, line) {
6829
+ return !!SOURCE_CURSOR && sourceTargetsEqual(SOURCE_CURSOR.target, target) && SOURCE_CURSOR.line === line;
6830
+ }
6831
+ function syncSourceCursorRows(target) {
6832
+ document.querySelectorAll("#content [data-line]").forEach((row) => {
6833
+ const line = Number(row.dataset.line || "0");
6834
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, line));
6835
+ });
6836
+ }
6837
+ function visibleSourceLineFallback() {
6838
+ const scroller = findMainScrollTarget();
6839
+ if (scroller)
6840
+ return Math.max(1, Math.floor(scroller.scrollTop / VIRTUAL_SOURCE_ROW_HEIGHT) + 1);
6841
+ const rows = $$("#content .gdp-source-table tr[data-line]");
6842
+ const contentTop = document.querySelector("#content")?.getBoundingClientRect().top ?? 0;
6843
+ const row = rows.find((item) => item.getBoundingClientRect().bottom >= Math.max(0, contentTop));
6844
+ return Math.max(1, Number(row?.dataset.line || "1"));
6845
+ }
6846
+ function ensureSourceCursor(target) {
6847
+ if (SOURCE_CURSOR && sourceTargetsEqual(SOURCE_CURSOR.target, target))
6848
+ return SOURCE_CURSOR;
6849
+ const routeLine = lineTargetStart(currentSourceLineTarget(target));
6850
+ SOURCE_CURSOR = { target, line: routeLine || visibleSourceLineFallback() };
6851
+ syncSourceCursorRows(target);
6852
+ return SOURCE_CURSOR;
6853
+ }
6854
+ function resetSourceCursorForTarget(target, totalLines) {
6855
+ const routeLine = lineTargetStart(currentSourceLineTarget(target));
6856
+ SOURCE_CURSOR = { target, line: Math.max(1, Math.min(totalLines, routeLine || 1)) };
6857
+ }
6858
+ function scrollSourceCursorIntoView(cursor, edge = "nearest") {
6859
+ const scroller = findMainScrollTarget();
6860
+ if (scroller) {
6861
+ const top = (cursor.line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT;
6862
+ const bottom = top + VIRTUAL_SOURCE_ROW_HEIGHT;
6863
+ const before = scroller.scrollTop;
6864
+ if (edge === "center")
6865
+ scroller.scrollTop = Math.max(0, top - Math.round(scroller.clientHeight / 2));
6866
+ else if (top < scroller.scrollTop)
6867
+ scroller.scrollTop = top;
6868
+ else if (bottom > scroller.scrollTop + scroller.clientHeight)
6869
+ scroller.scrollTop = bottom - scroller.clientHeight;
6870
+ if (scroller.scrollTop !== before)
6871
+ scroller.dispatchEvent(new Event("scroll"));
6872
+ scroller.__gdpRenderVirtualSource?.();
6873
+ syncSourceCursorRows(cursor.target);
6874
+ return;
6875
+ }
6876
+ document.querySelector('#content [data-line="' + cursor.line + '"]')?.scrollIntoView({ block: edge });
6877
+ }
6878
+ function moveSourceCursor(direction, unit, edge) {
6879
+ if (!hasVisibleSourceCodeSurface())
6880
+ return false;
6881
+ const target = sourceTargetFromRoute();
6882
+ if (!target)
6883
+ return false;
6884
+ const total = SOURCE_CURSOR_TOTALS.get(sourceCursorKey(target));
6885
+ if (!total)
6886
+ return false;
6887
+ const cursor = ensureSourceCursor(target);
6888
+ if (unit === "edge") {
6889
+ cursor.line = edge === "bottom" ? total : 1;
6890
+ syncSourceCursorRows(target);
6891
+ scrollSourceCursorIntoView(cursor, "center");
6892
+ return true;
6893
+ }
6894
+ const pageRows = Math.max(1, Math.floor((findMainScrollTarget()?.clientHeight || window.innerHeight) * 0.55 / (sourceLineScrollAmount() || VIRTUAL_SOURCE_ROW_HEIGHT)));
6895
+ const delta = unit === "page" ? pageRows : 1;
6896
+ cursor.line = Math.max(1, Math.min(total, cursor.line + direction * delta));
6897
+ syncSourceCursorRows(target);
6898
+ scrollSourceCursorIntoView(cursor);
6899
+ return true;
6900
+ }
6901
+ function scrollMainPanel(direction, repeated = false, unit = "line") {
6902
+ if (moveSourceCursor(direction, unit))
6903
+ return;
6904
+ const target = findMainScrollTarget();
6905
+ const viewportHeight = target?.clientHeight || document.scrollingElement?.clientHeight || window.innerHeight;
6906
+ const top = direction * (unit === "line" ? Math.round(sourceLineScrollAmount() || 32) : Math.round(viewportHeight * 0.55));
6907
+ const behavior = repeated ? "auto" : "smooth";
6908
+ if (target)
6909
+ target.scrollBy({ top, behavior });
6910
+ else
6911
+ window.scrollBy({ top, behavior });
6912
+ }
6913
+ function scrollMainToEdge(edge) {
6914
+ if (moveSourceCursor(edge === "bottom" ? 1 : -1, "edge", edge))
6915
+ return;
6916
+ const target = findMainScrollTarget();
6917
+ if (target) {
6918
+ target.scrollTo({ top: edge === "top" ? 0 : target.scrollHeight, behavior: "auto" });
6919
+ return;
6920
+ }
6921
+ const top = edge === "top" ? 0 : Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
6922
+ window.scrollTo({ top, behavior: "auto" });
6923
+ }
6924
+ function switchSourceTab(tab) {
6925
+ const tabs = document.querySelector("#content .gdp-source-tabs");
6926
+ if (!tabs)
6927
+ return false;
6928
+ const button = tabs.querySelector('button[data-source-tab="' + tab + '"]');
6929
+ if (!button || button.hidden || button.disabled)
6930
+ return false;
6931
+ button.click();
6932
+ focusMainPanel();
6933
+ return true;
6934
+ }
6935
+ function isFocusableClickTarget(target) {
6936
+ if (!(target instanceof Element))
6937
+ return false;
6938
+ return !!target.closest('a, button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]');
6939
+ }
6584
6940
  function invalidateRepoSidebar() {
6585
6941
  REPO_SIDEBAR_REF = null;
6586
6942
  REPO_SIDEBAR_LOAD_REF = null;
@@ -6885,8 +7241,10 @@
6885
7241
  }
6886
7242
  if (f2.type === "tree") {
6887
7243
  node.explicit = true;
6888
- if (f2.children_omitted === true)
7244
+ if (f2.children_omitted === true) {
6889
7245
  node.children_omitted = true;
7246
+ node.children_omitted_reason = f2.children_omitted_reason;
7247
+ }
6890
7248
  continue;
6891
7249
  }
6892
7250
  node.files.push(f2);
@@ -6923,17 +7281,24 @@
6923
7281
  const dir = item.dir;
6924
7282
  const li = document.createElement("li");
6925
7283
  li.className = "tree-dir";
7284
+ li.tabIndex = -1;
6926
7285
  li.dataset.dirpath = dir.path;
6927
7286
  if (dir.explicit)
6928
7287
  li.dataset.explicit = "true";
6929
7288
  if (dir.children_omitted) {
6930
7289
  li.classList.add("children-omitted");
6931
- li.title = "Directory contents are intentionally not listed";
7290
+ li.classList.add(dir.children_omitted_reason === "ignored" ? "children-omitted-ignored" : "children-omitted-internal");
7291
+ li.title = dir.children_omitted_reason === "ignored" ? "Ignored directory: open the detail pane to browse its contents" : "Internal Git metadata is not browsed";
6932
7292
  }
6933
7293
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
6934
7294
  const chev = document.createElement("span");
6935
- chev.className = "chev";
6936
- setChevronIcon(chev);
7295
+ if (dir.children_omitted) {
7296
+ chev.className = "chev-spacer";
7297
+ chev.setAttribute("aria-hidden", "true");
7298
+ } else {
7299
+ chev.className = "chev";
7300
+ setChevronIcon(chev);
7301
+ }
6937
7302
  li.appendChild(chev);
6938
7303
  const dirIcon = document.createElement("span");
6939
7304
  dirIcon.className = "dir-icon";
@@ -6947,9 +7312,9 @@
6947
7312
  label.appendChild(dn);
6948
7313
  if (dir.children_omitted) {
6949
7314
  const omitted = document.createElement("span");
6950
- omitted.className = "dir-omitted";
6951
- omitted.textContent = "skipped";
6952
- omitted.title = "Directory contents are intentionally not listed";
7315
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "ignored" ? "dir-omitted-ignored" : "dir-omitted-internal");
7316
+ omitted.textContent = dir.children_omitted_reason === "ignored" ? "ignored" : "private";
7317
+ omitted.title = dir.children_omitted_reason === "ignored" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
6953
7318
  label.appendChild(omitted);
6954
7319
  }
6955
7320
  li.appendChild(label);
@@ -6974,12 +7339,25 @@
6974
7339
  STATE.collapsedDirs.delete(dir.path);
6975
7340
  localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
6976
7341
  };
6977
- chev.addEventListener("click", toggleDir);
6978
- dirIcon.addEventListener("click", toggleDir);
7342
+ if (!dir.children_omitted) {
7343
+ chev.addEventListener("click", toggleDir);
7344
+ dirIcon.addEventListener("click", toggleDir);
7345
+ }
6979
7346
  if (onFileClick) {
6980
7347
  li.addEventListener("click", (e2) => {
6981
7348
  e2.stopPropagation();
6982
- onFileClick({ path: dir.path, display_path: dir.path, type: "tree", children_omitted: dir.children_omitted });
7349
+ if (dir.children_omitted_reason === "internal")
7350
+ return;
7351
+ if (dir.children_omitted_reason !== "internal") {
7352
+ onFileClick({
7353
+ path: dir.path,
7354
+ display_path: dir.path,
7355
+ type: "tree",
7356
+ children_omitted: dir.children_omitted,
7357
+ children_omitted_reason: dir.children_omitted_reason
7358
+ });
7359
+ }
7360
+ focusSidebarPanel();
6983
7361
  });
6984
7362
  } else {
6985
7363
  li.addEventListener("click", toggleDir);
@@ -6990,6 +7368,7 @@
6990
7368
  const f2 = item.file;
6991
7369
  const li = document.createElement("li");
6992
7370
  li.className = "tree-file";
7371
+ li.tabIndex = -1;
6993
7372
  li.dataset.path = f2.path;
6994
7373
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
6995
7374
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
@@ -7014,6 +7393,7 @@
7014
7393
  onFileClick(f2);
7015
7394
  else
7016
7395
  scrollToFile(f2.path);
7396
+ focusSidebarPanel();
7017
7397
  });
7018
7398
  if (!onFileClick)
7019
7399
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7024,6 +7404,7 @@
7024
7404
  function renderFlat(files, ul, onFileClick) {
7025
7405
  files.forEach((f2, i2) => {
7026
7406
  const li = document.createElement("li");
7407
+ li.tabIndex = -1;
7027
7408
  li.dataset.index = String(i2);
7028
7409
  li.dataset.path = f2.path;
7029
7410
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
@@ -7045,6 +7426,7 @@
7045
7426
  onFileClick(f2);
7046
7427
  else
7047
7428
  scrollToFile(f2.path);
7429
+ focusSidebarPanel();
7048
7430
  });
7049
7431
  if (!onFileClick)
7050
7432
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7056,6 +7438,8 @@
7056
7438
  ul.innerHTML = "";
7057
7439
  ul.classList.toggle("tree", STATE.sbView === "tree");
7058
7440
  STATE.files = files;
7441
+ SIDEBAR_FILES = files;
7442
+ SIDEBAR_ON_FILE_CLICK = onFileClick;
7059
7443
  if (!onFileClick)
7060
7444
  REPO_SIDEBAR_REF = null;
7061
7445
  if (STATE.sbView === "tree") {
@@ -7190,13 +7574,43 @@
7190
7574
  card.scrollIntoView({ behavior: "smooth", block: "start" });
7191
7575
  }
7192
7576
  }
7193
- function markActive(path) {
7577
+ function sidebarAncestorDirs(path) {
7578
+ const parts = path.split("/").filter(Boolean);
7579
+ const dirs = [];
7580
+ for (let i2 = 1;i2 < parts.length; i2++)
7581
+ dirs.push(parts.slice(0, i2).join("/"));
7582
+ return dirs;
7583
+ }
7584
+ function expandSidebarAncestors(path) {
7585
+ if (STATE.sbView !== "tree")
7586
+ return;
7587
+ let changed = false;
7588
+ for (const dir of sidebarAncestorDirs(path)) {
7589
+ if (STATE.collapsedDirs.delete(dir))
7590
+ changed = true;
7591
+ const row = document.querySelector('#filelist .tree-dir[data-dirpath="' + CSS.escape(dir) + '"]');
7592
+ row?.classList.remove("collapsed");
7593
+ const icon = row?.querySelector(".dir-icon");
7594
+ if (icon)
7595
+ setFolderIcon(icon, false);
7596
+ }
7597
+ if (changed)
7598
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
7599
+ }
7600
+ function markActive(path, options = {}) {
7194
7601
  STATE.activeFile = path;
7602
+ if (options.reveal && STATE.sbView === "tree")
7603
+ expandSidebarAncestors(path);
7195
7604
  $$("#filelist li").forEach((li) => {
7196
7605
  const itemPath = li.dataset.path || li.dataset.dirpath;
7197
7606
  if (itemPath)
7198
7607
  li.classList.toggle("active", itemPath === path);
7199
7608
  });
7609
+ if (options.reveal) {
7610
+ const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
7611
+ if (active)
7612
+ requestAnimationFrame(() => scrollSidebarItemIntoView(active));
7613
+ }
7200
7614
  }
7201
7615
  function applyViewedState() {
7202
7616
  $$("#filelist li[data-path]").forEach((li) => {
@@ -7313,6 +7727,12 @@
7313
7727
  function repoFileTargetFromRoute() {
7314
7728
  return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
7315
7729
  }
7730
+ function helpLanguageFromRoute() {
7731
+ return STATE.route.screen === "help" && HELP_LANGUAGES.includes(STATE.route.lang) ? STATE.route.lang : "en";
7732
+ }
7733
+ function helpSectionFromRoute() {
7734
+ return STATE.route.screen === "help" && HELP_SECTIONS.includes(STATE.route.section) ? STATE.route.section : "keybindings";
7735
+ }
7316
7736
  function setRoute(route, replace2 = false) {
7317
7737
  const nextRoute = route.screen === "unknown" ? { screen: "diff", range: route.range } : route;
7318
7738
  STATE.route = nextRoute;
@@ -7333,6 +7753,7 @@
7333
7753
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
7334
7754
  document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
7335
7755
  document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
7756
+ document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
7336
7757
  syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
7337
7758
  }
7338
7759
  function syncHeaderMenu() {
@@ -7347,12 +7768,104 @@
7347
7768
  if (link2.dataset.route === "diff") {
7348
7769
  link2.href = buildRoute({ screen: "diff", range: currentRange() });
7349
7770
  }
7771
+ if (link2.dataset.route === "help") {
7772
+ link2.href = buildRoute({ screen: "help", lang: helpLanguageFromRoute(), section: helpSectionFromRoute(), range: currentRange() });
7773
+ }
7350
7774
  });
7351
7775
  }
7352
7776
  function removeStandaloneSource() {
7353
7777
  document.querySelectorAll(".gdp-standalone-source").forEach((el) => el.remove());
7354
7778
  document.querySelectorAll(".gdp-repo-blob-layout").forEach((el) => el.remove());
7355
7779
  }
7780
+ function renderHelpPage() {
7781
+ cancelActiveSourceLoad("navigation");
7782
+ removeStandaloneSource();
7783
+ LOAD_QUEUE.length = 0;
7784
+ const target = $("#diff");
7785
+ const empty = $("#empty");
7786
+ empty.classList.add("hidden");
7787
+ $("#meta").textContent = "";
7788
+ $("#totals").textContent = "";
7789
+ $("#filelist").textContent = "";
7790
+ const lang = helpLanguageFromRoute();
7791
+ const section = helpSectionFromRoute();
7792
+ const content = HELP_CONTENT[lang];
7793
+ const sectionContent = content.sections[section];
7794
+ const shell = document.createElement("section");
7795
+ shell.className = "gdp-help-shell";
7796
+ const header = document.createElement("header");
7797
+ header.className = "gdp-help-header";
7798
+ const title = document.createElement("h1");
7799
+ title.textContent = content.title;
7800
+ const langSelect = document.createElement("select");
7801
+ langSelect.className = "gdp-help-language";
7802
+ langSelect.setAttribute("aria-label", content.languageLabel);
7803
+ HELP_LANGUAGES.forEach((optionLang) => {
7804
+ const option = document.createElement("option");
7805
+ option.value = optionLang;
7806
+ option.textContent = optionLang.toUpperCase();
7807
+ option.selected = optionLang === lang;
7808
+ langSelect.appendChild(option);
7809
+ });
7810
+ langSelect.addEventListener("change", () => {
7811
+ setRoute({ screen: "help", lang: langSelect.value, section, range: currentRange() });
7812
+ setPageMode();
7813
+ renderHelpPage();
7814
+ syncHeaderMenu();
7815
+ });
7816
+ header.append(title, langSelect);
7817
+ const layout = document.createElement("div");
7818
+ layout.className = "gdp-help-layout";
7819
+ const helpNav = document.createElement("nav");
7820
+ helpNav.className = "gdp-help-nav";
7821
+ HELP_SECTIONS.forEach((helpSection) => {
7822
+ const button = document.createElement("button");
7823
+ button.type = "button";
7824
+ button.className = helpSection === section ? "active" : "";
7825
+ button.textContent = content.sections[helpSection].nav;
7826
+ button.addEventListener("click", () => {
7827
+ setRoute({ screen: "help", lang, section: helpSection, range: currentRange() });
7828
+ renderHelpPage();
7829
+ syncHeaderMenu();
7830
+ });
7831
+ helpNav.appendChild(button);
7832
+ });
7833
+ const article = document.createElement("article");
7834
+ article.className = "gdp-help-content";
7835
+ const h2 = document.createElement("h2");
7836
+ h2.textContent = sectionContent.title;
7837
+ const intro = document.createElement("p");
7838
+ intro.textContent = sectionContent.intro;
7839
+ article.append(h2, intro);
7840
+ sectionContent.groups.forEach((group) => {
7841
+ const groupSection = document.createElement("section");
7842
+ groupSection.className = "gdp-help-group";
7843
+ const groupTitle = document.createElement("h3");
7844
+ groupTitle.textContent = group.title;
7845
+ const table2 = document.createElement("table");
7846
+ group.rows.forEach(([keys, description]) => {
7847
+ const tr = document.createElement("tr");
7848
+ const keyCell = document.createElement("th");
7849
+ keyCell.scope = "row";
7850
+ keys.split(" / ").forEach((key, index) => {
7851
+ if (index > 0)
7852
+ keyCell.append(" / ");
7853
+ const kbd = document.createElement("kbd");
7854
+ kbd.textContent = key;
7855
+ keyCell.appendChild(kbd);
7856
+ });
7857
+ const desc = document.createElement("td");
7858
+ desc.textContent = description;
7859
+ tr.append(keyCell, desc);
7860
+ table2.appendChild(tr);
7861
+ });
7862
+ groupSection.append(groupTitle, table2);
7863
+ article.appendChild(groupSection);
7864
+ });
7865
+ layout.append(helpNav, article);
7866
+ shell.append(header, layout);
7867
+ target.replaceChildren(shell);
7868
+ }
7356
7869
  function renderShell(meta) {
7357
7870
  const newFiles = meta.files || [];
7358
7871
  STATE.files = newFiles;
@@ -7780,7 +8293,8 @@
7780
8293
  path: entry.path,
7781
8294
  display_path: entry.path,
7782
8295
  type: entry.type,
7783
- children_omitted: entry.children_omitted
8296
+ children_omitted: entry.children_omitted,
8297
+ children_omitted_reason: entry.children_omitted_reason
7784
8298
  }));
7785
8299
  renderSidebar(files, (file) => {
7786
8300
  if (file.type === "tree") {
@@ -7807,7 +8321,7 @@
7807
8321
  return load2;
7808
8322
  }
7809
8323
  function activateRepoSidebarPath(currentPath) {
7810
- markActive(currentPath);
8324
+ markActive(currentPath, { reveal: true });
7811
8325
  applyFilter();
7812
8326
  }
7813
8327
  function createPlaceholder(f2) {
@@ -8754,6 +9268,8 @@
8754
9268
  function sourceDisplayKind(path) {
8755
9269
  if (isVideo(path))
8756
9270
  return "video";
9271
+ if (isAudio(path))
9272
+ return "audio";
8757
9273
  if (isImage(path))
8758
9274
  return "image";
8759
9275
  if (/\.pdf$/i.test(path))
@@ -8800,10 +9316,28 @@
8800
9316
  return "MP4 video";
8801
9317
  if (ext === "webm")
8802
9318
  return "WebM video";
9319
+ if (ext === "mp3")
9320
+ return "MP3 audio";
9321
+ if (ext === "wav")
9322
+ return "WAV audio";
9323
+ if (ext === "ogg")
9324
+ return "Ogg audio";
9325
+ if (ext === "flac")
9326
+ return "FLAC audio";
9327
+ if (ext === "m4a")
9328
+ return "M4A audio";
9329
+ if (ext === "aac")
9330
+ return "AAC audio";
9331
+ if (ext === "opus")
9332
+ return "Opus audio";
9333
+ if (ext === "mid" || ext === "midi")
9334
+ return "MIDI file";
8803
9335
  if (mime?.startsWith("image/"))
8804
9336
  return "Image";
8805
9337
  if (mime?.startsWith("video/"))
8806
9338
  return "Video";
9339
+ if (mime?.startsWith("audio/"))
9340
+ return "Audio";
8807
9341
  if (mime === "application/pdf")
8808
9342
  return "PDF document";
8809
9343
  if (fallback === "unsupported file")
@@ -8842,11 +9376,35 @@
8842
9376
  });
8843
9377
  return info;
8844
9378
  }
8845
- function createSourceTabs(active) {
9379
+ function createSourceCopyButton(textValue) {
9380
+ const copy = document.createElement("button");
9381
+ copy.type = "button";
9382
+ copy.className = "gdp-file-header-icon gdp-copy-source";
9383
+ copy.title = "Copy source";
9384
+ copy.setAttribute("aria-label", "Copy source");
9385
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
9386
+ copy.addEventListener("click", async () => {
9387
+ try {
9388
+ await navigator.clipboard.writeText(textValue);
9389
+ copy.classList.add("copied");
9390
+ setTimeout(() => {
9391
+ copy.classList.remove("copied");
9392
+ }, 1200);
9393
+ } catch {
9394
+ copy.classList.add("failed");
9395
+ setTimeout(() => {
9396
+ copy.classList.remove("failed");
9397
+ }, 1200);
9398
+ }
9399
+ });
9400
+ return copy;
9401
+ }
9402
+ function createSourceTabs(active, textValue) {
8846
9403
  const tabs = document.createElement("div");
8847
9404
  tabs.className = "gdp-source-tabs";
8848
9405
  const codeButton = document.createElement("button");
8849
9406
  codeButton.type = "button";
9407
+ codeButton.dataset.sourceTab = "code";
8850
9408
  codeButton.textContent = "Code";
8851
9409
  codeButton.classList.toggle("active", active === "code");
8852
9410
  tabs.appendChild(codeButton);
@@ -8854,10 +9412,13 @@
8854
9412
  if (active === "preview") {
8855
9413
  previewButton = document.createElement("button");
8856
9414
  previewButton.type = "button";
9415
+ previewButton.dataset.sourceTab = "preview";
8857
9416
  previewButton.className = "active";
8858
9417
  previewButton.textContent = "Preview";
8859
9418
  tabs.prepend(previewButton);
8860
9419
  }
9420
+ if (textValue != null)
9421
+ tabs.appendChild(createSourceCopyButton(textValue));
8861
9422
  return { tabs, codeButton, previewButton };
8862
9423
  }
8863
9424
  async function renderSourceText(card, target, textValue, signal) {
@@ -8865,6 +9426,8 @@
8865
9426
  `).replace(/\r/g, `
8866
9427
  `).split(`
8867
9428
  `) : [""];
9429
+ SOURCE_CURSOR_TOTALS.set(sourceCursorKey(target), lines.length);
9430
+ resetSourceCursorForTarget(target, lines.length);
8868
9431
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
8869
9432
  const isStandalone = card.classList.contains("gdp-standalone-source");
8870
9433
  const view = document.createElement("div");
@@ -8885,7 +9448,7 @@
8885
9448
  if (usesVirtualSource) {
8886
9449
  const virtualCode = renderVirtualSource(target, textValue, lines, hljsRef, lang);
8887
9450
  if (previewable) {
8888
- const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview");
9451
+ const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview", textValue);
8889
9452
  if (tabsHost) {
8890
9453
  tabsHost.hidden = false;
8891
9454
  tabsHost.replaceChildren(tabs2);
@@ -8947,6 +9510,7 @@
8947
9510
  const tr = document.createElement("tr");
8948
9511
  tr.dataset.line = String(index + 1);
8949
9512
  tr.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9513
+ tr.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
8950
9514
  const num = document.createElement("td");
8951
9515
  num.className = "gdp-source-line-number";
8952
9516
  num.textContent = String(index + 1);
@@ -8969,7 +9533,7 @@
8969
9533
  }
8970
9534
  }
8971
9535
  table2.appendChild(tbody);
8972
- const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code");
9536
+ const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code", textValue);
8973
9537
  if (tabsHost) {
8974
9538
  tabsHost.hidden = false;
8975
9539
  tabsHost.replaceChildren(tabs);
@@ -9120,19 +9684,21 @@
9120
9684
  actions.className = "gdp-source-virtual-actions";
9121
9685
  const copy = document.createElement("button");
9122
9686
  copy.type = "button";
9123
- copy.className = "gdp-source-virtual-action";
9124
- copy.textContent = "Copy all";
9687
+ copy.className = "gdp-file-header-icon gdp-copy-source gdp-source-virtual-copy";
9688
+ copy.title = "Copy source";
9689
+ copy.setAttribute("aria-label", "Copy source");
9690
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
9125
9691
  copy.addEventListener("click", async () => {
9126
9692
  try {
9127
9693
  await navigator.clipboard.writeText(textValue);
9128
- copy.textContent = "Copied";
9694
+ copy.classList.add("copied");
9129
9695
  setTimeout(() => {
9130
- copy.textContent = "Copy all";
9696
+ copy.classList.remove("copied");
9131
9697
  }, 1200);
9132
9698
  } catch {
9133
- copy.textContent = "Copy failed";
9699
+ copy.classList.add("failed");
9134
9700
  setTimeout(() => {
9135
- copy.textContent = "Copy all";
9701
+ copy.classList.remove("failed");
9136
9702
  }, 1600);
9137
9703
  }
9138
9704
  });
@@ -9180,6 +9746,7 @@
9180
9746
  row.className = "gdp-source-virtual-row";
9181
9747
  row.dataset.line = String(index + 1);
9182
9748
  row.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9749
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
9183
9750
  const num = document.createElement("span");
9184
9751
  num.className = "gdp-source-virtual-line-number";
9185
9752
  num.textContent = String(index + 1);
@@ -9206,6 +9773,7 @@
9206
9773
  if (!raf)
9207
9774
  raf = requestAnimationFrame(render);
9208
9775
  };
9776
+ scroller.__gdpRenderVirtualSource = render;
9209
9777
  scroller.addEventListener("scroll", schedule, { passive: true });
9210
9778
  let resizeObserver = null;
9211
9779
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
@@ -9241,6 +9809,12 @@
9241
9809
  video.controls = true;
9242
9810
  video.preload = "metadata";
9243
9811
  view.appendChild(video);
9812
+ } else if (mediaKind === "audio") {
9813
+ const audio = document.createElement("audio");
9814
+ audio.src = url;
9815
+ audio.controls = true;
9816
+ audio.preload = "metadata";
9817
+ view.appendChild(audio);
9244
9818
  } else if (mediaKind === "pdf") {
9245
9819
  const frame = document.createElement("iframe");
9246
9820
  frame.src = url;
@@ -9407,7 +9981,7 @@
9407
9981
  renderSourceUnsupported(card, target);
9408
9982
  return;
9409
9983
  }
9410
- if (displayKind === "image" || displayKind === "video" || displayKind === "pdf") {
9984
+ if (displayKind === "image" || displayKind === "video" || displayKind === "audio" || displayKind === "pdf") {
9411
9985
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
9412
9986
  return;
9413
9987
  finishSourceLoad(req);
@@ -9803,9 +10377,10 @@
9803
10377
  b2.addEventListener("scroll", () => mirror(b2, a2), { passive: true });
9804
10378
  });
9805
10379
  }
9806
- const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov)(\?.*)?$/i;
10380
+ const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
9807
10381
  const IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
9808
10382
  const VIDEO_RE = /\.(mp4|webm|mov)$/i;
10383
+ const AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
9809
10384
  function isMedia(p2) {
9810
10385
  return MEDIA_RE.test(p2);
9811
10386
  }
@@ -9815,6 +10390,9 @@
9815
10390
  function isVideo(p2) {
9816
10391
  return VIDEO_RE.test(p2);
9817
10392
  }
10393
+ function isAudio(p2) {
10394
+ return AUDIO_RE.test(p2);
10395
+ }
9818
10396
  function fileURL(path, ref) {
9819
10397
  return "/_file?path=" + encodeURIComponent(path) + "&ref=" + ref;
9820
10398
  }
@@ -9823,6 +10401,9 @@
9823
10401
  if (isVideo(path)) {
9824
10402
  return '<video src="' + url + '" controls preload="metadata"></video>';
9825
10403
  }
10404
+ if (isAudio(path)) {
10405
+ return '<audio src="' + url + '" controls preload="metadata"></audio>';
10406
+ }
9826
10407
  return '<img src="' + url + '" alt="" loading="lazy">';
9827
10408
  }
9828
10409
  function enhanceMediaCard(file, card) {
@@ -9916,12 +10497,21 @@
9916
10497
  b2.addEventListener("click", () => {
9917
10498
  STATE.sbView = b2.dataset.view || "tree";
9918
10499
  localStorage.setItem("gdp:sbview", STATE.sbView);
9919
- if (STATE.files && STATE.files.length)
9920
- renderSidebar(STATE.files);
10500
+ if (SIDEBAR_FILES.length)
10501
+ renderSidebar(SIDEBAR_FILES, SIDEBAR_ON_FILE_CLICK);
9921
10502
  });
9922
10503
  });
9923
10504
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
9924
10505
  $("#sb-collapse-all").addEventListener("click", () => setAllSidebarDirsCollapsed(true));
10506
+ prepareKeyboardPanels();
10507
+ const contentPanel = document.querySelector("#content");
10508
+ contentPanel?.addEventListener("focusin", () => setPanelFocusScope("main"));
10509
+ contentPanel?.addEventListener("mousedown", (event) => {
10510
+ if (isFocusableClickTarget(event.target))
10511
+ setPanelFocusScope("main");
10512
+ else
10513
+ focusMainPanel();
10514
+ });
9925
10515
  function applySidebarWidth(w) {
9926
10516
  const cw = Math.max(180, Math.min(900, w));
9927
10517
  document.documentElement.style.setProperty("--sidebar-w", cw + "px");
@@ -9940,6 +10530,13 @@
9940
10530
  sb.addEventListener("mousedown", mark);
9941
10531
  sb.addEventListener("touchstart", mark, { passive: true });
9942
10532
  sb.addEventListener("scroll", mark, { passive: true });
10533
+ sb.addEventListener("focusin", () => setPanelFocusScope("sidebar"));
10534
+ sb.addEventListener("mousedown", (event) => {
10535
+ if (isFocusableClickTarget(event.target))
10536
+ setPanelFocusScope("sidebar");
10537
+ else
10538
+ focusSidebarPanel();
10539
+ });
9943
10540
  })();
9944
10541
  (function setupResizer() {
9945
10542
  const handle = $("#sidebar-resizer");
@@ -10002,6 +10599,32 @@
10002
10599
  function visibleSidebarItems() {
10003
10600
  return $$("#filelist li[data-path], #filelist .tree-dir[data-dirpath]").filter(isSidebarRowVisible);
10004
10601
  }
10602
+ function scrollSidebarItemIntoView(item, block2 = "nearest") {
10603
+ const sidebar = document.querySelector("#sidebar");
10604
+ if (!sidebar) {
10605
+ item.scrollIntoView({ block: block2 });
10606
+ return;
10607
+ }
10608
+ const sidebarRect = sidebar.getBoundingClientRect();
10609
+ const itemRect = item.getBoundingClientRect();
10610
+ const stickyBottom = Math.max(sidebarRect.top, document.querySelector(".sb-head")?.getBoundingClientRect().bottom || sidebarRect.top, document.querySelector(".sb-filter-wrap")?.getBoundingClientRect().bottom || sidebarRect.top);
10611
+ const topPadding = Math.max(8, stickyBottom - sidebarRect.top + 8);
10612
+ const bottomPadding = 14;
10613
+ const visibleTop = sidebarRect.top + topPadding;
10614
+ const visibleBottom = sidebarRect.bottom - bottomPadding;
10615
+ if (block2 === "start") {
10616
+ sidebar.scrollTop += itemRect.top - visibleTop;
10617
+ return;
10618
+ }
10619
+ if (block2 === "end") {
10620
+ sidebar.scrollTop += itemRect.bottom - visibleBottom;
10621
+ return;
10622
+ }
10623
+ if (itemRect.top < visibleTop)
10624
+ sidebar.scrollTop += itemRect.top - visibleTop;
10625
+ else if (itemRect.bottom > visibleBottom)
10626
+ sidebar.scrollTop += itemRect.bottom - visibleBottom;
10627
+ }
10005
10628
  function isRepositorySidebarMode() {
10006
10629
  return document.body.classList.contains("gdp-repo-page") || document.body.classList.contains("gdp-repo-blob-page");
10007
10630
  }
@@ -10017,7 +10640,44 @@
10017
10640
  const path = target.dataset.path || target.dataset.dirpath;
10018
10641
  if (path)
10019
10642
  markActive(path);
10020
- target.scrollIntoView({ block: "nearest" });
10643
+ scrollSidebarItemIntoView(target);
10644
+ if (target.dataset.path)
10645
+ prefetchByPath(target.dataset.path);
10646
+ }
10647
+ function moveActiveSidebarPage(direction) {
10648
+ const items = visibleSidebarItems();
10649
+ if (!items.length)
10650
+ return;
10651
+ const repoSidebar = isRepositorySidebarMode();
10652
+ const sidebar = document.querySelector("#sidebar");
10653
+ const sample = items.find((item) => item.getBoundingClientRect().height > 0);
10654
+ const rowHeight = sample ? sample.getBoundingClientRect().height : 28;
10655
+ const halfPageRows = Math.max(1, Math.floor((sidebar?.clientHeight || window.innerHeight) / 2 / rowHeight));
10656
+ const current = items.findIndex((li) => li.classList.contains("active"));
10657
+ const start = current < 0 ? 0 : current;
10658
+ const idx = Math.max(0, Math.min(items.length - 1, start + direction * halfPageRows));
10659
+ const target = items[idx];
10660
+ const path = target.dataset.path || target.dataset.dirpath;
10661
+ if (!repoSidebar && target.dataset.path)
10662
+ target.click();
10663
+ else if (path)
10664
+ markActive(path);
10665
+ scrollSidebarItemIntoView(target);
10666
+ if (target.dataset.path)
10667
+ prefetchByPath(target.dataset.path);
10668
+ }
10669
+ function moveActiveSidebarToEdge(edge) {
10670
+ const items = visibleSidebarItems();
10671
+ const repoSidebar = isRepositorySidebarMode();
10672
+ const target = edge === "top" ? items[0] : items[items.length - 1];
10673
+ if (!target)
10674
+ return;
10675
+ const path = target.dataset.path || target.dataset.dirpath;
10676
+ if (!repoSidebar && target.dataset.path)
10677
+ target.click();
10678
+ else if (path)
10679
+ markActive(path);
10680
+ scrollSidebarItemIntoView(target, edge === "top" ? "start" : "end");
10021
10681
  if (target.dataset.path)
10022
10682
  prefetchByPath(target.dataset.path);
10023
10683
  }
@@ -10099,13 +10759,16 @@
10099
10759
  function closeSearchPalette() {
10100
10760
  if (!PALETTE)
10101
10761
  return;
10762
+ const previousFocusScope = PALETTE.previousFocusScope;
10102
10763
  PALETTE.controller?.abort();
10103
10764
  if (PALETTE.debounce)
10104
10765
  window.clearTimeout(PALETTE.debounce);
10105
10766
  PALETTE.root.remove();
10106
10767
  PALETTE = null;
10768
+ restorePanelFocusScope(previousFocusScope);
10107
10769
  }
10108
10770
  function createPalette(mode) {
10771
+ const previousFocusScope = PALETTE ? PALETTE.previousFocusScope : getPanelFocusScope();
10109
10772
  closeSearchPalette();
10110
10773
  const root = document.createElement("div");
10111
10774
  root.className = "gdp-palette-backdrop";
@@ -10147,9 +10810,11 @@
10147
10810
  selected: -1,
10148
10811
  items: [],
10149
10812
  composing: false,
10150
- diffSnapshot: [...STATE.files]
10813
+ diffSnapshot: [...STATE.files],
10814
+ previousFocusScope
10151
10815
  };
10152
10816
  PALETTE = state;
10817
+ setPanelFocusScope(null);
10153
10818
  root.addEventListener("mousedown", (e2) => {
10154
10819
  if (e2.target === root)
10155
10820
  closeSearchPalette();
@@ -10468,78 +11133,140 @@
10468
11133
  function openSearchPalette(mode) {
10469
11134
  createPalette(mode);
10470
11135
  }
10471
- document.addEventListener("keydown", (e2) => {
10472
- if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "k") {
10473
- e2.preventDefault();
10474
- if (PALETTE?.mode === "file")
10475
- return;
10476
- openSearchPalette("file");
10477
- return;
11136
+ function dispatchKeymapAction(action, scope, repeated = false) {
11137
+ if (action !== "start-g-sequence") {
11138
+ PENDING_G_SCOPE = null;
11139
+ PENDING_G_UNTIL = 0;
10478
11140
  }
10479
- if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "g") {
10480
- e2.preventDefault();
10481
- if (PALETTE?.mode === "grep")
10482
- return;
10483
- openSearchPalette("grep");
10484
- return;
11141
+ if (action === "open-file-palette") {
11142
+ if (PALETTE?.mode !== "file")
11143
+ openSearchPalette("file");
11144
+ return true;
10485
11145
  }
10486
- const targetEl = e2.target;
10487
- if (targetEl && (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA"))
10488
- return;
10489
- if (e2.key === "Escape" && !document.querySelector(".mkdp-lightbox")) {
10490
- if (cancelActiveSourceLoad("esc")) {
10491
- e2.preventDefault();
10492
- return;
10493
- }
11146
+ if (action === "open-grep-palette") {
11147
+ if (PALETTE?.mode !== "grep")
11148
+ openSearchPalette("grep");
11149
+ return true;
10494
11150
  }
10495
- if (e2.key === "/") {
10496
- e2.preventDefault();
11151
+ if (action === "focus-file-filter") {
10497
11152
  focusFileFilter();
10498
- } else if (e2.key === "Enter") {
10499
- if (isRepositorySidebarMode()) {
10500
- e2.preventDefault();
10501
- openActiveSidebarItem();
10502
- }
10503
- } else if (e2.key === "j" || e2.key === "k") {
10504
- e2.preventDefault();
11153
+ return true;
11154
+ }
11155
+ if (action === "focus-sidebar") {
11156
+ focusSidebarPanel();
11157
+ return true;
11158
+ }
11159
+ if (action === "focus-main") {
11160
+ focusMainPanel();
11161
+ return true;
11162
+ }
11163
+ if (action === "cancel-source-load") {
11164
+ cancelActiveSourceLoad("esc");
11165
+ return true;
11166
+ }
11167
+ if (action === "open-sidebar-item") {
11168
+ if (!isRepositorySidebarMode())
11169
+ return false;
11170
+ openActiveSidebarItem();
11171
+ focusMainPanel();
11172
+ return true;
11173
+ }
11174
+ if (action === "sidebar-next" || action === "sidebar-previous") {
10505
11175
  const repoSidebar = isRepositorySidebarMode();
10506
11176
  const items = repoSidebar ? visibleSidebarItems() : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
10507
11177
  if (!items.length)
10508
- return;
11178
+ return true;
10509
11179
  let idx = items.findIndex((li) => li.classList.contains("active"));
10510
11180
  if (idx < 0)
10511
11181
  idx = 0;
10512
11182
  else
10513
- idx = e2.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
11183
+ idx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
10514
11184
  const target = items[idx];
10515
11185
  const path = target?.dataset.path || target?.dataset.dirpath;
10516
11186
  if (!repoSidebar && target) {
10517
11187
  target.click();
10518
- target.scrollIntoView({ block: "nearest" });
11188
+ scrollSidebarItemIntoView(target);
10519
11189
  } else if (path) {
10520
11190
  markActive(path);
10521
- target.scrollIntoView({ block: "nearest" });
11191
+ scrollSidebarItemIntoView(target);
10522
11192
  }
10523
- const nextIdx = e2.key === "j" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
11193
+ const nextIdx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
10524
11194
  const nextItem = items[nextIdx];
10525
11195
  if (nextItem && nextItem !== target && nextItem.dataset.path)
10526
11196
  prefetchByPath(nextItem.dataset.path);
10527
- } else if (e2.key === "l") {
10528
- if (isRepositorySidebarMode()) {
10529
- e2.preventDefault();
10530
- toggleActiveSidebarDirectoryCollapsed();
10531
- }
10532
- } else if (e2.key === "h") {
10533
- if (isRepositorySidebarMode()) {
10534
- e2.preventDefault();
10535
- setActiveSidebarDirectoryCollapsed(true);
10536
- }
10537
- } else if (e2.key === "u")
11197
+ return true;
11198
+ }
11199
+ if (action === "sidebar-page-down" || action === "sidebar-page-up") {
11200
+ moveActiveSidebarPage(action === "sidebar-page-down" ? 1 : -1);
11201
+ return true;
11202
+ }
11203
+ if (action === "sidebar-expand") {
11204
+ if (!isRepositorySidebarMode())
11205
+ return false;
11206
+ toggleActiveSidebarDirectoryCollapsed();
11207
+ return true;
11208
+ }
11209
+ if (action === "sidebar-collapse") {
11210
+ if (!isRepositorySidebarMode())
11211
+ return false;
11212
+ setActiveSidebarDirectoryCollapsed(true);
11213
+ return true;
11214
+ }
11215
+ if (action === "scroll-main-down" || action === "scroll-main-up") {
11216
+ scrollMainPanel(action === "scroll-main-down" ? 1 : -1, repeated);
11217
+ return true;
11218
+ }
11219
+ if (action === "scroll-main-page-down" || action === "scroll-main-page-up") {
11220
+ scrollMainPanel(action === "scroll-main-page-down" ? 1 : -1, repeated, "page");
11221
+ return true;
11222
+ }
11223
+ if (action === "tab-preview" || action === "tab-code") {
11224
+ return switchSourceTab(action === "tab-preview" ? "preview" : "code");
11225
+ }
11226
+ if (action === "start-g-sequence") {
11227
+ PENDING_G_SCOPE = scope;
11228
+ PENDING_G_UNTIL = performance.now() + 900;
11229
+ return true;
11230
+ }
11231
+ if (action === "goto-top" || action === "goto-bottom") {
11232
+ const edge = action === "goto-top" ? "top" : "bottom";
11233
+ if (scope === "main")
11234
+ scrollMainToEdge(edge);
11235
+ else if (scope === "sidebar")
11236
+ moveActiveSidebarToEdge(edge);
11237
+ else
11238
+ window.scrollTo({ top: edge === "top" ? 0 : Math.max(document.documentElement.scrollHeight, document.body.scrollHeight), behavior: "auto" });
11239
+ return true;
11240
+ }
11241
+ if (action === "layout-unified") {
10538
11242
  setLayout("line-by-line");
10539
- else if (e2.key === "s")
11243
+ return true;
11244
+ }
11245
+ if (action === "layout-split") {
10540
11246
  setLayout("side-by-side");
10541
- else if (e2.key === "t")
11247
+ return true;
11248
+ }
11249
+ if (action === "toggle-theme") {
10542
11250
  $("#theme").click();
11251
+ return true;
11252
+ }
11253
+ return false;
11254
+ }
11255
+ document.addEventListener("keydown", (e2) => {
11256
+ const targetEl = e2.target;
11257
+ const scope = keymapScope(targetEl);
11258
+ const action = resolveKeymapAction(e2, {
11259
+ scope,
11260
+ editable: isEditableKeyTarget(targetEl),
11261
+ composing: e2.isComposing,
11262
+ paletteOpen: !!PALETTE,
11263
+ pendingG: PENDING_G_SCOPE === scope && performance.now() <= PENDING_G_UNTIL,
11264
+ lightboxOpen: !!document.querySelector(".mkdp-lightbox")
11265
+ });
11266
+ if (!action)
11267
+ return;
11268
+ if (dispatchKeymapAction(action, scope, e2.repeat))
11269
+ e2.preventDefault();
10543
11270
  });
10544
11271
  applyTheme();
10545
11272
  setLayout(STATE.layout);
@@ -10566,6 +11293,12 @@
10566
11293
  }).catch(() => setStatus("error"));
10567
11294
  }
10568
11295
  function load(options = {}) {
11296
+ if (STATE.route.screen === "help") {
11297
+ setStatus("live");
11298
+ renderHelpPage();
11299
+ syncHeaderMenu();
11300
+ return Promise.resolve();
11301
+ }
10569
11302
  if (STATE.route.screen === "repo")
10570
11303
  return loadRepo();
10571
11304
  setStatus("refreshing");
@@ -10584,7 +11317,10 @@
10584
11317
  setStatus("live");
10585
11318
  }).catch(() => setStatus("error"));
10586
11319
  }
10587
- if (STATE.route.screen === "repo")
11320
+ if (STATE.route.screen === "help") {
11321
+ setStatus("live");
11322
+ renderHelpPage();
11323
+ } else if (STATE.route.screen === "repo")
10588
11324
  loadRepo();
10589
11325
  else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
10590
11326
  setStatus("live");
@@ -10607,10 +11343,13 @@
10607
11343
  const range = currentRange();
10608
11344
  if (STATE.route.screen === "file") {
10609
11345
  setRoute({ screen: "file", path: STATE.route.path, ref: STATE.route.ref, range }, true);
11346
+ } else if (STATE.route.screen === "help") {
11347
+ setRoute({ screen: "help", lang: helpLanguageFromRoute(), section: helpSectionFromRoute(), range }, true);
11348
+ renderHelpPage();
10610
11349
  } else {
10611
11350
  setRoute({ screen: "diff", range }, true);
11351
+ load();
10612
11352
  }
10613
- load();
10614
11353
  }
10615
11354
  syncRefInputs();
10616
11355
  syncHeaderMenu();
@@ -10811,6 +11550,13 @@
10811
11550
  STATE.repoRef = STATE.route.ref || "worktree";
10812
11551
  syncRefInputs();
10813
11552
  syncHeaderMenu();
11553
+ if (STATE.route.screen === "help") {
11554
+ cancelActiveSourceLoad("navigation");
11555
+ setPageMode();
11556
+ renderHelpPage();
11557
+ setStatus("live");
11558
+ return;
11559
+ }
10814
11560
  if (STATE.route.screen === "repo") {
10815
11561
  cancelActiveSourceLoad("navigation");
10816
11562
  setPageMode();