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