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#[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 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 pub fn invalidate_tree(&mut self) {
154 self.tree = None;
155 }
156
157 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}