From fa82cac885cb4738fe36d1e49b4749b5a3e31a4a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 6 Sep 2026 17:14:40 -0300 Subject: syntax: a results buffer is coloured as the code it quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit +Grep, +Search and every language answer render rows like src/look.zig:718:12-16 fn grepText(path: []const u8, text: []const u8... — a location, a space, and a piece of some file. The location names the file, the file names the grammar, and the rest of the row is a fragment of that language, so a grep over Zig reads as Zig and one Markdown row in the same buffer does not pretend otherwise. The location itself is left uncoloured: it is not code, and painting it as code is how a path starts looking like a keyword. `look.parsePathLine` decides what counts as a location — the same primitive n/N already walks these buffers with, so the two agree by construction about which rows are locations. NOT `lookableLineSpan`, which is n/N's whole heuristic: it calls `resolve`, and a `realpath` per row per scroll is not something a render path can afford. A bare filename is refused too; only `path:line` counts, or a prose line whose first word ended in `.md` would colour the rest of a sentence. THE BUFFER IS COLOURED WHOLE, ONCE, WHEN IT IS FILLED. An adversarial pass measured the obvious per-window implementation and it was untenable: the rows are independent, so a window pass buys no fidelity, only amortisation, and pays a burst on every scroll that outran the covered range. Grammars compile their highlights query on first use — zig 26.9ms, cpp 18.9ms, rust 14.3ms — so a polyglot grep showing six languages stalled a frame by 66ms, moving a cost the syntax module had deliberately put on "opening a file" onto a scroll. It also raised tree-sitter's allocation rate 3.5x (8,785 per refresh against 2,454) into a 16 MiB bump arena that only reclaims LIFO, so ~16 scroll re-highlights exhausted it — and that arena is shared with real file panes, so a results pane could evict editing. A grep is capped at 512 rows; colouring it once makes the covered-range check true forever after and scrolling free. The rest of that pass, in the same spirit: injections off for a single row (both build a SECOND parser, per fenced block and per inline node, which is absurd for one truncated row that almost never contains a fence), one query cursor for the buffer instead of one per row, a one-entry extension memo so non-matching rows stop paying a 29-spec scan, and NO highlights at all when nothing painted — an all-zero run is not the same as none, and it defeated `recolorSyntax`'s fast path, making every +Help and +Config walk its graphemes every frame to paint nothing. One correctness bug from the same pass: a failed `setLanguage` has already nulled the parser's language, so leaving `held` on the previous grammar made every later row of it skip the call and silently lose colour. Documents keep `.source`: a New scratch and a real file are output-shaped but have one language and an edit per keystroke, and `saves` is the line between the two. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf --- src/file_pane.zig | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'src/file_pane.zig') diff --git a/src/file_pane.zig b/src/file_pane.zig index 5c7f4b77..9b828030 100644 --- a/src/file_pane.zig +++ b/src/file_pane.zig @@ -722,10 +722,25 @@ pub fn refreshHighlights(p: *Pardes) void { // a fidelity gain, and one that cannot reach a file shown from the top // (scroll 0 clamps the window to exactly what it always was). const slack: usize = if (f.highlights.len == 0) 0 else pane.rows; - const start = lineStart(p.gpa, f, f.scroll -| slack); - const end = @max(start, lineStart(p.gpa, f, f.scroll + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS + slack)); + // A RESULTS BUFFER IS COLOURED WHOLE, ONCE. Its rows are independent — + // each is parsed in isolation against its own grammar — so a window + // pass buys no fidelity, only amortisation, and pays for it with a + // burst on every scroll that outran the covered range: a fresh parser, + // a fresh query cursor and a tree per row, plus the first compile of + // any grammar the new rows introduce, all inside `render`. Colouring + // the whole buffer when it is FILLED makes the covered-range check + // above true forever after, so scrolling one costs nothing at all. + // Bounded by what fills them: `look.find_max_hits` caps a grep at 512 + // rows, and a rendering is never typed into. + const whole = pane.colorAlgo() == .locations; + const start = if (whole) 0 else lineStart(p.gpa, f, f.scroll -| slack); + const end = if (whole) + f.content.len + else + @max(start, lineStart(p.gpa, f, f.scroll + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS + slack)); const new_highlights = (switch (pane.colorAlgo()) { .diff => syntax.highlightDiff(p.tree_sitter_gpa, f.content, start, end), + .locations => syntax.highlightLocations(p.tree_sitter_gpa, f.content, start, end), else => syntax.highlightFileRange(p.tree_sitter_gpa, f.path, f.content, start, end), }) catch { f.syntax_dirty = false; -- cgit v1.3