Skip to main content

lattice_plugin_host/
boundary_context.rs

1//! The sticky-context boundary conversions (treesitter-context.md, TC.2).
2//!
3//! Mirrors `lattice_cells::context::ContextScope` — the structural scopes a
4//! context provider produces. Two directions:
5//!
6//!   - **`ContextScope`** crosses **guest→host** (the producer's return): a
7//!     [`WitBoundary`] round-trip. Four `u32`s per scope and nothing else, which
8//!     is the point — a whole file's structure is a few tens of kB, and the tree
9//!     itself never crosses.
10//!   - **`context-request`** crosses **host→guest** (one-way, the
11//!     `project_decoration_context` precedent). The host has the buffer metadata
12//!     off the render path when it triggers the producer; the parse tree rides a
13//!     call-scoped `borrow<tree-snapshot>` alongside, not this record.
14
15use crate::WitBoundary;
16use crate::lattice::plugin_host::types::{
17    ContextRequest as WitContextRequest, ContextScope as WitContextScope,
18};
19use lattice_cells::context::ContextScope as NativeContextScope;
20
21impl WitBoundary for NativeContextScope {
22    type Wit = WitContextScope;
23
24    fn to_wit(&self) -> Result<WitContextScope, String> {
25        Ok(WitContextScope {
26            scope_start: self.scope_start,
27            scope_end: self.scope_end,
28            header_start: self.header_start,
29            header_end: self.header_end,
30        })
31    }
32
33    fn from_wit(wit: WitContextScope) -> Result<Self, String> {
34        Ok(NativeContextScope {
35            scope_start: wit.scope_start,
36            scope_end: wit.scope_end,
37            header_start: wit.header_start,
38            header_end: wit.header_end,
39        })
40    }
41}
42
43/// Build the owned `context-request` the host hands a producer (host→guest,
44/// one-way).
45///
46/// A non-UTF-8 path drops to `None` rather than failing the trigger: a context
47/// producer keys off the *tree*, not the path text, so an un-representable path
48/// costs it nothing. (Same call as `project_decoration_context` makes, for the
49/// same reason.)
50pub fn project_context_request(
51    buffer_id: u64,
52    path: Option<&std::path::Path>,
53    line_count: u32,
54) -> WitContextRequest {
55    WitContextRequest {
56        buffer_id,
57        path: path.and_then(|p| p.to_str().map(str::to_string)),
58        line_count,
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    #![allow(clippy::unwrap_used, clippy::panic)]
65
66    use super::*;
67
68    #[test]
69    fn context_scope_round_trips_all_four_lines() {
70        // Distinct values in every field: a transposition (header_start and
71        // scope_start swapped, say) is the plausible mistake here, and equal
72        // values would hide it.
73        let native = NativeContextScope {
74            scope_start: 10,
75            scope_end: 99,
76            header_start: 11,
77            header_end: 13,
78        };
79
80        let back = NativeContextScope::from_wit(native.to_wit().unwrap()).unwrap();
81
82        assert_eq!(back, native);
83    }
84
85    #[test]
86    fn context_request_projects_metadata() {
87        let req = project_context_request(9, Some(std::path::Path::new("src/lib.rs")), 240);
88        assert_eq!(req.buffer_id, 9);
89        assert_eq!(req.path.as_deref(), Some("src/lib.rs"));
90        assert_eq!(req.line_count, 240);
91
92        // A pathless (scratch) buffer projects `None` rather than failing.
93        let scratch = project_context_request(1, None, 0);
94        assert!(scratch.path.is_none());
95    }
96}