mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(files): mark folders the scan limit stopped short of
The fair rounds give a directory its node before reading it, so every folder still queued when the 5,000-node budget ends sat in the tree with empty or partial children and read as an empty folder. Keep the node on the tree cursor and set an additive incomplete: true on every cursor left in the queue, then draw one dim, inert row under such a folder while it is expanded. The envelope check ignores keys it does not know, so old and new clients stay compatible. Stop each round at the top once the budget is spent, before the shift: the rest stay queued unread, and flagged, rather than read and sorted for nothing. Say what the walk is: a per-directory round-robin (breadth-first), fair per directory and not per subtree, with search results now shallow-first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bb42f8105e
commit
a908be05ac
12 files changed
+370
-15
No files matched your search
@@ -1136,6 +1136,7 @@
|
||||
Worktree: '工作树',
|
||||
'Session folder': '会话文件夹',
|
||||
'Back to worktree': '返回工作树',
|
||||
'Not fully scanned (file limit reached)': '未完全扫描(已达到文件数量上限)',
|
||||
'Any status': '任意状态',
|
||||
'Any time': '任意时间',
|
||||
'Last hour': '最近一小时',
|
||||
|
||||
@@ -3794,6 +3794,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
renderNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// The scan's file limit stopped short of this folder, so what is listed above is not all of
|
||||
// it. Not a .file-tree-item: the click wiring below would try to open it as a file.
|
||||
if (isDir && isExpanded && node.incomplete === true) {
|
||||
html.push(`<div class="file-tree-incomplete" data-depth="${depth + 1}">${escapeHtml('Not fully scanned (file limit reached)')}</div>`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const node of tree) {
|
||||
|
||||
@@ -12338,6 +12338,16 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Under an expanded folder the scan's file limit stopped short of. Inert: no hover, no click. */
|
||||
.file-tree-incomplete {
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-tree-icon {
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
@@ -12394,15 +12404,15 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
|
||||
/* File tree indent levels */
|
||||
.file-tree-item[data-depth="0"] { padding-left: 0.5rem; }
|
||||
.file-tree-item[data-depth="1"] { padding-left: 1.25rem; }
|
||||
.file-tree-item[data-depth="2"] { padding-left: 2rem; }
|
||||
.file-tree-item[data-depth="3"] { padding-left: 2.75rem; }
|
||||
.file-tree-item[data-depth="4"] { padding-left: 3.5rem; }
|
||||
.file-tree-item[data-depth="5"] { padding-left: 4.25rem; }
|
||||
.file-tree-item[data-depth="6"] { padding-left: 5rem; }
|
||||
.file-tree-item[data-depth="7"] { padding-left: 5.75rem; }
|
||||
.file-tree-item[data-depth="8"] { padding-left: 6.5rem; }
|
||||
.file-tree-item[data-depth="9"] { padding-left: 7.25rem; }
|
||||
.file-tree-item[data-depth="1"], .file-tree-incomplete[data-depth="1"] { padding-left: 1.25rem; }
|
||||
.file-tree-item[data-depth="2"], .file-tree-incomplete[data-depth="2"] { padding-left: 2rem; }
|
||||
.file-tree-item[data-depth="3"], .file-tree-incomplete[data-depth="3"] { padding-left: 2.75rem; }
|
||||
.file-tree-item[data-depth="4"], .file-tree-incomplete[data-depth="4"] { padding-left: 3.5rem; }
|
||||
.file-tree-item[data-depth="5"], .file-tree-incomplete[data-depth="5"] { padding-left: 4.25rem; }
|
||||
.file-tree-item[data-depth="6"], .file-tree-incomplete[data-depth="6"] { padding-left: 5rem; }
|
||||
.file-tree-item[data-depth="7"], .file-tree-incomplete[data-depth="7"] { padding-left: 5.75rem; }
|
||||
.file-tree-item[data-depth="8"], .file-tree-incomplete[data-depth="8"] { padding-left: 6.5rem; }
|
||||
.file-tree-item[data-depth="9"], .file-tree-incomplete[data-depth="9"] { padding-left: 7.25rem; }
|
||||
|
||||
/* ========== File Preview Overlay ========== */
|
||||
|
||||
|
||||
@@ -1665,6 +1665,8 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
size?: number;
|
||||
extension?: string;
|
||||
children?: FileTreeNode[];
|
||||
/** Set only on a folder the file limit stopped the walk short of, so `children` is not all of it. */
|
||||
incomplete?: boolean;
|
||||
}
|
||||
|
||||
let totalFiles = 0;
|
||||
@@ -1691,7 +1693,10 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
|
||||
const queue: SearchDirectoryCursor[] = [{ dirPath: walkRoot, depth: 1, index: 0 }];
|
||||
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
||||
// Give each discovered directory a slice before returning to a large sibling.
|
||||
// Per-directory round-robin (breadth-first): each queued directory gets an equal share of what
|
||||
// is left before any of them gets more, so results arrive shallow-first. Fair per directory,
|
||||
// not per subtree: a folder of thousands of small subfolders queues thousands of directories
|
||||
// and can still take most of the budget one level down.
|
||||
const directoriesInRound = queue.length;
|
||||
const nodesPerDirectory = Math.max(
|
||||
1,
|
||||
@@ -1699,6 +1704,8 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
);
|
||||
|
||||
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
||||
// Spent part way through a round: the rest stay queued unread rather than sorted for nothing.
|
||||
if (totalFiles + totalDirectories >= maxFiles || matches.length >= maxMatches) break;
|
||||
const cursor = queue.shift();
|
||||
if (!cursor) break;
|
||||
if (cursor.depth > maxDepth) {
|
||||
@@ -1734,7 +1741,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
if (matcher(entry.name, listedPath)) {
|
||||
matches.push({ name: entry.name, path: relativePath, type: 'directory' });
|
||||
}
|
||||
// Queue siblings together so one large subtree cannot starve later branches.
|
||||
// Queued, not entered: it waits for a later round with every other directory found.
|
||||
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, index: 0 });
|
||||
} else {
|
||||
totalFiles++;
|
||||
@@ -1756,7 +1763,6 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
}
|
||||
}
|
||||
if (cursor.index < cursor.entries.length) queue.push(cursor);
|
||||
if (matches.length >= maxMatches) break;
|
||||
}
|
||||
}
|
||||
if (queue.length > 0) truncated = true;
|
||||
@@ -1781,6 +1787,8 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
interface TreeDirectoryCursor {
|
||||
dirPath: string;
|
||||
depth: number;
|
||||
/** This directory's node. The root of the walk has none: `tree` is its list of children. */
|
||||
node?: FileTreeNode;
|
||||
nodes: FileTreeNode[];
|
||||
entries?: import('node:fs').Dirent[];
|
||||
index: number;
|
||||
@@ -1789,6 +1797,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
const tree: FileTreeNode[] = [];
|
||||
const queue: TreeDirectoryCursor[] = [{ dirPath: walkRoot, depth: 1, nodes: tree, index: 0 }];
|
||||
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles) {
|
||||
// The same per-directory round-robin as the search walk above, with the same limits.
|
||||
const directoriesInRound = queue.length;
|
||||
const nodesPerDirectory = Math.max(
|
||||
1,
|
||||
@@ -1796,6 +1805,8 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
);
|
||||
|
||||
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
||||
// Before the shift, so a cursor this skips stays queued and is flagged below.
|
||||
if (totalFiles + totalDirectories >= maxFiles) break;
|
||||
const cursor = queue.shift();
|
||||
if (!cursor) break;
|
||||
if (cursor.depth > maxDepth) {
|
||||
@@ -1833,7 +1844,7 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
children: [],
|
||||
};
|
||||
cursor.nodes.push(node);
|
||||
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, nodes: node.children!, index: 0 });
|
||||
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, node, nodes: node.children!, index: 0 });
|
||||
} else {
|
||||
totalFiles++;
|
||||
const ext = entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : undefined;
|
||||
@@ -1850,6 +1861,11 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
if (cursor.index < cursor.entries.length) queue.push(cursor);
|
||||
}
|
||||
}
|
||||
// Every directory still queued is unread or half read, and an empty `children` would read as an
|
||||
// empty folder. The root has no node to flag: `truncated` says the same for it.
|
||||
for (const cursor of queue) {
|
||||
if (cursor.node) cursor.node.incomplete = true;
|
||||
}
|
||||
if (queue.length > 0) truncated = true;
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user