Skip to main content

comfit_core/
document.rs

1//! Source documents that reuse syntax trees and cache highlighting.
2
3use crate::{
4    Error, HighlightConfig, HighlightResult, backend::Backend, highlight, language::Registry,
5    syntax::Syntax,
6};
7
8pub use crate::syntax::Edit;
9
10/// A source document that shares compiled queries with its language registry.
11///
12/// The document keeps the languages loaded at creation. Source updates reuse the
13/// root tree and compatible embedded-language trees. Highlighting queries rerun
14/// only when the source or rainbow option changes. Dropping the document releases
15/// its trees.
16pub struct Document<B: Backend> {
17    backend: B,
18    registry: Registry<B>,
19    language: String,
20    source: String,
21    syntax: Option<Syntax<B>>,
22    cached: Option<(bool, HighlightResult)>,
23}
24
25impl<B: Backend> Document<B> {
26    pub(crate) fn new(
27        registry: Registry<B>,
28        mut backend: B,
29        name: &str,
30        source: &str,
31    ) -> Result<Self, Error> {
32        let language = registry.resolve(name)?.to_owned();
33        let syntax = Syntax::parse(&mut backend, &registry, &language, source, None, None)?;
34        Ok(Self {
35            backend,
36            registry,
37            language,
38            source: source.to_owned(),
39            syntax: Some(syntax),
40            cached: None,
41        })
42    }
43
44    /// Return the current source.
45    pub fn source(&self) -> &str {
46        &self.source
47    }
48
49    /// Replace the source and reuse compatible trees with a UTF-8-safe edit.
50    /// If the source is unchanged, do nothing. A failed parse leaves the source unchanged.
51    /// The source buffer retains its capacity for subsequent updates.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if parsing or query compilation fails, or injection nesting is too deep.
56    pub fn update(&mut self, source: &str) -> Result<(), Error> {
57        if self.source == source {
58            return Ok(());
59        }
60        let edit = Edit::between(&self.source, source);
61        let syntax = Syntax::parse(
62            &mut self.backend,
63            &self.registry,
64            &self.language,
65            source,
66            self.syntax.take(),
67            Some(&edit),
68        )?;
69        self.source.clear();
70        self.source.push_str(source);
71        self.syntax = Some(syntax);
72        self.cached = None;
73        Ok(())
74    }
75
76    /// Return an owned snapshot. Changes to line emphasis reuse cached highlighting.
77    /// Snapshots remain valid after the document is edited or dropped.
78    pub fn highlight(&mut self, config: &HighlightConfig) -> Result<HighlightResult, Error> {
79        Ok(self.highlight_ref(config)?.clone())
80    }
81
82    /// Borrow cached highlighting without cloning tokens.
83    /// The source cannot be updated while the output is borrowed.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if parsing or highlighting fails.
88    pub fn highlight_ref(&mut self, config: &HighlightConfig) -> Result<&HighlightResult, Error> {
89        if self
90            .cached
91            .as_ref()
92            .is_none_or(|(rainbow, _)| *rainbow != config.rainbow)
93        {
94            if self.syntax.is_none() {
95                self.syntax = Some(Syntax::parse(
96                    &mut self.backend,
97                    &self.registry,
98                    &self.language,
99                    &self.source,
100                    None,
101                    None,
102                )?);
103            }
104            let highlighted = highlight::run(
105                &mut self.backend,
106                &self.registry,
107                &self.source,
108                self.syntax.as_ref().expect("parsed above"),
109                config,
110            )?;
111            self.cached = Some((config.rainbow, highlighted.to_owned()));
112        }
113        let result = &mut self.cached.as_mut().expect("highlighted above").1;
114        let mut selected = config.highlighted_lines.clone();
115        selected.sort_unstable();
116        let mut selected = selected.into_iter().peekable();
117
118        for line in &mut result.lines {
119            while selected.peek().is_some_and(|number| *number < line.number) {
120                selected.next();
121            }
122            line.highlighted = selected.peek() == Some(&line.number);
123        }
124        Ok(result)
125    }
126}