Skip to main content

lattice_plugin_host/
tree_resource.rs

1//! TS.1 host backing for the `tree-snapshot` / `node` WIT resources
2//! (plugin-treesitter-seam.md §3). The host owns the parse tree; a plugin gets
3//! read-only handles and calls back for the structure it needs — the tree never
4//! crosses the boundary (the `document`-handle model, applied to structure).
5//!
6//! **Snapshot backing.** A `tree-snapshot` wraps an `Arc<SyntaxSnapshot>` (an
7//! O(1) `ArcSwap` bump; `lattice-syntax` already runs reparses off-thread). It
8//! is immutable, so a handle stays coherent with its own point-in-time tree even
9//! as edits land underneath (§7). The trampoline mints it only when the snapshot
10//! actually has a `tree()` — so a handed-over resource always resolves.
11//!
12//! **Node backing = a path of child indices from the root.** A `NodeResource`
13//! is `(Arc<SyntaxSnapshot>, Vec<u32>)` where the vec is `Node::child` indices
14//! (ALL children, incl. anonymous) from root to the node. Every method
15//! re-resolves the node against the snapshot's tree. This is deliberately NOT a
16//! stored `tree_sitter::Node<'tree>` (which borrows the `Tree` and can't be a
17//! `'static` resource) and NOT a byte-range + kind (which collide on wrapper
18//! nodes sharing a span). The path is safe and unambiguous.
19//!
20//! **Resolution is NOT O(depth), which is what made this quadratic (OA.0a).**
21//! The final step of a path walk is `Node::child(i)`, and tree-sitter walks the
22//! sibling list to reach index `i` — so resolving the i-th child is O(i), not
23//! O(1). Re-resolving on every accessor therefore made a full pass over one
24//! node's children O(k²). `NodeResource` now memoises both its own facts and one
25//! cursor pass over its children; see the type's own docs.
26
27use std::sync::{Arc, OnceLock};
28
29use lattice_protocol::position::{Position, Range as NativeRange};
30use lattice_syntax::{Lang, SyntaxSnapshot};
31use streaming_iterator::StreamingIterator;
32use tree_sitter::{Node, Point, Query, QueryCursor, Tree};
33
34/// Backing for the `tree-snapshot` WIT resource: a point-in-time parse tree.
35pub struct TreeSnapshotResource {
36    snapshot: Arc<SyntaxSnapshot>,
37}
38
39/// Backing for a `node` WIT resource: a path of `Node::child` indices from the
40/// tree root (empty = root), re-resolved against `snapshot` on each call.
41///
42/// ## Why the two caches (OA.0a)
43///
44/// The path is the identity, and re-resolving it walks from the root — the last
45/// step, `Node::child(i)`, is O(i). So *every* accessor on the i-th child cost
46/// O(i), and `named_child` additionally rescanned the child list from zero on
47/// each call. Iterating one node's k children was therefore O(k²), which made a
48/// guest tree walk quadratic in file size: a 34 KB org file took 29 seconds to
49/// scan, and the agenda it fed never arrived.
50///
51/// Both caches close that without changing the WIT or the guest:
52///
53/// - `meta` — this node's own kind / range / flags, resolved at most once.
54/// - `children` — one `TreeCursor` pass over the children, which is O(k) and
55///   answers `named_child_count`, `named_child` and `child_by_field` in O(1)
56///   thereafter. Each entry carries enough to seed the child's own `meta`, so
57///   walking into a child resolves nothing at all.
58///
59/// A snapshot is immutable, so a cached answer cannot go stale.
60pub struct NodeResource {
61    snapshot: Arc<SyntaxSnapshot>,
62    path: Vec<u32>,
63    meta: OnceLock<NodeMeta>,
64    children: OnceLock<Arc<Children>>,
65}
66
67/// One cursor pass over a node's children, indexed both ways.
68///
69/// `named` holds positions into `all`, so `named_child(i)` is O(1). Walking it
70/// with `filter(..).nth(i)` instead left a residual O(k²) — cheap per step, but
71/// still quadratic, and quadratic is the thing being fixed.
72struct Children {
73    all: Vec<ChildMeta>,
74    named: Vec<u32>,
75}
76
77/// A node's own cheap facts, resolved once.
78#[derive(Clone)]
79struct NodeMeta {
80    kind: Arc<str>,
81    range: NativeRange,
82    is_named: bool,
83    is_error: bool,
84}
85
86/// One child, as seen from its parent's single cursor pass.
87struct ChildMeta {
88    /// Index among ALL children — the value that goes in a path.
89    index: u32,
90    field: Option<Arc<str>>,
91    meta: NodeMeta,
92}
93
94fn meta_of(node: Node<'_>) -> NodeMeta {
95    NodeMeta {
96        kind: Arc::from(node.kind()),
97        range: NativeRange {
98            start: position_of(node.start_position()),
99            end: position_of(node.end_position()),
100        },
101        is_named: node.is_named(),
102        is_error: node.is_error(),
103    }
104}
105
106/// TS.2 backing for the `query` WIT resource: a compiled tree-sitter query plus
107/// the language it was compiled against (so `run-query` can guard a mismatched
108/// snapshot). Owned by the guest; reusable across snapshots of the same language.
109pub struct QueryResource {
110    query: Query,
111    lang: Lang,
112}
113
114/// TS.2 backing for the `tree-cursor` WIT resource: a mutable position in the
115/// tree, represented as a `child`-index path (the `NodeResource` scheme) so the
116/// cursor stays a safe `(snapshot, path)` pair — no self-referential
117/// `tree_sitter::TreeCursor<'tree>`.
118pub struct CursorResource {
119    snapshot: Arc<SyntaxSnapshot>,
120    path: Vec<u32>,
121}
122
123fn point_of(pos: Position) -> Point {
124    Point {
125        row: pos.line as usize,
126        column: pos.byte as usize,
127    }
128}
129
130fn position_of(p: Point) -> Position {
131    Position {
132        line: p.row as u32,
133        byte: p.column as u32,
134    }
135}
136
137/// `a <= b` in (row, column) order — avoids relying on `Point: Ord`.
138fn point_le(a: Point, b: Point) -> bool {
139    (a.row, a.column) <= (b.row, b.column)
140}
141
142/// Walk `path` from the tree root, returning the node it names (or `None` if any
143/// index is stale — cannot happen for an immutable snapshot, but never panics).
144fn resolve<'t>(tree: &'t Tree, path: &[u32]) -> Option<Node<'t>> {
145    let mut node = tree.root_node();
146    for &i in path {
147        node = node.child(i)?;
148    }
149    Some(node)
150}
151
152/// The `child`-index path from root to `node` — walk up via `parent()`, finding
153/// `node`'s index among each parent's children by id. O(depth × siblings); used
154/// only by `run-query`, which runs off the sync path (a whole-tree query is
155/// forbidden from a sync grammar action, design §6), so the cost is acceptable.
156fn path_of(node: Node) -> Vec<u32> {
157    let mut path = Vec::new();
158    let mut cur = node;
159    while let Some(parent) = cur.parent() {
160        let mut idx = 0u32;
161        for i in 0..parent.child_count() {
162            if parent.child(i as u32).map(|c| c.id()) == Some(cur.id()) {
163                idx = i as u32;
164                break;
165            }
166        }
167        path.push(idx);
168        cur = parent;
169    }
170    path.reverse();
171    path
172}
173
174/// Descend from the root into the child whose span contains `point`, recording
175/// each `child` index, until no child contains it — the smallest node spanning
176/// `point`, plus its path. `None` when `point` is outside the root.
177///
178/// Uses a `TreeCursor`: `goto_first_child_for_point` binary-searches the child
179/// list (O(log fanout) per level), so a file with thousands of top-level items
180/// costs a handful of comparisons, NOT a linear sibling scan — the bound the
181/// sync-path perf claim rests on (§6). The primitive returns the first child
182/// *ending past* `point` (which may start after it — a gap between siblings), so
183/// the landed child is verified to actually contain `point` before descending.
184fn descend_to_point(tree: &Tree, point: Point) -> Option<Vec<u32>> {
185    let root = tree.root_node();
186    if !(point_le(root.start_position(), point) && point_le(point, root.end_position())) {
187        return None;
188    }
189    let mut cursor = tree.walk();
190    let mut path = Vec::new();
191    while let Some(idx) = cursor.goto_first_child_for_point(point) {
192        let child = cursor.node();
193        // Gap between siblings: the child starts past `point` → not a descent.
194        if !(point_le(child.start_position(), point) && point_le(point, child.end_position())) {
195            break;
196        }
197        path.push(idx as u32);
198    }
199    Some(path)
200}
201
202impl TreeSnapshotResource {
203    /// Wrap an immutable syntax snapshot as a `tree-snapshot` backing.
204    pub fn new(snapshot: Arc<SyntaxSnapshot>) -> Self {
205        Self { snapshot }
206    }
207
208    /// Whether the snapshot carries a parse tree. The trampoline mints a
209    /// resource only when this holds, so every resource method resolves.
210    pub fn has_tree(&self) -> bool {
211        self.snapshot.tree().is_some()
212    }
213
214    /// A fresh `NodeResource` at `path` anchored to this snapshot.
215    fn node_at_path(&self, path: Vec<u32>) -> NodeResource {
216        NodeResource {
217            snapshot: Arc::clone(&self.snapshot),
218            path,
219            meta: OnceLock::new(),
220            children: OnceLock::new(),
221        }
222    }
223
224    /// The tree root.
225    pub fn root(&self) -> NodeResource {
226        self.node_at_path(Vec::new())
227    }
228
229    /// The grammar id (e.g. `"rust"`).
230    pub fn language(&self) -> String {
231        self.snapshot.lang().name().to_string()
232    }
233
234    /// The smallest NAMED node spanning `pos` — descend to the smallest node,
235    /// then walk up to the nearest named ancestor (the root is always named).
236    /// `None` when there's no tree / `pos` is out of range.
237    pub fn node_at(&self, pos: Position) -> Option<NodeResource> {
238        let tree = self.snapshot.tree()?;
239        let mut path = descend_to_point(tree, point_of(pos))?;
240        while !path.is_empty() {
241            let node = resolve(tree, &path)?;
242            if node.is_named() {
243                break;
244            }
245            path.pop();
246        }
247        Some(self.node_at_path(path))
248    }
249
250    /// The nearest ancestor of `pos` (inclusive) whose `kind` is in `kinds` — the
251    /// auto-pair scope query (the native `scope_toward` precedent). `kinds` empty
252    /// → the nearest named ancestor (i.e. `node_at`). `None` when no ancestor
253    /// matches / no tree.
254    pub fn enclosing(&self, pos: Position, kinds: &[String]) -> Option<NodeResource> {
255        let tree = self.snapshot.tree()?;
256        let mut path = self.node_at(pos)?.path;
257        loop {
258            let node = resolve(tree, &path)?;
259            let matched = if kinds.is_empty() {
260                node.is_named()
261            } else {
262                kinds.iter().any(|k| k == node.kind())
263            };
264            if matched {
265                return Some(self.node_at_path(path));
266            }
267            if path.is_empty() {
268                return None;
269            }
270            path.pop();
271        }
272    }
273
274    /// TS.2: compile a tree-sitter query against this snapshot's grammar. `Err`
275    /// (the tree-sitter message) on a malformed query or a language with no
276    /// registered grammar.
277    pub fn compile_query(&self, source: &str) -> Result<QueryResource, String> {
278        let lang = self.snapshot.lang();
279        let language = self
280            .snapshot
281            .registry()
282            .tree_sitter_language(lang.name())
283            .ok_or_else(|| format!("no tree-sitter grammar for language '{}'", lang.name()))?;
284        let query = Query::new(&language, source).map_err(|e| e.to_string())?;
285        Ok(QueryResource { query, lang })
286    }
287
288    /// TS.2: run `query` over the whole tree (or `within` a point range),
289    /// returning the surviving captures — tree-sitter evaluates the `#eq?` /
290    /// `#match?` / `#any-of?` text predicates against the snapshot's source (the
291    /// `TextProvider`), so only matches that pass cross. Empty when there's no
292    /// tree or `query` was compiled for a different grammar (graceful).
293    pub fn run_query(
294        &self,
295        query: &QueryResource,
296        within: Option<NativeRange>,
297    ) -> Vec<(String, NodeResource)> {
298        let Some(tree) = self.snapshot.tree() else {
299            return Vec::new();
300        };
301        if query.lang != self.snapshot.lang() {
302            return Vec::new();
303        }
304        let source = self.snapshot.source();
305        let names = query.query.capture_names();
306        let mut cursor = QueryCursor::new();
307        if let Some(r) = within {
308            cursor.set_point_range(point_of(r.start)..point_of(r.end));
309        }
310        let mut matches = cursor.matches(&query.query, tree.root_node(), source);
311        let mut out = Vec::new();
312        while let Some(m) = matches.next() {
313            for cap in m.captures {
314                let name = names
315                    .get(cap.index as usize)
316                    .copied()
317                    .unwrap_or_default()
318                    .to_string();
319                out.push((
320                    name,
321                    NodeResource {
322                        snapshot: Arc::clone(&self.snapshot),
323                        path: path_of(cap.node),
324                        meta: OnceLock::from(meta_of(cap.node)),
325                        children: OnceLock::new(),
326                    },
327                ));
328            }
329        }
330        out
331    }
332
333    /// TS.2b: `run_query` reduced to extents — same traversal, same host-side
334    /// predicate filtering, but no `NodeResource` per capture.
335    ///
336    /// The saving is not the allocation: it is that every returned
337    /// `NodeResource` becomes a guest-visible resource-table entry holding its
338    /// own snapshot bump, which the guest must then drop one at a time across
339    /// the boundary. A structural query over a large file returns tens of
340    /// thousands of captures, and that per-capture round trip — not the query
341    /// itself — is what made whole-file structural queries too slow to run.
342    ///
343    /// The returned `u32` is the match ordinal within THIS call, so captures
344    /// from one pattern match stay groupable (`@context` with its
345    /// `@context.end`) without a containment test on the guest side.
346    pub fn run_query_ranges(
347        &self,
348        query: &QueryResource,
349        within: Option<NativeRange>,
350    ) -> Vec<(String, u32, NativeRange)> {
351        let Some(tree) = self.snapshot.tree() else {
352            return Vec::new();
353        };
354        if query.lang != self.snapshot.lang() {
355            return Vec::new();
356        }
357        let source = self.snapshot.source();
358        let names = query.query.capture_names();
359        let mut cursor = QueryCursor::new();
360        if let Some(r) = within {
361            cursor.set_point_range(point_of(r.start)..point_of(r.end));
362        }
363        let mut matches = cursor.matches(&query.query, tree.root_node(), source);
364        let mut out = Vec::new();
365        // Counted here rather than read from `m.id()`: tree-sitter's match id is
366        // not dense and is not stable across calls, and the guest only needs
367        // "same match or not" within one result list.
368        let mut match_index: u32 = 0;
369        while let Some(m) = matches.next() {
370            for cap in m.captures {
371                let name = names
372                    .get(cap.index as usize)
373                    .copied()
374                    .unwrap_or_default()
375                    .to_string();
376                out.push((
377                    name,
378                    match_index,
379                    NativeRange {
380                        start: position_of(cap.node.start_position()),
381                        end: position_of(cap.node.end_position()),
382                    },
383                ));
384            }
385            match_index = match_index.saturating_add(1);
386        }
387        out
388    }
389}
390
391impl NodeResource {
392    fn tree(&self) -> Option<&Tree> {
393        self.snapshot.tree()
394    }
395
396    /// The node's root-relative child-index path — read by the host `reset`
397    /// binding to reposition a cursor onto this node.
398    pub fn path(&self) -> &[u32] {
399        &self.path
400    }
401
402    fn with_path(&self, path: Vec<u32>) -> NodeResource {
403        NodeResource {
404            snapshot: Arc::clone(&self.snapshot),
405            path,
406            meta: OnceLock::new(),
407            children: OnceLock::new(),
408        }
409    }
410
411    /// `with_path`, plus the metadata the caller already holds — so stepping
412    /// into a child resolves nothing.
413    fn with_path_and_meta(&self, path: Vec<u32>, meta: NodeMeta) -> NodeResource {
414        NodeResource {
415            snapshot: Arc::clone(&self.snapshot),
416            path,
417            meta: OnceLock::from(meta),
418            children: OnceLock::new(),
419        }
420    }
421
422    /// This node's own facts, resolved at most once.
423    fn meta(&self) -> Option<&NodeMeta> {
424        if let Some(m) = self.meta.get() {
425            return Some(m);
426        }
427        let node = self.tree().and_then(|t| resolve(t, &self.path))?;
428        Some(self.meta.get_or_init(|| meta_of(node)))
429    }
430
431    /// Every child, in one `TreeCursor` pass. O(k) once, then cached.
432    fn children(&self) -> &Children {
433        self.children.get_or_init(|| {
434            let Some(node) = self.tree().and_then(|t| resolve(t, &self.path)) else {
435                return Arc::new(Children {
436                    all: Vec::new(),
437                    named: Vec::new(),
438                });
439            };
440            let mut cursor = node.walk();
441            let mut all: Vec<ChildMeta> = Vec::with_capacity(node.child_count());
442            let mut named: Vec<u32> = Vec::with_capacity(node.named_child_count());
443            if cursor.goto_first_child() {
444                let mut index = 0u32;
445                loop {
446                    let meta = meta_of(cursor.node());
447                    if meta.is_named {
448                        named.push(all.len() as u32);
449                    }
450                    all.push(ChildMeta {
451                        index,
452                        field: cursor.field_name().map(Arc::from),
453                        meta,
454                    });
455                    index = index.saturating_add(1);
456                    if !cursor.goto_next_sibling() {
457                        break;
458                    }
459                }
460            }
461            Arc::new(Children { all, named })
462        })
463    }
464
465    /// The node's grammar kind (empty string if the path can't resolve — never
466    /// panics; an immutable snapshot always resolves).
467    pub fn kind(&self) -> String {
468        self.meta().map(|m| m.kind.to_string()).unwrap_or_default()
469    }
470
471    pub fn is_named(&self) -> bool {
472        self.meta().map(|m| m.is_named).unwrap_or(false)
473    }
474
475    pub fn is_error(&self) -> bool {
476        self.meta().map(|m| m.is_error).unwrap_or(false)
477    }
478
479    /// The node's `[start, end)` span as byte-columns per line (matching the
480    /// native structural objects' `ProtoRange`). A zero range if unresolved.
481    pub fn byte_range(&self) -> NativeRange {
482        let z = Position { line: 0, byte: 0 };
483        self.meta()
484            .map(|m| m.range)
485            .unwrap_or(NativeRange { start: z, end: z })
486    }
487
488    /// The parent node, or `None` at the root.
489    pub fn parent(&self) -> Option<NodeResource> {
490        if self.path.is_empty() {
491            return None;
492        }
493        let mut path = self.path.clone();
494        path.pop();
495        Some(self.with_path(path))
496    }
497
498    pub fn named_child_count(&self) -> u32 {
499        self.children().named.len() as u32
500    }
501
502    /// The `index`-th NAMED child, mapped to its `child` (all-children) index so
503    /// the path stays in one indexing scheme.
504    pub fn named_child(&self, index: u32) -> Option<NodeResource> {
505        let children = self.children();
506        let at = *children.named.get(index as usize)? as usize;
507        let child = children.all.get(at)?;
508        let mut path = self.path.clone();
509        path.push(child.index);
510        Some(self.with_path_and_meta(path, child.meta.clone()))
511    }
512
513    /// The child under grammar field `name` (e.g. `"body"`), or `None`.
514    pub fn child_by_field(&self, name: &str) -> Option<NodeResource> {
515        let child = self
516            .children()
517            .all
518            .iter()
519            .find(|c| c.field.as_deref() == Some(name))?;
520        let mut path = self.path.clone();
521        path.push(child.index);
522        Some(self.with_path_and_meta(path, child.meta.clone()))
523    }
524
525    pub fn next_named_sibling(&self) -> Option<NodeResource> {
526        self.named_sibling(true)
527    }
528
529    pub fn prev_named_sibling(&self) -> Option<NodeResource> {
530        self.named_sibling(false)
531    }
532
533    /// Scan the parent's children after (`forward`) or before this node for the
534    /// nearest named sibling. The root has no siblings.
535    fn named_sibling(&self, forward: bool) -> Option<NodeResource> {
536        let (&last, parent_path) = self.path.split_last()?;
537        let tree = self.tree()?;
538        let parent = resolve(tree, parent_path)?;
539        let cur = last as usize;
540        let candidate = if forward {
541            (cur + 1..parent.child_count()).find(|&i| {
542                parent
543                    .child(i as u32)
544                    .map(|c| c.is_named())
545                    .unwrap_or(false)
546            })
547        } else {
548            (0..cur).rev().find(|&i| {
549                parent
550                    .child(i as u32)
551                    .map(|c| c.is_named())
552                    .unwrap_or(false)
553            })
554        };
555        candidate.map(|i| {
556            let mut path = parent_path.to_vec();
557            path.push(i as u32);
558            self.with_path(path)
559        })
560    }
561
562    /// TS.2: a walk cursor positioned at this node.
563    pub fn walk(&self) -> CursorResource {
564        CursorResource {
565            snapshot: Arc::clone(&self.snapshot),
566            path: self.path.clone(),
567        }
568    }
569}
570
571impl CursorResource {
572    fn tree(&self) -> Option<&Tree> {
573        self.snapshot.tree()
574    }
575
576    /// The node the cursor currently sits on.
577    pub fn current_node(&self) -> NodeResource {
578        NodeResource {
579            snapshot: Arc::clone(&self.snapshot),
580            path: self.path.clone(),
581            meta: OnceLock::new(),
582            children: OnceLock::new(),
583        }
584    }
585
586    /// The grammar field of the current node relative to its parent, or `None`
587    /// (root, or a child in no named field slot).
588    pub fn current_field(&self) -> Option<String> {
589        let (&last, parent_path) = self.path.split_last()?;
590        let tree = self.tree()?;
591        let parent = resolve(tree, parent_path)?;
592        parent.field_name_for_child(last).map(str::to_string)
593    }
594
595    /// Move to the first NAMED child; `false` (no move) if there is none.
596    pub fn goto_first_named_child(&mut self) -> bool {
597        let Some(tree) = self.tree() else {
598            return false;
599        };
600        let Some(node) = resolve(tree, &self.path) else {
601            return false;
602        };
603        for i in 0..node.child_count() {
604            if node.child(i as u32).map(|c| c.is_named()).unwrap_or(false) {
605                self.path.push(i as u32);
606                return true;
607            }
608        }
609        false
610    }
611
612    /// Move to the next NAMED sibling; `false` (no move) if there is none.
613    pub fn goto_next_named_sibling(&mut self) -> bool {
614        let Some((&last, parent_path)) = self.path.split_last() else {
615            return false;
616        };
617        let Some(tree) = self.tree() else {
618            return false;
619        };
620        let Some(parent) = resolve(tree, parent_path) else {
621            return false;
622        };
623        for i in (last as usize + 1)..parent.child_count() {
624            if parent
625                .child(i as u32)
626                .map(|c| c.is_named())
627                .unwrap_or(false)
628            {
629                let plen = self.path.len();
630                self.path[plen - 1] = i as u32;
631                return true;
632            }
633        }
634        false
635    }
636
637    /// Move to the parent; `false` (no move) at the root.
638    pub fn goto_parent(&mut self) -> bool {
639        if self.path.is_empty() {
640            return false;
641        }
642        self.path.pop();
643        true
644    }
645
646    /// Reposition onto `node` (assumed a node of the same snapshot).
647    pub fn reset(&mut self, node: &NodeResource) {
648        self.reset_to_path(node.path.clone());
649    }
650
651    /// Reposition onto the given root-relative child-index path. Used by the
652    /// host `reset` binding, which reads the target node's path from the resource
653    /// table (avoiding a simultaneous borrow of the cursor and the node).
654    pub fn reset_to_path(&mut self, path: Vec<u32>) {
655        self.path = path;
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    #![allow(clippy::unwrap_used, clippy::panic)]
662
663    use super::*;
664    use lattice_syntax::{Lang, Syntax};
665
666    fn rust_snapshot(src: &str) -> Arc<SyntaxSnapshot> {
667        let mut syntax = Syntax::for_language(Lang::Rust).unwrap().unwrap();
668        syntax.parse(src);
669        Arc::new(syntax.snapshot_owned())
670    }
671
672    fn pos(line: u32, byte: u32) -> Position {
673        Position { line, byte }
674    }
675
676    #[test]
677    fn root_is_the_source_file_and_language_is_rust() {
678        let snap = rust_snapshot("fn main() {}\n");
679        let ts = TreeSnapshotResource::new(snap);
680        assert!(ts.has_tree());
681        assert_eq!(ts.language(), "rust");
682        assert_eq!(ts.root().kind(), "source_file");
683        assert!(ts.root().is_named());
684        assert!(ts.root().parent().is_none());
685    }
686
687    #[test]
688    fn node_at_resolves_the_smallest_named_node() {
689        // `fn main() { let x = 1; }` — cursor on `x`.
690        let src = "fn main() { let x = 1; }\n";
691        let ts = TreeSnapshotResource::new(rust_snapshot(src));
692        let x_col = src.find('x').unwrap() as u32;
693        let node = ts.node_at(pos(0, x_col)).unwrap();
694        // The identifier under the cursor.
695        assert_eq!(node.kind(), "identifier");
696        assert!(node.is_named());
697        let r = node.byte_range();
698        assert_eq!(r.start, pos(0, x_col));
699        assert_eq!(r.end, pos(0, x_col + 1));
700    }
701
702    #[test]
703    fn enclosing_finds_the_named_scope_by_kind() {
704        // Cursor inside the block; `enclosing` for `block` returns the `{ … }`.
705        let src = "fn main() { let x = 1; }\n";
706        let ts = TreeSnapshotResource::new(rust_snapshot(src));
707        let x_col = src.find('x').unwrap() as u32;
708        let block = ts.enclosing(pos(0, x_col), &["block".to_string()]).unwrap();
709        assert_eq!(block.kind(), "block");
710        let r = block.byte_range();
711        // The block spans the braces.
712        assert_eq!(r.start, pos(0, src.find('{').unwrap() as u32));
713        assert_eq!(r.end, pos(0, (src.rfind('}').unwrap() + 1) as u32));
714    }
715
716    #[test]
717    fn enclosing_with_no_matching_kind_is_none() {
718        let src = "fn main() {}\n";
719        let ts = TreeSnapshotResource::new(rust_snapshot(src));
720        assert!(
721            ts.enclosing(pos(0, 3), &["nonexistent_kind".to_string()])
722                .is_none()
723        );
724    }
725
726    #[test]
727    fn enclosing_empty_kinds_is_the_nearest_named() {
728        let src = "fn main() { let x = 1; }\n";
729        let ts = TreeSnapshotResource::new(rust_snapshot(src));
730        let x_col = src.find('x').unwrap() as u32;
731        let node = ts.enclosing(pos(0, x_col), &[]).unwrap();
732        assert_eq!(node.kind(), "identifier");
733    }
734
735    /// OA.0a: the cached children pass indexes two ways — `named` holds
736    /// positions into `all` — and the path must carry the ALL-children index.
737    /// Getting that mapping wrong would silently return the wrong node for
738    /// every tree with anonymous children, which is every tree.
739    #[test]
740    fn named_child_indexes_past_anonymous_children() {
741        // `fn main() {}` — the root's children include anonymous tokens, and
742        // `function_item`'s children interleave named and anonymous:
743        // `fn` (anon) `main` (named) `parameters` (named) `block` (named).
744        let ts = TreeSnapshotResource::new(rust_snapshot("fn main() {}\n"));
745        let func = ts.root().named_child(0).unwrap();
746
747        let named: Vec<String> = (0..func.named_child_count())
748            .filter_map(|i| func.named_child(i))
749            .map(|n| n.kind())
750            .collect();
751        assert_eq!(named, vec!["identifier", "parameters", "block"]);
752
753        // The `name` field is the same node as named child 0, reached the
754        // other way — so both index schemes agree.
755        let by_field = func.child_by_field("name").unwrap();
756        let by_index = func.named_child(0).unwrap();
757        assert_eq!(by_field.kind(), by_index.kind());
758        assert_eq!(by_field.byte_range(), by_index.byte_range());
759        assert_eq!(by_field.path(), by_index.path());
760
761        // A node reached through the cache still navigates: its parent is the
762        // node we started from, which is only true if the path is right.
763        assert_eq!(by_index.parent().unwrap().kind(), "function_item");
764    }
765
766    /// The caches must not change answers. Every accessor is compared against
767    /// a freshly-constructed resource for the same path, which resolves from
768    /// the root the way the uncached code did.
769    #[test]
770    fn cached_answers_match_a_fresh_resolve() {
771        let src = "fn a() { let x = 1; }\nfn b(y: u32) -> u32 { y }\n";
772        let ts = TreeSnapshotResource::new(rust_snapshot(src));
773        let root = ts.root();
774        for i in 0..root.named_child_count() {
775            let warmed = root.named_child(i).unwrap();
776            // Touch everything, so the resource answers from cache.
777            let (kind, range) = (warmed.kind(), warmed.byte_range());
778            let fresh = ts.node_at_path(warmed.path().to_vec());
779            assert_eq!(kind, fresh.kind());
780            assert_eq!(range, fresh.byte_range());
781            assert_eq!(warmed.is_named(), fresh.is_named());
782            assert_eq!(warmed.is_error(), fresh.is_error());
783            assert_eq!(warmed.named_child_count(), fresh.named_child_count());
784        }
785    }
786
787    /// OA.0a diagnostic. Mirrors what a guest tree walk actually does —
788    /// iterate every named child, read its kind and range, resolve one field
789    /// — and reports how the cost scales with the number of children.
790    ///
791    /// Run with:
792    ///   cargo test -p lattice-plugin-host node_api_scaling -- --ignored --nocapture
793    #[test]
794    #[ignore = "diagnostic probe; prints a scaling table rather than asserting"]
795    fn node_api_scaling() {
796        for n in [50usize, 100, 200, 400, 800] {
797            let src: String = (0..n)
798                .map(|i| format!("fn f{i}() {{ let x = {i}; }}\n"))
799                .collect();
800            let ts = TreeSnapshotResource::new(rust_snapshot(&src));
801            let root = ts.root();
802            let started = std::time::Instant::now();
803            let count = root.named_child_count();
804            let mut matched = 0;
805            for i in 0..count {
806                let Some(child) = root.named_child(i) else {
807                    continue;
808                };
809                if child.kind() == "function_item" {
810                    matched += 1;
811                }
812                let _ = child.byte_range();
813                let _ = child.child_by_field("name");
814            }
815            println!(
816                "  children={count:<5} -> {:>12?}  ({matched} matched)",
817                started.elapsed()
818            );
819        }
820    }
821
822    #[test]
823    fn named_child_navigation_and_count() {
824        // The source_file's first named child is the function_item.
825        let ts = TreeSnapshotResource::new(rust_snapshot("fn main() {}\n"));
826        let root = ts.root();
827        assert_eq!(root.named_child_count(), 1);
828        let func = root.named_child(0).unwrap();
829        assert_eq!(func.kind(), "function_item");
830        assert!(root.named_child(1).is_none());
831        // The function's parent is the root.
832        assert_eq!(func.parent().unwrap().kind(), "source_file");
833    }
834
835    #[test]
836    fn child_by_field_resolves_grammar_fields() {
837        // `function_item` has a `name` field (the identifier `main`).
838        let ts = TreeSnapshotResource::new(rust_snapshot("fn main() {}\n"));
839        let func = ts.root().named_child(0).unwrap();
840        let name = func.child_by_field("name").unwrap();
841        assert_eq!(name.kind(), "identifier");
842        assert_eq!(name.byte_range().start, pos(0, 3));
843        assert!(func.child_by_field("no_such_field").is_none());
844    }
845
846    #[test]
847    fn named_siblings_walk_in_both_directions() {
848        // Two statements in a block: `let a = 1;` and `let b = 2;`.
849        let src = "fn m() { let a = 1; let b = 2; }\n";
850        let ts = TreeSnapshotResource::new(rust_snapshot(src));
851        let a_col = src.find('a').unwrap() as u32;
852        // The `let_declaration` enclosing `a`.
853        let first = ts
854            .enclosing(pos(0, a_col), &["let_declaration".to_string()])
855            .unwrap();
856        let second = first.next_named_sibling().unwrap();
857        assert_eq!(second.kind(), "let_declaration");
858        // `b` is in the second declaration.
859        let b_col = src.find('b').unwrap() as u32;
860        assert!(second.byte_range().start.byte <= b_col);
861        // Walk back.
862        let back = second.prev_named_sibling().unwrap();
863        assert_eq!(back.byte_range().start, first.byte_range().start);
864        assert!(first.prev_named_sibling().is_none());
865    }
866
867    #[test]
868    fn compile_and_run_query_returns_predicate_filtered_captures() {
869        let src = "fn alpha() {}\nfn beta() {}\n";
870        let ts = TreeSnapshotResource::new(rust_snapshot(src));
871        let q = ts
872            .compile_query("(function_item name: (identifier) @fname)")
873            .expect("valid query compiles");
874        let caps = ts.run_query(&q, None);
875        assert_eq!(caps.len(), 2, "both functions captured");
876        assert!(caps.iter().all(|(name, _)| name == "fname"));
877        // The captured nodes are the identifiers `alpha` / `beta`.
878        assert_eq!(caps[0].1.kind(), "identifier");
879        let starts: Vec<u32> = caps
880            .iter()
881            .map(|(_, n)| n.byte_range().start.byte)
882            .collect();
883        assert_eq!(starts, vec![3, 3]); // both at column 3 on their lines
884    }
885
886    #[test]
887    fn run_query_honors_a_text_predicate() {
888        let src = "fn alpha() {}\nfn beta() {}\n";
889        let ts = TreeSnapshotResource::new(rust_snapshot(src));
890        // `#eq?` predicate — only the function named exactly `beta` survives.
891        let q = ts
892            .compile_query("((function_item name: (identifier) @fname) (#eq? @fname \"beta\"))")
893            .expect("valid predicated query compiles");
894        let caps = ts.run_query(&q, None);
895        assert_eq!(caps.len(), 1, "the #eq? predicate is evaluated host-side");
896        assert_eq!(caps[0].1.byte_range().start.line, 1);
897    }
898
899    /// TS.2b: the ranges API must report the SAME extents as the node API.
900    ///
901    /// Two derivations of "where is this capture" is exactly the drift the
902    /// plugin would silently inherit — a strip drawn from the wrong lines is
903    /// indistinguishable from a strip drawn from the wrong scopes.
904    #[test]
905    fn run_query_ranges_agrees_with_run_query_extents() {
906        let src = "fn alpha() {}\nfn beta() {}\n";
907        let ts = TreeSnapshotResource::new(rust_snapshot(src));
908        let q = ts
909            .compile_query("(function_item name: (identifier) @fname)")
910            .expect("valid query compiles");
911
912        let nodes = ts.run_query(&q, None);
913        let ranges = ts.run_query_ranges(&q, None);
914
915        assert_eq!(ranges.len(), nodes.len());
916        for ((nname, node), (rname, _, range)) in nodes.iter().zip(ranges.iter()) {
917            assert_eq!(nname, rname);
918            let nr = node.byte_range();
919            assert_eq!(
920                (range.start.line, range.start.byte),
921                (nr.start.line, nr.start.byte)
922            );
923            assert_eq!((range.end.line, range.end.byte), (nr.end.line, nr.end.byte));
924        }
925    }
926
927    /// Captures from one pattern match share a `match_index`; captures from
928    /// different matches do not. This is what lets a query pair a construct
929    /// with its body (`@context` + `@context.end`) in one pass — without it
930    /// the guest would need a containment test, which is ambiguous for nested
931    /// constructs.
932    #[test]
933    fn run_query_ranges_groups_captures_by_match() {
934        let src = "fn alpha() {\n  let x = 1;\n}\nfn beta() {}\n";
935        let ts = TreeSnapshotResource::new(rust_snapshot(src));
936        let q = ts
937            .compile_query("(function_item name: (identifier) @fname body: (_) @fbody)")
938            .expect("valid two-capture query compiles");
939
940        let caps = ts.run_query_ranges(&q, None);
941        assert_eq!(caps.len(), 4, "two functions x two captures");
942
943        // The name and the body of ONE function carry one index.
944        let alpha: Vec<&(String, u32, NativeRange)> =
945            caps.iter().filter(|c| c.1 == caps[0].1).collect();
946        assert_eq!(alpha.len(), 2);
947        let mut names: Vec<&str> = alpha.iter().map(|c| c.0.as_str()).collect();
948        names.sort();
949        assert_eq!(names, vec!["fbody", "fname"]);
950
951        // ... and the second function a different one.
952        assert!(
953            caps.iter().any(|c| c.1 != caps[0].1),
954            "the second match must not share the first match's index"
955        );
956    }
957
958    #[test]
959    fn compile_query_rejects_a_malformed_query() {
960        let ts = TreeSnapshotResource::new(rust_snapshot("fn m() {}\n"));
961        let result = ts.compile_query("(this is not a valid query");
962        assert!(
963            matches!(&result, Err(msg) if !msg.is_empty()),
964            "malformed query is a typed error"
965        );
966    }
967
968    #[test]
969    fn cursor_walks_the_tree() {
970        let src = "fn m() { let x = 1; }\n";
971        let ts = TreeSnapshotResource::new(rust_snapshot(src));
972        let mut cursor = ts.root().walk();
973        assert_eq!(cursor.current_node().kind(), "source_file");
974        // Down into the function_item — it sits in no named field of source_file.
975        assert!(cursor.goto_first_named_child());
976        assert_eq!(cursor.current_node().kind(), "function_item");
977        assert_eq!(cursor.current_field(), None);
978        // Back up to the root; no parent beyond.
979        assert!(cursor.goto_parent());
980        assert_eq!(cursor.current_node().kind(), "source_file");
981        assert!(!cursor.goto_parent());
982        // No named siblings at the root's single child level after reset.
983        cursor.reset(&ts.root());
984        assert!(cursor.goto_first_named_child());
985        assert!(!cursor.goto_next_named_sibling(), "one top-level item");
986    }
987
988    #[test]
989    fn cursor_current_field_reports_the_grammar_field() {
990        let ts = TreeSnapshotResource::new(rust_snapshot("fn m() {}\n"));
991        // Navigate to the function's `name` child and check the field.
992        let func = ts.root().named_child(0).unwrap();
993        let name = func.child_by_field("name").unwrap();
994        let mut cursor = name.walk();
995        assert_eq!(cursor.current_field(), Some("name".to_string()));
996        cursor.goto_parent();
997        assert_eq!(cursor.current_node().kind(), "function_item");
998    }
999
1000    #[test]
1001    fn run_query_for_a_different_language_is_empty() {
1002        // A query compiled against Rust, run on a Rust snapshot, is fine; the
1003        // language guard only trips on a genuine mismatch, which we can't easily
1004        // construct here (one language per snapshot) — so assert the same-language
1005        // path yields matches (guard does not false-trip).
1006        let ts = TreeSnapshotResource::new(rust_snapshot("fn m() {}\n"));
1007        let q = ts.compile_query("(identifier) @id").unwrap();
1008        assert!(!ts.run_query(&q, None).is_empty());
1009    }
1010
1011    #[test]
1012    fn no_tree_snapshot_reports_absent() {
1013        // A snapshot taken before the first parse has no tree — the trampoline
1014        // would pass `none` (parse pending; `Lang::Plain` has no grammar at all).
1015        let snap = Arc::new(
1016            Syntax::for_language(Lang::Rust)
1017                .unwrap()
1018                .unwrap()
1019                .snapshot_owned(),
1020        );
1021        let ts = TreeSnapshotResource::new(snap);
1022        assert!(!ts.has_tree());
1023        assert!(ts.node_at(pos(0, 0)).is_none());
1024    }
1025}