@youtyan/code-viewer 0.1.28 → 0.1.30

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.
@@ -280,6 +280,8 @@ var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
280
280
  "out",
281
281
  "target",
282
282
  ".gradle",
283
+ ".pnpm-store",
284
+ ".turbo",
283
285
  "__pycache__",
284
286
  ".pytest_cache",
285
287
  ".tox",
@@ -1279,6 +1281,9 @@ function startWorktreeUpdateWatch(options) {
1279
1281
  const debounceMs = options.debounceMs ?? 250;
1280
1282
  const watchers = new Map;
1281
1283
  const signatures = new Map;
1284
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
1285
+ const initialScanQueue = [];
1286
+ let initialScanTimer = null;
1282
1287
  let timer = null;
1283
1288
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
1284
1289
  const scheduleUpdate = () => {
@@ -1301,6 +1306,11 @@ function startWorktreeUpdateWatch(options) {
1301
1306
  }
1302
1307
  };
1303
1308
  const closeAll = () => {
1309
+ if (initialScanTimer) {
1310
+ clearTimer(initialScanTimer);
1311
+ initialScanTimer = null;
1312
+ }
1313
+ initialScanQueue.length = 0;
1304
1314
  for (const watcher of [...watchers.values()]) {
1305
1315
  try {
1306
1316
  watcher.close?.();
@@ -1309,7 +1319,36 @@ function startWorktreeUpdateWatch(options) {
1309
1319
  watchers.clear();
1310
1320
  signatures.clear();
1311
1321
  };
1312
- const watchDirectory = (dir) => {
1322
+ const readChildDirectories = (dir) => {
1323
+ let entries;
1324
+ try {
1325
+ entries = readDirs(dir);
1326
+ } catch (error) {
1327
+ options.onError?.(error);
1328
+ return [];
1329
+ }
1330
+ const children = [];
1331
+ for (const entry of entries) {
1332
+ if (!entry.isDirectory())
1333
+ continue;
1334
+ children.push(join4(dir, entry.name));
1335
+ }
1336
+ return children;
1337
+ };
1338
+ const processInitialScanQueue = () => {
1339
+ initialScanTimer = null;
1340
+ const next = initialScanQueue.shift();
1341
+ if (next)
1342
+ watchDirectory(next, true);
1343
+ if (initialScanQueue.length)
1344
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
1345
+ };
1346
+ const queueInitialChildren = (dir) => {
1347
+ initialScanQueue.push(...readChildDirectories(dir));
1348
+ if (!initialScanTimer)
1349
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
1350
+ };
1351
+ const watchDirectory = (dir, initialScan = false) => {
1313
1352
  if (watchers.has(dir))
1314
1353
  return;
1315
1354
  const rel = normalizeRelativePath(relative(options.root, dir));
@@ -1364,20 +1403,14 @@ function startWorktreeUpdateWatch(options) {
1364
1403
  options.onError?.(error);
1365
1404
  return;
1366
1405
  }
1367
- let entries;
1368
- try {
1369
- entries = readDirs(dir);
1370
- } catch (error) {
1371
- options.onError?.(error);
1406
+ if (initialScanAsync && initialScan) {
1407
+ queueInitialChildren(dir);
1372
1408
  return;
1373
1409
  }
1374
- for (const entry of entries) {
1375
- if (!entry.isDirectory())
1376
- continue;
1377
- watchDirectory(join4(dir, entry.name));
1378
- }
1410
+ for (const child of readChildDirectories(dir))
1411
+ watchDirectory(child);
1379
1412
  };
1380
- watchDirectory(options.root);
1413
+ watchDirectory(options.root, true);
1381
1414
  return { started: watchers.size > 0, close: closeAll };
1382
1415
  }
1383
1416
 
@@ -3147,6 +3180,7 @@ startWorktreeUpdateWatch({
3147
3180
  omitDirNames: scopeOmitDirNames,
3148
3181
  excludeNames: scopeExcludeNames,
3149
3182
  watch,
3183
+ initialScanMode: "async",
3150
3184
  onUpdate: triggerUpdate,
3151
3185
  onError: (error) => {
3152
3186
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -90,6 +90,12 @@
90
90
  function trailingClickRange(hunkEndNew, step) {
91
91
  return { start: hunkEndNew, end: hunkEndNew + step - 1 };
92
92
  }
93
+ function trailingExpandTargetIndex(hunkCount) {
94
+ return hunkCount > 0 ? hunkCount - 1 : null;
95
+ }
96
+ function shouldAttachTrailingExpand(probeLineCount) {
97
+ return probeLineCount > 0;
98
+ }
93
99
  function applyTrailingResult(state, receivedCount, step) {
94
100
  return {
95
101
  newStart: state.newStart + receivedCount,
@@ -106,6 +112,8 @@
106
112
  applyUp,
107
113
  applyDown,
108
114
  mapNewToOld,
115
+ trailingExpandTargetIndex,
116
+ shouldAttachTrailingExpand,
109
117
  trailingClickRange,
110
118
  applyTrailingResult
111
119
  };
@@ -7002,7 +7010,9 @@ ${frontmatter.yaml}
7002
7010
  let SIDEBAR_VISIBLE_ROWS = [];
7003
7011
  let SIDEBAR_ROW_BY_PATH = new Map;
7004
7012
  let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
7005
- const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
7013
+ let SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
7014
+ const SIDEBAR_LAZY_LOADED_DIRS = new Set;
7015
+ const SIDEBAR_LAZY_LOADING_DIRS = new Map;
7006
7016
  let REPO_SORT = {
7007
7017
  key: "name",
7008
7018
  direction: "asc"
@@ -8058,6 +8068,86 @@ ${frontmatter.yaml}
8058
8068
  SIDEBAR_TREE_ITEMS_CACHE.set(node, items);
8059
8069
  return items;
8060
8070
  }
8071
+ function sidebarTreeNodeHasChildren(node) {
8072
+ return Object.keys(node.dirs).length > 0 || node.files.length > 0;
8073
+ }
8074
+ function shouldLazyLoadSidebarDir(dir) {
8075
+ return isRepositorySidebarMode() && isVirtualSidebarActive() && !dir.children_omitted && !sidebarTreeNodeHasChildren(dir) && !SIDEBAR_LAZY_LOADED_DIRS.has(dir.path);
8076
+ }
8077
+ function upsertSidebarTreeEntry(entry, order) {
8078
+ if (!SIDEBAR_TREE_ROOT)
8079
+ return;
8080
+ const parts = entry.path.split("/").filter(Boolean);
8081
+ if (!parts.length)
8082
+ return;
8083
+ let node = SIDEBAR_TREE_ROOT;
8084
+ let acc = "";
8085
+ const dirPartCount = entry.type === "tree" ? parts.length : parts.length - 1;
8086
+ for (let i2 = 0;i2 < dirPartCount; i2++) {
8087
+ const part = parts[i2];
8088
+ acc = acc ? `${acc}/${part}` : part;
8089
+ if (!node.dirs[part]) {
8090
+ node.dirs[part] = {
8091
+ name: part,
8092
+ dirs: {},
8093
+ files: [],
8094
+ path: acc,
8095
+ minOrder: order
8096
+ };
8097
+ }
8098
+ node = node.dirs[part];
8099
+ node.minOrder = Math.min(node.minOrder, order);
8100
+ }
8101
+ if (entry.type === "tree") {
8102
+ node.explicit = true;
8103
+ if (entry.children_omitted === true) {
8104
+ node.children_omitted = true;
8105
+ node.children_omitted_reason = entry.children_omitted_reason;
8106
+ }
8107
+ return;
8108
+ }
8109
+ if (!node.files.some((file) => file.path === entry.path))
8110
+ node.files.push({ ...entry, order });
8111
+ }
8112
+ function mergeSidebarTreeEntries(entries) {
8113
+ entries.forEach((entry, index) => {
8114
+ upsertSidebarTreeEntry(entry, entry.order ?? index + 1);
8115
+ });
8116
+ SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
8117
+ if (SIDEBAR_TREE_ROOT)
8118
+ buildSidebarTreeRows(SIDEBAR_TREE_ROOT);
8119
+ }
8120
+ function ensureVirtualSidebarDirLoaded(dir) {
8121
+ if (!shouldLazyLoadSidebarDir(dir))
8122
+ return Promise.resolve();
8123
+ const existing = SIDEBAR_LAZY_LOADING_DIRS.get(dir.path);
8124
+ if (existing)
8125
+ return existing;
8126
+ const params = new URLSearchParams;
8127
+ params.set("ref", REPO_SIDEBAR_REF || "worktree");
8128
+ params.set("path", dir.path);
8129
+ appendScopeParams(params);
8130
+ const load2 = trackLoad(fetch(`/_tree?${params.toString()}`).then((response) => {
8131
+ if (!response.ok)
8132
+ throw new Error("failed to load repository tree");
8133
+ return response.json();
8134
+ })).then((meta) => {
8135
+ const entries = meta.entries.map((entry, index) => ({
8136
+ order: dir.minOrder + (index + 1) / 1e5,
8137
+ path: entry.path,
8138
+ display_path: entry.path,
8139
+ type: entry.type,
8140
+ children_omitted: entry.children_omitted,
8141
+ children_omitted_reason: entry.children_omitted_reason
8142
+ }));
8143
+ mergeSidebarTreeEntries(entries);
8144
+ SIDEBAR_LAZY_LOADED_DIRS.add(dir.path);
8145
+ }).finally(() => {
8146
+ SIDEBAR_LAZY_LOADING_DIRS.delete(dir.path);
8147
+ });
8148
+ SIDEBAR_LAZY_LOADING_DIRS.set(dir.path, load2);
8149
+ return load2;
8150
+ }
8061
8151
  function createTreeDirRow(dir, depth, onFileClick) {
8062
8152
  const li = document.createElement("li");
8063
8153
  li.className = "tree-dir";
@@ -8105,16 +8195,26 @@ ${frontmatter.yaml}
8105
8195
  const updateIcon = () => {
8106
8196
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
8107
8197
  };
8108
- const toggleDir = (e2) => {
8198
+ const toggleDir = async (e2) => {
8109
8199
  e2.stopPropagation();
8110
- li.classList.toggle("collapsed");
8111
- updateIcon();
8112
- if (li.classList.contains("collapsed"))
8113
- STATE.collapsedDirs.add(dir.path);
8114
- else
8115
- STATE.collapsedDirs.delete(dir.path);
8116
- localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8117
- rerenderVirtualSidebar();
8200
+ if (li.dataset.toggling === "true")
8201
+ return;
8202
+ const expanding = li.classList.contains("collapsed");
8203
+ li.dataset.toggling = "true";
8204
+ try {
8205
+ if (expanding)
8206
+ await ensureVirtualSidebarDirLoaded(dir);
8207
+ li.classList.toggle("collapsed");
8208
+ updateIcon();
8209
+ if (li.classList.contains("collapsed"))
8210
+ STATE.collapsedDirs.add(dir.path);
8211
+ else
8212
+ STATE.collapsedDirs.delete(dir.path);
8213
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8214
+ rerenderVirtualSidebar();
8215
+ } finally {
8216
+ delete li.dataset.toggling;
8217
+ }
8118
8218
  };
8119
8219
  li.classList.toggle("collapsed", STATE.collapsedDirs.has(dir.path));
8120
8220
  updateIcon();
@@ -8382,6 +8482,8 @@ ${frontmatter.yaml}
8382
8482
  SIDEBAR_TREE_ROWS = [];
8383
8483
  SIDEBAR_VISIBLE_ROWS = [];
8384
8484
  SIDEBAR_ROW_BY_PATH = new Map;
8485
+ SIDEBAR_LAZY_LOADED_DIRS.clear();
8486
+ SIDEBAR_LAZY_LOADING_DIRS.clear();
8385
8487
  STATE.files = files;
8386
8488
  SIDEBAR_FILES = files;
8387
8489
  SIDEBAR_ON_FILE_CLICK = onFileClick;
@@ -9851,6 +9953,14 @@ ${frontmatter.yaml}
9851
9953
  function activateRepoSidebarPath(currentPath) {
9852
9954
  markActive(currentPath, { reveal: true });
9853
9955
  applyFilter();
9956
+ const row = SIDEBAR_ROW_BY_PATH.get(currentPath);
9957
+ if (row?.kind === "dir" && row.dir && shouldLazyLoadSidebarDir(row.dir))
9958
+ ensureVirtualSidebarDirLoaded(row.dir).then(() => {
9959
+ if (SIDEBAR_VIRTUAL_ACTIVE_PATH === currentPath) {
9960
+ rerenderVirtualSidebar();
9961
+ scrollVirtualSidebarPathIntoView(currentPath);
9962
+ }
9963
+ });
9854
9964
  }
9855
9965
  function createPlaceholder(f2) {
9856
9966
  const card = document.createElement("div");
@@ -10208,6 +10318,10 @@ ${frontmatter.yaml}
10208
10318
  for (const item of infoRows) {
10209
10319
  attachExpandControls(item, file, ref, refPath);
10210
10320
  }
10321
+ const trailingIndex = window.GdpExpandLogic.trailingExpandTargetIndex(infoRows.length);
10322
+ if (trailingIndex != null) {
10323
+ probeAndAttachTrailingExpandControls(infoRows[trailingIndex], file, ref, refPath);
10324
+ }
10211
10325
  }
10212
10326
  function attachExpandControls(item, file, ref, refPath) {
10213
10327
  const { hunk, prevHunkEndNew, prevHunkEndOld } = item;
@@ -10349,7 +10463,10 @@ ${frontmatter.yaml}
10349
10463
  requestAnimationFrame(syncHeight);
10350
10464
  setTimeout(syncHeight, 100);
10351
10465
  }
10352
- function _attachTrailingExpandControls(item, file, ref, refPath) {
10466
+ function attachTrailingExpandControls(item, file, ref, refPath) {
10467
+ const hasTrailingRow = (item.siblings || []).some((sib) => !!sib.tr.parentElement?.querySelector(".gdp-trailing-expand-row"));
10468
+ if (hasTrailingRow)
10469
+ return;
10353
10470
  const STEP = 20;
10354
10471
  let nextNewStart = nextNewLine(item.hunk);
10355
10472
  let nextOldStart = nextOldLine(item.hunk);
@@ -10383,9 +10500,16 @@ ${frontmatter.yaml}
10383
10500
  };
10384
10501
  const fetchAndInsert = () => {
10385
10502
  const range = window.GdpExpandLogic.trailingClickRange(nextNewStart, STEP);
10503
+ const myGen = SERVER_GENERATION;
10386
10504
  setBusy(true);
10387
10505
  const url = "/file_range?path=" + refPath + "&ref=" + encodeURIComponent(ref) + "&start=" + range.start + "&end=" + range.end;
10388
10506
  trackLoad(fetch(url).then((r2) => r2.json())).then((data) => {
10507
+ if (myGen !== SERVER_GENERATION || data.generation && data.generation !== SERVER_GENERATION) {
10508
+ setBusy(false);
10509
+ return;
10510
+ }
10511
+ if (!item.tr.isConnected)
10512
+ return;
10389
10513
  const lines = data?.lines || [];
10390
10514
  if (!lines.length) {
10391
10515
  rows.forEach((row) => {
@@ -10424,6 +10548,25 @@ ${frontmatter.yaml}
10424
10548
  });
10425
10549
  syncExpandRowHeights(rows.map((row) => row.tr), rows[0].tr);
10426
10550
  }
10551
+ function probeAndAttachTrailingExpandControls(item, file, ref, refPath) {
10552
+ const start = nextNewLine(item.hunk);
10553
+ const myGen = SERVER_GENERATION;
10554
+ const url = "/file_range?path=" + refPath + "&ref=" + encodeURIComponent(ref) + "&start=" + start + "&end=" + start;
10555
+ trackLoad(fetch(url).then((r2) => r2.json())).then((data) => {
10556
+ if (myGen !== SERVER_GENERATION)
10557
+ return;
10558
+ if (data.generation && data.generation !== SERVER_GENERATION)
10559
+ return;
10560
+ if (!item.tr.isConnected)
10561
+ return;
10562
+ const hasTrailingRow = (item.siblings || []).some((sib) => !!sib.tr.parentElement?.querySelector(".gdp-trailing-expand-row"));
10563
+ if (hasTrailingRow)
10564
+ return;
10565
+ if (!window.GdpExpandLogic.shouldAttachTrailingExpand(data?.lines?.length || 0))
10566
+ return;
10567
+ attachTrailingExpandControls(item, file, ref, refPath);
10568
+ }).catch(() => {});
10569
+ }
10427
10570
  function insertContextRows(targetTr, lines, newStart, oldStart, dir, sideIndex) {
10428
10571
  const tbody = targetTr.parentElement;
10429
10572
  if (!tbody)