@youtyan/code-viewer 0.1.12 → 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,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,177 @@
6581
6764
  let REPO_SIDEBAR_REF = null;
6582
6765
  let REPO_SIDEBAR_LOAD_REF = null;
6583
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
+ }
6584
6938
  function invalidateRepoSidebar() {
6585
6939
  REPO_SIDEBAR_REF = null;
6586
6940
  REPO_SIDEBAR_LOAD_REF = null;
@@ -6923,6 +7277,7 @@
6923
7277
  const dir = item.dir;
6924
7278
  const li = document.createElement("li");
6925
7279
  li.className = "tree-dir";
7280
+ li.tabIndex = -1;
6926
7281
  li.dataset.dirpath = dir.path;
6927
7282
  if (dir.explicit)
6928
7283
  li.dataset.explicit = "true";
@@ -6980,6 +7335,7 @@
6980
7335
  li.addEventListener("click", (e2) => {
6981
7336
  e2.stopPropagation();
6982
7337
  onFileClick({ path: dir.path, display_path: dir.path, type: "tree", children_omitted: dir.children_omitted });
7338
+ focusSidebarPanel();
6983
7339
  });
6984
7340
  } else {
6985
7341
  li.addEventListener("click", toggleDir);
@@ -6990,6 +7346,7 @@
6990
7346
  const f2 = item.file;
6991
7347
  const li = document.createElement("li");
6992
7348
  li.className = "tree-file";
7349
+ li.tabIndex = -1;
6993
7350
  li.dataset.path = f2.path;
6994
7351
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
6995
7352
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
@@ -7014,6 +7371,7 @@
7014
7371
  onFileClick(f2);
7015
7372
  else
7016
7373
  scrollToFile(f2.path);
7374
+ focusSidebarPanel();
7017
7375
  });
7018
7376
  if (!onFileClick)
7019
7377
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7024,6 +7382,7 @@
7024
7382
  function renderFlat(files, ul, onFileClick) {
7025
7383
  files.forEach((f2, i2) => {
7026
7384
  const li = document.createElement("li");
7385
+ li.tabIndex = -1;
7027
7386
  li.dataset.index = String(i2);
7028
7387
  li.dataset.path = f2.path;
7029
7388
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
@@ -7045,6 +7404,7 @@
7045
7404
  onFileClick(f2);
7046
7405
  else
7047
7406
  scrollToFile(f2.path);
7407
+ focusSidebarPanel();
7048
7408
  });
7049
7409
  if (!onFileClick)
7050
7410
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7313,6 +7673,12 @@
7313
7673
  function repoFileTargetFromRoute() {
7314
7674
  return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
7315
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
+ }
7316
7682
  function setRoute(route, replace2 = false) {
7317
7683
  const nextRoute = route.screen === "unknown" ? { screen: "diff", range: route.range } : route;
7318
7684
  STATE.route = nextRoute;
@@ -7333,6 +7699,7 @@
7333
7699
  document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
7334
7700
  document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
7335
7701
  document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
7702
+ document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
7336
7703
  syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
7337
7704
  }
7338
7705
  function syncHeaderMenu() {
@@ -7347,12 +7714,104 @@
7347
7714
  if (link2.dataset.route === "diff") {
7348
7715
  link2.href = buildRoute({ screen: "diff", range: currentRange() });
7349
7716
  }
7717
+ if (link2.dataset.route === "help") {
7718
+ link2.href = buildRoute({ screen: "help", lang: helpLanguageFromRoute(), section: helpSectionFromRoute(), range: currentRange() });
7719
+ }
7350
7720
  });
7351
7721
  }
7352
7722
  function removeStandaloneSource() {
7353
7723
  document.querySelectorAll(".gdp-standalone-source").forEach((el) => el.remove());
7354
7724
  document.querySelectorAll(".gdp-repo-blob-layout").forEach((el) => el.remove());
7355
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
+ }
7356
7815
  function renderShell(meta) {
7357
7816
  const newFiles = meta.files || [];
7358
7817
  STATE.files = newFiles;
@@ -8842,11 +9301,35 @@
8842
9301
  });
8843
9302
  return info;
