From 6bebd770e1063fa96ec1b710a5609899cba927ee Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sat, 30 May 2026 10:47:43 -0400 Subject: [PATCH] =?UTF-8?q?S1=20follow-up=20=E2=80=94=20scope=20the=20per-?= =?UTF-8?q?edit=20highlight=20queries=20(typing=20perf,=20Q#S6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scrolling became fast after S1 but typing stayed slow: scrolling doesn't bump the CRDT generation, so the daemon's StyleGate caches and no query runs — but every keystroke bumps the generation and forced TWO whole-file tree-sitter passes on the daemon, which S1 deferred as Q#S6. With the GPU now O(visible), this was the remaining O(file) per-keystroke cost. 1. StyleSpans query scoped to the viewport. New `compute_highlight_spans_in_range` sets `QueryCursor::set_byte_range` so the capture walk is proportional to the visible range, not the whole tree; `scoped_style_spans` passes the declared viewport. The StyleGate still recomputes on the edit's generation bump (M11.7 resync), but that recompute is now O(visible). 2. FileStyleSummary (the minimap — inherently a whole-file pass) debounced to reparse-completion: skip the recompute while a reparse is in flight (`pending_edit_count() > 0`). During continuous typing the whole-file pass runs at reparse rate, not keystroke rate; when typing settles and the parse lands, it recomputes once. Together these drop the daemon's per-keystroke cost from two whole-file tree-sitter passes to one viewport-scoped pass (+ an amortized whole-file summary). Only the semantic (pmacs-gpu) path is affected; the grid/TUI path doesn't use this producer. Gates green: fmt; clippy --all-targets --workspace -D warnings (default + crdt); pmacs lib 1334; syntax 6; semantic_render 28; m11_5_semantic_acceptance 2; m4_acceptance 88. Awaiting visual confirmation: typing in a large file is now responsive. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/semantic_render.rs | 22 +++++++++++++++++++++- src/syntax.rs | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/semantic_render.rs b/src/semantic_render.rs index 7b065d5..66d5b8b 100644 --- a/src/semantic_render.rs +++ b/src/semantic_render.rs @@ -356,6 +356,18 @@ impl SemanticRenderState { buffer_id: BufferId, generation: u64, ) -> Option { + // The summary is a *whole-file* tree-sitter pass (the minimap + // needs every line). Recomputing it on every edit's generation + // bump was a per-keystroke O(file) cost — a major part of the + // typing slowness. For a grammar-backed buffer, debounce it to + // reparse-completion: skip while a reparse is in flight + // (`current_fresh` is `None`), so during continuous typing the + // whole-file pass runs at reparse rate, not keystroke rate. + if let Some(handle) = state.syntax_registry.view(buffer_id) + && handle.pending_edit_count() > 0 + { + return None; + } if self.last_summary.get(&buffer_id).copied() == Some(generation) { return None; } @@ -871,7 +883,15 @@ fn scoped_style_spans(state: &EditorState, vp: &DeclaredViewport) -> Vec Vec { + compute_highlight_spans_in_range(query, bundle, None) +} + +/// Like [`compute_highlight_spans`], but restricts the query to nodes +/// intersecting `byte_range` when `Some`. tree-sitter's +/// `QueryCursor::set_byte_range` makes the capture walk proportional to +/// the range, not the whole tree — the semantic producer passes the +/// declared viewport so styling a screenful of a huge file is +/// O(visible), not O(file) (the per-edit typing cost; framing Q#S6). +#[must_use] +pub fn compute_highlight_spans_in_range( + query: &tree_sitter::Query, + bundle: &ParseTreeBundle, + byte_range: Option>, ) -> Vec { let mut spans = Vec::new(); let mut cursor = tree_sitter::QueryCursor::new(); + if let Some(range) = byte_range { + cursor.set_byte_range(range); + } let source: &[u8] = bundle.source.as_ref(); let root = bundle.tree.root_node(); let mut iter = cursor.captures(query, root, source);