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
61fn 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 pub fn invalidate_tree(&mut self) {
158 self.tree = None;
159 }
160
161 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}