@youtyan/code-viewer 0.1.27 → 0.1.29
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/dist/code-viewer.js +214 -34
- package/package.json +1 -1
- package/web/app.js +112 -10
package/dist/code-viewer.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
closeSync,
|
|
6
6
|
constants,
|
|
7
7
|
existsSync as existsSync3,
|
|
8
|
-
lstatSync as
|
|
8
|
+
lstatSync as lstatSync4,
|
|
9
9
|
mkdirSync,
|
|
10
10
|
openSync,
|
|
11
11
|
readFileSync as readFileSync2,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
writeFileSync
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
|
-
import { basename as basename2, dirname as dirname2, extname, join as
|
|
20
|
+
import { basename as basename2, dirname as dirname2, extname, join as join5, relative as relative2 } from "node:path";
|
|
21
21
|
|
|
22
22
|
// web-src/directory-name.ts
|
|
23
23
|
function normalizeNewDirectoryName(name) {
|
|
@@ -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",
|
|
@@ -1240,9 +1242,181 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
|
|
|
1240
1242
|
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1241
1243
|
}
|
|
1242
1244
|
|
|
1245
|
+
// web-src/server/worktree-watcher.ts
|
|
1246
|
+
import {
|
|
1247
|
+
lstatSync as lstatSync3,
|
|
1248
|
+
readdirSync as nodeReaddirSync,
|
|
1249
|
+
watch as nodeWatch
|
|
1250
|
+
} from "node:fs";
|
|
1251
|
+
import { join as join4, relative } from "node:path";
|
|
1252
|
+
function normalizeRelativePath(path) {
|
|
1253
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1254
|
+
}
|
|
1255
|
+
function isInsideRoot(root, path) {
|
|
1256
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
1257
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
1258
|
+
}
|
|
1259
|
+
function startWorktreeUpdateWatch(options) {
|
|
1260
|
+
const watch = options.watch || nodeWatch;
|
|
1261
|
+
const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
|
|
1262
|
+
const isDirectory = options.isDirectory || ((path) => {
|
|
1263
|
+
try {
|
|
1264
|
+
return lstatSync3(path).isDirectory();
|
|
1265
|
+
} catch {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
const directorySignature = options.directorySignature || ((path) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const stats = lstatSync3(path);
|
|
1272
|
+
if (!stats.isDirectory())
|
|
1273
|
+
return null;
|
|
1274
|
+
return `${stats.dev}:${stats.ino}`;
|
|
1275
|
+
} catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
1280
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
1281
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
1282
|
+
const watchers = new Map;
|
|
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;
|
|
1287
|
+
let timer = null;
|
|
1288
|
+
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
1289
|
+
const scheduleUpdate = () => {
|
|
1290
|
+
if (timer)
|
|
1291
|
+
clearTimer(timer);
|
|
1292
|
+
timer = setTimer(() => {
|
|
1293
|
+
timer = null;
|
|
1294
|
+
options.onUpdate();
|
|
1295
|
+
}, debounceMs);
|
|
1296
|
+
};
|
|
1297
|
+
const closeSubtree = (dir) => {
|
|
1298
|
+
for (const [watchedDir, watcher] of [...watchers]) {
|
|
1299
|
+
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
1300
|
+
continue;
|
|
1301
|
+
try {
|
|
1302
|
+
watcher.close?.();
|
|
1303
|
+
} catch {}
|
|
1304
|
+
watchers.delete(watchedDir);
|
|
1305
|
+
signatures.delete(watchedDir);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
const closeAll = () => {
|
|
1309
|
+
if (initialScanTimer) {
|
|
1310
|
+
clearTimer(initialScanTimer);
|
|
1311
|
+
initialScanTimer = null;
|
|
1312
|
+
}
|
|
1313
|
+
initialScanQueue.length = 0;
|
|
1314
|
+
for (const watcher of [...watchers.values()]) {
|
|
1315
|
+
try {
|
|
1316
|
+
watcher.close?.();
|
|
1317
|
+
} catch {}
|
|
1318
|
+
}
|
|
1319
|
+
watchers.clear();
|
|
1320
|
+
signatures.clear();
|
|
1321
|
+
};
|
|
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) => {
|
|
1352
|
+
if (watchers.has(dir))
|
|
1353
|
+
return;
|
|
1354
|
+
const rel = normalizeRelativePath(relative(options.root, dir));
|
|
1355
|
+
if (rel && ignored(rel))
|
|
1356
|
+
return;
|
|
1357
|
+
try {
|
|
1358
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
1359
|
+
if (!filename) {
|
|
1360
|
+
scheduleUpdate();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const changed = normalizeRelativePath(join4(rel, filename.toString()));
|
|
1364
|
+
if (ignored(changed))
|
|
1365
|
+
return;
|
|
1366
|
+
const fullChangedPath = join4(options.root, changed);
|
|
1367
|
+
if (!isInsideRoot(options.root, fullChangedPath))
|
|
1368
|
+
return;
|
|
1369
|
+
const known = watchers.has(fullChangedPath);
|
|
1370
|
+
if (isDirectory(fullChangedPath)) {
|
|
1371
|
+
if (known) {
|
|
1372
|
+
const signature2 = directorySignature(fullChangedPath);
|
|
1373
|
+
if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
|
|
1374
|
+
closeSubtree(fullChangedPath);
|
|
1375
|
+
watchDirectory(fullChangedPath);
|
|
1376
|
+
}
|
|
1377
|
+
scheduleUpdate();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
watchDirectory(fullChangedPath);
|
|
1381
|
+
} else if (known) {
|
|
1382
|
+
closeSubtree(fullChangedPath);
|
|
1383
|
+
}
|
|
1384
|
+
scheduleUpdate();
|
|
1385
|
+
}) || {};
|
|
1386
|
+
watchers.set(dir, watcher);
|
|
1387
|
+
const signature = directorySignature(dir);
|
|
1388
|
+
if (signature)
|
|
1389
|
+
signatures.set(dir, signature);
|
|
1390
|
+
watcher.on?.("error", () => {
|
|
1391
|
+
if (watchers.get(dir) === watcher) {
|
|
1392
|
+
watchers.delete(dir);
|
|
1393
|
+
signatures.delete(dir);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
watcher.on?.("close", () => {
|
|
1397
|
+
if (watchers.get(dir) === watcher) {
|
|
1398
|
+
watchers.delete(dir);
|
|
1399
|
+
signatures.delete(dir);
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
options.onError?.(error);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (initialScanAsync && initialScan) {
|
|
1407
|
+
queueInitialChildren(dir);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
for (const child of readChildDirectories(dir))
|
|
1411
|
+
watchDirectory(child);
|
|
1412
|
+
};
|
|
1413
|
+
watchDirectory(options.root, true);
|
|
1414
|
+
return { started: watchers.size > 0, close: closeAll };
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1243
1417
|
// web-src/server/preview.ts
|
|
1244
|
-
var WEB_ROOT =
|
|
1245
|
-
var VERSION = JSON.parse(readFileSync2(
|
|
1418
|
+
var WEB_ROOT = join5(ROOT, "web");
|
|
1419
|
+
var VERSION = JSON.parse(readFileSync2(join5(ROOT, "package.json"), "utf8")).version;
|
|
1246
1420
|
var DEFAULT_ARGS = ["HEAD"];
|
|
1247
1421
|
var PREVIEW_HUNKS_DEFAULT = 3;
|
|
1248
1422
|
var PREVIEW_LINES_DEFAULT = 1200;
|
|
@@ -1448,7 +1622,7 @@ function staticFile(pathname) {
|
|
|
1448
1622
|
const spec = map[pathname];
|
|
1449
1623
|
if (!spec)
|
|
1450
1624
|
return null;
|
|
1451
|
-
const full =
|
|
1625
|
+
const full = join5(WEB_ROOT, spec[0]);
|
|
1452
1626
|
if (!existsSync3(full))
|
|
1453
1627
|
return text("not found", 404);
|
|
1454
1628
|
return new Response(readFileSync2(full), {
|
|
@@ -1682,7 +1856,7 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
1682
1856
|
return normalizeScopeExcludeNames(names);
|
|
1683
1857
|
}
|
|
1684
1858
|
function loadProjectConfig() {
|
|
1685
|
-
const full =
|
|
1859
|
+
const full = join5(cwd, ".code-viewer.json");
|
|
1686
1860
|
if (!existsSync3(full))
|
|
1687
1861
|
return null;
|
|
1688
1862
|
let realCwd;
|
|
@@ -1747,7 +1921,7 @@ function safeWorktreePath(path) {
|
|
|
1747
1921
|
return null;
|
|
1748
1922
|
if (isGitInternalPath(path))
|
|
1749
1923
|
return null;
|
|
1750
|
-
const full =
|
|
1924
|
+
const full = join5(cwd, path);
|
|
1751
1925
|
if (!existsSync3(full))
|
|
1752
1926
|
return null;
|
|
1753
1927
|
let realCwd;
|
|
@@ -1758,7 +1932,7 @@ function safeWorktreePath(path) {
|
|
|
1758
1932
|
} catch {
|
|
1759
1933
|
return null;
|
|
1760
1934
|
}
|
|
1761
|
-
const rel =
|
|
1935
|
+
const rel = relative2(realCwd, realFull);
|
|
1762
1936
|
if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
|
|
1763
1937
|
return null;
|
|
1764
1938
|
if (isGitInternalPath(rel))
|
|
@@ -1766,7 +1940,7 @@ function safeWorktreePath(path) {
|
|
|
1766
1940
|
return realFull;
|
|
1767
1941
|
}
|
|
1768
1942
|
function worktreePath(path) {
|
|
1769
|
-
return
|
|
1943
|
+
return join5(cwd, path);
|
|
1770
1944
|
}
|
|
1771
1945
|
function safeOpenWorktreePath(path) {
|
|
1772
1946
|
if (path === "") {
|
|
@@ -1963,7 +2137,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
1963
2137
|
continue;
|
|
1964
2138
|
let stat;
|
|
1965
2139
|
try {
|
|
1966
|
-
stat =
|
|
2140
|
+
stat = lstatSync4(full);
|
|
1967
2141
|
} catch {
|
|
1968
2142
|
continue;
|
|
1969
2143
|
}
|
|
@@ -2529,8 +2703,8 @@ async function handleUploadFiles(req) {
|
|
|
2529
2703
|
total += file.size;
|
|
2530
2704
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
2531
2705
|
return text("upload too large", 413);
|
|
2532
|
-
const target =
|
|
2533
|
-
if (
|
|
2706
|
+
const target = join5(realDir, safeName);
|
|
2707
|
+
if (relative2(realDir, dirname2(target)) !== "")
|
|
2534
2708
|
return text("invalid filename", 400);
|
|
2535
2709
|
if (existsSync3(target))
|
|
2536
2710
|
return text("file exists", 409);
|
|
@@ -2557,10 +2731,7 @@ async function handleUploadFiles(req) {
|
|
|
2557
2731
|
return text("file exists", 409);
|
|
2558
2732
|
return text("upload failed", 500);
|
|
2559
2733
|
}
|
|
2560
|
-
|
|
2561
|
-
fileCache.clear();
|
|
2562
|
-
metaCache.clear();
|
|
2563
|
-
sendSse("update");
|
|
2734
|
+
triggerUpdate();
|
|
2564
2735
|
return json({
|
|
2565
2736
|
ok: true,
|
|
2566
2737
|
files: uploads.map((upload) => upload.name),
|
|
@@ -2647,10 +2818,15 @@ function clearMutableCaches() {
|
|
|
2647
2818
|
metaCache.clear();
|
|
2648
2819
|
fileListCache.clear();
|
|
2649
2820
|
}
|
|
2821
|
+
function triggerUpdate() {
|
|
2822
|
+
generation++;
|
|
2823
|
+
clearMutableCaches();
|
|
2824
|
+
sendSse("update");
|
|
2825
|
+
}
|
|
2650
2826
|
function moveMacPathIntoTrash(path) {
|
|
2651
|
-
const trashDir =
|
|
2827
|
+
const trashDir = join5(homedir(), ".Trash");
|
|
2652
2828
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
2653
|
-
const target =
|
|
2829
|
+
const target = join5(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2654
2830
|
try {
|
|
2655
2831
|
mkdirSync(trashDir, { recursive: true });
|
|
2656
2832
|
renameSync(path, target);
|
|
@@ -2660,7 +2836,7 @@ function moveMacPathIntoTrash(path) {
|
|
|
2660
2836
|
}
|
|
2661
2837
|
}
|
|
2662
2838
|
function movePathToTrash(path) {
|
|
2663
|
-
|
|
2839
|
+
lstatSync4(path);
|
|
2664
2840
|
if (process.platform === "darwin") {
|
|
2665
2841
|
return moveMacPathIntoTrash(path);
|
|
2666
2842
|
}
|
|
@@ -2692,8 +2868,8 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
2692
2868
|
if (!existsSync3(trashPath))
|
|
2693
2869
|
return { ok: false, error: "trash item not found" };
|
|
2694
2870
|
try {
|
|
2695
|
-
const trashRoot =
|
|
2696
|
-
const trashRelative =
|
|
2871
|
+
const trashRoot = join5(homedir(), ".Trash");
|
|
2872
|
+
const trashRelative = relative2(trashRoot, trashPath);
|
|
2697
2873
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2698
2874
|
return { ok: false, error: "invalid trash handle" };
|
|
2699
2875
|
mkdirSync(dirname2(original), { recursive: true });
|
|
@@ -2799,9 +2975,7 @@ async function handleTrashPath(req) {
|
|
|
2799
2975
|
trashPath: moved.trashPath
|
|
2800
2976
|
}
|
|
2801
2977
|
};
|
|
2802
|
-
|
|
2803
|
-
clearMutableCaches();
|
|
2804
|
-
sendSse("update");
|
|
2978
|
+
triggerUpdate();
|
|
2805
2979
|
return json({ ok: true, generation, undo });
|
|
2806
2980
|
}
|
|
2807
2981
|
async function handleCreateDirectory(req) {
|
|
@@ -2844,7 +3018,7 @@ async function handleCreateDirectory(req) {
|
|
|
2844
3018
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
2845
3019
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
2846
3020
|
return text("invalid target", 400);
|
|
2847
|
-
const target =
|
|
3021
|
+
const target = join5(parent, name);
|
|
2848
3022
|
if (existsSync3(target))
|
|
2849
3023
|
return text("already exists", 409);
|
|
2850
3024
|
try {
|
|
@@ -2854,9 +3028,7 @@ async function handleCreateDirectory(req) {
|
|
|
2854
3028
|
return text("already exists", 409);
|
|
2855
3029
|
return text("create failed", 500);
|
|
2856
3030
|
}
|
|
2857
|
-
|
|
2858
|
-
clearMutableCaches();
|
|
2859
|
-
sendSse("update");
|
|
3031
|
+
triggerUpdate();
|
|
2860
3032
|
return json({ ok: true, path: targetPath, generation });
|
|
2861
3033
|
}
|
|
2862
3034
|
async function handleRestoreTrash(req) {
|
|
@@ -2888,9 +3060,7 @@ async function handleRestoreTrash(req) {
|
|
|
2888
3060
|
const restored = restoreTrashPath(originalPath, trashPath || undefined);
|
|
2889
3061
|
if (!restored.ok)
|
|
2890
3062
|
return text(restored.error || "undo failed", 409);
|
|
2891
|
-
|
|
2892
|
-
clearMutableCaches();
|
|
2893
|
-
sendSse("update");
|
|
3063
|
+
triggerUpdate();
|
|
2894
3064
|
return json({ ok: true, generation });
|
|
2895
3065
|
}
|
|
2896
3066
|
function sendSse(event, data = "tick") {
|
|
@@ -2954,9 +3124,7 @@ var server = await startServer({
|
|
|
2954
3124
|
if (url.pathname === "/refresh" && req.method === "POST") {
|
|
2955
3125
|
if (!sideEffectRequestAllowed(req))
|
|
2956
3126
|
return text("forbidden", 403);
|
|
2957
|
-
|
|
2958
|
-
clearMutableCaches();
|
|
2959
|
-
sendSse("update");
|
|
3127
|
+
triggerUpdate();
|
|
2960
3128
|
return json({ ok: true, generation });
|
|
2961
3129
|
}
|
|
2962
3130
|
if (url.pathname === "/events") {
|
|
@@ -3007,5 +3175,17 @@ startDevAssetReload({
|
|
|
3007
3175
|
watch,
|
|
3008
3176
|
sendReload: () => sendSse("reload")
|
|
3009
3177
|
});
|
|
3178
|
+
startWorktreeUpdateWatch({
|
|
3179
|
+
root: cwd,
|
|
3180
|
+
omitDirNames: scopeOmitDirNames,
|
|
3181
|
+
excludeNames: scopeExcludeNames,
|
|
3182
|
+
watch,
|
|
3183
|
+
initialScanMode: "async",
|
|
3184
|
+
onUpdate: triggerUpdate,
|
|
3185
|
+
onError: (error) => {
|
|
3186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3187
|
+
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3010
3190
|
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
3011
3191
|
console.log(`git-diff-preview serving ${cwd}`);
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -7002,7 +7002,9 @@ ${frontmatter.yaml}
|
|
|
7002
7002
|
let SIDEBAR_VISIBLE_ROWS = [];
|
|
7003
7003
|
let SIDEBAR_ROW_BY_PATH = new Map;
|
|
7004
7004
|
let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
|
|
7005
|
-
|
|
7005
|
+
let SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
|
|
7006
|
+
const SIDEBAR_LAZY_LOADED_DIRS = new Set;
|
|
7007
|
+
const SIDEBAR_LAZY_LOADING_DIRS = new Map;
|
|
7006
7008
|
let REPO_SORT = {
|
|
7007
7009
|
key: "name",
|
|
7008
7010
|
direction: "asc"
|
|
@@ -8058,6 +8060,86 @@ ${frontmatter.yaml}
|
|
|
8058
8060
|
SIDEBAR_TREE_ITEMS_CACHE.set(node, items);
|
|
8059
8061
|
return items;
|
|
8060
8062
|
}
|
|
8063
|
+
function sidebarTreeNodeHasChildren(node) {
|
|
8064
|
+
return Object.keys(node.dirs).length > 0 || node.files.length > 0;
|
|
8065
|
+
}
|
|
8066
|
+
function shouldLazyLoadSidebarDir(dir) {
|
|
8067
|
+
return isRepositorySidebarMode() && isVirtualSidebarActive() && !dir.children_omitted && !sidebarTreeNodeHasChildren(dir) && !SIDEBAR_LAZY_LOADED_DIRS.has(dir.path);
|
|
8068
|
+
}
|
|
8069
|
+
function upsertSidebarTreeEntry(entry, order) {
|
|
8070
|
+
if (!SIDEBAR_TREE_ROOT)
|
|
8071
|
+
return;
|
|
8072
|
+
const parts = entry.path.split("/").filter(Boolean);
|
|
8073
|
+
if (!parts.length)
|
|
8074
|
+
return;
|
|
8075
|
+
let node = SIDEBAR_TREE_ROOT;
|
|
8076
|
+
let acc = "";
|
|
8077
|
+
const dirPartCount = entry.type === "tree" ? parts.length : parts.length - 1;
|
|
8078
|
+
for (let i2 = 0;i2 < dirPartCount; i2++) {
|
|
8079
|
+
const part = parts[i2];
|
|
8080
|
+
acc = acc ? `${acc}/${part}` : part;
|
|
8081
|
+
if (!node.dirs[part]) {
|
|
8082
|
+
node.dirs[part] = {
|
|
8083
|
+
name: part,
|
|
8084
|
+
dirs: {},
|
|
8085
|
+
files: [],
|
|
8086
|
+
path: acc,
|
|
8087
|
+
minOrder: order
|
|
8088
|
+
};
|
|
8089
|
+
}
|
|
8090
|
+
node = node.dirs[part];
|
|
8091
|
+
node.minOrder = Math.min(node.minOrder, order);
|
|
8092
|
+
}
|
|
8093
|
+
if (entry.type === "tree") {
|
|
8094
|
+
node.explicit = true;
|
|
8095
|
+
if (entry.children_omitted === true) {
|
|
8096
|
+
node.children_omitted = true;
|
|
8097
|
+
node.children_omitted_reason = entry.children_omitted_reason;
|
|
8098
|
+
}
|
|
8099
|
+
return;
|
|
8100
|
+
}
|
|
8101
|
+
if (!node.files.some((file) => file.path === entry.path))
|
|
8102
|
+
node.files.push({ ...entry, order });
|
|
8103
|
+
}
|
|
8104
|
+
function mergeSidebarTreeEntries(entries) {
|
|
8105
|
+
entries.forEach((entry, index) => {
|
|
8106
|
+
upsertSidebarTreeEntry(entry, entry.order ?? index + 1);
|
|
8107
|
+
});
|
|
8108
|
+
SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
|
|
8109
|
+
if (SIDEBAR_TREE_ROOT)
|
|
8110
|
+
buildSidebarTreeRows(SIDEBAR_TREE_ROOT);
|
|
8111
|
+
}
|
|
8112
|
+
function ensureVirtualSidebarDirLoaded(dir) {
|
|
8113
|
+
if (!shouldLazyLoadSidebarDir(dir))
|
|
8114
|
+
return Promise.resolve();
|
|
8115
|
+
const existing = SIDEBAR_LAZY_LOADING_DIRS.get(dir.path);
|
|
8116
|
+
if (existing)
|
|
8117
|
+
return existing;
|
|
8118
|
+
const params = new URLSearchParams;
|
|
8119
|
+
params.set("ref", REPO_SIDEBAR_REF || "worktree");
|
|
8120
|
+
params.set("path", dir.path);
|
|
8121
|
+
appendScopeParams(params);
|
|
8122
|
+
const load2 = trackLoad(fetch(`/_tree?${params.toString()}`).then((response) => {
|
|
8123
|
+
if (!response.ok)
|
|
8124
|
+
throw new Error("failed to load repository tree");
|
|
8125
|
+
return response.json();
|
|
8126
|
+
})).then((meta) => {
|
|
8127
|
+
const entries = meta.entries.map((entry, index) => ({
|
|
8128
|
+
order: dir.minOrder + (index + 1) / 1e5,
|
|
8129
|
+
path: entry.path,
|
|
8130
|
+
display_path: entry.path,
|
|
8131
|
+
type: entry.type,
|
|
8132
|
+
children_omitted: entry.children_omitted,
|
|
8133
|
+
children_omitted_reason: entry.children_omitted_reason
|
|
8134
|
+
}));
|
|
8135
|
+
mergeSidebarTreeEntries(entries);
|
|
8136
|
+
SIDEBAR_LAZY_LOADED_DIRS.add(dir.path);
|
|
8137
|
+
}).finally(() => {
|
|
8138
|
+
SIDEBAR_LAZY_LOADING_DIRS.delete(dir.path);
|
|
8139
|
+
});
|
|
8140
|
+
SIDEBAR_LAZY_LOADING_DIRS.set(dir.path, load2);
|
|
8141
|
+
return load2;
|
|
8142
|
+
}
|
|
8061
8143
|
function createTreeDirRow(dir, depth, onFileClick) {
|
|
8062
8144
|
const li = document.createElement("li");
|
|
8063
8145
|
li.className = "tree-dir";
|
|
@@ -8105,16 +8187,26 @@ ${frontmatter.yaml}
|
|
|
8105
8187
|
const updateIcon = () => {
|
|
8106
8188
|
setFolderIcon(dirIcon, li.classList.contains("collapsed"));
|
|
8107
8189
|
};
|
|
8108
|
-
const toggleDir = (e2) => {
|
|
8190
|
+
const toggleDir = async (e2) => {
|
|
8109
8191
|
e2.stopPropagation();
|
|
8110
|
-
li.
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8192
|
+
if (li.dataset.toggling === "true")
|
|
8193
|
+
return;
|
|
8194
|
+
const expanding = li.classList.contains("collapsed");
|
|
8195
|
+
li.dataset.toggling = "true";
|
|
8196
|
+
try {
|
|
8197
|
+
if (expanding)
|
|
8198
|
+
await ensureVirtualSidebarDirLoaded(dir);
|
|
8199
|
+
li.classList.toggle("collapsed");
|
|
8200
|
+
updateIcon();
|
|
8201
|
+
if (li.classList.contains("collapsed"))
|
|
8202
|
+
STATE.collapsedDirs.add(dir.path);
|
|
8203
|
+
else
|
|
8204
|
+
STATE.collapsedDirs.delete(dir.path);
|
|
8205
|
+
localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
|
|
8206
|
+
rerenderVirtualSidebar();
|
|
8207
|
+
} finally {
|
|
8208
|
+
delete li.dataset.toggling;
|
|
8209
|
+
}
|
|
8118
8210
|
};
|
|
8119
8211
|
li.classList.toggle("collapsed", STATE.collapsedDirs.has(dir.path));
|
|
8120
8212
|
updateIcon();
|
|
@@ -8382,6 +8474,8 @@ ${frontmatter.yaml}
|
|
|
8382
8474
|
SIDEBAR_TREE_ROWS = [];
|
|
8383
8475
|
SIDEBAR_VISIBLE_ROWS = [];
|
|
8384
8476
|
SIDEBAR_ROW_BY_PATH = new Map;
|
|
8477
|
+
SIDEBAR_LAZY_LOADED_DIRS.clear();
|
|
8478
|
+
SIDEBAR_LAZY_LOADING_DIRS.clear();
|
|
8385
8479
|
STATE.files = files;
|
|
8386
8480
|
SIDEBAR_FILES = files;
|
|
8387
8481
|
SIDEBAR_ON_FILE_CLICK = onFileClick;
|
|
@@ -9851,6 +9945,14 @@ ${frontmatter.yaml}
|
|
|
9851
9945
|
function activateRepoSidebarPath(currentPath) {
|
|
9852
9946
|
markActive(currentPath, { reveal: true });
|
|
9853
9947
|
applyFilter();
|
|
9948
|
+
const row = SIDEBAR_ROW_BY_PATH.get(currentPath);
|
|
9949
|
+
if (row?.kind === "dir" && row.dir && shouldLazyLoadSidebarDir(row.dir))
|
|
9950
|
+
ensureVirtualSidebarDirLoaded(row.dir).then(() => {
|
|
9951
|
+
if (SIDEBAR_VIRTUAL_ACTIVE_PATH === currentPath) {
|
|
9952
|
+
rerenderVirtualSidebar();
|
|
9953
|
+
scrollVirtualSidebarPathIntoView(currentPath);
|
|
9954
|
+
}
|
|
9955
|
+
});
|
|
9854
9956
|
}
|
|
9855
9957
|
function createPlaceholder(f2) {
|
|
9856
9958
|
const card = document.createElement("div");
|