Expand description
SyntaxHandle – the async wrapper around per-document
Syntax that runs reparses off the UI thread.
§Why this exists
The audit’s C1 finding: Syntax::parse runs synchronously on
whatever thread calls it, and the App was calling it from
App::apply (every Action) and App::refresh_highlights
(per frame). On a multi-MB buffer that’s a sub-millisecond
to multi-millisecond stall on the UI thread – a direct
violation of paramount goal #1 (“UI thread does no … parsing”).
§Architecture
Mirrors the patterns we use for the document actor and the LSP supervisor:
- Worker task. Owns the
Syntaxinstance +Parser. Receives(from_version, text_version, buffer, edits)reparse requests on an unbounded mpsc channel. Each request runs the parse ontokio::task::spawn_blockingso the long-running tree-sitter call doesn’t tie up a worker thread for the whole runtime; on completion the worker stores a freshSyntaxSnapshotin the handle’sArcSwapcell. - Buffer-not-text (slice B.5). The request carries a
Buffer(O(1) Arc-bump clone via ropey’s internal sharing) instead of a pre-materializedString. The worker callsbuffer.as_string()on itsspawn_blockingthread, so the O(n) source materialization stays off the input thread per paramount goal #1. - Incremental reparse with intermediate publish (slices
B.2 + C.2). Non-empty
editsroute toSyntax::try_apply_intermediatefirst – this appliestree.edit()per delta and updates the cached source +text_version, but does NOT yet runParser::parse. The worker then publishes an intermediateArcSwap::storeof this byte-shifted-but-pre-parse-shape snapshot, so renderers immediately see byte-aligned spans for unchanged content (only the changed region’s tree shape is briefly stale). THENreparse_with_cached_treerunsParser::parse(_, Some(&old_tree))which reuses unchanged subtrees, and the worker publishes the final snapshot. Empty edits or any guard violation intry_apply_intermediatefalls through to full reparse with a single publish. - Coalescing. When multiple requests are queued, the
worker accumulates
editsin arrival order, takes the latestbufferandtext_version, keeps the earliestfrom_version. Preserves edit ordering across the burst; the burst maps to a singleParser::parse. Coalesced edit count is capped atMAX_INCREMENTAL_EDITS_PER_REQUEST(256) to bound worst-casetree.edit()overhead; pathological bursts fall through to full reparse. - Wait-free reads. The App / renderer / fold provider
reads the latest snapshot via
handle.snapshot()(oneArcSwap::load_full). No mutex, no actor round-trip.
§Test path
For sync tests that pre-build a parsed Syntax and want to
plug it into the handle directly, SyntaxHandle::seeded
constructs a handle whose snapshot starts populated and
whose worker task either runs (if a tokio runtime is
present) or is skipped (if not – read-only handle).
Structs§
- Syntax
Handle - Editor-facing handle. Cheap to clone; cloning gives a reference to the same underlying snapshot cell + the same reparse-request channel.