// syntax.rs --- T M4.1 tree-sitter integration: parse types, the // per-buffer ParseView, and the worker-side run_parse function. // T M4.2 layers bundled grammars; T M4.3 adds highlight-query // loading and the capture-walk that feeds the highlight view. //! Tree-sitter integration (T M4.1 -- T M4.3). //! //! The async runtime ([`crate::async_runtime`]) already carries the //! dispatch shape Tree-sitter needs (parse-on-worker, supersede, //! frame-cadence settle). This module adds: //! //! * [`ParseRequest`] / [`ParseTreeBundle`] --- the inputs and outputs //! that travel between the main thread and a worker. //! * [`run_parse`] --- the worker-side body. Synchronous; called from //! the parse closure submitted by [`crate::async_runtime::AsyncRuntime::dispatch_parse`]. //! * [`ParseView`] / [`ParseViewHandle`] --- the per-buffer //! [`crate::view::View`] implementation that mirrors buffer bytes, //! captures every [`Edit`] as a [`tree_sitter::InputEdit`] (with //! correct row/col [`tree_sitter::Point`]s), and holds the most //! recent [`ParseTreeBundle`]. State lives behind an //! [`Arc>`] so the buffer-owned `Box` //! and the Lua-side glue (which needs to read the tree and feed //! back installed bundles) share the same backing store. //! * [`HighlightSpan`] / [`compute_highlight_spans`] --- T M4.3 //! capture-walk over a settled tree using a bundled //! `highlights.scm`. The resulting spans, sorted "wider first", //! feed [`crate::highlight::SyntaxHighlightView`]'s render path. //! //! M4.2 wires concrete grammars (`tree-sitter-rust`, //! `tree-sitter-lua`) on top of this module. M4.1 uses //! `tree-sitter-rust` only as a `dev-dependency` to drive acceptance //! tests. use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tree_sitter::{Node, Point, Range, StreamingIterator}; use crate::async_runtime::JobId; use crate::buffer::{Buffer, BufferError, BufferId}; use crate::highlight::{Theme, ThemeHandle}; use crate::rope::Edit; use crate::view::View; /// Description of a parse job: the source bytes to parse, the /// language to parse against, the prior tree (if any) for incremental /// re-parse, and the [`tree_sitter::InputEdit`] descriptions /// accumulated since that prior tree was produced. /// /// All fields are owned ([R31]) so the closure submitted to a worker /// holds nothing borrowed from the main thread. #[derive(Clone, Debug)] pub struct ParseRequest { /// Bytes to parse. Materialized from the buffer's rope on the /// main thread before dispatch. pub source: Arc<[u8]>, /// Grammar language. `tree_sitter::Language` is a cheap /// pointer-to-static and `Send + Sync + Clone`. pub language: tree_sitter::Language, /// Human-readable language label, surfaced through Lua and the /// `*workers*` buffer ([T M3.7]). pub language_name: String, /// Tree from a prior parse of the same buffer, or `None` for a /// cold parse. The worker calls [`tree_sitter::Tree::edit`] for /// every entry in [`Self::edits`] before re-parsing. pub prior_tree: Option, /// Edits accumulated by the [`ParseView`] since `prior_tree` was /// produced. Empty for cold parses; non-empty drives incremental /// re-parse. pub edits: Vec, /// Snapshot of the injection alias map (framing Q#IJ4). The worker /// resolves a dynamic `@injection.language` fence-name (`py`, `ts`, /// `c++`) through this map — case-folded — before matching it against /// [`BUILTIN_LANGUAGES`]. Snapshotted from the registry at dispatch so /// the worker never touches the main-thread `Rc` registry or a Lua /// table. Empty for the non-layered/legacy callers (no injections /// resolve, root parse unaffected). pub injection_aliases: Arc>, } /// Output of [`run_parse`]. The runtime's parse-handoff side map /// holds these by [`Arc`]; `Lua` introspection ([`crate::lua_bindings`]) /// resolves a buffer id to its current bundle and walks the tree. #[derive(Debug)] pub struct ParseTreeBundle { /// Injection layers (framing Q#IJ1). `layers[0]` is the root layer /// (the whole buffer, parsed with the buffer's own grammar); /// subsequent entries are injected child layers in depth-ascending /// order. Always non-empty — a parse produces at least the root, so /// [`Self::root_tree`] never panics. pub layers: Vec, /// Source bytes every layer's tree was parsed against. Co-owned with /// the request so node-byte-range lookups can read the underlying /// text (T M4.1 acceptance: "parse tree introspectable via Lua" /// implies the source the tree references). Child layers parse the /// *same* full source via `set_included_ranges`, so their node /// offsets are absolute into these bytes (framing mechanic #1). pub source: Arc<[u8]>, /// Root language label (`layers[0].language_name`). Kept here so Lua /// and the `*workers*` buffer can ask "what grammar produced this?" /// without indexing the layer vec. pub language_name: String, /// Wall-clock duration of the **root** parse (excludes injection layer /// building and dispatch/materialization/bus overhead). The M4.1 /// acceptance perf gates are stated in this metric, so it stays the /// single-tree cost even as injection layers are added on top. pub parse_duration: Duration, /// True if injection expansion hit the total-layer backstop (framing /// Q#IJ3) and dropped some regions. Surfaced (not silent) at settle via /// `pmacs.error`; only a pathological file (thousands of embedded /// regions) can set it. pub injection_capped: bool, } /// Lexically-local identifier ranges derived from a grammar's bundled /// `locals.scm` query. Ranges are sorted and deduplicated so highlight /// predicate checks are allocation-free binary searches. #[derive(Debug, Default)] pub struct LocalFacts { ranges: Box<[(u32, u32)]>, } /// One injection layer within a [`ParseTreeBundle`] (framing Q#IJ1). A /// layer pairs a parse tree with the language that produced it and the /// injection-nesting depth (root = 0). `highlight_query` is resolved on /// the main thread at settle from the registry cache (framing Q#IJ2) — /// the worker leaves it `None`. #[derive(Debug)] pub struct Layer { /// Canonical language name of the grammar that produced `tree`. pub language_name: String, /// The layer's parse tree. Node offsets are absolute into the /// bundle's `source` (child layers use `set_included_ranges`). pub tree: tree_sitter::Tree, /// Injection depth: 0 for the root, 1 for a direct injection, etc. pub depth: u16, /// Compiled `highlights.scm` for `language_name`, resolved at settle. /// `None` when the language ships no highlights, or on the worker /// (pre-settle). Producers read it to style this layer. pub highlight_query: Option>, /// Lexically-local definitions and resolved references for this tree. /// Present only when the highlight query asks about the `local` /// property; computed once when the bundle settles. pub local_facts: Option>, } impl ParseTreeBundle { /// The root layer's tree (`layers[0]`) — the whole-buffer parse. /// Never panics: [`run_parse`] always seeds the root layer. #[must_use] pub fn root_tree(&self) -> &tree_sitter::Tree { &self.layers[0].tree } } /// Run a parse. This is the worker-side body that the runtime's /// `dispatch_parse` closure invokes after pulling a job from the /// queue. Always synchronous --- there is no internal yielding. /// /// Returns `Err` if the language is rejected by [`tree_sitter::Parser`] /// (ABI mismatch, almost always a build issue) or if the parser /// itself returns no tree (cancellation flag flipped, exhausted /// timeout --- neither wired in M4.1, so under M4.1 contracts this /// path is unreachable in practice). pub fn run_parse(req: ParseRequest) -> Result { let mut parser = tree_sitter::Parser::new(); parser .set_language(&req.language) .map_err(|e| format!("set_language: {e}"))?; let mut prior = req.prior_tree; if let Some(tree) = prior.as_mut() { for edit in &req.edits { tree.edit(edit); } } let started = Instant::now(); let root_tree = parser .parse(req.source.as_ref(), prior.as_ref()) .ok_or_else(|| "parser produced no tree".to_owned())?; // `parse_duration` measures the root parse only — the metric the M4.1 // acceptance gates are stated in. Injection layer building (below) is an // additive phase separately guarded by the settle-time budget test; it // must not retroactively inflate this metric. let parse_duration = started.elapsed(); // Seed the root layer, then expand injection layers (framing Q#IJ1). // Injection expansion is best-effort and isolated to the child // (Q#IJ3): a failed/unknown/over-budget child drops that child only — // the root always installs, so this returns `Ok` whenever the root // parsed. let mut layers = vec![Layer { language_name: req.language_name.clone(), tree: root_tree, depth: 0, highlight_query: None, local_facts: None, }]; let injection_capped = build_injection_layers(&mut layers, req.source.as_ref(), &req.injection_aliases); Ok(ParseTreeBundle { layers, source: req.source, language_name: req.language_name, parse_duration, injection_capped, }) } // --------------------------------------------------------------------------- // Injection layers (framing Q#IJ2 -- Q#IJ5). Worker-side: this runs on a // parse worker, so it touches no `Rc` registry and no Lua — it resolves // injected languages by indexing the `&'static BUILTIN_LANGUAGES` table // (loaders + `injections_query` sources are `Send`) and case-folds fence // names through the `ParseRequest`'s alias snapshot. // --------------------------------------------------------------------------- /// Max injection nesting depth (framing Q#IJ3). markdown→rust is depth 1. const MAX_INJECTION_DEPTH: u16 = 3; /// Runaway backstop on total layers per buffer (framing Q#IJ3) — set well /// above any real document (a markdown doc's one-inline-layer-per-paragraph /// sits far under this). Purely anti-runaway; the perf bound is the /// settle-time acceptance guard, not this number. If hit, tail layers are /// dropped (degraded highlighting on a pathological file only). const MAX_INJECTION_LAYERS: usize = 4096; /// The default fence-name → canonical-language alias map (framing Q#IJ4). /// Keys are lowercase; the resolver case-folds before lookup. Seeded into /// the registry and snapshotted into each [`ParseRequest`]; also handy for /// tests that build a request without the registry. #[must_use] pub fn default_injection_aliases() -> HashMap { [ ("js", "javascript"), ("jsx", "javascriptreact"), ("ts", "typescript"), ("tsx", "typescriptreact"), ("py", "python"), ("py3", "python"), ("python3", "python"), ("rs", "rust"), ("sh", "bash"), ("shell", "bash"), ("shellscript", "bash"), ("zsh", "bash"), ("c++", "cpp"), ("cxx", "cpp"), ("cc", "cpp"), ("golang", "go"), ("yml", "yaml"), ("md", "markdown"), // Lean 4 (framing Q#LN17). A ```lean fence is overwhelmingly Lean 4 // in practice, so the Lean 3 spelling is deliberately mapped forward // rather than left unresolved. `lean4` needs no alias — it is the // entry name. `lean4-mode` does the equivalent through // `markdown-code-lang-modes`. ("lean", "lean4"), ] .into_iter() .map(|(a, b)| (a.to_owned(), b.to_owned())) .collect() } /// One injection region resolved from a parent layer's `injections.scm`. /// Ranges are already child-excluded and normalized (Q#IJ5) but not yet /// intersected with the parent layer's ranges (that happens per-parent in /// [`build_injection_layers`], which knows the parent's included ranges). struct InjectionMatch { /// Raw language name — dynamic capture text or a static `#set!` value. language: String, /// Child-excluded content ranges for this match, sorted/non-overlapping. ranges: Vec, } /// Expand injection layers under the already-parsed root (`layers[0]`), /// appending children in depth-ascending order (Q#IJ1, Q#IJ6 rely on this /// ordering). Bounded by depth, total layer count, and a /// `(language, ranges)` visited guard (Q#IJ3). BFS by depth so siblings /// at a level are grouped before descending. Returns `true` if the /// total-layer backstop was hit and some regions were dropped (surfaced at /// settle, framing Q#IJ3). fn build_injection_layers( layers: &mut Vec, source: &[u8], aliases: &HashMap, ) -> bool { let mut query_cache: HashMap>> = HashMap::new(); let mut visited: HashSet<(String, Vec<(usize, usize)>)> = HashSet::new(); // Frontier entries are (layer index, that layer's included ranges). let mut frontier: Vec<(usize, Vec)> = vec![(0, vec![whole_source_range(source)])]; let mut depth: u16 = 0; let mut capped = false; while depth < MAX_INJECTION_DEPTH && !frontier.is_empty() { // Children discovered this level: (layer, its ranges) to append and // (if any injections themselves) descend into next level. let mut children: Vec<(Layer, Vec)> = Vec::new(); 'parents: for (parent_idx, parent_ranges) in &frontier { let parent_lang = layers[*parent_idx].language_name.clone(); let Some(query) = injection_query_cached(&mut query_cache, &parent_lang) else { continue; }; for m in collect_injection_matches(&query, &layers[*parent_idx].tree, source) { if layers.len() + children.len() >= MAX_INJECTION_LAYERS { capped = true; break 'parents; // runaway backstop; tail dropped } let Some(child_lang) = resolve_injected_language(&m.language, aliases) else { continue; // unknown/unaliased language — skip this child only }; let mut ranges = intersect_ranges(&m.ranges, parent_ranges, source); normalize_ranges(&mut ranges); if ranges.is_empty() { continue; } let key = (child_lang.to_owned(), ranges_key(&ranges)); if !visited.insert(key) { continue; // same (language, ranges) already parsed — cycle guard } let Some(tree) = parse_child(child_lang, &ranges, source) else { continue; // child parse failed — skip this child only }; children.push(( Layer { language_name: child_lang.to_owned(), tree, depth: depth + 1, highlight_query: None, local_facts: None, }, ranges, )); } } if children.is_empty() { break; } let mut next_frontier = Vec::with_capacity(children.len()); for (layer, ranges) in children { let idx = layers.len(); layers.push(layer); next_frontier.push((idx, ranges)); } frontier = next_frontier; depth += 1; } capped } /// Compile (once, cached) the `injections.scm` for `lang` from the static /// [`BUILTIN_LANGUAGES`] table, or `None` if the language ships none. fn injection_query_cached( cache: &mut HashMap>>, lang: &str, ) -> Option> { if let Some(slot) = cache.get(lang) { return slot.clone(); } let compiled = BUILTIN_LANGUAGES .iter() .find(|e| e.name == lang) .and_then(|entry| { let source = entry.injections_query.join("\n"); if source.trim().is_empty() { return None; } let language = (entry.loader)(); tree_sitter::Query::new(&language, &source) .ok() .map(Arc::new) }); cache.insert(lang.to_owned(), compiled.clone()); compiled } /// Run `query` over `tree` and return each injection region: its raw /// language name (dynamic `@injection.language` node text, or static /// `#set! injection.language`) and its child-excluded content ranges. fn collect_injection_matches( query: &tree_sitter::Query, tree: &tree_sitter::Tree, source: &[u8], ) -> Vec { let names = query.capture_names(); let content_cap = names.iter().position(|n| *n == "injection.content"); let Some(content_cap) = content_cap.map(|i| i as u32) else { return Vec::new(); }; let lang_cap = names .iter() .position(|n| *n == "injection.language") .map(|i| i as u32); let mut out = Vec::new(); let mut cursor = tree_sitter::QueryCursor::new(); let mut it = cursor.matches(query, tree.root_node(), source); while let Some(m) = it.next() { // Static language + include-children from `#set!` property settings. let mut static_lang: Option = None; let mut include_children = false; for prop in query.property_settings(m.pattern_index) { match &*prop.key { "injection.language" => { static_lang = prop.value.as_deref().map(str::to_owned); } "injection.include-children" => include_children = true, _ => {} } } let mut dyn_lang: Option = None; let mut ranges: Vec = Vec::new(); for cap in m.captures { if Some(cap.index) == lang_cap { if let Ok(text) = cap.node.utf8_text(source) { dyn_lang = Some(text.to_owned()); } } else if cap.index == content_cap { ranges.extend(content_node_ranges(cap.node, include_children)); } } let Some(language) = static_lang.or(dyn_lang) else { continue; }; normalize_ranges(&mut ranges); if ranges.is_empty() { continue; } out.push(InjectionMatch { language, ranges }); } out } /// The included ranges for one `@injection.content` node (framing Q#IJ5 / /// mechanic #3). With `include_children`, the whole node span; otherwise /// the node's extent minus its **named** children's ranges. Anonymous /// token children are *kept* — they are the injected text itself, not /// structure to exclude. (This matches `tree-sitter-md`'s own inline /// splitter, `bindings/rust/parser.rs:410`, which filters on `is_named()`: /// excluding a block `inline` node's anonymous text tokens would shred the /// paragraph into unparseable fragments. Our real injection sites — a /// childless `code_fence_content`, an `inline` with only anonymous /// children, an `include-children` macro `token_tree` — all resolve /// correctly under this rule.) A node with no named children yields its /// whole span. fn content_node_ranges(node: Node, include_children: bool) -> Vec { if include_children { return vec![node.range()]; } let mut ranges = Vec::new(); let mut start_byte = node.start_byte(); let mut start_point = node.start_position(); let mut cursor = node.walk(); if cursor.goto_first_child() { loop { let child = cursor.node(); if child.is_named() { if child.start_byte() > start_byte { ranges.push(Range { start_byte, end_byte: child.start_byte(), start_point, end_point: child.start_position(), }); } start_byte = child.end_byte(); start_point = child.end_position(); } if !cursor.goto_next_sibling() { break; } } } if node.end_byte() > start_byte { ranges.push(Range { start_byte, end_byte: node.end_byte(), start_point, end_point: node.end_position(), }); } ranges } /// Clip `candidate` ranges to `parent` ranges (framing Q#IJ5): a nested /// injection cannot reintroduce bytes its parent excluded. Points are /// recomputed only for a clipped edge (unclipped edges keep the node's /// exact point). At depth 1 the parent is the whole buffer, so this is a /// pass-through. fn intersect_ranges(candidate: &[Range], parent: &[Range], source: &[u8]) -> Vec { let mut out = Vec::new(); for c in candidate { for p in parent { let start = c.start_byte.max(p.start_byte); let end = c.end_byte.min(p.end_byte); if end > start { out.push(Range { start_byte: start, end_byte: end, start_point: if start == c.start_byte { c.start_point } else { byte_to_point(source, start) }, end_point: if end == c.end_byte { c.end_point } else { byte_to_point(source, end) }, }); } } } out } /// Sort, drop empty, and merge overlapping ranges so the result satisfies /// `set_included_ranges`' sorted/non-overlapping/non-empty contract. fn normalize_ranges(ranges: &mut Vec) { ranges.retain(|r| r.end_byte > r.start_byte); ranges.sort_by_key(|r| r.start_byte); let mut merged: Vec = Vec::with_capacity(ranges.len()); for r in ranges.drain(..) { if let Some(last) = merged.last_mut() && r.start_byte < last.end_byte { if r.end_byte > last.end_byte { last.end_byte = r.end_byte; last.end_point = r.end_point; } continue; } merged.push(r); } *ranges = merged; } /// A hashable identity for a range set (framing Q#IJ3 visited guard). fn ranges_key(ranges: &[Range]) -> Vec<(usize, usize)> { ranges.iter().map(|r| (r.start_byte, r.end_byte)).collect() } /// Case-fold `raw`, apply the alias map, then resolve against the bundled /// table (framing Q#IJ4). Returns the canonical `&'static` name, or `None` /// for an unknown language. fn resolve_injected_language(raw: &str, aliases: &HashMap) -> Option<&'static str> { let lower = raw.trim().to_ascii_lowercase(); if lower.is_empty() { return None; } let candidate: &str = aliases.get(&lower).map_or(lower.as_str(), String::as_str); BUILTIN_LANGUAGES .iter() .find(|e| e.name == candidate) .map(|e| e.name) } /// Cold-parse `source` restricted to `ranges` with `lang`'s grammar. Node /// offsets in the returned tree are absolute into `source` (mechanic #1). fn parse_child(lang: &str, ranges: &[Range], source: &[u8]) -> Option { let entry = BUILTIN_LANGUAGES.iter().find(|e| e.name == lang)?; let language = (entry.loader)(); let mut parser = tree_sitter::Parser::new(); parser.set_language(&language).ok()?; parser.set_included_ranges(ranges).ok()?; parser.parse(source, None) } /// The whole-buffer range, the root layer's parent range. fn whole_source_range(source: &[u8]) -> Range { Range { start_byte: 0, end_byte: source.len(), start_point: Point::new(0, 0), end_point: byte_to_point(source, source.len()), } } /// Convert a byte offset within `source` to a tree-sitter /// `(row, column)` [`tree_sitter::Point`]. `byte` is clamped to /// `source.len()`. /// /// O(byte) on a linear scan. For 5000-line files (~150 KB) this is /// tens of microseconds per call --- well under the 5 ms incremental /// budget. A precomputed line-start index would be the obvious /// follow-up if profiling argues for it. #[must_use] pub fn byte_to_point(source: &[u8], byte: usize) -> tree_sitter::Point { let bounded = byte.min(source.len()); let mut row: usize = 0; let mut last_nl: Option = None; for (i, b) in source[..bounded].iter().enumerate() { if *b == b'\n' { row += 1; last_nl = Some(i); } } let column = match last_nl { Some(nl) => bounded - nl - 1, None => bounded, }; tree_sitter::Point::new(row, column) } /// Mutable state shared between the buffer-attached [`ParseView`] /// and any external [`ParseViewHandle`] clones. struct ParseViewInner { language: tree_sitter::Language, language_name: String, /// Source bytes mirror, kept in sync with the buffer. Updated /// inside `on_edit`. source: Vec, /// Edits accumulated since `current` was produced. Drained on /// `make_request`; cleared on `install`. pending: Vec, /// Most recent settled parse, or `None` if no parse has run yet. current: Option>, } /// Per-buffer parse-tree state. Attached to a [`Buffer`] as a /// [`View`]; `on_edit` mirrors the rope edit into a parallel /// `Vec` source buffer and pushes a corresponding /// [`tree_sitter::InputEdit`] onto a pending list. /// /// Internally a thin wrapper over `Arc>` so /// callers (Lua bindings, dispatch glue) can hold a /// [`ParseViewHandle`] clone and read/modify the same state without /// having to detach the view from the buffer. pub struct ParseView { inner: Arc>, } /// External handle to a [`ParseView`]'s state. Cheap to clone /// (`Arc` bump). Used by [`crate::lua_bindings`] to (a) build a /// [`ParseRequest`] before dispatch, (b) install the produced /// [`ParseTreeBundle`] after settle, (c) introspect the tree from /// Lua. #[derive(Clone)] pub struct ParseViewHandle { inner: Arc>, } impl ParseView { /// Construct a view by snapshotting the buffer's current bytes. /// The snapshot becomes the view's source mirror, so the first /// dispatched parse has byte-accurate input even before any edit /// is observed. #[must_use] pub fn new(buf: &Buffer, language: tree_sitter::Language, language_name: String) -> Self { let len = buf.len(); let mut source = Vec::with_capacity(len as usize); if len > 0 { for chunk in buf.snapshot_rope().chunks(0, len) { source.extend_from_slice(chunk); } } let inner = ParseViewInner { language, language_name, source, pending: Vec::new(), current: None, }; Self { inner: Arc::new(Mutex::new(inner)), } } /// Cheap clone of the shared state handle. #[must_use] pub fn handle(&self) -> ParseViewHandle { ParseViewHandle { inner: self.inner.clone(), } } } impl ParseViewHandle { /// Language this view parses against. #[must_use] pub fn language(&self) -> tree_sitter::Language { self.inner .lock() .expect("ParseView mutex poisoned") .language .clone() } /// Human-readable language label. #[must_use] pub fn language_name(&self) -> String { self.inner .lock() .expect("ParseView mutex poisoned") .language_name .clone() } /// Most recent parse, if any has settled. #[must_use] pub fn current(&self) -> Option> { self.inner .lock() .expect("ParseView mutex poisoned") .current .clone() } /// Number of pending edits waiting for the next parse dispatch. #[must_use] pub fn pending_edit_count(&self) -> usize { self.inner .lock() .expect("ParseView mutex poisoned") .pending .len() } /// Snapshot of the source mirror's current contents. Test /// helper; the worker receives the same bytes as `req.source` /// when a parse is dispatched. #[must_use] pub fn source_snapshot(&self) -> Vec { self.inner .lock() .expect("ParseView mutex poisoned") .source .clone() } /// Build a [`ParseRequest`] reflecting the current state. Drains /// the pending-edit list. Caller is expected to dispatch the /// request and feed the settled bundle back via [`Self::install`]. pub fn make_request(&self) -> ParseRequest { let mut inner = self.inner.lock().expect("ParseView mutex poisoned"); let edits = std::mem::take(&mut inner.pending); let prior_tree = inner.current.as_ref().map(|b| b.root_tree().clone()); ParseRequest { source: Arc::from(inner.source.clone()), language: inner.language.clone(), language_name: inner.language_name.clone(), prior_tree, edits, // Empty by default; the dispatch binding overrides with the // registry's alias snapshot (framing Q#IJ4). Callers that need // injections and bypass the registry set this themselves. injection_aliases: Arc::new(HashMap::new()), } } /// Install a freshly-parsed bundle. The caller is responsible /// for matching the bundle to the request that produced it --- /// installing a stale bundle would desynchronize the source /// mirror from the tree. pub fn install(&self, bundle: Arc) { self.inner.lock().expect("ParseView mutex poisoned").current = Some(bundle); } } /// One row of the bundled-grammar config (T M4.2). Adding a new /// grammar is a one-line addition to [`BUILTIN_LANGUAGES`] (plus the /// matching `tree-sitter-foo` line in `Cargo.toml`). /// /// The `loader` is a function pointer rather than a pre-materialized /// [`tree_sitter::Language`] so the C-side grammar object isn't /// touched until the first buffer of that language is opened --- /// "load grammar lazily" per the M4.2 acceptance criterion. The /// [`Self::highlights_query`] fragments ship as `&'static str` /// constants in the binary; T M4.3 concatenates and compiles them /// into a [`tree_sitter::Query`] on first highlight attach. pub struct LanguageEntry { /// Canonical language name. Used by [`SyntaxRegistry::language`] /// lookups, surfaced through Lua as the grammar label. pub name: &'static str, /// File extensions (without the leading dot) that should auto- /// attach this grammar's [`ParseView`] when a file is opened. /// First match wins; ordering inside [`BUILTIN_LANGUAGES`] is /// the tiebreaker for ambiguous extensions. pub extensions: &'static [&'static str], /// Producer for the [`tree_sitter::Language`]. Called at most /// once per registry lifetime --- the result is cached under /// `name` after the first invocation. pub loader: fn() -> tree_sitter::Language, /// Bundled `highlights.scm` query fragments (T M4.3), concatenated /// in order (base grammar first) to form the effective query. Most /// grammars ship one self-contained fragment. A grammar whose /// bundled query is a tree-sitter `; inherits: ` delta lists /// the inherited base queries ahead of its own, because pmacs does /// not resolve `inherits:` directives — CUDA, for instance, ships a /// two-capture delta over C++ and must carry the C and C++ queries /// explicitly or ordinary C/C++ syntax goes unhighlighted. An empty /// slice (or all-empty fragments) means no highlights: the view /// runs but emits nothing. pub highlights_query: &'static [&'static str], /// Bundled `locals.scm` query fragments, composed base-first like /// [`Self::highlights_query`]. The query supplies lexical scopes, /// definitions, values, and references for `local` property predicates. pub locals_query: &'static [&'static str], /// Bundled `injections.scm` fragments (framing Q#IJ2), joined with a /// newline and compiled on the parse worker to find embedded-language /// regions. Empty for the many grammars that ship none (or don't /// inject). Names are inconsistent across crates — markdown exposes /// `INJECTION_QUERY_BLOCK`, rust `INJECTIONS_QUERY`, most none — the /// same shape `highlights_query` already absorbs. pub injections_query: &'static [&'static str], } /// Bundled grammars (T M4.2 + M4.3). The order is significant only /// for extensions that map to multiple languages --- none of the /// v0.1 entries collide. /// /// Adding a grammar: /// 1. Add `tree-sitter-foo = "X.Y"` to `Cargo.toml`. /// 2. Add one [`LanguageEntry`] here, with /// `highlights_query: &[tree_sitter_foo::HIGHLIGHTS_QUERY]` (or the /// inherited base queries ahead of it, if `foo`'s bundled query is /// a `; inherits:` delta — see the `cuda` entry). /// 3. (Done.) The Lua side picks up the new grammar through the /// `buffer.after-load` hook automatically and the highlight /// overlay attaches in the same step. pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[ LanguageEntry { name: "rust", extensions: &["rs"], loader: || tree_sitter_rust::LANGUAGE.into(), highlights_query: &[tree_sitter_rust::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[tree_sitter_rust::INJECTIONS_QUERY], }, LanguageEntry { name: "lua", extensions: &["lua"], loader: || tree_sitter_lua::LANGUAGE.into(), highlights_query: &[tree_sitter_lua::HIGHLIGHTS_QUERY], locals_query: &[tree_sitter_lua::LOCALS_QUERY], injections_query: &[], }, // T M9.7: markdown block grammar (`tree_sitter_md::LANGUAGE`) — headers, // lists, fenced code blocks, blockquotes. Its `injections.scm` (framing // Q#IJ10) drives two layer kinds: fenced code blocks inject the fence's // named language, and paragraph/heading text injects `markdown_inline` // (the entry below) — so inline emphasis/links are now highlighted, and // the former M9.7 "block-only, inline unhighlighted" floor is retired. // Note the constant name: `HIGHLIGHT_QUERY_BLOCK` (singular) is // the markdown crate's idiom; `tree-sitter-rust` and // `tree-sitter-lua` use `HIGHLIGHTS_QUERY` (plural). LanguageEntry { name: "markdown", extensions: &["md", "markdown"], loader: || tree_sitter_md::LANGUAGE.into(), highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_BLOCK], locals_query: &[], injections_query: &[tree_sitter_md::INJECTION_QUERY_BLOCK], }, // markdown_inline (framing Q#IJ10) — the inline grammar the block // grammar injects for paragraph/heading text (`#set! injection.language // "markdown_inline"`). No file extension: it is injection-only, never // opened directly by name. Ships an inline highlights query (emphasis, // links, code spans) and its own injections (e.g. inline HTML), so it // recurses like any other layer. Retires the M9.7 block-only floor. LanguageEntry { name: "markdown_inline", extensions: &[], loader: || tree_sitter_md::INLINE_LANGUAGE.into(), highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_INLINE], locals_query: &[], injections_query: &[tree_sitter_md::INJECTION_QUERY_INLINE], }, // T M_B3 — C / C++. Lexical highlighting (keywords / strings / // operators) so the grid TUI shows code-shaped C++ on first open. // `LspStyleView` layers on top with semantic refinement // (functions / types / macros / namespaces) from clangd's // semantic tokens — the two views' styles merge via // `crate::overlay::merge_styles`. // // `.h` is ambiguous C / C++; the `c` entry below claims it // (matches the LSP filetype map's default in `lsp.lua`). Users // who want `.h` parsed as C++ can override via Lua. // Note the const names: `tree-sitter-c` and `tree-sitter-cpp` // expose `HIGHLIGHT_QUERY` (singular), matching the `tree-sitter-md` // crate's `HIGHLIGHT_QUERY_BLOCK` style; `tree-sitter-rust` and // `tree-sitter-lua` use `HIGHLIGHTS_QUERY` (plural). No semantic // difference — same bundled `highlights.scm` either way. LanguageEntry { name: "c", extensions: &["c", "h"], loader: || tree_sitter_c::LANGUAGE.into(), highlights_query: &[tree_sitter_c::HIGHLIGHT_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "cpp", extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cppm"], loader: || tree_sitter_cpp::LANGUAGE.into(), highlights_query: &[tree_sitter_cpp::HIGHLIGHT_QUERY], locals_query: &[], injections_query: &[], }, // CUDA (`.cu` source, `.cuh` header). A dedicated grammar rather // than reusing `cpp`: CUDA extends C++ with `__global__`/`__device__` // qualifiers, `<<>>` kernel-launch syntax, and builtin // types the C++ grammar misparses. Neither extension collides with // an entry above, so ordering is irrelevant here. `LspStyleView` // layers clangd's CUDA semantic tokens on top, exactly as for C/C++. // // Note the const name: `tree-sitter-cuda` exposes `HIGHLIGHTS_QUERY` // (plural, the `tree-sitter-rust`/`tree-sitter-lua` idiom), NOT the // singular `HIGHLIGHT_QUERY` that `tree-sitter-c`/`-cpp`/`-md` use. // // The CUDA `highlights.scm` opens with `; inherits: cpp` and defines // only the CUDA-specific captures (`<<<...>>>` launch brackets, the // `__global__`/`__device__` modifiers) — two capture classes on its // own. pmacs does not resolve `inherits:`, so the C and C++ base // queries are prepended explicitly; the three compile together into // ~16 capture classes against the CUDA grammar (which is a superset // of C++). Order is base-first (C, then C++, then CUDA) so later // fragments refine earlier ones. Without this, ordinary C/C++ syntax // in a `.cu` file would go almost entirely unhighlighted. LanguageEntry { name: "cuda", extensions: &["cu", "cuh"], loader: || tree_sitter_cuda::LANGUAGE.into(), highlights_query: &[ tree_sitter_c::HIGHLIGHT_QUERY, tree_sitter_cpp::HIGHLIGHT_QUERY, tree_sitter_cuda::HIGHLIGHTS_QUERY, ], locals_query: &[], injections_query: &[], }, // Shell / bash. Lexical highlighting for the shell family; the LSP // half (bash-language-server) was already wired in `lsp.lua`. Unlike // `cuda`, bash's `highlights.scm` is self-contained (no `; inherits:` // delta), so a single fragment suffices. The extension set is wider // than the `.sh`/`.bash` the LSP filetype map covered: `.zsh`/`.ksh`/ // `.ash` are close-enough dialects and `.bats` is bash. None collide // with an entry above. Because the language name is `bash` — matching // the `pmacs.lsp.config.bash` key — opening any of these also // auto-attaches bash-language-server. Extensionless shell scripts are // resolved by shebang, and rc dotfiles (`.bashrc`, `PKGBUILD`) by the // filename map — both in `builtin/runtime/syntax.lua`. LanguageEntry { name: "bash", extensions: &["sh", "bash", "zsh", "ksh", "ash", "bats"], loader: || tree_sitter_bash::LANGUAGE.into(), highlights_query: &[tree_sitter_bash::HIGHLIGHT_QUERY], locals_query: &[], injections_query: &[], }, // Filename-identified languages. These files usually have no useful // extension (`Dockerfile`, `Makefile`, `CMakeLists.txt`), so the bulk // of detection is the filename map in `syntax.lua`; the extensions // here catch the `.dockerfile`/`.mk`/`.cmake` variants. All three ship // self-contained highlights (no `; inherits:`), so single fragments. // // Dockerfile uses the `tree-sitter-containerfile` crate (the // ABI-current grammar; also covers Containerfile); its root node is // `source_file`. Make roots at `makefile`, CMake at `source_file`. LanguageEntry { name: "dockerfile", extensions: &["dockerfile", "containerfile"], loader: || tree_sitter_containerfile::LANGUAGE.into(), highlights_query: &[tree_sitter_containerfile::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "make", extensions: &["mk", "make"], loader: || tree_sitter_make::LANGUAGE.into(), highlights_query: &[tree_sitter_make::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "cmake", extensions: &["cmake"], loader: || tree_sitter_cmake::LANGUAGE.into(), highlights_query: &[tree_sitter_cmake::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, // Grammar-gap languages — these already had LSP configs but no // grammar, so they rendered without lexical color. Each language name // matches its existing `pmacs.lsp.config.` key, so grammar // detection (which wins over the filetype map) resolves the same id // the server keys off. Root kinds: python `module`, go/zig // `source_file`, js/ts family `program`, toml `document`. LanguageEntry { name: "python", extensions: &["py", "pyi"], loader: || tree_sitter_python::LANGUAGE.into(), highlights_query: &[tree_sitter_python::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "go", extensions: &["go"], loader: || tree_sitter_go::LANGUAGE.into(), highlights_query: &[tree_sitter_go::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, // JavaScript / TypeScript. One `tree-sitter-javascript` grammar parses // both `.js` and `.jsx`; `tree-sitter-typescript` ships two grammars // (`LANGUAGE_TYPESCRIPT`, `LANGUAGE_TSX`). Highlights inherit: the TS // query is a ~5-capture delta over JavaScript, and JSX is a further // `JSX_HIGHLIGHT_QUERY` delta — so the `*react` and `typescript*` // entries compose base-first (js → jsx → ts), the same pattern as // `cuda` over C/C++. The four names mirror the LSP filetype map // (typescriptreact/javascriptreact) so tsserver enables the JSX parser. LanguageEntry { name: "javascript", extensions: &["js", "mjs", "cjs"], loader: || tree_sitter_javascript::LANGUAGE.into(), highlights_query: &[tree_sitter_javascript::HIGHLIGHT_QUERY], locals_query: &[tree_sitter_javascript::LOCALS_QUERY], injections_query: &[], }, LanguageEntry { name: "javascriptreact", extensions: &["jsx"], loader: || tree_sitter_javascript::LANGUAGE.into(), highlights_query: &[ tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_javascript::JSX_HIGHLIGHT_QUERY, ], locals_query: &[tree_sitter_javascript::LOCALS_QUERY], injections_query: &[], }, LanguageEntry { name: "typescript", extensions: &["ts", "mts", "cts"], loader: || tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), highlights_query: &[ tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_typescript::HIGHLIGHTS_QUERY, ], locals_query: &[ tree_sitter_javascript::LOCALS_QUERY, tree_sitter_typescript::LOCALS_QUERY, ], injections_query: &[], }, LanguageEntry { name: "typescriptreact", extensions: &["tsx"], loader: || tree_sitter_typescript::LANGUAGE_TSX.into(), highlights_query: &[ tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_javascript::JSX_HIGHLIGHT_QUERY, tree_sitter_typescript::HIGHLIGHTS_QUERY, ], locals_query: &[ tree_sitter_javascript::LOCALS_QUERY, tree_sitter_typescript::LOCALS_QUERY, ], injections_query: &[], }, LanguageEntry { name: "toml", extensions: &["toml"], loader: || tree_sitter_toml_ng::LANGUAGE.into(), highlights_query: &[tree_sitter_toml_ng::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "zig", extensions: &["zig", "zon"], loader: || tree_sitter_zig::LANGUAGE.into(), highlights_query: &[tree_sitter_zig::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, // JSON + YAML — config formats, both self-contained highlights and no // injections of their own. Registering `yaml` also lights up markdown // `---` frontmatter via the #122 injection engine (the markdown block // injection query sets `injection.language "yaml"` for `minus_metadata`; // `+++` TOML frontmatter already works). Root kinds: json `document`, // yaml `stream`. `.jsonc`/`.json5` (comments / trailing commas) are a // deferred variant — the plain JSON grammar rejects them. LanguageEntry { name: "json", extensions: &["json"], loader: || tree_sitter_json::LANGUAGE.into(), highlights_query: &[tree_sitter_json::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, LanguageEntry { name: "yaml", extensions: &["yaml", "yml"], loader: || tree_sitter_yaml::LANGUAGE.into(), highlights_query: &[tree_sitter_yaml::HIGHLIGHTS_QUERY], locals_query: &[], injections_query: &[], }, // LaTeX / TeX. The grammar crate exports no query constants (unlike every // entry above), so the highlights query is the in-repo overlay // `builtin/queries/latex/highlights.scm`, `include_str!`'d as // `LATEX_HIGHLIGHTS` below — the first such overlay in the tree (framing // Q#LX2; the `audit-rules.scm` include is the precedent). Locals and // injections are empty for v0; `(math_environment) @math` injection // detection is deferred to the inline-math arc. LanguageEntry { name: "latex", extensions: &["tex", "latex", "sty", "cls"], loader: || codebook_tree_sitter_latex::LANGUAGE.into(), highlights_query: &[LATEX_HIGHLIGHTS], locals_query: &[], injections_query: &[], }, // HTML + CSS (framing `docs/web-grammars-html-css-framing.md`). Both crates // export their query constants (no overlay). HTML's `INJECTIONS_QUERY` // wires `