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#[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 pub fn invalidate_tree(&mut self) {
160 self.tree = None;
161 }
162
163 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}