feat(ui): the whole file — full source with gutter ports and intra-file call arcs (CG-52)
The File view gains a Source tab: the file itself, top to bottom, with the Symbol view's line grid, gutter ports and call-site links, a line-anchored callee rail, and — in the left margin — an arc for every call that stays inside the file, drawn from the calling line to the callee's definition line. The arcs are the point. Source order is already a layout, chosen by whoever wrote the file, so a file's internal call structure can be drawn with no algorithm placing anything. Crabviz's idea, in the one place it is legible. Everything is arithmetic, not measurement. The Symbol view queries the laid-out DOM to place a callee row beside its line; a 6 820-line file cannot afford that. Here a line is exactly 20px at `10 + (n - 1) x 20`, so ~90 line elements exist at a time and the arcs, ports, rail rows and connectors are all functions of a line number. `src/mcp/tools.ts` scrolls at a 16.6ms median frame. - `GET /api/filecode/<path>` — outline, one call group per (caller, callee) PAIR with its call-site lines, unresolved references, and the file's length. The source is NOT in it: it pages through `/api/source` 800 lines at a time with a discarded 150-line lead-in, so a page starting inside a block comment does not render prose as code, and so the ports and arcs are complete from the first frame while the text fills in behind them. - `intraFileCalls` is counted over the groups actually returned, so the header and the picture under it cannot disagree once a cap bites. - Above 40 arcs the diagram narrows to the symbol under the pointer (or the one the scroll position is inside) and the header states the total. Accent is for the pointer only, never for the filter. - Sticky outline rail at >= 1400px, following the reader down the file. - `QueryBuilder.getUnresolvedReferencesInFile` — one indexed lookup instead of one per symbol; `buildOutlineEntries` lifted out of `/api/file` so both readings of a file draw the same rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
62e0a89b0e
commit
bd99c5e99a
@@ -173,6 +173,60 @@ export function hasDriftedOnDisk(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The drift verdict AND the file's length, from one read.
|
||||
*
|
||||
* The whole-file view needs both before it draws anything: the drift banner,
|
||||
* and the line count that fixes the height of the scrolling document (every
|
||||
* line is a fixed 20px, so the total IS the layout). Asking
|
||||
* {@link hasDriftedOnDisk} and then a source page would answer the first
|
||||
* question against one read of the file and the second against another, which
|
||||
* is exactly the window in which a file can change underneath the two.
|
||||
*
|
||||
* Unlike `hasDriftedOnDisk` there is no stat-only fast path: the bytes have to
|
||||
* be read to be counted. That is the cost of knowing the length, and it is
|
||||
* bounded by {@link MAX_SOURCE_BYTES} like every other read here.
|
||||
*/
|
||||
export function readFileShape(
|
||||
projectRoot: string,
|
||||
storedPath: string,
|
||||
record: FileRecord
|
||||
): { drift: boolean; totalLines: number | null; reason?: string } {
|
||||
let absolute: string;
|
||||
try {
|
||||
absolute = resolveProjectFile(projectRoot, storedPath);
|
||||
} catch {
|
||||
// A refusal on a path the INDEX handed us is not a request to refuse — the
|
||||
// caller already passed the chokepoint. Treat it as unreadable.
|
||||
return { drift: false, totalLines: null };
|
||||
}
|
||||
try {
|
||||
const stats = fs.statSync(absolute);
|
||||
if (stats.size > MAX_SOURCE_BYTES) {
|
||||
return { drift: false, totalLines: null, reason: 'The file is too large to read here.' };
|
||||
}
|
||||
const content = fs.readFileSync(absolute, 'utf-8');
|
||||
const drift = createHash('sha256').update(content).digest('hex') !== record.contentHash;
|
||||
return {
|
||||
drift,
|
||||
totalLines: splitLines(content).length,
|
||||
...(drift
|
||||
? {
|
||||
reason:
|
||||
'This file changed on disk after the last index sync, so the line ' +
|
||||
'numbers the graph holds no longer match it.',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
drift: true,
|
||||
totalLines: null,
|
||||
reason: 'The file is in the index but could not be read from disk.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface SourceResult {
|
||||
file: string;
|
||||
language: string;
|
||||
|
||||
Reference in New Issue
Block a user