Skip to main content

freya_code_editor/
syntax.rs

1use std::ops::Range;
2
3use freya_core::prelude::Color;
4use ropey::Rope;
5use rustc_hash::FxHashMap;
6use smallvec::SmallVec;
7use tree_sitter::{
8    InputEdit,
9    Language,
10    Parser,
11    Point,
12    Query,
13    QueryCursor,
14    StreamingIterator,
15    Tree,
16};
17
18use crate::{
19    editor_theme::EditorSyntaxTheme,
20    languages::EditorLanguage,
21};
22
23#[allow(dead_code)]
24fn capture_color(name: &str, theme: &EditorSyntaxTheme) -> Color {
25    match name {
26        "attribute" => theme.attribute,
27        "boolean" => theme.boolean,
28        "comment" | "comment.documentation" => theme.comment,
29        "constant" | "constant.builtin" => theme.constant,
30        "constructor" => theme.constructor,
31        "escape" => theme.escape,
32        "function" | "function.builtin" => theme.function,
33        "function.macro" => theme.function_macro,
34        "function.method" => theme.function_method,
35        "keyword" => theme.keyword,
36        "label" => theme.label,
37        "module" => theme.module,
38        "number" => theme.number,
39        "operator" => theme.operator,
40        "property" => theme.property,
41        "punctuation" => theme.punctuation,
42        "punctuation.bracket" => theme.punctuation_bracket,
43        "punctuation.delimiter" => theme.punctuation_delimiter,
44        "punctuation.special" => theme.punctuation_special,
45        "string" => theme.string,
46        "string.escape" => theme.string_escape,
47        "string.special" | "string.special.key" | "string.special.symbol" => theme.string_special,
48        "tag" => theme.tag,
49        "text.literal" => theme.text_literal,
50        "text.reference" => theme.text_reference,
51        "text.title" => theme.text_title,
52        "text.uri" => theme.text_uri,
53        "text.emphasis" | "text.strong" => theme.text_emphasis,
54        "type" | "type.builtin" => theme.type_,
55        "variable" => theme.variable,
56        "variable.builtin" => theme.variable_builtin,
57        "variable.parameter" => theme.variable_parameter,
58        _ => theme.text,
59    }
60}
61
62/// Tries exact match, then strips trailing dot-segments for hierarchical fallback.
63#[allow(dead_code)]
64fn resolve_capture_color(name: &str, theme: &EditorSyntaxTheme) -> Color {
65    let color = capture_color(name, theme);
66    if color != theme.text {
67        return color;
68    }
69    let mut candidate = name;
70    while let Some(pos) = candidate.rfind('.') {
71        candidate = &candidate[..pos];
72        let c = capture_color(candidate, theme);
73        if c != theme.text {
74            return c;
75        }
76    }
77    theme.text
78}
79
80pub enum TextNode {
81    Range(Range<usize>),
82    LineOfChars { len: usize, char: char },
83}
84
85pub type SyntaxLine = SmallVec<[(Color, TextNode); 4]>;
86
87#[derive(Default)]
88pub struct SyntaxBlocks {
89    blocks: FxHashMap<usize, SyntaxLine>,
90}
91
92impl SyntaxBlocks {
93    pub fn push_line(&mut self, line: SyntaxLine) {
94        self.blocks.insert(self.len(), line);
95    }
96
97    pub fn get_line(&self, line: usize) -> &[(Color, TextNode)] {
98        self.blocks.get(&line).unwrap()
99    }
100
101    pub fn len(&self) -> usize {
102        self.blocks.len()
103    }
104
105    pub fn is_empty(&self) -> bool {
106        self.blocks.is_empty()
107    }
108
109    pub fn clear(&mut self) {
110        self.blocks.clear();
111    }
112}
113
114struct LangConfig {
115    language: Language,
116    query: Query,
117    capture_colors: Vec<Color>,
118}
119
120pub struct SyntaxHighlighter {
121    parser: Parser,
122    tree: Option<Tree>,
123    config: Option<LangConfig>,
124    cursor: QueryCursor,
125}
126
127impl Default for SyntaxHighlighter {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl SyntaxHighlighter {
134    pub fn new() -> Self {
135        Self {
136            parser: Parser::new(),
137            tree: None,
138            config: None,
139            cursor: QueryCursor::new(),
140        }
141    }
142
143    /// Configures the language used for highlighting, or disables it with `None`.
144    pub fn set_language(&mut self, language: Option<&EditorLanguage>, theme: &EditorSyntaxTheme) {
145        self.tree = None;
146        self.config = language.and_then(|language| language.lang_config(theme));
147        if let Some(cfg) = &self.config {
148            let _ = self.parser.set_language(&cfg.language);
149        }
150    }
151
152    /// Discard the cached parse tree, forcing a full re-parse next time.
153    pub fn invalidate_tree(&mut self) {
154        self.tree = None;
155    }
156
157    /// Incrementally re-parse the rope and rebuild syntax blocks.
158    pub fn parse(
159        &mut self,
160        rope: &Rope,
161        syntax_blocks: &mut SyntaxBlocks,
162        edit: Option<InputEdit>,
163        theme: &EditorSyntaxTheme,
164    ) {
165        syntax_blocks.clear();
166
167        if let Some(input_edit) = edit
168            && let Some(tree) = &mut self.tree
169        {
170            tree.edit(&input_edit);
171        }
172
173        let new_tree = {
174            let len = rope.len_bytes();
175            self.parser.parse_with_options(
176                &mut |byte_offset: usize, _position: Point| {
177                    if byte_offset >= len {
178                        return &[] as &[u8];
179                    }
180                    let (chunk, chunk_start, _, _) = rope.chunk_at_byte(byte_offset);
181                    &chunk.as_bytes()[byte_offset - chunk_start..]
182                },
183                self.tree.as_ref(),
184                None,
185            )
186        };
187
188        if let Some(new_tree) = new_tree {
189            if let Some(cfg) = &self.config {
190                build_syntax_blocks(&new_tree, cfg, &mut self.cursor, rope, syntax_blocks, theme);
191            } else {
192                build_plain_blocks(rope, syntax_blocks, theme);
193            }
194            self.tree = Some(new_tree);
195        } else {
196            build_plain_blocks(rope, syntax_blocks, theme);
197        }
198    }
199}
200
201pub trait InputEditExt {
202    fn new_edit(
203        start_byte: usize,
204        old_end_byte: usize,
205        new_end_byte: usize,
206        start_position: (usize, usize),
207        old_end_position: (usize, usize),
208        new_end_position: (usize, usize),
209    ) -> InputEdit;
210}
211
212impl InputEditExt for InputEdit {
213    fn new_edit(
214        start_byte: usize,
215        old_end_byte: usize,
216        new_end_byte: usize,
217        start_position: (usize, usize),
218        old_end_position: (usize, usize),
219        new_end_position: (usize, usize),
220    ) -> InputEdit {
221        InputEdit {
222            start_byte,
223            old_end_byte,
224            new_end_byte,
225            start_position: Point::new(start_position.0, start_position.1),
226            old_end_position: Point::new(old_end_position.0, old_end_position.1),
227            new_end_position: Point::new(new_end_position.0, new_end_position.1),
228        }
229    }
230}
231
232struct Span {
233    start_byte: usize,
234    end_byte: usize,
235    color: Color,
236}
237
238fn build_syntax_blocks(
239    tree: &Tree,
240    cfg: &LangConfig,
241    cursor: &mut QueryCursor,
242    rope: &Rope,
243    syntax_blocks: &mut SyntaxBlocks,
244    theme: &EditorSyntaxTheme,
245) {
246    let root = tree.root_node();
247    cursor.set_byte_range(0..usize::MAX);
248
249    let mut spans: Vec<Span> = Vec::new();
250    let mut captures = cursor.captures(&cfg.query, root, RopeTextProvider { rope });
251
252    while let Some((match_result, capture_idx)) = {
253        captures.advance();
254        captures.get()
255    } {
256        let capture = &match_result.captures[*capture_idx];
257        let node = capture.node;
258        let color = cfg.capture_colors[capture.index as usize];
259        spans.push(Span {
260            start_byte: node.start_byte(),
261            end_byte: node.end_byte(),
262            color,
263        });
264    }
265
266    spans.sort_by_key(|s| s.start_byte);
267    build_lines_from_spans(rope, &spans, syntax_blocks, theme);
268}
269
270fn build_lines_from_spans(
271    rope: &Rope,
272    spans: &[Span],
273    syntax_blocks: &mut SyntaxBlocks,
274    theme: &EditorSyntaxTheme,
275) {
276    let total_lines = rope.len_lines();
277    let mut span_idx = 0;
278
279    for line_idx in 0..total_lines {
280        let line_start_byte = rope.line_to_byte(line_idx);
281        let line_slice = rope.line(line_idx);
282        let line_byte_len = line_slice.len_bytes();
283        let line_end_byte = line_start_byte + line_byte_len;
284
285        let content_end_byte = {
286            let chars = line_slice.len_chars();
287            let mut end = line_end_byte;
288            if chars > 0 && line_slice.char(chars - 1) == '\n' {
289                end -= 1;
290                if chars > 1 && line_slice.char(chars - 2) == '\r' {
291                    end -= 1;
292                }
293            }
294            end
295        };
296
297        while span_idx < spans.len() && spans[span_idx].end_byte <= line_start_byte {
298            span_idx += 1;
299        }
300
301        let content_bytes = content_end_byte - line_start_byte;
302        if content_bytes == 0 {
303            syntax_blocks.push_line(SmallVec::new());
304            continue;
305        }
306
307        let mut byte_colors: SmallVec<[Color; 256]> =
308            smallvec::smallvec![theme.text; content_bytes];
309
310        let mut si = span_idx;
311        while si < spans.len() && spans[si].start_byte < content_end_byte {
312            let span = &spans[si];
313            si += 1;
314            if span.end_byte <= line_start_byte {
315                continue;
316            }
317            let s = span.start_byte.max(line_start_byte) - line_start_byte;
318            let e = span.end_byte.min(content_end_byte) - line_start_byte;
319            if s < e {
320                for c in &mut byte_colors[s..e] {
321                    *c = span.color;
322                }
323            }
324        }
325
326        let mut line_spans: SyntaxLine = SyntaxLine::new();
327        let mut beginning_of_line = true;
328        let mut run_start: usize = 0;
329
330        while run_start < content_bytes {
331            let run_color = byte_colors[run_start];
332            let mut run_end = run_start + 1;
333            while run_end < content_bytes && byte_colors[run_end] == run_color {
334                run_end += 1;
335            }
336
337            let abs_start_byte = line_start_byte + run_start;
338            let abs_end_byte = line_start_byte + run_end;
339            let start_char = rope.byte_to_char(abs_start_byte);
340            let end_char = rope.byte_to_char(abs_end_byte);
341
342            if beginning_of_line {
343                let slice = rope.slice(start_char..end_char);
344                let is_whitespace = slice.chars().all(|c| c.is_whitespace() && c != '\n');
345                if is_whitespace {
346                    let len = end_char - start_char;
347                    line_spans.push((
348                        theme.whitespace,
349                        TextNode::LineOfChars {
350                            len,
351                            char: '\u{00B7}',
352                        },
353                    ));
354                    run_start = run_end;
355                    continue;
356                }
357                beginning_of_line = false;
358            }
359
360            line_spans.push((run_color, TextNode::Range(start_char..end_char)));
361            run_start = run_end;
362        }
363
364        syntax_blocks.push_line(line_spans);
365    }
366}
367
368fn build_plain_blocks(rope: &Rope, syntax_blocks: &mut SyntaxBlocks, theme: &EditorSyntaxTheme) {
369    for (n, line) in rope.lines().enumerate() {
370        let mut line_blocks = SmallVec::default();
371        let start = rope.line_to_char(n);
372        let end = line.len_chars();
373        if end > 0 {
374            line_blocks.push((theme.text, TextNode::Range(start..start + end)));
375        }
376        syntax_blocks.push_line(line_blocks);
377    }
378}
379
380pub struct RopeTextProvider<'a> {
381    rope: &'a Rope,
382}
383
384impl<'a> tree_sitter::TextProvider<&'a [u8]> for RopeTextProvider<'a> {
385    type I = RopeChunkIter<'a>;
386
387    fn text(&mut self, node: tree_sitter::Node) -> Self::I {
388        let start = node.start_byte();
389        let end = node.end_byte();
390        RopeChunkIter {
391            rope: self.rope,
392            byte_offset: start,
393            end_byte: end,
394        }
395    }
396}
397
398pub struct RopeChunkIter<'a> {
399    rope: &'a Rope,
400    byte_offset: usize,
401    end_byte: usize,
402}
403
404impl<'a> Iterator for RopeChunkIter<'a> {
405    type Item = &'a [u8];
406
407    fn next(&mut self) -> Option<Self::Item> {
408        if self.byte_offset >= self.end_byte {
409            return None;
410        }
411        let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(self.byte_offset);
412        let chunk_bytes = chunk.as_bytes();
413        let offset_in_chunk = self.byte_offset - chunk_start;
414        let available = &chunk_bytes[offset_in_chunk..];
415        let remaining = self.end_byte - self.byte_offset;
416        let slice = if available.len() > remaining {
417            &available[..remaining]
418        } else {
419            available
420        };
421        self.byte_offset += slice.len();
422        Some(slice)
423    }
424}
425
426impl EditorLanguage {
427    fn lang_config(&self, theme: &EditorSyntaxTheme) -> Option<LangConfig> {
428        let language = self.language.clone();
429        let query = Query::new(&language, self.highlights_query.as_ref()).ok()?;
430        let capture_colors: Vec<Color> = query
431            .capture_names()
432            .iter()
433            .map(|name| resolve_capture_color(name, theme))
434            .collect();
435
436        Some(LangConfig {
437            language,
438            query,
439            capture_colors,
440        })
441    }
442}