@youtyan/code-viewer 0.1.15 → 0.1.17

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 },
@@ -6772,6 +6784,7 @@
6772
6784
  const DEFAULT_RANGE = { from: "HEAD", to: "worktree" };
6773
6785
  const VIRTUAL_SOURCE_LINE_THRESHOLD = 3000;
6774
6786
  const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
6787
+ const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
6775
6788
  const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
6776
6789
  const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
6777
6790
  let highlightLoadPromise = null;
@@ -6912,6 +6925,8 @@
6912
6925
  const before = scroller.scrollTop;
6913
6926
  if (edge === "center")
6914
6927
  scroller.scrollTop = Math.max(0, top - Math.round(scroller.clientHeight / 2));
6928
+ else if (edge === "start")
6929
+ scroller.scrollTop = top;
6915
6930
  else if (top < scroller.scrollTop)
6916
6931
  scroller.scrollTop = top;
6917
6932
  else if (bottom > scroller.scrollTop + scroller.clientHeight)
@@ -6944,7 +6959,7 @@
6944
6959
  const delta = unit === "page" ? pageRows : 1;
6945
6960
  cursor.line = Math.max(1, Math.min(total, cursor.line + direction * delta));
6946
6961
  syncSourceCursorRows(target);
6947
- scrollSourceCursorIntoView(cursor);
6962
+ scrollSourceCursorIntoView(cursor, unit === "page" ? "start" : "nearest");
6948
6963
  return true;
6949
6964
  }
6950
6965
  function scrollMainPanel(direction, repeated = false, unit = "line") {
@@ -6959,6 +6974,31 @@
6959
6974
  else
6960
6975
  window.scrollBy({ top, behavior });
6961
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
+ }
6962
7002
  function scrollMainToEdge(edge) {
6963
7003
  if (moveSourceCursor(edge === "bottom" ? 1 : -1, "edge", edge))
6964
7004
  return;
@@ -7586,7 +7626,7 @@
7586
7626
  children_omitted: dir.children_omitted,
7587
7627
  children_omitted_reason: dir.children_omitted_reason
7588
7628
  });
7589
- focusSidebarPanel();
7629
+ scheduleMainSurfaceFocus();
7590
7630
  });
7591
7631
  } else {
7592
7632
  li.addEventListener("click", toggleDir);
@@ -7622,7 +7662,7 @@
7622
7662
  onFileClick(f2);
7623
7663
  else
7624
7664
  scrollToFile(f2.path);
7625
- focusSidebarPanel();
7665
+ scheduleMainSurfaceFocus();
7626
7666
  });
7627
7667
  if (!onFileClick)
7628
7668
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -7655,7 +7695,7 @@
7655
7695
  onFileClick(f2);
7656
7696
  else
7657
7697
  scrollToFile(f2.path);
7658
- focusSidebarPanel();
7698
+ scheduleMainSurfaceFocus();
7659
7699
  });
7660
7700
  if (!onFileClick)
7661
7701
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), { passive: true });
@@ -9875,6 +9915,9 @@
9875
9915
  url.searchParams.delete("virtual");
9876
9916
  return url.pathname + url.search;
9877
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
+ }
9878
9921
  function currentSourceLineTarget(target) {
9879
9922
  const routeTarget = sourceTargetFromRoute();
9880
9923
  return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : undefined;
@@ -9938,6 +9981,161 @@
9938
9981
  document.addEventListener("mouseup", () => {
9939
9982
  SOURCE_LINE_DRAG = null;
9940
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
+ }
9941
10139
  function renderVirtualSource(target, textValue, lines, hljsRef, lang) {
9942
10140
  const wrap = document.createElement("div");
9943
10141
  wrap.className = "gdp-source-virtual";
@@ -9986,6 +10184,9 @@
9986
10184
  info.append(badge, summary, actions);
9987
10185
  const scroller = document.createElement("div");
9988
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");
9989
10190
  const spacer = document.createElement("div");
9990
10191
  spacer.className = "gdp-source-virtual-spacer";
9991
10192
  spacer.style.height = Math.max(1, lines.length * VIRTUAL_SOURCE_ROW_HEIGHT) + "px";
@@ -9997,6 +10198,7 @@
9997
10198
  let raf = 0;
9998
10199
  let renderedStart = -1;
9999
10200
  let renderedEnd = -1;
10201
+ let search = null;
10000
10202
  const render = () => {
10001
10203
  raf = 0;
10002
10204
  const viewportHeight = scroller.clientHeight || window.innerHeight;
@@ -10023,7 +10225,9 @@
10023
10225
  const code2 = document.createElement("span");
10024
10226
  code2.className = "gdp-source-virtual-line-code";
10025
10227
  const line = lines[index] ?? "";
10026
- 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))) {
10027
10231
  try {
10028
10232
  code2.innerHTML = hljsRef.highlight(line, { language: lang, ignoreIllegals: true }).value;
10029
10233
  code2.classList.add("hljs");
@@ -10044,6 +10248,8 @@
10044
10248
  };
10045
10249
  scroller.__gdpRenderVirtualSource = render;
10046
10250
  scroller.addEventListener("scroll", schedule, { passive: true });
10251
+ search = createVirtualSourceSearch(wrap, scroller, (query) => Promise.resolve(collectVirtualSourceSearchMatches(lines, query)), render);
10252
+ wrap.__gdpVirtualSourceSearch = search;
10047
10253
  let resizeObserver = null;
10048
10254
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
10049
10255
  if (!scroller.isConnected) {
@@ -10058,6 +10264,253 @@
10058
10264
  schedule();
10059
10265
  return wrap;
10060
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
+ }
10061
10514
  function renderSourceMedia(card, target, mediaKind) {
10062
10515
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
10063
10516
  const isStandalone = card.classList.contains("gdp-standalone-source");
@@ -10259,6 +10712,19 @@
10259
10712
  return;
10260
10713
  }
10261
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
+ }
10262
10728
  const response = await trackLoad(fetch(buildRawFileUrl(target), { signal: controller.signal }));
10263
10729
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
10264
10730
  return;
@@ -10296,6 +10762,7 @@
10296
10762
  if (virtualScroller) {
10297
10763
  const centeredOffset = virtualScroller.clientHeight / 2 - VIRTUAL_SOURCE_ROW_HEIGHT / 2;
10298
10764
  virtualScroller.scrollTop = Math.max(0, (line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - Math.max(0, centeredOffset));
10765
+ virtualScroller.__gdpRenderVirtualSource?.();
10299
10766
  return;
10300
10767
  }
10301
10768
  const row = card.querySelector('.gdp-source-table tr[data-line="' + String(line) + '"]');
@@ -11541,8 +12008,50 @@
11541
12008
  }
11542
12009
  return false;
11543
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 });
11544
12045
  document.addEventListener("keydown", (e2) => {
12046
+ if (e2.__gdpVirtualSourcePagingHandled)
12047
+ return;
11545
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
+ }
11546
12055
  const scope = keymapScope(targetEl);
11547
12056
  const action = resolveKeymapAction(e2, {
11548
12057
  scope,