Skip to main content

clankerdiff_syntax/
incremental.rs

1use crate::{SyntaxError, language::resolve_language};
2use arborium_highlight::{Injection, Span};
3use arborium_tree_sitter::{
4    InputEdit, Language, Node, Parser, Point, Query, QueryCursor, StreamingIterator, Tree,
5};
6use std::{
7    collections::{BTreeMap, HashMap},
8    fmt,
9    sync::Arc,
10};
11
12/// Bytes handed to Tree-sitter per input callback. `parser_input_bytes` counts
13/// these requests, so smaller chunks measure re-lexing more precisely.
14const PARSER_CHUNK_BYTES: usize = 64;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub struct SyntaxWorkStats {
18    pub parser_input_bytes: usize,
19    pub queried_bytes: usize,
20    pub full_parses: usize,
21    pub incremental_parses: usize,
22    pub projected_bytes: usize,
23    pub projected_lines: usize,
24    pub reused_lines: usize,
25    pub compared_nodes: usize,
26}
27
28#[derive(Default)]
29pub(crate) struct Grammars(HashMap<String, Arc<Grammar>>);
30
31/// Shared state for one append across a document and its injections.
32pub(crate) struct AppendContext<'a> {
33    pub grammars: &'a mut Grammars,
34    pub stats: &'a mut SyntaxWorkStats,
35    /// Line starts of the root stream, for Tree-sitter edit positions.
36    pub line_starts: &'a [usize],
37}
38
39pub(crate) struct IncrementalDocument {
40    parser: Option<Parser>,
41    tree: Option<Tree>,
42    grammar: Arc<Grammar>,
43    length: usize,
44    spans: BTreeMap<usize, Vec<Span>>,
45    injections: BTreeMap<usize, Vec<InjectedDocument>>,
46    boundaries: BTreeMap<usize, usize>,
47}
48
49impl Grammars {
50    /// Creates an empty document, or `None` when no grammar is bundled.
51    pub(crate) fn document(
52        &mut self,
53        language: &str,
54    ) -> Result<Option<IncrementalDocument>, SyntaxError> {
55        let grammar = if let Some(grammar) = self.0.get(language) {
56            Arc::clone(grammar)
57        } else {
58            let Some((language_fn, highlights, injections)) = grammar_spec(language) else {
59                return Ok(None);
60            };
61            let compile = |source| {
62                Query::new(&language_fn, source).map_err(|source| SyntaxError::Query {
63                    language: language.to_owned(),
64                    source,
65                })
66            };
67            let query_source = format!("{highlights}\n{injections}");
68            let highlights = compile(highlights)?;
69            let injections = compile(injections)?;
70            let non_local = [&highlights, &injections].into_iter().any(|query| {
71                (0..query.pattern_count()).any(|index| query.is_pattern_non_local(index))
72            });
73            let grammar = Arc::new(Grammar {
74                highlights,
75                injections,
76                language: language_fn,
77                non_local,
78                query_source,
79            });
80            self.0.insert(language.to_owned(), Arc::clone(&grammar));
81            grammar
82        };
83        Ok(Some(IncrementalDocument {
84            parser: None,
85            tree: None,
86            grammar,
87            length: 0,
88            spans: BTreeMap::new(),
89            injections: BTreeMap::new(),
90            boundaries: BTreeMap::new(),
91        }))
92    }
93}
94
95impl IncrementalDocument {
96    /// Appends the bytes of `source` beyond the previous length. `base` is the
97    /// document's offset within the root stream.
98    pub(crate) fn append(
99        &mut self,
100        source: &str,
101        base: usize,
102        depth: usize,
103        cx: &mut AppendContext<'_>,
104    ) -> Result<usize, SyntaxError> {
105        if source.len() == self.length && self.tree.is_some() {
106            return Ok(source.len());
107        }
108        let tree = self.parse_tree(source, base, cx)?;
109        let mut start = if self.tree.is_none() || self.grammar.non_local {
110            0
111        } else {
112            self.length
113        };
114        if let Some(previous) = &self.tree {
115            for change in previous.changed_ranges(&tree) {
116                let refined = if change.start_byte == 0 {
117                    self.grammar
118                        .error_prefix(previous.root_node(), tree.root_node(), cx.stats)
119                } else {
120                    None
121                };
122                start = start.min(refined.unwrap_or(change.start_byte));
123            }
124        }
125        // A capture ending exactly at the edit point may be an unterminated
126        // token that the appended text extends, so widen to it as well.
127        loop {
128            let earlier = self
129                .boundaries
130                .range(start..)
131                .map(|(_, &begin)| begin)
132                .min()
133                .unwrap_or(start);
134            if earlier >= start {
135                break;
136            }
137            start = earlier;
138        }
139        let (spans, injections) = loop {
140            let result = query(&self.grammar, &tree, source, start, cx.stats);
141            let capture_start = result
142                .0
143                .iter()
144                .map(|span| span.start as usize)
145                .chain(result.1.iter().map(|injection| injection.start as usize))
146                .min()
147                .unwrap_or(start);
148            if capture_start < start {
149                start = capture_start;
150            } else {
151                break result;
152            }
153        };
154        self.boundaries.split_off(&start.saturating_add(1));
155        for (from, to) in spans.iter().map(|span| (span.start, span.end)).chain(
156            injections
157                .iter()
158                .map(|injection| (injection.start, injection.end)),
159        ) {
160            self.boundaries
161                .entry(to as usize)
162                .and_modify(|begin| *begin = (*begin).min(from as usize))
163                .or_insert(from as usize);
164        }
165        self.spans.split_off(&start);
166        for span in spans {
167            self.spans
168                .entry(span.start as usize)
169                .or_default()
170                .push(span);
171        }
172        self.update_injections(source, base, start, injections, depth, cx)?;
173        self.tree = Some(tree);
174        self.length = source.len();
175        Ok(start)
176    }
177
178    fn parse_tree(
179        &mut self,
180        source: &str,
181        base: usize,
182        cx: &mut AppendContext<'_>,
183    ) -> Result<Tree, SyntaxError> {
184        if self.parser.is_none() {
185            let mut parser = Parser::new();
186            parser.set_language(&self.grammar.language)?;
187            self.parser = Some(parser);
188        }
189        if let Some(tree) = &mut self.tree {
190            let old_end = end_point(cx.line_starts, base, self.length);
191            tree.edit(&InputEdit {
192                start_byte: self.length,
193                old_end_byte: self.length,
194                new_end_byte: source.len(),
195                start_position: old_end,
196                old_end_position: old_end,
197                new_end_position: end_point(cx.line_starts, base, source.len()),
198            });
199            cx.stats.incremental_parses += 1;
200        } else {
201            cx.stats.full_parses += 1;
202        }
203        let bytes = source.as_bytes();
204        let stats = &mut *cx.stats;
205        self.parser
206            .as_mut()
207            .expect("initialized parser")
208            .parse_with_options(
209                &mut |offset, _| {
210                    let end = offset.saturating_add(PARSER_CHUNK_BYTES).min(bytes.len());
211                    let input = &bytes[offset.min(bytes.len())..end];
212                    stats.parser_input_bytes += input.len();
213                    input
214                },
215                self.tree.as_ref(),
216                None,
217            )
218            .ok_or(SyntaxError::NoTree)
219    }
220
221    fn update_injections(
222        &mut self,
223        source: &str,
224        base: usize,
225        start: usize,
226        injections: Vec<Injection>,
227        depth: usize,
228        cx: &mut AppendContext<'_>,
229    ) -> Result<(), SyntaxError> {
230        let mut previous_injections = self.injections.split_off(&start);
231        if depth > 0 {
232            for injection in injections {
233                let from = injection.start as usize;
234                let to = injection.end as usize;
235                let Some(text) = source.get(from..to).filter(|text| !text.is_empty()) else {
236                    continue;
237                };
238                let language = resolve_language(injection.language.as_str(), text)
239                    .unwrap_or(&injection.language);
240                let previous = previous_injections.get_mut(&from).and_then(|entries| {
241                    entries
242                        .iter()
243                        .position(|entry| {
244                            entry.language == language && text.len() >= entry.document.length
245                        })
246                        .map(|index| entries.swap_remove(index))
247                });
248                let mut entry = match previous {
249                    Some(entry) => entry,
250                    None => match cx.grammars.document(language)? {
251                        Some(document) => InjectedDocument {
252                            language: language.to_owned(),
253                            document,
254                        },
255                        None => continue,
256                    },
257                };
258                entry.document.append(text, base + from, depth - 1, cx)?;
259                self.injections.entry(from).or_default().push(entry);
260            }
261        }
262        Ok(())
263    }
264
265    pub(crate) fn spans_from(&self, start: usize) -> Vec<Span> {
266        let mut result: Vec<_> = self
267            .spans
268            .range(start..)
269            .flat_map(|(_, spans)| spans.iter().cloned())
270            .collect();
271        for (&offset, injections) in self.injections.range(start..) {
272            for injection in injections {
273                for mut span in injection.document.spans_from(0) {
274                    span.start += u32::try_from(offset).expect("source limit fits u32");
275                    span.end += u32::try_from(offset).expect("source limit fits u32");
276                    result.push(span);
277                }
278            }
279        }
280        result
281    }
282}
283
284impl Clone for IncrementalDocument {
285    fn clone(&self) -> Self {
286        Self {
287            parser: None,
288            tree: self.tree.clone(),
289            grammar: Arc::clone(&self.grammar),
290            length: self.length,
291            spans: self.spans.clone(),
292            injections: self.injections.clone(),
293            boundaries: self.boundaries.clone(),
294        }
295    }
296}
297
298impl fmt::Debug for IncrementalDocument {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.debug_struct("IncrementalDocument")
301            .field("length", &self.length)
302            .finish_non_exhaustive()
303    }
304}
305
306#[derive(Clone)]
307struct InjectedDocument {
308    language: String,
309    document: IncrementalDocument,
310}
311
312struct Grammar {
313    language: Language,
314    highlights: Query,
315    injections: Query,
316    non_local: bool,
317    query_source: String,
318}
319
320impl Grammar {
321    fn error_prefix(
322        &self,
323        before: Node<'_>,
324        after: Node<'_>,
325        stats: &mut SyntaxWorkStats,
326    ) -> Option<usize> {
327        if self.non_local
328            || self.query_source.contains("ERROR")
329            || self.query_source.contains("(_ ")
330            || self.query_source.contains("(_\n")
331        {
332            return None;
333        }
334        let before = self.error_root(before)?;
335        let after = self.error_root(after)?;
336        let mut end = before.start_byte().min(after.start_byte());
337        let mut left = before.walk();
338        let mut right = after.walk();
339        for (before, after) in before.children(&mut left).zip(after.children(&mut right)) {
340            if !same_subtree(before, after, stats) {
341                break;
342            }
343            end = before.end_byte().min(after.end_byte());
344        }
345        Some(end)
346    }
347
348    fn error_root<'a>(&self, node: Node<'a>) -> Option<Node<'a>> {
349        if node.is_error() {
350            Some(node)
351        } else if !self.query_source.contains(node.kind()) && node.child_count() == 1 {
352            node.child(0).filter(Node::is_error)
353        } else {
354            None
355        }
356    }
357}
358
359fn same_subtree(before: Node<'_>, after: Node<'_>, stats: &mut SyntaxWorkStats) -> bool {
360    stats.compared_nodes += 1;
361    if before.byte_range() != after.byte_range() || before.has_changes() || after.has_changes() {
362        return false;
363    }
364    if before.id() == after.id() {
365        return true;
366    }
367    if before.kind_id() != after.kind_id() || before.child_count() != after.child_count() {
368        return false;
369    }
370    let mut left = before.walk();
371    let mut right = after.walk();
372    before
373        .children(&mut left)
374        .zip(after.children(&mut right))
375        .enumerate()
376        .all(|(index, (left, right))| {
377            let Ok(index) = u32::try_from(index) else {
378                return false;
379            };
380            before.field_name_for_child(index) == after.field_name_for_child(index)
381                && same_subtree(left, right, stats)
382        })
383}
384
385/// Row and column of byte `len` within a document that starts at `base` in
386/// the root stream whose line starts are `line_starts`.
387fn end_point(line_starts: &[usize], base: usize, len: usize) -> Point {
388    let first = line_starts.partition_point(|&start| start <= base);
389    let last = line_starts.partition_point(|&start| start <= base + len);
390    let column = line_starts[first..last]
391        .last()
392        .map_or(len, |&start| base + len - start);
393    Point::new(last - first, column)
394}
395
396fn query(
397    grammar: &Grammar,
398    tree: &Tree,
399    source: &str,
400    start: usize,
401    stats: &mut SyntaxWorkStats,
402) -> (Vec<Span>, Vec<Injection>) {
403    let mut cursor = QueryCursor::new();
404    cursor.set_byte_range(start..source.len());
405    stats.queried_bytes += source.len() - start;
406    let mut matches = cursor.matches(&grammar.highlights, tree.root_node(), source.as_bytes());
407    let mut spans = Vec::new();
408    while let Some(found) = matches.next() {
409        for capture in found.captures {
410            let name = grammar.highlights.capture_names()[capture.index as usize];
411            if name.starts_with('_') || name.starts_with("injection.") {
412                continue;
413            }
414            spans.push(Span {
415                start: u32::try_from(capture.node.start_byte()).expect("source limit fits u32"),
416                end: u32::try_from(capture.node.end_byte()).expect("source limit fits u32"),
417                capture: name.to_owned(),
418                pattern_index: u32::try_from(found.pattern_index).expect("query pattern fits u32"),
419            });
420        }
421    }
422    stats.queried_bytes += source.len() - start;
423    let mut matches = cursor.matches(&grammar.injections, tree.root_node(), source.as_bytes());
424    let mut injections = Vec::new();
425    while let Some(found) = matches.next() {
426        let mut content = None;
427        let mut language = None;
428        let mut include_children = false;
429        for property in grammar.injections.property_settings(found.pattern_index) {
430            match property.key.as_ref() {
431                "injection.language" => language = property.value.as_deref().map(str::to_owned),
432                "injection.include-children" => include_children = true,
433                _ => {}
434            }
435        }
436        for capture in found.captures {
437            match grammar.injections.capture_names()[capture.index as usize] {
438                "injection.content" => content = Some(capture.node),
439                "injection.language" if language.is_none() => {
440                    language = capture
441                        .node
442                        .utf8_text(source.as_bytes())
443                        .ok()
444                        .map(str::to_owned);
445                }
446                _ => {}
447            }
448        }
449        if let (Some(node), Some(language)) = (content, language) {
450            injections.push(Injection {
451                start: u32::try_from(node.start_byte()).expect("source limit fits u32"),
452                end: u32::try_from(node.end_byte()).expect("source limit fits u32"),
453                language,
454                include_children,
455            });
456        }
457    }
458    (spans, injections)
459}
460
461fn grammar_spec(language: &str) -> Option<(Language, &'static str, &'static str)> {
462    macro_rules! grammar {
463        ($module:ident) => {{
464            use arborium::$module as grammar;
465            (
466                grammar::language().into(),
467                &grammar::HIGHLIGHTS_QUERY,
468                &grammar::INJECTIONS_QUERY,
469            )
470        }};
471    }
472    Some(match language {
473        "asm" => grammar!(lang_asm),
474        "bash" => grammar!(lang_bash),
475        "batch" => grammar!(lang_batch),
476        "c" => grammar!(lang_c),
477        "c-sharp" => grammar!(lang_c_sharp),
478        "clojure" => grammar!(lang_clojure),
479        "cmake" => grammar!(lang_cmake),
480        "commonlisp" => grammar!(lang_commonlisp),
481        "cpp" => grammar!(lang_cpp),
482        "css" => grammar!(lang_css),
483        "dart" => grammar!(lang_dart),
484        "diff" => grammar!(lang_diff),
485        "dockerfile" => grammar!(lang_dockerfile),
486        "elixir" => grammar!(lang_elixir),
487        "erlang" => grammar!(lang_erlang),
488        "fish" => grammar!(lang_fish),
489        "go" => grammar!(lang_go),
490        "graphql" => grammar!(lang_graphql),
491        "haskell" => grammar!(lang_haskell),
492        "hcl" => grammar!(lang_hcl),
493        "html" => grammar!(lang_html),
494        "ini" => grammar!(lang_ini),
495        "java" => grammar!(lang_java),
496        "javascript" => grammar!(lang_javascript),
497        "json" => grammar!(lang_json),
498        "just" => grammar!(lang_just),
499        "kotlin" => grammar!(lang_kotlin),
500        "lua" => grammar!(lang_lua),
501        "make" => grammar!(lang_make),
502        "markdown" => grammar!(lang_markdown),
503        "meson" => grammar!(lang_meson),
504        "ninja" => grammar!(lang_ninja),
505        "nix" => grammar!(lang_nix),
506        "objc" => grammar!(lang_objc),
507        "ocaml" => grammar!(lang_ocaml),
508        "perl" => grammar!(lang_perl),
509        "php" => grammar!(lang_php),
510        "powershell" => grammar!(lang_powershell),
511        "proto" => grammar!(lang_proto),
512        "python" => grammar!(lang_python),
513        "r" => grammar!(lang_r),
514        "rego" => grammar!(lang_rego),
515        "ruby" => grammar!(lang_ruby),
516        "rust" => grammar!(lang_rust),
517        "scala" => grammar!(lang_scala),
518        "scheme" => grammar!(lang_scheme),
519        "scss" => grammar!(lang_scss),
520        "solidity" => grammar!(lang_solidity),
521        "sql" => grammar!(lang_sql),
522        "starlark" => grammar!(lang_starlark),
523        "svelte" => grammar!(lang_svelte),
524        "swift" => grammar!(lang_swift),
525        "toml" => grammar!(lang_toml),
526        "tsx" => grammar!(lang_tsx),
527        "typescript" => grammar!(lang_typescript),
528        "vue" => grammar!(lang_vue),
529        "x86asm" => grammar!(lang_x86asm),
530        "xml" => grammar!(lang_xml),
531        "yaml" => grammar!(lang_yaml),
532        "zig" => grammar!(lang_zig),
533        "zsh" => grammar!(lang_zsh),
534        _ => return None,
535    })
536}