@youtyan/code-viewer 0.1.14 → 0.1.16

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
@@ -378,6 +378,18 @@
378
378
  { action: "scroll-main-up", key: "k", scope: "main" },
379
379
  { action: "scroll-main-page-down", key: "d", scope: "main", ctrl: true },
380
380
  { action: "scroll-main-page-up", key: "u", scope: "main", ctrl: true },
381
+ { action: "scroll-main-page-down", key: "pagedown", scope: "main" },
382
+ { action: "scroll-main-page-up", key: "pageup", scope: "main" },
383
+ { action: "scroll-main-page-down", key: "pagedown", scope: "global" },
384
+ { action: "scroll-main-page-up", key: "pageup", scope: "global" },
385
+ { action: "scroll-main-page-down", key: "pagedown", scope: "sidebar" },
386
+ { action: "scroll-main-page-up", key: "pageup", scope: "sidebar" },
387
+ { action: "scroll-main-page-down", key: "arrowdown", scope: "main", ctrl: true },
388
+ { action: "scroll-main-page-up", key: "arrowup", scope: "main", ctrl: true },
389
+ { action: "scroll-main-page-down", key: "arrowdown", scope: "global", ctrl: true },
390
+ { action: "scroll-main-page-up", key: "arrowup", scope: "global", ctrl: true },
391
+ { action: "scroll-main-page-down", key: "arrowdown", scope: "sidebar", ctrl: true },
392
+ { action: "scroll-main-page-up", key: "arrowup", scope: "sidebar", ctrl: true },
381
393
  { action: "tab-preview", key: "p", scope: "main", pendingG: true },
382
394
  { action: "tab-code", key: "c", scope: "main", pendingG: true },
383
395
  { action: "goto-top", key: "g", pendingG: true },
@@ -6746,8 +6758,25 @@
6746
6758
  ];
6747
6759
  const FILE_16_PATH = "M2 1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 12.25 16h-8.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 8 4.25V1.5Zm5.75.062V4.25c0 .138.112.25.25.25h2.688Z";
6748
6760
  const OPEN_EXTERNAL_16_PATH = "M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Zm6.5 0a.75.75 0 0 0 0 1.5h1.19L7.72 7.22a.749.749 0 1 0 1.06 1.06l3.72-3.72v1.19a.75.75 0 0 0 1.5 0v-3A.75.75 0 0 0 13.25 2h-3Z";
6749
- const UNFOLD_16_PATH = "m8.177.677 2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25a.75.75 0 0 1-1.5 0V4H5.104a.25.25 0 0 1-.177-.427L7.823.677a.25.25 0 0 1 .354 0ZM7.25 10.75a.75.75 0 0 1 1.5 0V12h2.146a.25.25 0 0 1 .177.427l-2.896 2.896a.25.25 0 0 1-.354 0l-2.896-2.896A.25.25 0 0 1 5.104 12H7.25v-1.25Zm-5-2a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z";
6750
- const FOLD_16_PATH = "M10.896 2H8.75V.75a.75.75 0 0 0-1.5 0V2H5.104a.25.25 0 0 0-.177.427l2.896 2.896a.25.25 0 0 0 .354 0l2.896-2.896A.25.25 0 0 0 10.896 2ZM8.75 15.25a.75.75 0 0 1-1.5 0V14H5.104a.25.25 0 0 1-.177-.427l2.896-2.896a.25.25 0 0 1 .354 0l2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25Zm-6.5-6.5a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z";
6761
+ const GIT_BRANCH_16_PATH = "M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z";
6762
+ const TRIANGLE_DOWN_16_PATH = "m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z";
6763
+ const SIDEBAR_SHOW_16_PATHS = [
6764
+ "M6.823 7.823a.25.25 0 0 1 0 .354l-2.396 2.396A.25.25 0 0 1 4 10.396V5.604a.25.25 0 0 1 .427-.177Z",
6765
+ "M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 1.75v12.5c0 .138.112.25.25.25H9.5v-13H1.75a.25.25 0 0 0-.25.25ZM11 14.5h3.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H11Z"
6766
+ ];
6767
+ const SIDEBAR_HIDE_16_PATHS = [
6768
+ "m4.177 7.823 2.396-2.396A.25.25 0 0 1 7 5.604v4.792a.25.25 0 0 1-.427.177L4.177 8.177a.25.25 0 0 1 0-.354Z",
6769
+ "M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25H9.5v-13Zm12.5 13a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H11v13Z"
6770
+ ];
6771
+ const GEAR_16_PATH = "M8 0a8.2 8.2 0 0 1 1.7.18.75.75 0 0 1 .6.86l-.14.93c.23.1.45.21.67.34l.73-.6a.75.75 0 0 1 1.03.08c.47.37.89.78 1.23 1.23a.75.75 0 0 1 .07 1.03l-.59.73c.13.22.24.44.34.67l.93-.14a.75.75 0 0 1 .86.6c.12.55.18 1.12.18 1.7s-.06 1.15-.18 1.7a.75.75 0 0 1-.86.6l-.93-.14c-.1.23-.21.45-.34.67l.59.73a.75.75 0 0 1-.07 1.03c-.34.45-.76.86-1.23 1.23a.75.75 0 0 1-1.03.08l-.73-.6c-.22.13-.44.24-.67.34l.14.93a.75.75 0 0 1-.6.86A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-1.7-.18.75.75 0 0 1-.6-.86l.14-.93a5.9 5.9 0 0 1-.67-.34l-.73.6a.75.75 0 0 1-1.03-.08 8.1 8.1 0 0 1-1.23-1.23.75.75 0 0 1-.07-1.03l.59-.73a5.9 5.9 0 0 1-.34-.67l-.93.14a.75.75 0 0 1-.86-.6A8.2 8.2 0 0 1 0 8c0-.58.06-1.15.18-1.7a.75.75 0 0 1 .86-.6l.93.14c.1-.23.21-.45.34-.67l-.59-.73a.75.75 0 0 1 .07-1.03 8.1 8.1 0 0 1 1.23-1.23.75.75 0 0 1 1.03-.08l.73.6c.22-.13.44-.24.67-.34l-.14-.93a.75.75 0 0 1 .6-.86A8.2 8.2 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z";
6772
+ const EXPAND_ALL_16_PATHS = [
6773
+ "M3.22 4.47a.75.75 0 0 1 1.06 0L8 8.19l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 5.53a.75.75 0 0 1 0-1.06Z",
6774
+ "M3.22 8.47a.75.75 0 0 1 1.06 0L8 12.19l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 9.53a.75.75 0 0 1 0-1.06Z"
6775
+ ];
6776
+ const COLLAPSE_ALL_16_PATHS = [
6777
+ "M7.47 2.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L8 3.81 4.28 7.53a.75.75 0 0 1-1.06-1.06Z",
6778
+ "M7.47 6.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L8 7.81l-3.72 3.72a.75.75 0 1 1-1.06-1.06Z"
6779
+ ];
6751
6780
  const $ = (sel) => document.querySelector(sel);
6752
6781
  const $$ = (sel) => Array.from(document.querySelectorAll(sel));
6753
6782
  const diffCardSelector = (path) => '.gdp-file-shell[data-path="' + (window.CSS && CSS.escape ? CSS.escape(path) : path) + '"]';
@@ -6755,6 +6784,7 @@
6755
6784
  const DEFAULT_RANGE = { from: "HEAD", to: "worktree" };
6756
6785
  const VIRTUAL_SOURCE_LINE_THRESHOLD = 3000;
6757
6786
  const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
6787
+ const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
6758
6788
  const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
6759
6789
  const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
6760
6790
  let highlightLoadPromise = null;
@@ -6766,12 +6796,44 @@
6766
6796
  let REPO_SIDEBAR_LOAD = null;
6767
6797
  let SIDEBAR_FILES = [];
6768
6798
  let SIDEBAR_ON_FILE_CLICK;
6799
+ let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
6769
6800
  let PENDING_G_SCOPE = null;
6770
6801
  let PENDING_G_UNTIL = 0;
6771
6802
  let SOURCE_CURSOR = null;
6772
6803
  const SOURCE_CURSOR_TOTALS = new Map;
6773
6804
  const HELP_LANGUAGES = ["en", "ja"];
6774
6805
  const HELP_SECTIONS = ["keybindings"];
