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::SyntaxTheme,
20    languages::LanguageId,
21};
22
23#[allow(dead_code)]
24fn capture_color(name: &str, theme: &SyntaxTheme) -> 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: &SyntaxTheme) -> 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    language_id: LanguageId,
126}
127
128impl Default for SyntaxHighlighter {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl SyntaxHighlighter {
135    pub fn new() -> Self {
136        Self {
137            parser: Parser::new(),
138            tree: None,
139            config: None,
140            cursor: QueryCursor::new(),
141            language_id: LanguageId::Unknown,
142        }
143    }
144
145    pub fn set_language(&mut self, language_id: LanguageId, theme: &SyntaxTheme) {
146        if self.language_id == language_id {
147            return;
148        }
149        self.language_id = language_id;
150        self.tree = None;
151
152        self.config = language_id.lang_config(theme);
153        if let Some(cfg) = &self.config {
154            let _ = self.parser.set_language(&cfg.language);
155        }
156    }
157
158    /// Discard the cached parse tree, forcing a full re-parse next time.
159    pub fn invalidate_tree(&mut self) {
160        self.tree = None;
161    }
162
163    /// Incrementally re-parse the rope and rebuild syntax blocks.
164    pub fn parse(
165        &mut self,
166        rope: &Rope,
167        syntax_blocks: &mut SyntaxBlocks,
168        edit: Option<InputEdit>,
169        theme: &SyntaxTheme,
170    ) {
171        syntax_blocks.clear();
172
173        if let Some(input_edit) = edit
174            && let Some(tree) = &mut self.tree
175        {
176            tree.edit(&input_edit);
177        }
178
179        let new_tree = {
180            let len = rope.len_bytes();
181            self.parser.parse_with_options(
182                &mut |byte_offset: usize, _position: Point| {
183                    if byte_offset >= len {
184                        return &[] as &[u8];
185                    }
186                    let (chunk, chunk_start, _, _) = rope.chunk_at_byte(byte_offset);
187                    &chunk.as_bytes()[byte_offset - chunk_start..]
188                },
189                self.tree.as_ref(),
190                None,
191            )
192        };
193
194        if let Some(new_tree) = new_tree {
195            if let Some(cfg) = &self.config {
196                build_syntax_blocks(&new_tree, cfg, &mut self.cursor, rope, syntax_blocks, theme);
197            } else {
198                build_plain_blocks(rope, syntax_blocks, theme);
199            }
200            self.tree = Some(new_tree);
201        } else {
202            build_plain_blocks(rope, syntax_blocks, theme);
203        }
204    }
205}
206
207pub trait InputEditExt {
208    fn new_edit(
209        start_byte: usize,
210        old_end_byte: usize,
211        new_end_byte: usize,
212        start_position: (usize, usize),
213        old_end_position: (usize, usize),
214        new_end_position: (usize, usize),
215    ) -> InputEdit;
216}
217
218impl InputEditExt for InputEdit {
219    fn new_edit(
220        start_byte: usize,
221        old_end_byte: usize,
222        new_end_byte: usize,
223        start_position: (usize, usize),
224        old_end_position: (usize, usize),
225        new_end_position: (usize, usize),
226    ) -> InputEdit {
227        InputEdit {
228            start_byte,
229            old_end_byte,
230            new_end_byte,
231            start_position: Point::new(start_position.0, start_position.1),
232            old_end_position: Point::new(old_end_position.0, old_end_position.1),
233            new_end_position: Point::new(new_end_position.0, new_end_position.1),
234        }
235    }
236}
237
238struct Span {
239    start_byte: usize,
240    end_byte: usize,
241    color: Color,
242}
243
244fn build_syntax_blocks(
245    tree: &Tree,
246    cfg: &LangConfig,
247    cursor: &mut QueryCursor,
248    rope: &Rope,
249    syntax_blocks: &mut SyntaxBlocks,
250    theme: &SyntaxTheme,
251) {
252    let root = tree.root_node();
253    cursor.set_byte_range(0..usize::MAX);
254
255    let mut spans: Vec<Span> = Vec::new();
256    let mut captures = cursor.captures(&cfg.query, root, RopeTextProvider { rope });
257
258    while let Some((match_result, capture_idx)) = {
259        captures.advance();
260        captures.get()
261    } {
262        let capture = &match_result.captures[*capture_idx];
263        let node = capture.node;
264        let color = cfg.capture_colors[capture.index as usize];
265        spans.push(Span {
266            start_byte: node.start_byte(),
267            end_byte: node.end_byte(),
268            color,
269        });
270    }
271
272    spans.sort_by_key(|s| s.start_byte);
273    build_lines_from_spans(rope, &spans, syntax_blocks, theme);
274}
275
276fn build_lines_from_spans(
277    rope: &Rope,
278    spans: &[Span],
279    syntax_blocks: &mut SyntaxBlocks,
280    theme: &SyntaxTheme,
281) {
282    let total_lines = rope.len_lines();
283    let mut span_idx = 0;
284
285    for line_idx in 0..total_lines {
286        let line_start_byte = rope.line_to_byte(line_idx);
287        let line_slice = rope.line(line_idx);
288        let line_byte_len = line_slice.len_bytes();
289        let line_end_byte = line_start_byte + line_byte_len;
290
291        let content_end_byte = {
292            let chars = line_slice.len_chars();
293            let mut end = line_end_byte;
294            if chars > 0 && line_slice.char(chars - 1) == '\n' {
295                end -= 1;
296                if chars > 1 && line_slice.char(chars - 2) == '\r' {
297                    end -= 1;
298                }
299            }
300            end
301        };
302
303        while span_idx < spans.len() && spans[span_idx].end_byte <= line_start_byte {
304            span_idx += 1;
305        }
306
307        let content_bytes = content_end_byte - line_start_byte;
308        if content_bytes == 0 {
309            syntax_blocks.push_line(SmallVec::new());
310            continue;
311        }
312
313        let mut byte_colors: SmallVec<[Color; 256]> =
314            smallvec::smallvec![theme.text; content_bytes];
315
316        let mut si = span_idx;
317        while si < spans.len() && spans[si].start_byte < content_end_byte {
318            let span = &spans[si];
319            si += 1;
320            if span.end_byte <= line_start_byte {
321                continue;
322            }
323            let s = span.start_byte.max(line_start_byte) - line_start_byte;
324            let e = span.end_byte.min(content_end_byte) - line_start_byte;
325            if s < e {
326                for c in &mut byte_colors[s..e] {
327                    *c = span.color;
328                }
329            }
330        }
331
332        let mut line_spans: SyntaxLine = SyntaxLine::new();
333        let mut beginning_of_line = true;
334        let mut run_start: usize = 0;
335
336        while run_start < content_bytes {
337            let run_color = byte_colors[run_start];
338            let mut run_end = run_start + 1;
339            while run_end < content_bytes && byte_colors[run_end] == run_color {
340                run_end += 1;
341            }
342
343            let abs_start_byte = line_start_byte + run_start;
344            let abs_end_byte = line_start_byte + run_end;
345            let start_char = rope.byte_to_char(abs_start_byte);
346            let end_char = rope.byte_to_char(abs_end_byte);
347
348            if beginning_of_line {
349                let slice = rope.slice(start_char..end_char);
350                let is_whitespace = slice.chars().all(|c| c.is_whitespace() && c != '\n');
351                if is_whitespace {
352                    let len = end_char - start_char;
353                    line_spans.push((
354                        theme.whitespace,
355                        TextNode::LineOfChars {
356                            len,
357                            char: '\u{00B7}',
358                        },
359                    ));
360                    run_start = run_end;
361                    continue;
362                }
363                beginning_of_line = false;
364            }
365
366            line_spans.push((run_color, TextNode::Range(start_char..end_char)));
367            run_start = run_end;
368        }
369
370        syntax_blocks.push_line(line_spans);
371    }
372}
373
374fn build_plain_blocks(rope: &Rope, syntax_blocks: &mut SyntaxBlocks, theme: &SyntaxTheme) {
375    for (n, line) in rope.lines().enumerate() {
376        let mut line_blocks = SmallVec::default();
377        let start = rope.line_to_char(n);
378        let end = line.len_chars();
379        if end > 0 {
380            line_blocks.push((theme.text, TextNode::Range(start..start + end)));
381        }
382        syntax_blocks.push_line(line_blocks);
383    }
384}
385
386pub struct RopeTextProvider<'a> {
387    rope: &'a Rope,
388}
389
390impl<'a> tree_sitter::TextProvider<&'a [u8]> for RopeTextProvider<'a> {
391    type I = RopeChunkIter<'a>;
392
393    fn text(&mut self, node: tree_sitter::Node) -> Self::I {
394        let start = node.start_byte();
395        let end = node.end_byte();
396        RopeChunkIter {
397            rope: self.rope,
398            byte_offset: start,
399            end_byte: end,
400        }
401    }
402}
403
404pub struct RopeChunkIter<'a> {
405    rope: &'a Rope,
406    byte_offset: usize,
407    end_byte: usize,
408}
409
410impl<'a> Iterator for RopeChunkIter<'a> {
411    type Item = &'a [u8];
412
413    fn next(&mut self) -> Option<Self::Item> {
414        if self.byte_offset >= self.end_byte {
415            return None;
416        }
417        let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(self.byte_offset);
418        let chunk_bytes = chunk.as_bytes();
419        let offset_in_chunk = self.byte_offset - chunk_start;
420        let available = &chunk_bytes[offset_in_chunk..];
421        let remaining = self.end_byte - self.byte_offset;
422        let slice = if available.len() > remaining {
423            &available[..remaining]
424        } else {
425            available
426        };
427        self.byte_offset += slice.len();
428        Some(slice)
429    }
430}
431
432impl LanguageId {
433    fn lang_config(&self, theme: &SyntaxTheme) -> Option<LangConfig> {
434        let (language, highlights_query) = match self {
435            #[cfg(feature = "rust")]
436            LanguageId::Rust => (
437                tree_sitter_rust::LANGUAGE.into(),
438                tree_sitter_rust::HIGHLIGHTS_QUERY,
439            ),
440            #[cfg(feature = "json")]
441            LanguageId::Json => (
442                tree_sitter_json::LANGUAGE.into(),
443                tree_sitter_json::HIGHLIGHTS_QUERY,
444            ),
445            #[cfg(feature = "toml")]
446            LanguageId::Toml => (
447                tree_sitter_toml_ng::LANGUAGE.into(),
448                tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
449            ),
450            #[cfg(feature = "md")]
451            LanguageId::Markdown => (
452                tree_sitter_md::LANGUAGE.into(),
453                tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
454            ),
455            #[cfg(feature = "sql")]
456            LanguageId::SQL => (
457                tree_sitter_sequel::LANGUAGE.into(),
458                tree_sitter_sequel::HIGHLIGHTS_QUERY,
459            ),
460            _ => return None,
461        };
462
463        let query = Query::new(&language, highlights_query).ok()?;
464        let capture_colors: Vec<Color> = query
465            .capture_names()
466            .iter()
467            .map(|name| resolve_capture_color(name, theme))
468            .collect();
469
470        Some(LangConfig {
471            language,
472            query,
473            capture_colors,
474        })
475    }
476}