Skip to main content

Module handle

Module handle 

Source
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 Syntax instance + Parser. Receives (from_version, text_version, buffer, edits) reparse requests on an unbounded mpsc channel. Each request runs the parse on tokio::task::spawn_blocking so the long-running tree-sitter call doesn’t tie up a worker thread for the whole runtime; on completion the worker stores a fresh SyntaxSnapshot in the handle’s ArcSwap cell.
  • 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-materialized String. The worker calls buffer.as_string() on its spawn_blocking thread, 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 edits route to Syntax::try_apply_intermediate first – this applies tree.edit() per delta and updates the cached source + text_version, but does NOT yet run Parser::parse. The worker then publishes an intermediate ArcSwap::store of 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). THEN reparse_with_cached_tree runs Parser::parse(_, Some(&old_tree)) which reuses unchanged subtrees, and the worker publishes the final snapshot. Empty edits or any guard violation in try_apply_intermediate falls through to full reparse with a single publish.
  • Coalescing. When multiple requests are queued, the worker accumulates edits in arrival order, takes the latest buffer and text_version, keeps the earliest from_version. Preserves edit ordering across the burst; the burst maps to a single Parser::parse. Coalesced edit count is capped at MAX_INCREMENTAL_EDITS_PER_REQUEST (256) to bound worst-case tree.edit() overhead; pathological bursts fall through to full reparse.
  • Wait-free reads. The App / renderer / fold provider reads the latest snapshot via handle.snapshot() (one ArcSwap::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§

SyntaxHandle
Editor-facing handle. Cheap to clone; cloning gives a reference to the same underlying snapshot cell + the same reparse-request channel.