6806
+ const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
6807
+ const SIDEBAR_FONT_SIZE_STORAGE_KEY = "gdp:sidebar-font-size";
6808
+ const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
6809
+ const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
6810
+ "node_modules",
6811
+ ".venv",
6812
+ "venv",
6813
+ ".next",
6814
+ ".nuxt",
6815
+ ".svelte-kit",
6816
+ ".astro",
6817
+ ".vercel",
6818
+ "dist",
6819
+ "build",
6820
+ "out",
6821
+ "target",
6822
+ ".gradle",
6823
+ "__pycache__",
6824
+ ".pytest_cache",
6825
+ ".tox",
6826
+ ".terraform",
6827
+ ".idea",
6828
+ ".vscode",
6829
+ "vendor",
6830
+ ".cache",
6831
+ "coverage",
6832
+ "DerivedData",
6833
+ "Pods",
6834
+ "bin",
6835
+ "obj"
6836
+ ];
6775
6837
  const HELP_CONTENT = {
6776
6838
  en: {
6777
6839
  languageLabel: "Language",
@@ -6863,6 +6925,8 @@
6863
6925
  const before = scroller.scrollTop;
6864
6926
  if (edge === "center")
6865
6927
  scroller.scrollTop = Math.max(0, top - Math.round(scroller.clientHeight / 2));
6928
+ else if (edge === "start")
6929
+ scroller.scrollTop = top;
6866
6930
  else if (top < scroller.scrollTop)
6867
6931
  scroller.scrollTop = top;
6868
6932
  else if (bottom > scroller.scrollTop + scroller.clientHeight)
@@ -6895,7 +6959,7 @@
6895
6959
  const delta = unit === "page" ? pageRows : 1;
6896
6960
  cursor.line = Math.max(1, Math.min(total, cursor.line + direction * delta));
6897
6961
  syncSourceCursorRows(target);
6898
- scrollSourceCursorIntoView(cursor);
6962
+ scrollSourceCursorIntoView(cursor, unit === "page" ? "start" : "nearest");
6899
6963
  return true;
6900
6964
  }
6901
6965
  function scrollMainPanel(direction, repeated = false, unit = "line") {
@@ -6910,6 +6974,31 @@
6910
6974
  else
6911
6975
  window.scrollBy({ top, behavior });
6912
6976
  }
6977
+ let MAIN_SURFACE_FOCUS_SEQ = 0;
6978
+ function focusMainSurface() {
6979
+ const target = findMainScrollTarget();
6980
+ if (target?.matches("#content .gdp-source-virtual-scroller")) {
6981
+ target.focus({ preventScroll: true });
6982
+ setPanelFocusScope("main");
6983
+ return;
6984
+ }
6985
+ focusMainPanel();
6986
+ }
6987
+ function scheduleMainSurfaceFocus() {
6988
+ const seq = ++MAIN_SURFACE_FOCUS_SEQ;
6989
+ const apply = () => {
6990
+ if (seq !== MAIN_SURFACE_FOCUS_SEQ || PALETTE)
6991
+ return;
6992
+ if (isEditableKeyTarget(document.activeElement))
6993
+ return;
6994
+ focusMainSurface();
6995
+ };
6996
+ focusMainPanel();
6997
+ queueMicrotask(apply);
6998
+ requestAnimationFrame(apply);
6999
+ setTimeout(apply, 100);
7000
+ setTimeout(apply, 300);
7001
+ }
6913
7002
  function scrollMainToEdge(edge) {
6914
7003
  if (moveSourceCursor(edge === "bottom" ? 1 : -1, "edge", edge))
6915
7004
  return;
@@ -6942,6 +7031,90 @@
6942
7031
  REPO_SIDEBAR_LOAD_REF = null;
6943
7032
  REPO_SIDEBAR_LOAD = null;
6944
7033
  }
7034
+ function normalizeScopeOmitDirs(value) {
7035
+ const raw = Array.isArray(value) ? value : value.split(/[\n,]+/);
7036
+ return [...new Set(raw.map((item) => item.trim()).filter((item) => item && item.length <= 64 && !item.includes("/") && !item.includes("\\") && item !== "." && item !== ".." && item !== ".git"))].slice(0, 100).sort((a2, b2) => a2.localeCompare(b2));
7037
+ }
7038
+ function scopeOmitDirsStorageKey() {
7039
+ return SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
7040
+ }
7041
+ function setProjectName(project) {
7042
+ if (!project)
7043
+ return;
7044
+ PROJECT_NAME = project;
7045
+ document.title = project + " - code viewer";
7046
+ }
7047
+ function savedScopeOmitDirs() {
7048
+ const raw = localStorage.getItem(scopeOmitDirsStorageKey());
7049
+ if (raw == null)
7050
+ return null;
7051
+ try {
7052
+ const parsed = JSON.parse(raw);
7053
+ return normalizeScopeOmitDirs(Array.isArray(parsed) ? parsed : []);
7054
+ } catch {
7055
+ return normalizeScopeOmitDirs(raw);
7056
+ }
7057
+ }
7058
+ function serverScopeOmitDirsDefault() {
7059
+ return SERVER_SCOPE_OMIT_DIRS_DEFAULT.length ? SERVER_SCOPE_OMIT_DIRS_DEFAULT : CLIENT_SCOPE_OMIT_DIRS_DEFAULT;
7060
+ }
7061
+ function effectiveScopeOmitDirs() {
7062
+ return savedScopeOmitDirs() ?? serverScopeOmitDirsDefault();
7063
+ }
7064
+ function appendScopeOmitDirsParam(params) {
7065
+ const saved = savedScopeOmitDirs();
7066
+ if (saved != null)
7067
+ params.set("omit_dirs", saved.join(","));
7068
+ }
7069
+ function normalizeViewerFontSize(value) {
7070
+ return value === "compact" || value === "large" || value === "xlarge" ? value : "regular";
7071
+ }
7072
+ function savedSidebarFontSize() {
7073
+ return normalizeViewerFontSize(localStorage.getItem(SIDEBAR_FONT_SIZE_STORAGE_KEY));
7074
+ }
7075
+ function savedCodeFontSize() {
7076
+ return normalizeViewerFontSize(localStorage.getItem(CODE_FONT_SIZE_STORAGE_KEY));
7077
+ }
7078
+ function applySidebarFontSize(size = savedSidebarFontSize()) {
7079
+ document.body.dataset.sidebarFontSize = size;
7080
+ }
7081
+ function applyCodeFontSize(size = savedCodeFontSize()) {
7082
+ document.body.dataset.codeFontSize = size;
7083
+ }
7084
+ function syncSidebarHeaderHeight() {
7085
+ requestAnimationFrame(() => {
7086
+ const head = document.querySelector(".sb-head");
7087
+ if (head)
7088
+ document.documentElement.style.setProperty("--sidebar-head-h", Math.ceil(head.getBoundingClientRect().height) + "px");
7089
+ });
7090
+ }
7091
+ function observeSidebarHeaderHeight() {
7092
+ const head = document.querySelector(".sb-head");
7093
+ if (!head || typeof ResizeObserver === "undefined") {
7094
+ syncSidebarHeaderHeight();
7095
+ return;
7096
+ }
7097
+ const observer = new ResizeObserver(syncSidebarHeaderHeight);
7098
+ observer.observe(head);
7099
+ syncSidebarHeaderHeight();
7100
+ }
7101
+ function repoFileCacheKey(ref) {
7102
+ const saved = savedScopeOmitDirs();
7103
+ return `${ref}\x00${saved ? saved.join("\x00") : "server"}`;
7104
+ }
7105
+ async function loadSettings() {
7106
+ try {
7107
+ const res = await fetch("/_settings");
7108
+ if (!res.ok)
7109
+ return null;
7110
+ const settings = await res.json();
7111
+ setProjectName(settings.project || "");
7112
+ SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
7113
+ return settings;
7114
+ } catch {
7115
+ return null;
7116
+ }
7117
+ }
6945
7118
  function isRepoSidebarReusable(ref) {
6946
7119
  return REPO_SIDEBAR_REF === (ref || "worktree") && isRepositorySidebarMode();
6947
7120
  }
@@ -6958,6 +7131,7 @@
6958
7131
  theme: localStorage.getItem("gdp:theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"),
6959
7132
  sbView: localStorage.getItem("gdp:sbview") || "tree",
6960
7133
  sbWidth: parseInt(localStorage.getItem("gdp:sbwidth")) || 308,
7134
+ sidebarHidden: localStorage.getItem("gdp:sidebar-hidden") === "1",
6961
7135
  collapsedDirs: new Set(JSON.parse(localStorage.getItem("gdp:collapsed-dirs") || "[]")),
6962
7136
  ignoreWs: igRaw === null ? true : igRaw === "1",
6963
7137
  from: route.range.from,
@@ -7212,15 +7386,112 @@
7212
7386
  return;
7213
7387
  button.setAttribute("aria-pressed", expanded ? "true" : "false");
7214
7388
  button.title = expanded ? "Collapse expanded lines" : "Expand all lines";
7215
- button.innerHTML = expanded ? iconSvg("octicon-fold", FOLD_16_PATH) : iconSvg("octicon-unfold", UNFOLD_16_PATH);
7389
+ button.innerHTML = expanded ? iconSvg("octicon-fold", COLLAPSE_ALL_16_PATHS) : iconSvg("octicon-unfold", EXPAND_ALL_16_PATHS);
7216
7390
  }
