Skip to main content

comfit_core/
output.rs

1//! Borrowed highlighting results and owned snapshots.
2
3use smol_str::SmolStr;
4
5/// Highlighting options used by the engine and renderers.
6#[derive(Debug, Clone)]
7pub struct HighlightConfig {
8    /// One-based source lines to emphasize.
9    pub highlighted_lines: Vec<usize>,
10    /// Collect bracket nesting depths from the language rainbow query. Defaults to `true`.
11    pub rainbow: bool,
12}
13
14impl Default for HighlightConfig {
15    fn default() -> Self {
16        Self {
17            highlighted_lines: Vec::new(),
18            rainbow: true,
19        }
20    }
21}
22
23/// Highlighted lines borrowing source text and query capture names.
24///
25/// Even an empty source produces one line. A trailing newline produces a final
26/// empty line. Tokens exclude newline separators.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Highlighted<'a> {
29    /// Lines in source order, starting at line one.
30    pub lines: Vec<Line<'a>>,
31}
32
33/// A source line and its tokens, without its newline separator.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Line<'a> {
36    /// One-based source line number.
37    pub number: usize,
38    /// Whether the caller selected this line for emphasis.
39    pub highlighted: bool,
40    /// Tokens in source order. Token boundaries may differ from syntax node boundaries.
41    pub tokens: Vec<Token<'a>>,
42}
43
44/// A borrowed source slice with an optional capture style and bracket depth.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Token<'a> {
47    /// Source text, without newline separators.
48    pub text: &'a str,
49    /// Query capture name, or `None` for unstyled text.
50    pub highlight: Option<&'a str>,
51    /// Zero-based nesting depth, independent of the rendering palette.
52    pub rainbow: Option<usize>,
53}
54
55/// An owned highlighting snapshot that remains valid after its document changes.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct HighlightResult {
58    /// Lines in source order, including a final empty line after a trailing newline.
59    pub lines: Vec<OwnedLine>,
60}
61
62/// An owned source line without its newline separator.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct OwnedLine {
65    /// One-based source line number.
66    pub number: usize,
67    /// Whether the caller selected this line for emphasis.
68    pub highlighted: bool,
69    /// Tokens in source order, without newline separators.
70    pub tokens: Vec<OwnedToken>,
71}
72
73/// Owned token text, capture name, and optional bracket depth.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct OwnedToken {
76    /// Source text, without newline separators.
77    pub text: String,
78    /// Query capture name, or `None` for unstyled text.
79    pub highlight: Option<SmolStr>,
80    /// Zero-based nesting depth, independent of the rendering palette.
81    pub rainbow: Option<usize>,
82}
83
84impl Highlighted<'_> {
85    /// Copy this borrowed output into an independent snapshot.
86    pub fn to_owned(&self) -> HighlightResult {
87        HighlightResult {
88            lines: self
89                .lines
90                .iter()
91                .map(|line| OwnedLine {
92                    number: line.number,
93                    highlighted: line.highlighted,
94                    tokens: line
95                        .tokens
96                        .iter()
97                        .map(|token| OwnedToken {
98                            text: token.text.to_owned(),
99                            highlight: token.highlight.map(SmolStr::new),
100                            rainbow: token.rainbow,
101                        })
102                        .collect(),
103                })
104                .collect(),
105        }
106    }
107}
108
109impl HighlightResult {
110    /// Create a borrowed view of this snapshot without parsing or running queries.
111    ///
112    /// The view borrows token strings and allocates new line and token vectors.
113    /// The view cannot outlive this snapshot.
114    pub fn as_highlighted(&self) -> Highlighted<'_> {
115        Highlighted {
116            lines: self
117                .lines
118                .iter()
119                .map(|line| Line {
120                    number: line.number,
121                    highlighted: line.highlighted,
122                    tokens: line
123                        .tokens
124                        .iter()
125                        .map(|token| Token {
126                            text: &token.text,
127                            highlight: token.highlight.as_deref(),
128                            rainbow: token.rainbow,
129                        })
130                        .collect(),
131                })
132                .collect(),
133        }
134    }
135
136    /// Create a borrowed view with different line emphasis, without re-highlighting.
137    pub fn with_highlighted_lines(&self, lines: &[usize]) -> Highlighted<'_> {
138        let mut highlighted = self.as_highlighted();
139        let mut selected = lines.to_vec();
140        selected.sort_unstable();
141        let mut selected = selected.into_iter().peekable();
142
143        for line in &mut highlighted.lines {
144            while selected.peek().is_some_and(|number| *number < line.number) {
145                selected.next();
146            }
147            line.highlighted = selected.peek() == Some(&line.number);
148        }
149        highlighted
150    }
151
152    #[cfg(feature = "render-html")]
153    pub fn render_html(&self, theme: &crate::Theme, config: &crate::RenderHtmlConfig) -> String {
154        crate::render::html::render(self.lines.iter(), Some(theme), &[], config, false)
155    }
156
157    #[cfg(feature = "render-html")]
158    pub fn render_html_classes(&self, config: &crate::RenderHtmlConfig) -> String {
159        crate::render::html::render(self.lines.iter(), None, &[], config, false)
160    }
161
162    #[cfg(feature = "render-html")]
163    pub fn render_html_themes(
164        &self,
165        themes: &[(&str, &crate::Theme)],
166        default_theme: &str,
167        config: &crate::RenderHtmlConfig,
168    ) -> Result<String, crate::Error> {
169        crate::render::html::render_themes(self.lines.iter(), themes, default_theme, config)
170    }
171
172    #[cfg(feature = "render-ansi")]
173    pub fn render_ansi(&self, theme: &crate::Theme) -> String {
174        crate::render::ansi::render(self.lines.iter(), theme)
175    }
176}