8844
9303
  }
8845
- 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) {
8846
9328
  const tabs = document.createElement("div");
8847
9329
  tabs.className = "gdp-source-tabs";
8848
9330
  const codeButton = document.createElement("button");
8849
9331
  codeButton.type = "button";
9332
+ codeButton.dataset.sourceTab = "code";
8850
9333
  codeButton.textContent = "Code";
8851
9334
  codeButton.classList.toggle("active", active === "code");
8852
9335
  tabs.appendChild(codeButton);
@@ -8854,10 +9337,13 @@
8854
9337
  if (active === "preview") {
8855
9338
  previewButton = document.createElement("button");
8856
9339
  previewButton.type = "button";
9340
+ previewButton.dataset.sourceTab = "preview";
8857
9341
  previewButton.className = "active";
8858
9342
  previewButton.textContent = "Preview";
8859
9343
  tabs.prepend(previewButton);
8860
9344
  }
9345
+ if (textValue != null)
9346
+ tabs.appendChild(createSourceCopyButton(textValue));
8861
9347
  return { tabs, codeButton, previewButton };
8862
9348
  }
8863
9349
  async function renderSourceText(card, target, textValue, signal) {
@@ -8865,6 +9351,8 @@
8865
9351
  `).replace(/\r/g, `
8866
9352
  `).split(`
8867
9353
  `) : [""];
9354
+ SOURCE_CURSOR_TOTALS.set(sourceCursorKey(target), lines.length);
9355
+ resetSourceCursorForTarget(target, lines.length);
8868
9356
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
8869
9357
  const isStandalone = card.classList.contains("gdp-standalone-source");
8870
9358
  const view = document.createElement("div");
@@ -8885,7 +9373,7 @@
8885
9373
  if (usesVirtualSource) {
8886
9374
  const virtualCode = renderVirtualSource(target, textValue, lines, hljsRef, lang);
8887
9375
  if (previewable) {
8888
- const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview");
9376
+ const { tabs: tabs2, codeButton: codeButton2, previewButton: previewButton2 } = createSourceTabs("preview", textValue);
8889
9377
  if (tabsHost) {
8890
9378
  tabsHost.hidden = false;
8891
9379
  tabsHost.replaceChildren(tabs2);
@@ -8947,6 +9435,7 @@
8947
9435
  const tr = document.createElement("tr");
8948
9436
  tr.dataset.line = String(index + 1);
8949
9437
  tr.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9438
+ tr.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
8950
9439
  const num = document.createElement("td");
8951
9440
  num.className = "gdp-source-line-number";
8952
9441
  num.textContent = String(index + 1);
@@ -8969,7 +9458,7 @@
8969
9458
  }
8970
9459
  }
8971
9460
  table2.appendChild(tbody);
8972
- const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code");
9461
+ const { tabs, codeButton, previewButton } = createSourceTabs(previewable ? "preview" : "code", textValue);
8973
9462
  if (tabsHost) {
8974
9463
  tabsHost.hidden = false;
8975
9464
  tabsHost.replaceChildren(tabs);
@@ -9120,19 +9609,21 @@
9120
9609
  actions.className = "gdp-source-virtual-actions";
9121
9610
  const copy = document.createElement("button");
9122
9611
  copy.type = "button";
9123
- copy.className = "gdp-source-virtual-action";
9124
- 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);
9125
9616
  copy.addEventListener("click", async () => {
9126
9617
  try {
9127
9618
  await navigator.clipboard.writeText(textValue);
9128
- copy.textContent = "Copied";
9619
+ copy.classList.add("copied");
9129
9620
  setTimeout(() => {
9130
- copy.textContent = "Copy all";
9621
+ copy.classList.remove("copied");
9131
9622
  }, 1200);
9132
9623
  } catch {
9133
- copy.textContent = "Copy failed";
9624
+ copy.classList.add("failed");
9134
9625
  setTimeout(() => {
9135
- copy.textContent = "Copy all";
9626
+ copy.classList.remove("failed");
9136
9627
  }, 1600);
9137
9628
  }
9138
9629
  });
@@ -9180,6 +9671,7 @@
9180
9671
  row.className = "gdp-source-virtual-row";
9181
9672
  row.dataset.line = String(index + 1);
9182
9673
  row.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
9674
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, index + 1));
9183
9675
  const num = document.createElement("span");
9184
9676
  num.className = "gdp-source-virtual-line-number";
9185
9677
  num.textContent = String(index + 1);
@@ -9206,6 +9698,7 @@
9206
9698
  if (!raf)
9207
9699
  raf = requestAnimationFrame(render);
9208
9700
  };
9701
+ scroller.__gdpRenderVirtualSource = render;
9209
9702
  scroller.addEventListener("scroll", schedule, { passive: true });
9210
9703
  let resizeObserver = null;
9211
9704
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
@@ -9922,6 +10415,15 @@
9922
10415
  });
9923
10416
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
9924
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
+ });
9925
10427
  function applySidebarWidth(w) {
9926
10428
  const cw = Math.max(180, Math.min(900, w));
9927
10429
  document.documentElement.style.setProperty("--sidebar-w", cw + "px");
@@ -9940,6 +10442,13 @@
9940
10442
  sb.addEventListener("mousedown", mark);
9941
10443
  sb.addEventListener("touchstart", mark, { passive: true });
9942
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
+ });
9943
10452
  })();
9944
10453
  (function setupResizer() {
9945
10454
  const handle = $("#sidebar-resizer");
@@ -10002,6 +10511,32 @@
10002
10511
  function visibleSidebarItems() {
10003
10512
  return $$("#filelist li[data-path], #filelist .tree-dir[data-dirpath]").filter(isSidebarRowVisible);
10004
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
+ }
10005
10540
  function isRepositorySidebarMode() {
10006
10541
  return document.body.classList.contains("gdp-repo-page") || document.body.classList.contains("gdp-repo-blob-page");
10007
10542
  }
@@ -10017,7 +10552,44 @@
10017
10552
  const path = target.dataset.path || target.dataset.dirpath;
10018
10553
  if (path)
10019
10554
  markActive(path);
10020
- 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");
10021
10593
  if (target.dataset.path)
10022
10594
  prefetchByPath(target.dataset.path);
10023
10595
  }
@@ -10099,13 +10671,16 @@
10099
10671
  function closeSearchPalette() {
10100
10672
  if (!PALETTE)
10101
10673
  return;
10674
+ const previousFocusScope = PALETTE.previousFocusScope;
10102
10675
  PALETTE.controller?.abort();
10103
10676
  if (PALETTE.debounce)
10104
10677
  window.clearTimeout(PALETTE.debounce);
10105
10678
  PALETTE.root.remove();
10106
10679
  PALETTE = null;
10680
+ restorePanelFocusScope(previousFocusScope);
10107
10681
  }
10108
10682
  function createPalette(mode) {
10683
+ const previousFocusScope = PALETTE ? PALETTE.previousFocusScope : getPanelFocusScope();
10109
10684
  closeSearchPalette();
10110
10685
  const root = document.createElement("div");
10111
10686
  root.className = "gdp-palette-backdrop";
@@ -10147,9 +10722,11 @@
10147
10722
  selected: -1,
10148
10723
  items: [],
10149
10724
  composing: false,
10150
- diffSnapshot: [...STATE.files]
10725
+ diffSnapshot: [...STATE.files],
10726
+ previousFocusScope
10151
10727
  };
10152
10728
  PALETTE = state;
10729
+ setPanelFocusScope(null);
10153
10730
  root.addEventListener("mousedown", (e2) => {
10154
10731
  if (e2.target === root)
10155
10732
  closeSearchPalette();
@@ -10468,78 +11045,140 @@
10468
11045
  function openSearchPalette(mode) {
10469
11046
  createPalette(mode);
10470
11047
  }
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;
11048
+ function dispatchKeymapAction(action, scope, repeated = false) {
11049
+ if (action !== "start-g-sequence") {
11050
+ PENDING_G_SCOPE = null;
11051
+ PENDING_G_UNTIL = 0;
10478
11052
  }
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;
11053
+ if (action === "open-file-palette") {
11054
+ if (PALETTE?.mode !== "file")
11055
+ openSearchPalette("file");
11056
+ return true;
10485
11057
  }
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
- }
11058
+ if (action === "open-grep-palette") {
11059
+ if (PALETTE?.mode !== "grep")
11060
+ openSearchPalette("grep");
11061
+ return true;
10494
11062
  }
10495
- if (e2.key === "/") {
10496
- e2.preventDefault();
11063
+ if (action === "focus-file-filter") {
10497
11064
  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();
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") {
10505
11087
  const repoSidebar = isRepositorySidebarMode();
10506
11088
  const items = repoSidebar ? visibleSidebarItems() : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
10507
11089
  if (!items.length)
10508
- return;
11090
+ return true;
10509
11091
  let idx = items.findIndex((li) => li.classList.contains("active"));
10510
11092
  if (idx < 0)
10511
11093
  idx = 0;
10512
11094
  else
10513
- 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);
10514
11096
  const target = items[idx];
10515
11097
  const path = target?.dataset.path || target?.dataset.dirpath;
10516
11098
  if (!repoSidebar && target) {
10517
11099
  target.click();
10518
- target.scrollIntoView({ block: "nearest" });
11100
+ scrollSidebarItemIntoView(target);
10519
11101
  } else if (path) {
10520
11102
  markActive(path);
10521
- target.scrollIntoView({ block: "nearest" });
11103
+ scrollSidebarItemIntoView(target);
10522
11104
  }
10523
- 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);
10524
11106
  const nextItem = items[nextIdx];
10525
11107
  if (nextItem && nextItem !== target && nextItem.dataset.path)
10526
11108
  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")
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") {
10538
11154
  setLayout("line-by-line");
10539
- else if (e2.key === "s")
11155
+ return true;
11156
+ }
11157
+ if (action === "layout-split") {
10540
11158
  setLayout("side-by-side");
10541
- else if (e2.key === "t")
11159
+ return true;
11160
+ }
11161
+ if (action === "toggle-theme") {
10542
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();
10543
11182
  });
10544
11183
  applyTheme();
10545
11184
  setLayout(STATE.layout);
@@ -10566,6 +11205,12 @@
10566
11205
  }).catch(() => setStatus("error"));
10567
11206
  }
10568
11207
  function load(options = {}) {
11208
+ if (STATE.route.screen === "help") {
11209
+ setStatus("live");
11210
+ renderHelpPage();
11211
+ syncHeaderMenu();
11212
+ return Promise.resolve();
11213
+ }
10569
11214
  if (STATE.route.screen === "repo")
10570
11215
  return loadRepo();
10571
11216
  setStatus("refreshing");
@@ -10584,7 +11229,10 @@
10584
11229
  setStatus("live");
10585
11230
  }).catch(() => setStatus("error"));
10586
11231
  }
10587
- if (STATE.route.screen === "repo")
11232
+ if (STATE.route.screen === "help") {
11233
+ setStatus("live");
11234
+ renderHelpPage();
11235
+ } else if (STATE.route.screen === "repo")
10588
11236
  loadRepo();
10589
11237
  else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
10590
11238
  setStatus("live");
@@ -10607,10 +11255,13 @@
10607
11255
  const range = currentRange();
10608
11256
  if (STATE.route.screen === "file") {
10609
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();
10610
11261
  } else {
10611
11262
  setRoute({ screen: "diff", range }, true);
11263
+ load();
10612
11264
  }
10613
- load();
10614
11265
  }
10615
11266
  syncRefInputs();
10616
11267
  syncHeaderMenu();
@@ -10811,6 +11462,13 @@
10811
11462
  STATE.repoRef = STATE.route.ref || "worktree";
10812
11463
  syncRefInputs();
10813
11464
  syncHeaderMenu();
11465
+ if (STATE.route.screen === "help") {
11466
+ cancelActiveSourceLoad("navigation");
11467
+ setPageMode();
11468
+ renderHelpPage();
11469
+ setStatus("live");
11470
+ return;
11471
+ }
10814
11472
  if (STATE.route.screen === "repo") {
10815
11473
  cancelActiveSourceLoad("navigation");
10816
11474
  setPageMode();