7217
7391
  function setSidebarTreeActionIcons() {
7392
+ const settings = document.querySelector("#viewer-settings");
7393
+ const sidebarToggle = document.querySelector("#sidebar-toggle");
7218
7394
  const expand = document.querySelector("#sb-expand-all");
7219
7395
  const collapse = document.querySelector("#sb-collapse-all");
7396
+ if (settings)
7397
+ settings.innerHTML = iconSvg("octicon-gear", GEAR_16_PATH);
7398
+ if (sidebarToggle)
7399
+ sidebarToggle.innerHTML = iconSvg("octicon-sidebar", STATE.sidebarHidden ? SIDEBAR_SHOW_16_PATHS : SIDEBAR_HIDE_16_PATHS);
7220
7400
  if (expand)
7221
- expand.innerHTML = iconSvg("octicon-unfold", UNFOLD_16_PATH);
7401
+ expand.innerHTML = iconSvg("octicon-chevron-down", EXPAND_ALL_16_PATHS);
7222
7402
  if (collapse)
7223
- collapse.innerHTML = iconSvg("octicon-fold", FOLD_16_PATH);
7403
+ collapse.innerHTML = iconSvg("octicon-chevron-up", COLLAPSE_ALL_16_PATHS);
7404
+ }
7405
+ function attachSidebarToggle(host) {
7406
+ const button = document.querySelector("#sidebar-toggle");
7407
+ if (!button || button.parentElement === host)
7408
+ return;
7409
+ host.prepend(button);
7410
+ }
7411
+ function placeSidebarToggle() {
7412
+ const sidebarHead = document.querySelector(".sb-head");
7413
+ const toolbar = document.querySelector(".gdp-repo-toolbar, .gdp-file-detail-header");
7414
+ const restoreHost = toolbar || document.querySelector("#topbar") || document.querySelector("#global-header");
7415
+ if (STATE.sidebarHidden && restoreHost)
7416
+ attachSidebarToggle(restoreHost);
7417
+ else if (sidebarHead)
7418
+ attachSidebarToggle(sidebarHead);
7419
+ }
7420
+ function applySidebarHidden(hidden = STATE.sidebarHidden) {
7421
+ STATE.sidebarHidden = hidden;
7422
+ document.body.classList.toggle("gdp-sidebar-hidden", hidden);
7423
+ localStorage.setItem("gdp:sidebar-hidden", hidden ? "1" : "0");
7424
+ const button = document.querySelector("#sidebar-toggle");
7425
+ if (button) {
7426
+ button.setAttribute("aria-pressed", hidden ? "true" : "false");
7427
+ button.title = hidden ? "show sidebar" : "hide sidebar";
7428
+ button.setAttribute("aria-label", hidden ? "show sidebar" : "hide sidebar");
7429
+ }
7430
+ setSidebarTreeActionIcons();
7431
+ placeSidebarToggle();
7432
+ syncSidebarHeaderHeight();
7433
+ }
7434
+ function toggleSidebarHidden() {
7435
+ applySidebarHidden(!STATE.sidebarHidden);
7436
+ }
7437
+ function scopeOmitSourceLabel() {
7438
+ return savedScopeOmitDirs() != null ? "Browser override" : "Server default";
7439
+ }
7440
+ function refreshRepositoryTreeAfterSettings() {
7441
+ REPO_FILE_CACHE.clear();
7442
+ invalidateRepoSidebar();
7443
+ if (STATE.route.screen === "repo") {
7444
+ loadRepo();
7445
+ return;
7446
+ }
7447
+ const target = sourceTargetFromRoute();
7448
+ if (target)
7449
+ renderRepoBlobSidebar(target.path, target.ref || "worktree");
7450
+ }
7451
+ async function openScopeSettings() {
7452
+ const pop = document.querySelector("#scope-settings-popover");
7453
+ const input = document.querySelector("#scope-omit-dirs");
7454
+ const sidebarFontSize = document.querySelector("#sidebar-font-size");
7455
+ const codeFontSize = document.querySelector("#code-font-size");
7456
+ const source = document.querySelector("#scope-omit-source");
7457
+ if (!pop || !input || !sidebarFontSize || !codeFontSize || !source)
7458
+ return;
7459
+ await loadSettings();
7460
+ sidebarFontSize.value = savedSidebarFontSize();
7461
+ codeFontSize.value = savedCodeFontSize();
7462
+ input.value = effectiveScopeOmitDirs().join(`
7463
+ `);
7464
+ source.textContent = 'Saved for project "' + (PROJECT_NAME || "default") + '" in this browser. Source: ' + scopeOmitSourceLabel() + ". Used by tree, Ctrl+K, and Ctrl+G. Reset removes the browser override.";
7465
+ pop.hidden = false;
7466
+ sidebarFontSize.focus();
7467
+ }
7468
+ function closeScopeSettings() {
7469
+ const pop = document.querySelector("#scope-settings-popover");
7470
+ if (pop)
7471
+ pop.hidden = true;
7472
+ }
7473
+ function saveScopeSettings() {
7474
+ const input = document.querySelector("#scope-omit-dirs");
7475
+ const sidebarFontSize = document.querySelector("#sidebar-font-size");
7476
+ const codeFontSize = document.querySelector("#code-font-size");
7477
+ if (!input || !sidebarFontSize || !codeFontSize)
7478
+ return;
7479
+ localStorage.setItem(SIDEBAR_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(sidebarFontSize.value));
7480
+ localStorage.setItem(CODE_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(codeFontSize.value));
7481
+ applySidebarFontSize();
7482
+ applyCodeFontSize();
7483
+ localStorage.setItem(scopeOmitDirsStorageKey(), JSON.stringify(normalizeScopeOmitDirs(input.value)));
7484
+ closeScopeSettings();
7485
+ refreshRepositoryTreeAfterSettings();
7486
+ }
7487
+ function resetScopeSettings() {
7488
+ localStorage.removeItem(SIDEBAR_FONT_SIZE_STORAGE_KEY);
7489
+ localStorage.removeItem(CODE_FONT_SIZE_STORAGE_KEY);
7490
+ applySidebarFontSize("regular");
7491
+ applyCodeFontSize("regular");
7492
+ localStorage.removeItem(scopeOmitDirsStorageKey());
7493
+ closeScopeSettings();
7494
+ refreshRepositoryTreeAfterSettings();
7224
7495
  }
7225
7496
  function buildTree(files) {
7226
7497
  const root = { name: "", dirs: {}, files: [], path: "", minOrder: Infinity, explicit: true };
@@ -7287,8 +7558,8 @@
7287
7558
  li.dataset.explicit = "true";
7288
7559
  if (dir.children_omitted) {
7289
7560
  li.classList.add("children-omitted");
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";
7561
+ li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
7562
+ li.title = dir.children_omitted_reason === "heavy" ? "Large generated/vendor directory: open the detail pane to browse its contents" : "Internal Git metadata is not browsed";
7292
7563
  }
7293
7564
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
7294
7565
  const chev = document.createElement("span");
@@ -7312,9 +7583,9 @@
7312
7583
  label.appendChild(dn);
7313
7584
  if (dir.children_omitted) {
7314
7585
  const omitted = document.createElement("span");
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";
7586
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
7587
+ omitted.textContent = dir.children_omitted_reason === "heavy" ? "skipped" : "private";
7588
+ omitted.title = dir.children_omitted_reason === "heavy" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
7318
7589
  label.appendChild(omitted);
7319
7590
  }
7320
7591
  li.appendChild(label);
@@ -7346,18 +7617,16 @@
7346
7617
  if (onFileClick) {
7347
7618
  li.addEventListener("click", (e2) => {
7348
7619
  e2.stopPropagation();
7349
- if (dir.children_omitted_reason === "internal")
7620
+ if (dir.children_omitted_reason === "internal" || dir.children_omitted_reason === "truncated")
7350
7621
  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();
7622
+ onFileClick({
7623
+ path: dir.path,
7624
+ display_path: dir.path,
7625
+ type: "tree",
7626
+ children_omitted: dir.children_omitted,
7627
+ children_omitted_reason: dir.children_omitted_reason
7628
+ });
7629
+ scheduleMainSurfaceFocus();
7361
7630
  });
7362
7631
  } else {
7363
7632
  li.addEventListener("click", toggleDir);
@@ -7393,7 +7662,7 @@
7393
7662
  onFileClick(f2);
7394
7663
  else
7395
7664
  scrollToFile(f2.path);
7396
- focusSidebarPanel();
7665
+ scheduleMainSurfaceFocus();
7397
7666
  });
7398
7667
  if (!onFileClick)
7399
7668
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7426,7 +7695,7 @@
7426
7695
  onFileClick(f2);
7427
7696
  else
7428
7697
  scrollToFile(f2.path);
7429
- focusSidebarPanel();
7698
+ scheduleMainSurfaceFocus();
7430
7699
  });
7431
7700
  if (!onFileClick)
7432
7701
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7482,6 +7751,49 @@
7482
7751
  return;
7483
7752
  input.value = ref || "worktree";
7484
7753
  wrap.hidden = !(STATE.route.screen === "file" && STATE.route.view === "blob");
