lattice_syntax/oneshot.rs
1//! D.3.b.2 (2026-05-29): one-shot synchronous highlight.
2//!
3//! Wraps `Syntax::for_language_with_registry` + `parse` +
4//! `snapshot().highlight_lines` into a single call so callers
5//! that don't own a long-lived `SyntaxHandle` (notably the
6//! diff overlay's deletion-block renderer, which parses
7//! an ephemeral baseline rope) can get styled spans without
8//! standing up an actor or wiring incremental state.
9//!
10//! No caching. Every call re-parses the source from scratch.
11//! The caller is expected to cache the returned spans
12//! alongside whatever revision it tracks for the source.
13//!
14//! See `docs/dev/architecture/diff-system.md` ยง6.6.
15
16use std::sync::Arc;
17
18use crate::{Lang, LangRegistry, StyledSpan, Syntax};
19
20/// Run a fresh tree-sitter parse over `source` and return
21/// per-line styled spans for lines in `start_line..end_line`.
22///
23/// Returns `None` when:
24/// - `lang` is `Lang::Plain` (no grammar to apply),
25/// - the registry has no registered grammar for `lang`,
26/// - the parse or highlight call fails for any reason
27/// (registry lookup error, malformed source, etc.) โ fail
28/// silently so the caller's "no syntax" fallback path
29/// (monochrome rendering) takes over without a panic.
30///
31/// `start_line` and `end_line` are inclusive-exclusive in
32/// line indices, matching
33/// [`crate::SyntaxSnapshot::highlight_lines`]. Returns one
34/// `Vec<StyledSpan>` per line in the range. Spans within
35/// each inner vec are line-relative byte offsets.
36///
37/// **Cost model.** Tree-sitter parse is O(source_len);
38/// highlight query walk is O(matches). For the diff
39/// deletion-block case where a typical baseline is hundreds
40/// to thousands of lines, both are well under 10 ms on
41/// modern hardware โ within
42/// `DiffOverlayRefreshTask`'s off-thread budget. Caller is
43/// expected to invoke at hunk-publish frequency (debounced
44/// at the session level), not per-frame.
45pub fn oneshot_highlight_lines(
46 lang: Lang,
47 registry: Arc<LangRegistry>,
48 source: &str,
49 start_line: u32,
50 end_line: u32,
51) -> Option<Vec<Vec<StyledSpan>>> {
52 let mut syntax = Syntax::for_language_with_registry(lang, registry)
53 .ok()
54 .flatten()?;
55 syntax.parse(source);
56 syntax.snapshot().highlight_lines(start_line, end_line).ok()
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn returns_none_for_plain_language() {
65 let registry = LangRegistry::standard().expect("standard registry");
66 let out = oneshot_highlight_lines(Lang::Plain, registry, "anything", 0, 1);
67 assert!(out.is_none());
68 }
69
70 #[test]
71 fn returns_some_for_rust_source_with_lines() {
72 let registry = LangRegistry::standard().expect("standard registry");
73 let source = "fn main() {\n let x = 1;\n}\n";
74 let out = oneshot_highlight_lines(Lang::Rust, registry, source, 0, 3);
75 let lines = out.expect("rust grammar registered");
76 // Three source lines requested.
77 assert_eq!(lines.len(), 3);
78 // First line "fn main() {" should produce at least
79 // one styled span (the `fn` keyword).
80 assert!(!lines[0].is_empty(), "expected styled spans on line 0");
81 }
82
83 #[test]
84 fn empty_range_returns_empty_vec() {
85 let registry = LangRegistry::standard().expect("standard registry");
86 let out = oneshot_highlight_lines(Lang::Rust, registry, "fn main() {}", 0, 0).expect("ok");
87 assert!(out.is_empty());
88 }
89
90 #[test]
91 fn handles_empty_source_gracefully() {
92 // Parsing an empty string shouldn't panic; the
93 // highlight call against an empty rope returns
94 // either an empty vec or one empty line โ either is
95 // acceptable for the caller's fallback path. Just
96 // assert the call doesn't return None for a known
97 // language.
98 let registry = LangRegistry::standard().expect("standard registry");
99 let out = oneshot_highlight_lines(Lang::Rust, registry, "", 0, 1);
100 assert!(out.is_some());
101 }
102}