7754
+ syncSidebarHeaderHeight();
7755
+ }
7756
+ function createRefSelectorInput(options) {
7757
+ const wrap = document.createElement("div");
7758
+ wrap.className = "ref-selector" + (options.extraClass ? " " + options.extraClass : "");
7759
+ wrap.dataset.refSelector = "";
7760
+ if (options.wrapperId)
7761
+ wrap.id = options.wrapperId;
7762
+ if (options.hidden)
7763
+ wrap.hidden = true;
7764
+ const icon = document.createElement("span");
7765
+ icon.className = "ref-selector-icon";
7766
+ icon.setAttribute("aria-hidden", "true");
7767
+ icon.innerHTML = iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
7768
+ const input = document.createElement("input");
7769
+ input.className = "ref-input";
7770
+ input.id = options.id;
7771
+ input.readOnly = true;
7772
+ input.autocomplete = "off";
7773
+ input.placeholder = options.placeholder;
7774
+ if (options.title)
7775
+ input.title = options.title;
7776
+ if (options.value != null)
7777
+ input.value = options.value;
7778
+ const caret = document.createElement("span");
7779
+ caret.className = "ref-selector-caret";
7780
+ caret.setAttribute("aria-hidden", "true");
7781
+ caret.innerHTML = iconSvg("octicon-triangle-down", TRIANGLE_DOWN_16_PATH);
7782
+ wrap.append(icon, input, caret);
7783
+ return { wrap, input };
7784
+ }
7785
+ function hydrateRefSelectorMounts() {
7786
+ document.querySelectorAll("[data-ref-selector-mount]").forEach((mount) => {
7787
+ const { wrap } = createRefSelectorInput({
7788
+ id: mount.dataset.refId || "",
7789
+ placeholder: mount.dataset.placeholder || "ref...",
7790
+ title: mount.dataset.title,
7791
+ wrapperId: mount.dataset.wrapperId,
7792
+ extraClass: mount.dataset.extraClass,
7793
+ hidden: mount.hidden
7794
+ });
7795
+ mount.replaceWith(wrap);
7796
+ });
7485
7797
  }
7486
7798
  function renderMeta(meta) {
7487
7799
  const el = $("#meta");
@@ -7489,8 +7801,7 @@
7489
7801
  el.textContent = "";
7490
7802
  return;
7491
7803
  }
7492
- PROJECT_NAME = meta.project || PROJECT_NAME;
7493
- document.title = (meta.project ? meta.project + " - " : "") + "code viewer";
7804
+ setProjectName(meta.project || "");
7494
7805
  el.innerHTML = "";
7495
7806
  if (meta.branch) {
7496
7807
  const b2 = document.createElement("span");
@@ -7757,7 +8068,7 @@
7757
8068
  syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
7758
8069
  }
7759
8070
  function syncHeaderMenu() {
7760
- document.querySelectorAll(".app-menu-item").forEach((link2) => {
8071
+ document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
7761
8072
  const fileRouteOwner = STATE.route.screen === "file" && STATE.route.view === "blob" ? "repo" : "diff";
7762
8073
  const active = link2.dataset.route === STATE.route.screen || STATE.route.screen === "file" && link2.dataset.route === fileRouteOwner;
7763
8074
  link2.classList.toggle("active", active);
@@ -8061,28 +8372,23 @@
8061
8372
  function repoRoute(ref, path) {
8062
8373
  return { screen: "repo", ref: ref || "worktree", path, range: currentRange() };
8063
8374
  }
8064
- function wireRepoTargetPicker(input, onPick) {
8065
- input.addEventListener("focus", () => openPopover(input));
8066
- input.addEventListener("click", (e2) => {
8375
+ function wireRefSelectorInput(input, onPick) {
8376
+ const wrap = input.closest("[data-ref-selector]");
8377
+ wrap?.addEventListener("click", (e2) => {
8067
8378
  e2.stopPropagation();
8068
8379
  openPopover(input);
8069
8380
  });
8070
- input.addEventListener("mousedown", (e2) => {
8071
- if (popover.hidden) {
8072
- e2.preventDefault();
8073
- input.focus();
8074
- }
8075
- });
8076
8381
  input.addEventListener("keydown", (e2) => {
8077
- if (e2.key === "Enter") {
8382
+ if (e2.key === "Enter" || e2.key === " ") {
8078
8383
  e2.preventDefault();
8079
- closePopover();
8384
+ openPopover(input);
8080
8385
  } else if (e2.key === "Escape") {
8081
8386
  closePopover();
8082
8387
  input.blur();
8083
8388
  }
8084
8389
  });
8085
- input.addEventListener("change", () => onPick(input.value || "worktree"));
8390
+ if (onPick)
8391
+ input.addEventListener("change", () => onPick(input.value || "worktree"));
8086
8392
  }
8087
8393
  function createRepoBreadcrumb(target, path) {
8088
8394
  const nav = document.createElement("nav");
@@ -8117,7 +8423,7 @@
8117
8423
  return nav;
8118
8424
  }
8119
8425
  async function renderRepo(meta) {
8120
- PROJECT_NAME = meta.project || PROJECT_NAME;
8426
+ setProjectName(meta.project || "");
8121
8427
  setPageMode();
8122
8428
  removeStandaloneSource();
8123
8429
  $("#empty").classList.add("hidden");
@@ -8130,21 +8436,19 @@
8130
8436
  const target = $("#diff");
8131
8437
  const shell = document.createElement("section");
8132
8438
  shell.className = "gdp-repo-shell";
8133
- const targetPicker = document.createElement("input");
8134
- targetPicker.className = "ref-input gdp-repo-target";
8135
- targetPicker.id = "repo-ref";
8136
- targetPicker.readOnly = true;
8137
- targetPicker.autocomplete = "off";
8138
- targetPicker.value = meta.ref || "worktree";
8139
- targetPicker.placeholder = "ref...";
8140
- targetPicker.title = "repository ref";
8141
- wireRepoTargetPicker(targetPicker, (ref) => {
8439
+ const { wrap: targetPickerWrap, input: targetPicker } = createRefSelectorInput({
8440
+ id: "repo-ref",
8441
+ placeholder: "ref...",
8442
+ title: "repository ref",
8443
+ value: meta.ref || "worktree"
8444
+ });
8445
+ wireRefSelectorInput(targetPicker, (ref) => {
8142
8446
  setRoute(repoRoute(ref, ""));
8143
8447
  loadRepo();
8144
8448
  });
8145
8449
  const toolbar = document.createElement("div");
8146
8450
  toolbar.className = "gdp-file-detail-header gdp-repo-toolbar";
8147
- toolbar.append(createRepoBreadcrumb(meta.ref, meta.path || ""), createOpenPathButton(meta.path || "", "directory", "open this folder in OS"), targetPicker);
8451
+ toolbar.append(targetPickerWrap, createRepoBreadcrumb(meta.ref, meta.path || ""), createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
8148
8452
  shell.appendChild(toolbar);
8149
8453
  const listCard = document.createElement("section");
8150
8454
  listCard.className = "gdp-file-shell loaded gdp-repo-list-shell";
@@ -8263,6 +8567,7 @@
8263
8567
  shell.appendChild(readme);
8264
8568
  }
8265
8569
  target.appendChild(shell);
8570
+ placeSidebarToggle();
8266
8571
  }
8267
8572
  function renderRepoBlobSidebar(currentPath, ref) {
8268
8573
  syncRepoTargetInput(ref);
@@ -8279,6 +8584,7 @@
8279
8584
  const params = new URLSearchParams;
8280
8585
  params.set("ref", normalizedRef);
8281
8586
  params.set("recursive", "1");
8587
+ appendScopeOmitDirsParam(params);
8282
8588
  REPO_SIDEBAR_LOAD_REF = normalizedRef;
8283
8589
  const load2 = trackLoad(fetch("/_tree?" + params.toString()).then((r2) => {
8284
8590
  if (!r2.ok)
@@ -8363,7 +8669,10 @@
8363
8669
  document.querySelectorAll(".gdp-file-shell.pending").forEach((c2) => lazyObserver.observe(c2));
8364
8670
  }
8365
8671
  window.addEventListener("scroll", () => enqueueInitialLoads(), { passive: true });
8366
- window.addEventListener("resize", () => enqueueInitialLoads(), { passive: true });
8672
+ window.addEventListener("resize", () => {
8673
+ enqueueInitialLoads();
8674
+ syncSidebarHeaderHeight();
8675
+ }, { passive: true });
8367
8676
  document.addEventListener("visibilitychange", () => {
8368
8677
  if (!document.hidden)
8369
8678
  enqueueInitialLoads();
@@ -9606,6 +9915,9 @@
9606
9915
  url.searchParams.delete("virtual");
9607
9916
  return url.pathname + url.search;
9608
9917
  }
9918
+ function buildFileRangeUrl(target, start, end) {
9919
+ return "/file_range?path=" + encodeURIComponent(target.path) + "&ref=" + encodeURIComponent(target.ref || "worktree") + "&start=" + encodeURIComponent(String(start)) + "&end=" + encodeURIComponent(String(end));
9920
+ }
9609
9921
  function currentSourceLineTarget(target) {
9610
9922
  const routeTarget = sourceTargetFromRoute();
9611
9923
  return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : undefined;
@@ -9669,6 +9981,161 @@
9669
9981
  document.addEventListener("mouseup", () => {
9670
9982
  SOURCE_LINE_DRAG = null;
9671
9983
  });
9984
+ function virtualSourceSearchRanges(line, query) {
9985
+ const needle = query.toLowerCase();
9986
+ if (!needle)
9987
+ return [];
9988
+ const haystack = line.toLowerCase();
9989
+ const ranges = [];
9990
+ let cursor = 0;
9991
+ while (cursor <= haystack.length) {
9992
+ const index = haystack.indexOf(needle, cursor);
9993
+ if (index < 0)
9994
+ break;
9995
+ ranges.push({ start: index, end: index + query.length });
9996
+ cursor = Math.max(index + query.length, index + 1);
9997
+ }
9998
+ return ranges;
9999
+ }
10000
+ function collectVirtualSourceSearchMatches(lines, query, max = 5000) {
10001
+ const matches = [];
10002
+ for (let index = 0;index < lines.length && matches.length < max; index++) {
10003
+ for (const range of virtualSourceSearchRanges(lines[index] || "", query)) {
10004
+ matches.push({ line: index + 1, start: range.start, end: range.end });
10005
+ if (matches.length >= max)
10006
+ break;
10007
+ }
10008
+ }
10009
+ return matches;
10010
+ }
10011
+ function appendVirtualSourceLineCode(code2, line, query, activeRange, lineNumber) {
10012
+ const ranges = virtualSourceSearchRanges(line, query);
10013
+ if (!ranges.length)
10014
+ return false;
10015
+ let cursor = 0;
10016
+ for (const range of ranges) {
10017
+ if (range.start > cursor)
10018
+ code2.appendChild(document.createTextNode(line.slice(cursor, range.start)));
10019
+ const mark = document.createElement("mark");
10020
+ const active = !!activeRange && activeRange.line === lineNumber && activeRange.start === range.start && activeRange.end === range.end;
10021
+ mark.className = active ? "gdp-source-virtual-search-hit active" : "gdp-source-virtual-search-hit";
10022
+ mark.textContent = line.slice(range.start, range.end);
10023
+ code2.appendChild(mark);
10024
+ cursor = range.end;
10025
+ }
10026
+ if (cursor < line.length)
10027
+ code2.appendChild(document.createTextNode(line.slice(cursor)));
10028
+ return true;
10029
+ }
10030
+ function createVirtualSourceSearch(wrap, scroller, findMatches, renderFn) {
10031
+ const bar = document.createElement("div");
10032
+ bar.className = "gdp-source-virtual-search";
10033
+ const input = document.createElement("input");
10034
+ input.type = "search";
10035
+ input.placeholder = "Find in file";
10036
+ input.autocomplete = "off";
10037
+ input.spellcheck = false;
10038
+ const count = document.createElement("span");
10039
+ count.className = "gdp-source-virtual-search-count";
10040
+ const previous = document.createElement("button");
10041
+ previous.type = "button";
10042
+ previous.textContent = "Prev";
10043
+ const next = document.createElement("button");
10044
+ next.type = "button";
10045
+ next.textContent = "Next";
10046
+ const close = document.createElement("button");
10047
+ close.type = "button";
10048
+ close.textContent = "Close";
10049
+ bar.append(input, count, previous, next, close);
10050
+ wrap.querySelector(".gdp-source-virtual-info")?.appendChild(bar);
10051
+ bar.hidden = true;
10052
+ let matches = [];
10053
+ let active = -1;
10054
+ let debounce = 0;
10055
+ let searchVersion = 0;
10056
+ const hide = () => {
10057
+ bar.hidden = true;
10058
+ renderFn();
10059
+ scroller.focus({ preventScroll: true });
10060
+ };
10061
+ const sync = () => {
10062
+ const query = input.value;
10063
+ const version = ++searchVersion;
10064
+ if (!query) {
10065
+ matches = [];
10066
+ active = -1;
10067
+ count.textContent = "";
10068
+ renderFn();
10069
+ return;
10070
+ }
10071
+ count.textContent = "Searching...";
10072
+ findMatches(query).then((nextMatches) => {
10073
+ if (version !== searchVersion)
10074
+ return;
10075
+ matches = nextMatches;
10076
+ active = matches.length ? Math.max(0, Math.min(active, matches.length - 1)) : -1;
10077
+ count.textContent = matches.length ? active + 1 + " / " + matches.length : "0 / 0";
10078
+ if (active >= 0)
10079
+ scroller.scrollTop = Math.max(0, (matches[active].line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - VIRTUAL_SOURCE_ROW_HEIGHT * 3);
10080
+ renderFn();
10081
+ }).catch(() => {
10082
+ if (version !== searchVersion)
10083
+ return;
10084
+ matches = [];
10085
+ active = -1;
10086
+ count.textContent = "Search failed";
10087
+ renderFn();
10088
+ });
10089
+ };
10090
+ const scheduleSync = () => {
10091
+ if (debounce)
10092
+ window.clearTimeout(debounce);
10093
+ debounce = window.setTimeout(sync, 120);
10094
+ };
10095
+ const move = (direction) => {
10096
+ if (!matches.length)
10097
+ return;
10098
+ active = (active + direction + matches.length) % matches.length;
10099
+ count.textContent = active + 1 + " / " + matches.length;
10100
+ scroller.scrollTop = Math.max(0, (matches[active].line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - VIRTUAL_SOURCE_ROW_HEIGHT * 3);
10101
+ renderFn();
10102
+ };
10103
+ input.addEventListener("input", () => {
10104
+ active = 0;
10105
+ scheduleSync();
10106
+ });
10107
+ input.addEventListener("keydown", (e2) => {
10108
+ if (e2.key === "Escape") {
10109
+ e2.preventDefault();
10110
+ hide();
10111
+ } else if (e2.key === "Enter") {
10112
+ e2.preventDefault();
10113
+ move(e2.shiftKey ? -1 : 1);
10114
+ }
10115
+ });
10116
+ previous.addEventListener("click", () => move(-1));
10117
+ next.addEventListener("click", () => move(1));
10118
+ close.addEventListener("click", hide);
10119
+ return {
10120
+ open: () => {
10121
+ bar.hidden = false;
10122
+ input.focus();
10123
+ input.select();
10124
+ sync();
10125
+ },
10126
+ query: () => bar.hidden ? "" : input.value,
10127
+ activeRange: () => active >= 0 ? matches[active] || null : null
10128
+ };
10129
+ }
10130
+ function openVirtualSourceSearchFromKeyboard(targetEl) {
10131
+ const active = targetEl?.closest("#content .gdp-source-virtual");
10132
+ const fallback = document.querySelector("#content .gdp-source-viewer.virtual .gdp-source-virtual:not([hidden])");
10133
+ const search = active?.__gdpVirtualSourceSearch || fallback?.__gdpVirtualSourceSearch;
10134
+ if (!search)
10135
+ return false;
10136
+ search.open();
10137
+ return true;
10138
+ }
9672
10139
  function renderVirtualSource(target, textValue, lines, hljsRef, lang) {
9673
10140
  const wrap = document.createElement("div");
9674
10141
  wrap.className = "gdp-source-virtual";
@@ -9717,6 +10184,9 @@
9717
10184
  info.append(badge, summary, actions);
9718
10185
  const scroller = document.createElement("div");
9719
10186
  scroller.className = "gdp-source-virtual-scroller";
10187
+ scroller.tabIndex = 0;
10188
+ scroller.setAttribute("role", "region");
10189
+ scroller.setAttribute("aria-label", target.path + " source code");
9720
10190
  const spacer = document.createElement("div");
9721
10191
  spacer.className = "gdp-source-virtual-spacer";
9722
10192
  spacer.style.height = Math.max(1, lines.length * VIRTUAL_SOURCE_ROW_HEIGHT) + "px";
@@ -9728,6 +10198,7 @@
9728
10198
  let raf = 0;
9729
10199
  let renderedStart = -1;
9730
10200
  let renderedEnd = -1;
10201
+ let search = null;
9731
10202
  const render = () => {
9732
10203
  raf = 0;
9733
10204
  const viewportHeight = scroller.clientHeight || window.innerHeight;
@@ -9754,7 +10225,9 @@
9754
10225
  const code2 = document.createElement("span");
9755
10226
  code2.className = "gdp-source-virtual-line-code";
9756
10227
  const line = lines[index] ?? "";
9757
- if (hljsRef && hljsRef.highlight && lang && line.length <= VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
10228
+ const searchQuery = search?.query() || "";
10229
+ const activeRange = search?.activeRange() || null;
10230
+ if (appendVirtualSourceLineCode(code2, line, searchQuery, activeRange, index + 1)) {} else if (hljsRef && hljsRef.highlight && lang && line.length <= VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
9758
10231
  try {
9759
10232
  code2.innerHTML = hljsRef.highlight(line, { language: lang, ignoreIllegals: true }).value;
9760
10233
  code2.classList.add("hljs");
@@ -9775,6 +10248,8 @@
9775
10248
  };
9776
10249
  scroller.__gdpRenderVirtualSource = render;
9777
10250
  scroller.addEventListener("scroll", schedule, { passive: true });
10251
+ search = createVirtualSourceSearch(wrap, scroller, (query) => Promise.resolve(collectVirtualSourceSearchMatches(lines, query)), render);
10252
+ wrap.__gdpVirtualSourceSearch = search;
9778
10253
  let resizeObserver = null;
9779
10254
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
9780
10255
  if (!scroller.isConnected) {
@@ -9789,6 +10264,253 @@
9789
10264
  schedule();
9790
10265
  return wrap;
9791
10266
  }
10267
+ function renderPagedVirtualSource(target, size, initialStart, initialLines, initialComplete, initialTotal, hljsRef, lang, signal) {
10268
+ const wrap = document.createElement("div");
10269
+ wrap.className = "gdp-source-virtual";
10270
+ const info = document.createElement("div");
10271
+ info.className = "gdp-source-virtual-info";
10272
+ const badge = document.createElement("span");
10273
+ badge.className = "gdp-source-virtual-badge";
10274
+ badge.textContent = "Virtual mode";
10275
+ const summary = document.createElement("span");
10276
+ summary.className = "gdp-source-virtual-summary";
10277
+ const actions = document.createElement("span");
10278
+ actions.className = "gdp-source-virtual-actions";
10279
+ const raw = document.createElement("a");
10280
+ raw.className = "gdp-source-virtual-action";
10281
+ raw.href = buildRawFileUrl(target);
10282
+ raw.target = "_blank";
10283
+ raw.rel = "noreferrer";
10284
+ raw.textContent = "Open raw";
10285
+ const full = document.createElement("a");
10286
+ full.className = "gdp-source-virtual-action";
10287
+ full.href = buildCurrentFileRouteWithVirtualMode(target, "off");
10288
+ full.textContent = "Open full view";
10289
+ full.title = "Render every line without paged loading. This can be slow for large files.";
10290
+ full.addEventListener("click", (e2) => {
10291
+ e2.preventDefault();
10292
+ const url = new URL(full.href, window.location.origin);
10293
+ setRoute(parseRoute(url.pathname, url.search, currentRange()), true);
10294
+ renderStandaloneSource(target);
10295
+ });
10296
+ actions.append(raw, full);
10297
+ info.append(badge, summary, actions);
10298
+ const scroller = document.createElement("div");
10299
+ scroller.className = "gdp-source-virtual-scroller";
10300
+ scroller.tabIndex = 0;
10301
+ scroller.setAttribute("role", "region");
10302
+ scroller.setAttribute("aria-label", target.path + " source code");
10303
+ const spacer = document.createElement("div");
10304
+ spacer.className = "gdp-source-virtual-spacer";
10305
+ const windowEl = document.createElement("div");
10306
+ windowEl.className = "gdp-source-virtual-window";
10307
+ spacer.appendChild(windowEl);
10308
+ scroller.appendChild(spacer);
10309
+ wrap.append(info, scroller);
10310
+ const lines = new Map;
10311
+ const requestedPages = new Set;
10312
+ const failedPages = new Set;
10313
+ const targetLine = lineTargetStart(currentSourceLineTarget(target)) || 1;
10314
+ let complete = initialComplete;
10315
+ let totalRows = initialComplete ? Math.max(1, initialTotal) : Math.max(initialTotal || 1, initialStart + initialLines.length - 1, targetLine + VIRTUAL_SOURCE_PAGE_SIZE);
10316
+ initialLines.forEach((line, index) => lines.set(initialStart + index, line));
10317
+ requestedPages.add(Math.max(0, Math.floor((initialStart - 1) / VIRTUAL_SOURCE_PAGE_SIZE)));
10318
+ for (let line = initialStart;line < initialStart + initialLines.length; line += VIRTUAL_SOURCE_PAGE_SIZE) {
10319
+ requestedPages.add(Math.max(0, Math.floor((line - 1) / VIRTUAL_SOURCE_PAGE_SIZE)));
10320
+ }
10321
+ const updateTotals = () => {
10322
+ SOURCE_CURSOR_TOTALS.set(sourceCursorKey(target), totalRows);
10323
+ summary.textContent = (complete ? totalRows.toLocaleString() : lines.size.toLocaleString() + "+") + " lines loaded from " + formatBytes(size) + ". More rows load as you scroll.";
10324
+ spacer.style.height = Math.max(1, totalRows * VIRTUAL_SOURCE_ROW_HEIGHT) + "px";
10325
+ };
10326
+ const loadPage = (line) => {
10327
+ if (signal?.aborted || complete && line > totalRows)
10328
+ return;
10329
+ const page = Math.max(0, Math.floor((line - 1) / VIRTUAL_SOURCE_PAGE_SIZE));
10330
+ if (requestedPages.has(page))
10331
+ return;
10332
+ if (failedPages.has(page))
10333
+ return;
10334
+ requestedPages.add(page);
10335
+ const start = page * VIRTUAL_SOURCE_PAGE_SIZE + 1;
10336
+ const end = start + VIRTUAL_SOURCE_PAGE_SIZE - 1;
10337
+ trackLoad(fetch(buildFileRangeUrl(target, start, end), { signal }).then((res) => res.ok ? res.json() : null).then((data) => {
10338
+ if (!data || signal?.aborted)
10339
+ return;
10340
+ data.lines.forEach((lineValue, index) => lines.set(data.start + index, lineValue));
10341
+ totalRows = data.complete ? Math.max(1, data.total) : Math.max(totalRows, data.total, end + VIRTUAL_SOURCE_PAGE_SIZE);
10342
+ complete = data.complete === true;
10343
+ updateTotals();
10344
+ renderedStart = -1;
10345
+ renderedEnd = -1;
10346
+ render();
10347
+ }).catch((err) => {
10348
+ if (!isAbortError(err)) {
10349
+ failedPages.add(page);
10350
+ renderedStart = -1;
10351
+ renderedEnd = -1;
10352
+ schedule();
10353
+ }
10354
+ }));
10355
+ };
10356
+ let raf = 0;
10357
+ let renderedStart = -1;
10358
+ let renderedEnd = -1;
10359
+ let search = null;
10360
+ let searchController = null;
10361
+ const render = () => {
10362
+ raf = 0;
10363
+ const viewportHeight = scroller.clientHeight || window.innerHeight;
10364
+ const overscan = 20;
10365
+ const start = Math.max(0, Math.floor(scroller.scrollTop / VIRTUAL_SOURCE_ROW_HEIGHT) - overscan);
10366
+ const end = Math.min(totalRows, Math.ceil((scroller.scrollTop + viewportHeight) / VIRTUAL_SOURCE_ROW_HEIGHT) + overscan);
10367
+ if (start === renderedStart && end === renderedEnd)
10368
+ return;
10369
+ renderedStart = start;
10370
+ renderedEnd = end;
10371
+ windowEl.replaceChildren();
10372
+ windowEl.style.transform = "translateY(" + start * VIRTUAL_SOURCE_ROW_HEIGHT + "px)";
10373
+ const fragment = document.createDocumentFragment();
10374
+ for (let index = start;index < end; index++) {
10375
+ const lineNumber = index + 1;
10376
+ if (!lines.has(lineNumber))
10377
+ loadPage(lineNumber);
10378
+ const row = document.createElement("div");
10379
+ row.className = "gdp-source-virtual-row";
10380
+ row.dataset.line = String(lineNumber);
10381
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(lineNumber, currentSourceLineTarget(target)));
10382
+ row.classList.toggle("gdp-source-cursor", sourceCursorMatches(target, lineNumber));
10383
+ const num = document.createElement("span");
10384
+ num.className = "gdp-source-virtual-line-number";
10385
+ num.textContent = String(lineNumber);
10386
+ bindSourceLineNumber(num, wrap, target, lineNumber);
10387
+ const code2 = document.createElement("span");
10388
+ code2.className = "gdp-source-virtual-line-code";
10389
+ const line = lines.get(lineNumber);
10390
+ if (line == null) {
10391
+ code2.textContent = "";
10392
+ } else if (appendVirtualSourceLineCode(code2, line, search?.query() || "", search?.activeRange() || null, lineNumber)) {} else if (hljsRef && hljsRef.highlight && lang && line.length <= VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH && (!hljsRef.getLanguage || hljsRef.getLanguage(lang))) {
10393
+ try {
10394
+ code2.innerHTML = hljsRef.highlight(line, { language: lang, ignoreIllegals: true }).value;
10395
+ code2.classList.add("hljs");
10396
+ } catch {
10397
+ code2.textContent = line;
10398
+ }
10399
+ } else {
10400
+ code2.textContent = line;
10401
+ }
10402
+ row.append(num, code2);
10403
+ fragment.appendChild(row);
10404
+ }
10405
+ windowEl.appendChild(fragment);
10406
+ if (!complete && totalRows - end < VIRTUAL_SOURCE_PAGE_SIZE) {
10407
+ totalRows += VIRTUAL_SOURCE_PAGE_SIZE;
10408
+ updateTotals();
10409
+ }
10410
+ };
10411
+ const schedule = () => {
10412
+ if (!raf)
10413
+ raf = requestAnimationFrame(render);
10414
+ };
10415
+ scroller.__gdpRenderVirtualSource = render;
10416
+ scroller.addEventListener("scroll", schedule, { passive: true });
10417
+ const findPagedMatches = async (query, matchSignal) => {
10418
+ const matches = [];
10419
+ let startLine = 1;
10420
+ let done = false;
10421
+ while (!done && matches.length < 5000) {
10422
+ const endLine = startLine + VIRTUAL_SOURCE_PAGE_SIZE - 1;
10423
+ const data = await trackLoad(fetch(buildFileRangeUrl(target, startLine, endLine), { signal: matchSignal }).then((res) => {
10424
+ if (!res.ok)
10425
+ throw new Error("file range failed");
10426
+ return res.json();
10427
+ }));
10428
+ if (matchSignal?.aborted)
10429
+ return [];
10430
+ data.lines.forEach((lineValue, index) => {
10431
+ const lineNumber = data.start + index;
10432
+ lines.set(lineNumber, lineValue);
10433
+ for (const range of virtualSourceSearchRanges(lineValue, query)) {
10434
+ matches.push({ line: lineNumber, start: range.start, end: range.end });
10435
+ if (matches.length >= 5000)
10436
+ break;
10437
+ }
10438
+ });
10439
+ totalRows = data.complete ? Math.max(1, data.total) : Math.max(totalRows, data.total, endLine + VIRTUAL_SOURCE_PAGE_SIZE);
10440
+ complete = data.complete === true;
10441
+ updateTotals();
10442
+ if (data.complete || !data.lines.length)
10443
+ done = true;
10444
+ else
10445
+ startLine = data.start + data.lines.length;
10446
+ }
10447
+ renderedStart = -1;
10448
+ renderedEnd = -1;
10449
+ schedule();
10450
+ return matches;
10451
+ };
10452
+ search = createVirtualSourceSearch(wrap, scroller, (query) => {
10453
+ searchController?.abort();
10454
+ searchController = new AbortController;
10455
+ return findPagedMatches(query, searchController.signal);
10456
+ }, render);
10457
+ wrap.__gdpVirtualSourceSearch = search;
10458
+ let resizeObserver = null;
10459
+ resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
10460
+ if (!scroller.isConnected) {
10461
+ resizeObserver?.disconnect();
10462
+ resizeObserver = null;
10463
+ return;
10464
+ }
10465
+ schedule();
10466
+ }) : null;
10467
+ resizeObserver?.observe(scroller);
10468
+ updateTotals();
10469
+ if (targetLine <= 1) {
10470
+ render();
10471
+ schedule();
10472
+ }
10473
+ return wrap;
10474
+ }
10475
+ async function renderPagedSourceText(card, target, size, signal) {
10476
+ const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
10477
+ const isStandalone = card.classList.contains("gdp-standalone-source");
10478
+ const view = document.createElement("div");
10479
+ view.className = "gdp-source-viewer virtual";
10480
+ const header = isStandalone ? null : document.createElement("div");
10481
+ if (header) {
10482
+ header.className = "gdp-source-meta";
10483
+ header.textContent = target.path + " @ " + target.ref;
10484
+ view.appendChild(header);
10485
+ }
10486
+ const lineTarget = lineTargetStart(currentSourceLineTarget(target)) || 1;
10487
+ const initialPage = Math.max(0, Math.floor((lineTarget - 1) / VIRTUAL_SOURCE_PAGE_SIZE));
10488
+ const initialStart = initialPage * VIRTUAL_SOURCE_PAGE_SIZE + 1;
10489
+ const initialEnd = initialStart + VIRTUAL_SOURCE_PAGE_SIZE - 1;
10490
+ const lang = inferLang(target.path);
10491
+ const hljsRef = STATE.syntaxHighlight ? await loadSyntaxHighlighter() : null;
10492
+ if (signal?.aborted)
10493
+ return false;
10494
+ const initial = await trackLoad(fetch(buildFileRangeUrl(target, initialStart, initialEnd), { signal }).then((res) => res.ok ? res.json() : null));
10495
+ if (!initial)
10496
+ return false;
10497
+ if (signal?.aborted)
10498
+ return false;
10499
+ const tabsHost = card.querySelector(".gdp-file-detail-tabs");
10500
+ if (tabsHost) {
10501
+ tabsHost.hidden = false;
10502
+ tabsHost.replaceChildren(createSourceTabs("code").tabs);
10503
+ }
10504
+ SOURCE_CURSOR_TOTALS.set(sourceCursorKey(target), Math.max(1, initial.total, lineTarget));
10505
+ resetSourceCursorForTarget(target, Math.max(1, initial.total, lineTarget));
10506
+ const virtualCode = renderPagedVirtualSource(target, size, initialStart, initial.lines, initial.complete === true, initial.total, hljsRef, lang, signal);
10507
+ view.appendChild(virtualCode);
10508
+ if (body)
10509
+ body.replaceWith(view);
10510
+ else
10511
+ card.appendChild(view);
10512
+ return true;
10513
+ }
9792
10514
  function renderSourceMedia(card, target, mediaKind) {
9793
10515
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
9794
10516
  const isStandalone = card.classList.contains("gdp-standalone-source");
@@ -9969,6 +10691,7 @@
9969
10691
  } else {
9970
10692
  root.prepend(card);
9971
10693
  }
10694
+ placeSidebarToggle();
9972
10695
  const controller = new AbortController;
9973
10696
  ACTIVE_SOURCE_LOAD = { controller, req, target, card };
9974
10697
  renderSourceLoading(card, target, () => cancelActiveSourceLoad("user"));
@@ -9989,6 +10712,19 @@
9989
10712
  return;
9990
10713
  }
9991
10714
  if (displayKind === "text") {
10715
+ const meta = await loadRawFileInfo(target);
10716
+ if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
10717
+ return;
10718
+ if (!isVirtualSourceDisabled() && meta.size != null && meta.size >= VIRTUAL_SOURCE_SIZE_THRESHOLD) {
10719
+ const rendered2 = await renderPagedSourceText(card, target, meta.size, controller.signal);
10720
+ if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
10721
+ return;
10722
+ if (!rendered2)
10723
+ return;
10724
+ scrollStandaloneSourceLine(card, lineTargetStart(STATE.route.screen === "file" ? STATE.route.line : undefined));
10725
+ finishSourceLoad(req);
10726
+ return;
10727
+ }
9992
10728
  const response = await trackLoad(fetch(buildRawFileUrl(target), { signal: controller.signal }));
9993
10729
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
9994
10730
  return;
@@ -10026,6 +10762,7 @@
10026
10762
  if (virtualScroller) {
10027
10763
  const centeredOffset = virtualScroller.clientHeight / 2 - VIRTUAL_SOURCE_ROW_HEIGHT / 2;
10028
10764
  virtualScroller.scrollTop = Math.max(0, (line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - Math.max(0, centeredOffset));
10765
+ virtualScroller.__gdpRenderVirtualSource?.();
10029
10766
  return;
10030
10767
  }
10031
10768
  const row = card.querySelector('.gdp-source-table tr[data-line="' + String(line) + '"]');
@@ -10492,6 +11229,11 @@
10492
11229
  body.style.display = STATE.collapsed ? "none" : "";
10493
11230
  });
10494
11231
  }
11232
+ applySidebarFontSize();
11233
+ applyCodeFontSize();
11234
+ applySidebarHidden();
11235
+ observeSidebarHeaderHeight();
11236
+ hydrateRefSelectorMounts();
10495
11237
  setSidebarTreeActionIcons();
10496
11238
  $$(".sb-view-seg button").forEach((b2) => {
10497
11239
  b2.addEventListener("click", () => {
@@ -10503,6 +11245,15 @@
10503
11245
  });
10504
11246
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
10505
11247
  $("#sb-collapse-all").addEventListener("click", () => setAllSidebarDirsCollapsed(true));
11248
+ $("#sidebar-toggle")?.addEventListener("click", toggleSidebarHidden);
11249
+ $("#viewer-settings")?.addEventListener("click", openScopeSettings);
11250
+ $("#scope-settings-close")?.addEventListener("click", closeScopeSettings);
11251
+ $("#scope-omit-save")?.addEventListener("click", saveScopeSettings);
11252
+ $("#scope-omit-reset")?.addEventListener("click", resetScopeSettings);
11253
+ $("#scope-settings-popover")?.addEventListener("keydown", (e2) => {
11254
+ if (e2.key === "Escape")
11255
+ closeScopeSettings();
11256
+ });
10506
11257
  prepareKeyboardPanels();
10507
11258
  const contentPanel = document.querySelector("#content");
10508
11259
  contentPanel?.addEventListener("focusin", () => setPanelFocusScope("main"));
@@ -10937,17 +11688,19 @@
10937
11688
  });
10938
11689
  }
10939
11690
  async function repoPaletteFiles(ref) {
10940
- const cached = REPO_FILE_CACHE.get(ref);
11691
+ const cacheKey = repoFileCacheKey(ref);
11692
+ const cached = REPO_FILE_CACHE.get(cacheKey);
10941
11693
  if (cached && cached.generation === SERVER_GENERATION)
10942
11694
  return cached;
10943
11695
  const params = new URLSearchParams;
10944
11696
  params.set("ref", ref);
11697
+ appendScopeOmitDirsParam(params);
10945
11698
  const res = await trackLoad(fetch("/_files?" + params.toString()).then((r2) => {
10946
11699
  if (!r2.ok)
10947
11700
  throw new Error("failed to load files");
10948
11701
  return r2.json();
10949
11702
  }));
10950
- REPO_FILE_CACHE.set(ref, res);
11703
+ REPO_FILE_CACHE.set(cacheKey, res);
10951
11704
  return res;
10952
11705
  }
10953
11706
  function diffFilePaletteItems(state, query) {
@@ -11035,6 +11788,7 @@
11035
11788
  params.set("max", "200");
11036
11789
  if (state.grepRegex)
11037
11790
  params.set("regex", "1");
11791
+ appendScopeOmitDirsParam(params);
11038
11792
  if (source === "diff") {
11039
11793
  for (const file of state.diffSnapshot)
11040
11794
  params.append("path", file.path);
@@ -11153,6 +11907,8 @@
11153
11907
  return true;
11154
11908
  }
11155
11909
  if (action === "focus-sidebar") {
11910
+ if (STATE.sidebarHidden)
11911
+ applySidebarHidden(false);
11156
11912
  focusSidebarPanel();
11157
11913
  return true;
11158
11914
  }
@@ -11252,8 +12008,50 @@
11252
12008
  }
11253
12009
  return false;
11254
12010
  }
12011
+ function handleVirtualSourcePagingKey(e2, targetEl) {
12012
+ if (e2.__gdpVirtualSourcePagingHandled)
12013
+ return true;
12014
+ if (e2.defaultPrevented || e2.isComposing || PALETTE || document.querySelector(".mkdp-lightbox"))
12015
+ return false;
12016
+ const editable = isEditableKeyTarget(targetEl);
12017
+ const inVirtualSearch = !!targetEl?.closest(".gdp-source-virtual-search");
12018
+ if (editable && !inVirtualSearch)
12019
+ return false;
12020
+ const key = e2.key.toLowerCase();
12021
+ if (e2.altKey || e2.metaKey)
12022
+ return false;
12023
+ const isPlainPageKey = (key === "pagedown" || key === "pageup") && !e2.ctrlKey && !e2.shiftKey;
12024
+ const isCtrlArrowKey = (key === "arrowdown" || key === "arrowup") && e2.ctrlKey && !e2.shiftKey;
12025
+ if (!isPlainPageKey && !isCtrlArrowKey)
12026
+ return false;
12027
+ const scroller = findMainScrollTarget();
12028
+ if (!scroller || !scroller.matches("#content .gdp-source-virtual-scroller"))
12029
+ return false;
12030
+ const pageDown = key === "pagedown" || key === "arrowdown";
12031
+ const pageUp = key === "pageup" || key === "arrowup";
12032
+ if (!pageDown && !pageUp)
12033
+ return false;
12034
+ e2.__gdpVirtualSourcePagingHandled = true;
12035
+ e2.preventDefault();
12036
+ e2.stopPropagation();
12037
+ scrollMainPanel(pageDown ? 1 : -1, e2.repeat, "page");
12038
+ focusMainSurface();
12039
+ return true;
12040
+ }
12041
+ function handleVirtualSourcePagingKeydown(e2) {
12042
+ handleVirtualSourcePagingKey(e2, e2.target);
12043
+ }
12044
+ document.addEventListener("keydown", handleVirtualSourcePagingKeydown, { capture: true });
11255
12045
  document.addEventListener("keydown", (e2) => {
12046
+ if (e2.__gdpVirtualSourcePagingHandled)
12047
+ return;
11256
12048
  const targetEl = e2.target;
12049
+ if ((e2.ctrlKey || e2.metaKey) && e2.key.toLowerCase() === "f" && !isEditableKeyTarget(targetEl)) {
12050
+ if (openVirtualSourceSearchFromKeyboard(targetEl)) {
12051
+ e2.preventDefault();
12052
+ return;
12053
+ }
12054
+ }
11257
12055
  const scope = keymapScope(targetEl);
11258
12056
  const action = resolveKeymapAction(e2, {
11259
12057
  scope,
@@ -11282,6 +12080,7 @@
11282
12080
  params.set("ref", STATE.route.ref || "worktree");
11283
12081
  if (STATE.route.path)
11284
12082
  params.set("path", STATE.route.path);
12083
+ appendScopeOmitDirsParam(params);
11285
12084
  return trackLoad(fetch("/_tree?" + params.toString()).then((r2) => {
11286
12085
  if (!r2.ok)
11287
12086
  throw new Error("failed to load repository tree");
@@ -11317,16 +12116,18 @@
11317
12116
  setStatus("live");
11318
12117
  }).catch(() => setStatus("error"));
11319
12118
  }
11320
- if (STATE.route.screen === "help") {
11321
- setStatus("live");
11322
- renderHelpPage();
11323
- } else if (STATE.route.screen === "repo")
11324
- loadRepo();
11325
- else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
11326
- setStatus("live");
11327
- applySourceRouteToShell();
11328
- } else
11329
- load();
12119
+ loadSettings().finally(() => {
12120
+ if (STATE.route.screen === "help") {
12121
+ setStatus("live");
12122
+ renderHelpPage();
12123
+ } else if (STATE.route.screen === "repo")
12124
+ loadRepo();
12125
+ else if (STATE.route.screen === "file" && STATE.route.view === "blob") {
12126
+ setStatus("live");
12127
+ applySourceRouteToShell();
12128
+ } else
12129
+ load();
12130
+ });
11330
12131
  function syncRefInputs() {
11331
12132
  const fi = $("#ref-from"), ti = $("#ref-to");
11332
12133
  if (fi)
@@ -11445,40 +12246,21 @@
11445
12246
  popover.hidden = true;
11446
12247
  popTarget = null;
11447
12248
  }
11448
- ["#ref-from", "#ref-to"].forEach((sel) => {
11449
- const el = $(sel);
11450
- el.addEventListener("focus", () => openPopover(el));
11451
- el.addEventListener("mousedown", (e2) => {
11452
- if (popover.hidden) {
11453
- e2.preventDefault();
11454
- el.focus();
11455
- }
11456
- });
11457
- el.addEventListener("click", (e2) => {
11458
- e2.stopPropagation();
11459
- openPopover(el);
11460
- });
11461
- el.addEventListener("keydown", (e2) => {
11462
- if (e2.key === "Enter") {
11463
- e2.preventDefault();
11464
- closePopover();
11465
- } else if (e2.key === "Escape") {
11466
- closePopover();
11467
- el.blur();
11468
- }
11469
- });
12249
+ const refFromInput = $("#ref-from");
12250
+ const refToInput = $("#ref-to");
12251
+ wireRefSelectorInput(refFromInput, () => {
12252
+ const otherEmpty = !refToInput.value;
12253
+ setRange(refFromInput.value, refToInput.value);
12254
+ if (otherEmpty)
12255
+ setTimeout(() => openPopover(refToInput), 0);
11470
12256
  });
11471
- wireRepoTargetPicker($("#repo-target"), (ref) => {
12257
+ wireRefSelectorInput(refToInput, () => setRange(refFromInput.value, refToInput.value));
12258
+ wireRefSelectorInput($("#repo-target"), (ref) => {
11472
12259
  if (STATE.route.screen !== "file")
11473
12260
  return;
11474
12261
  setRoute({ screen: "file", path: STATE.route.path, ref, view: "blob", range: currentRange() });
11475
12262
  renderStandaloneSource({ path: STATE.route.path, ref });
11476
12263
  });
11477
- document.addEventListener("focusin", (e2) => {
11478
- const el = e2.target;
11479
- if (el instanceof HTMLInputElement && (el.id === "repo-ref" || el.id === "repo-target"))
11480
- openPopover(el);
11481
- });
11482
12264
  popSearch.addEventListener("input", () => buildPopBody(popSearch.value));
11483
12265
  popSearch.addEventListener("keydown", (e2) => {
11484
12266
  if (e2.key === "Escape") {
@@ -11495,24 +12277,8 @@
11495
12277
  return;
11496
12278
  const pickedTarget = popTarget;
11497
12279
  pickedTarget.value = val;
11498
- if (pickedTarget.id === "repo-ref") {
11499
- closePopover();
11500
- pickedTarget.dispatchEvent(new Event("change"));
11501
- return;
11502
- }
11503
- if (pickedTarget.id === "repo-target") {
11504
- closePopover();
11505
- pickedTarget.dispatchEvent(new Event("change"));
11506
- return;
11507
- }
11508
- const targetWasFrom = pickedTarget.id === "ref-from";
11509
- const otherEmpty = !$("#ref-to").value;
11510
12280
  closePopover();
11511
- setRange($("#ref-from").value, $("#ref-to").value);
11512
- if (targetWasFrom && otherEmpty) {
11513
- const ti = $("#ref-to");
11514
- setTimeout(() => ti.focus(), 0);
11515
- }
12281
+ pickedTarget.dispatchEvent(new Event("change"));
11516
12282
  }
11517
12283
  popBody.addEventListener("click", (e2) => {
11518
12284
  const item = e2.target.closest(".rp-item-commit, .rp-item-ref");