1use std::collections::HashMap;
13use std::ops::Range;
14use std::path::Path;
15use std::sync::Arc;
16
17use hjkl_bonsai::runtime::{Grammar, LoadHandle};
18use hjkl_bonsai::{
19 CommentMarkerPass, DotFallbackTheme, HEX_BG_KEY, HEX_COLOR_CAPTURE, HEX_FG_KEY, HexColorPass,
20 Highlighter, InputEdit, MetaValue, Point, RAINBOW_BRACKET_CAPTURE, RAINBOW_DEPTH_KEY, Theme,
21 extract_fold_ranges_rope, rainbow_spans_rope,
22};
23use hjkl_engine::Query;
24use hjkl_lang::{GrammarRequest, LanguageDirectory};
25
26pub use hjkl_theme::{Color, Modifiers, StyleSpec};
27
28pub use hjkl_buffer::BufferId;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub struct DiagSign {
55 pub row: usize,
57 pub ch: char,
59 pub priority: u8,
61}
62
63impl Default for DiagSign {
64 fn default() -> Self {
65 Self {
66 row: 0,
67 ch: 'E',
68 priority: 0,
69 }
70 }
71}
72
73impl DiagSign {
74 pub fn new(row: usize, ch: char, priority: u8) -> Self {
84 Self { row, ch, priority }
85 }
86}
87
88#[derive(Default, Debug, Clone, Copy)]
99#[non_exhaustive]
100pub struct PerfBreakdown {
101 pub source_build_us: u128,
103 pub parse_us: u128,
105 pub highlight_us: u128,
107 pub by_row_us: u128,
109 pub diag_us: u128,
111}
112
113impl PerfBreakdown {
114 pub fn new() -> Self {
124 Self::default()
125 }
126}
127
128#[derive(Debug, Clone)]
141#[non_exhaustive]
142pub struct RenderOutput {
143 pub buffer_id: BufferId,
145 pub spans: Vec<Vec<(usize, usize, StyleSpec)>>,
147 pub signs: Vec<DiagSign>,
149 pub key: (u64, usize, usize),
151 pub perf: PerfBreakdown,
153}
154
155impl RenderOutput {
156 pub fn new(
166 buffer_id: BufferId,
167 spans: Vec<Vec<(usize, usize, StyleSpec)>>,
168 signs: Vec<DiagSign>,
169 key: (u64, usize, usize),
170 perf: PerfBreakdown,
171 ) -> Self {
172 Self {
173 buffer_id,
174 spans,
175 signs,
176 key,
177 perf,
178 }
179 }
180}
181
182impl PartialEq for RenderOutput {
183 fn eq(&self, other: &Self) -> bool {
184 self.spans == other.spans
185 && self.signs.len() == other.signs.len()
186 && self
187 .signs
188 .iter()
189 .zip(other.signs.iter())
190 .all(|(a, b)| a.row == b.row && a.ch == b.ch && a.priority == b.priority)
191 }
192}
193
194#[non_exhaustive]
209pub enum SetLanguageOutcome {
210 Ready,
212 Loading(#[allow(dead_code)] String),
214 Unknown,
216}
217
218impl SetLanguageOutcome {
219 pub fn is_known(&self) -> bool {
221 matches!(self, Self::Ready | Self::Loading(_))
222 }
223}
224
225#[non_exhaustive]
239pub enum LoadEvent {
240 Ready { id: BufferId, name: String },
242 Failed {
244 id: BufferId,
245 name: String,
246 error: String,
247 },
248}
249
250#[derive(Debug)]
252pub enum LoadEventKind<'a> {
253 Ready { id: BufferId, name: &'a str },
255 Failed {
257 id: BufferId,
258 name: &'a str,
259 error: &'a str,
260 },
261}
262
263struct PendingLoad {
268 id: BufferId,
269 name: String,
270 handle: LoadHandle,
271}
272
273struct BufferClient {
279 has_language: bool,
280 current_lang: Option<Arc<Grammar>>,
281 highlighter: Option<Highlighter>,
283 cache_dirty_gen: Option<u64>,
285 cache_rows: Range<usize>,
287 cache_spans: Vec<Vec<(usize, usize, StyleSpec)>>,
289 cache_row_starts: Option<(u64, Arc<Vec<usize>>)>,
291 parsed_dirty_gen: Option<u64>,
293 cache_signs: Option<(u64, usize, usize, Vec<DiagSign>)>,
295}
296
297impl Default for BufferClient {
298 fn default() -> Self {
299 Self {
300 has_language: false,
301 current_lang: None,
302 highlighter: None,
303 cache_dirty_gen: None,
304 cache_rows: 0..0,
305 cache_spans: Vec::new(),
306 cache_row_starts: None,
307 parsed_dirty_gen: None,
308 cache_signs: None,
309 }
310 }
311}
312
313impl BufferClient {
314 fn invalidate_cache(&mut self) {
315 self.cache_dirty_gen = None;
316 self.cache_rows = 0..0;
317 self.cache_spans.clear();
318 self.cache_row_starts = None;
319 self.parsed_dirty_gen = None;
320 self.cache_signs = None;
321 }
322}
323
324pub struct SyntaxLayer {
344 pub directory: Arc<LanguageDirectory>,
346 theme: Arc<dyn Theme + Send + Sync>,
347 clients: HashMap<BufferId, BufferClient>,
348 pending_loads: Vec<PendingLoad>,
349 colorizer: bool,
351 colorizer_filetypes: Vec<String>,
353 rainbow_brackets: bool,
355}
356
357impl SyntaxLayer {
358 pub fn new(theme: Arc<dyn Theme + Send + Sync>, directory: Arc<LanguageDirectory>) -> Self {
373 Self {
374 directory,
375 theme,
376 clients: HashMap::new(),
377 pending_loads: Vec::new(),
378 colorizer: true,
379 colorizer_filetypes: vec![
380 "css".to_string(),
381 "scss".to_string(),
382 "sass".to_string(),
383 "less".to_string(),
384 "html".to_string(),
385 "vue".to_string(),
386 "svelte".to_string(),
387 "tailwindcss".to_string(),
388 "toml".to_string(),
389 "lua".to_string(),
390 "vim".to_string(),
391 ],
392 rainbow_brackets: true,
393 }
394 }
395
396 pub fn set_rainbow_brackets(&mut self, enabled: bool) {
400 if self.rainbow_brackets == enabled {
401 return;
402 }
403 self.rainbow_brackets = enabled;
404 for client in self.clients.values_mut() {
405 client.invalidate_cache();
406 }
407 }
408
409 pub fn set_colorizer(&mut self, enabled: bool, filetypes: Vec<String>) {
417 if self.colorizer == enabled && self.colorizer_filetypes == filetypes {
418 return;
419 }
420 self.colorizer = enabled;
421 self.colorizer_filetypes = filetypes;
422 for client in self.clients.values_mut() {
423 client.invalidate_cache();
424 }
425 }
426
427 pub fn directory(&self) -> &Arc<LanguageDirectory> {
429 &self.directory
430 }
431
432 fn client_mut(&mut self, id: BufferId) -> &mut BufferClient {
433 self.clients.entry(id).or_default()
434 }
435
436 pub fn set_language_for_path(&mut self, id: BufferId, path: &Path) -> SetLanguageOutcome {
459 match self.directory.request_for_path(path) {
460 GrammarRequest::Cached(grammar) => {
461 self.attach_grammar(id, grammar.clone());
462 let c = self.client_mut(id);
463 c.current_lang = Some(grammar);
464 c.has_language = true;
465 SetLanguageOutcome::Ready
466 }
467 GrammarRequest::Loading { name, handle } => {
468 let c = self.client_mut(id);
469 c.current_lang = None;
470 c.has_language = false;
471 c.highlighter = None;
472 c.invalidate_cache();
473 self.pending_loads.push(PendingLoad {
474 id,
475 name: name.clone(),
476 handle,
477 });
478 SetLanguageOutcome::Loading(name)
479 }
480 GrammarRequest::Unknown | _ => {
481 let c = self.client_mut(id);
482 c.current_lang = None;
483 c.has_language = false;
484 c.highlighter = None;
485 c.invalidate_cache();
486 SetLanguageOutcome::Unknown
487 }
488 }
489 }
490
491 fn attach_grammar(&mut self, id: BufferId, grammar: Arc<Grammar>) {
493 let c = self.clients.entry(id).or_default();
494 c.invalidate_cache();
495 match Highlighter::new(grammar) {
496 Ok(h) => {
497 c.highlighter = Some(h);
498 }
499 Err(e) => {
500 tracing::error!(buffer_id = id, error = %e, "failed to attach highlighter");
501 c.highlighter = None;
502 }
503 }
504 }
505
506 pub fn poll_pending_loads(&mut self) -> Vec<LoadEvent> {
510 let mut events = Vec::new();
511 let mut i = 0;
512 while i < self.pending_loads.len() {
513 match self.pending_loads[i].handle.try_recv() {
514 None => {
515 i += 1;
516 }
517 Some(Ok(lib_path)) => {
518 let name = self.pending_loads[i].name.clone();
519 let bid = self.pending_loads[i].id;
520 self.pending_loads.swap_remove(i);
521 match self.directory.complete_load(&name, lib_path) {
522 Ok(grammar) => {
523 self.attach_grammar(bid, grammar.clone());
524 let c = self.client_mut(bid);
525 c.current_lang = Some(grammar);
526 c.has_language = true;
527 events.push(LoadEvent::Ready { id: bid, name });
528 }
529 Err(e) => {
530 events.push(LoadEvent::Failed {
531 id: bid,
532 name,
533 error: format!("{e:#}"),
534 });
535 }
536 }
537 }
538 Some(Err(err)) => {
539 let name = self.pending_loads[i].name.clone();
540 let bid = self.pending_loads[i].id;
541 self.pending_loads.swap_remove(i);
542 events.push(LoadEvent::Failed {
543 id: bid,
544 name,
545 error: err.to_string(),
546 });
547 }
548 }
549 }
550 events
551 }
552
553 pub fn forget(&mut self, id: BufferId) {
555 self.clients.remove(&id);
556 }
557
558 pub fn set_theme(&mut self, theme: Arc<dyn Theme + Send + Sync>) {
560 self.theme = theme;
561 for c in self.clients.values_mut() {
563 c.invalidate_cache();
564 }
565 }
566
567 pub fn apply_edits(&mut self, id: BufferId, edits: &[hjkl_engine::ContentEdit]) {
573 let c = match self.clients.get_mut(&id) {
574 Some(c) if c.has_language => c,
575 _ => return,
576 };
577 let h = match c.highlighter.as_mut() {
578 Some(h) => h,
579 None => return,
580 };
581 for e in edits {
582 h.edit(&InputEdit {
583 start_byte: e.start_byte,
584 old_end_byte: e.old_end_byte,
585 new_end_byte: e.new_end_byte,
586 start_position: Point {
587 row: e.start_position.0 as usize,
588 column: e.start_position.1 as usize,
589 },
590 old_end_position: Point {
591 row: e.old_end_position.0 as usize,
592 column: e.old_end_position.1 as usize,
593 },
594 new_end_position: Point {
595 row: e.new_end_position.0 as usize,
596 column: e.new_end_position.1 as usize,
597 },
598 });
599 }
600 c.parsed_dirty_gen = None;
603 c.cache_row_starts = None;
604 c.cache_signs = None;
605 }
606
607 pub fn reset(&mut self, id: BufferId) {
611 if let Some(c) = self.clients.get_mut(&id) {
612 if let Some(h) = c.highlighter.as_mut() {
613 h.reset();
614 }
615 c.invalidate_cache();
616 }
617 }
618
619 pub fn extract_fold_ranges(
639 &mut self,
640 id: BufferId,
641 buffer: &impl hjkl_engine::Query,
642 ) -> Option<Vec<(usize, usize)>> {
643 let client = match self.clients.get_mut(&id) {
644 Some(c) if c.has_language => c,
645 _ => return None,
647 };
648 let highlighter = client.highlighter.as_mut()?;
650 let tree = highlighter.tree()?;
652 let grammar = highlighter.grammar()?;
653 let rope = buffer.rope();
654 Some(extract_fold_ranges_rope(tree, grammar, &rope))
656 }
657
658 pub fn render_viewport(
666 &mut self,
667 id: BufferId,
668 buffer: &impl Query,
669 viewport_top: usize,
670 viewport_height: usize,
671 ) -> Option<RenderOutput> {
672 let client = self.clients.get_mut(&id)?;
673 if !client.has_language {
674 return None;
675 }
676 let dg = buffer.dirty_gen();
677 let row_count = buffer.line_count() as usize;
678 if row_count == 0 || viewport_height == 0 {
679 return None;
680 }
681
682 let vp_top = viewport_top.min(row_count);
683 let vp_end = (vp_top + viewport_height).min(row_count);
684 if vp_end <= vp_top {
685 return None;
686 }
687
688 if client.cache_dirty_gen != Some(dg) {
690 client.invalidate_cache();
691 }
692
693 let rope = buffer.rope();
697
698 let row_starts: Arc<Vec<usize>> = if client
702 .cache_row_starts
703 .as_ref()
704 .is_some_and(|(g, _)| *g == dg)
705 {
706 Arc::clone(&client.cache_row_starts.as_ref().unwrap().1)
707 } else {
708 let mut rs: Vec<usize> = Vec::with_capacity(row_count + 1);
711 rs.push(0);
712 let mut chunk_pos = 0usize;
713 for chunk in rope.chunks() {
714 for nl in memchr::memchr_iter(b'\n', chunk.as_bytes()) {
715 rs.push(chunk_pos + nl + 1);
716 }
717 chunk_pos += chunk.len();
718 }
719 let arc = Arc::new(rs);
720 client.cache_row_starts = Some((dg, Arc::clone(&arc)));
721 arc
722 };
723
724 let needs_reparse = client.parsed_dirty_gen != Some(dg);
728 {
729 let highlighter = client.highlighter.as_mut()?;
730 if highlighter.tree().is_none() {
731 highlighter.parse_initial_rope(&rope);
732 if highlighter.tree().is_some() {
733 client.parsed_dirty_gen = Some(dg);
734 }
735 } else if needs_reparse {
736 let ok = highlighter.parse_incremental_rope(&rope);
742 if ok && highlighter.tree().is_some() {
743 client.parsed_dirty_gen = Some(dg);
744 }
745 }
746 }
747
748 let colorizer_enabled = {
751 let c = self.clients.get(&id)?;
752 let lang_name = c.current_lang.as_ref().map(|g| g.name()).unwrap_or("");
753 self.colorizer
754 && (self.colorizer_filetypes.is_empty()
755 || self.colorizer_filetypes.iter().any(|ft| ft == lang_name))
756 };
757 let rainbow_brackets_enabled = self.rainbow_brackets;
758
759 let client = self.clients.get_mut(&id)?;
761 let highlighter = client.highlighter.as_mut()?;
762
763 highlighter.tree()?;
765
766 let theme = self.theme.as_ref();
767 let directory = Arc::clone(&self.directory);
768
769 if client.cache_rows.is_empty() {
771 client.cache_spans = walk_rows(
773 highlighter,
774 &rope,
775 &row_starts,
776 row_count,
777 vp_top,
778 vp_end,
779 theme,
780 &directory,
781 colorizer_enabled,
782 rainbow_brackets_enabled,
783 );
784 client.cache_rows = vp_top..vp_end;
785 client.cache_dirty_gen = Some(dg);
786 } else {
787 let cache_covers_overlap =
788 vp_top < client.cache_rows.end && vp_end > client.cache_rows.start;
789 if !cache_covers_overlap {
790 client.cache_spans = walk_rows(
792 highlighter,
793 &rope,
794 &row_starts,
795 row_count,
796 vp_top,
797 vp_end,
798 theme,
799 &directory,
800 colorizer_enabled,
801 rainbow_brackets_enabled,
802 );
803 client.cache_rows = vp_top..vp_end;
804 } else {
805 if vp_top < client.cache_rows.start {
807 let new_rows = walk_rows(
808 highlighter,
809 &rope,
810 &row_starts,
811 row_count,
812 vp_top,
813 client.cache_rows.start,
814 theme,
815 &directory,
816 colorizer_enabled,
817 rainbow_brackets_enabled,
818 );
819 let mut combined = new_rows;
820 combined.append(&mut client.cache_spans);
821 client.cache_spans = combined;
822 client.cache_rows.start = vp_top;
823 }
824 if vp_end > client.cache_rows.end {
826 let new_rows = walk_rows(
827 highlighter,
828 &rope,
829 &row_starts,
830 row_count,
831 client.cache_rows.end,
832 vp_end,
833 theme,
834 &directory,
835 colorizer_enabled,
836 rainbow_brackets_enabled,
837 );
838 client.cache_spans.extend(new_rows);
839 client.cache_rows.end = vp_end;
840 }
841 }
842 client.cache_dirty_gen = Some(dg);
843 }
844
845 let offset = vp_top - client.cache_rows.start;
847 let len = vp_end - vp_top;
848 let spans: Vec<Vec<(usize, usize, StyleSpec)>> =
849 client.cache_spans[offset..offset + len].to_vec();
850
851 let signs = if client
853 .cache_signs
854 .as_ref()
855 .is_some_and(|(g, t, e, _)| *g == dg && *t == vp_top && *e == vp_end)
856 {
857 client.cache_signs.as_ref().unwrap().3.clone()
858 } else {
859 let s = collect_diag_signs_range(highlighter, &rope, &row_starts, vp_top, vp_end);
860 client.cache_signs = Some((dg, vp_top, vp_end, s.clone()));
861 s
862 };
863
864 Some(RenderOutput {
865 buffer_id: id,
866 spans,
867 signs,
868 key: (dg, vp_top, viewport_height),
869 perf: PerfBreakdown::default(),
870 })
871 }
872
873 pub fn name_for_path(&self, path: &Path) -> Option<String> {
875 self.directory.name_for_path(path)
876 }
877
878 #[doc(hidden)]
880 pub fn has_client(&self, id: BufferId) -> bool {
881 self.clients.contains_key(&id)
882 }
883
884 pub fn dispatch_load_event(
904 event: &LoadEvent,
905 mut handler: impl FnMut(LoadEventKind<'_>),
906 ) -> bool {
907 #[allow(unreachable_patterns)]
908 match event {
909 LoadEvent::Ready { id, name } => {
910 handler(LoadEventKind::Ready { id: *id, name });
911 true
912 }
913 LoadEvent::Failed { id, name, error } => {
914 handler(LoadEventKind::Failed {
915 id: *id,
916 name,
917 error,
918 });
919 true
920 }
921 _ => false,
922 }
923 }
924}
925
926const RAINBOW_PALETTE: [Color; 7] = [
933 Color::rgb(255, 100, 100), Color::rgb(255, 175, 80), Color::rgb(255, 230, 80), Color::rgb(100, 220, 100), Color::rgb(80, 210, 220), Color::rgb(100, 140, 255), Color::rgb(190, 120, 255), ];
941
942#[allow(clippy::too_many_arguments)]
947fn walk_rows(
948 highlighter: &mut Highlighter,
949 rope: &ropey::Rope,
950 row_starts: &[usize],
951 row_count: usize,
952 seg_start: usize,
953 seg_end: usize,
954 theme: &dyn Theme,
955 directory: &Arc<LanguageDirectory>,
956 colorizer: bool,
957 rainbow_brackets: bool,
958) -> Vec<Vec<(usize, usize, StyleSpec)>> {
959 let rope_len = rope.len_bytes();
960 let byte_start = row_starts.get(seg_start).copied().unwrap_or(rope_len);
961 let byte_end = row_starts
962 .get(seg_end)
963 .copied()
964 .unwrap_or(rope_len)
965 .min(rope_len)
966 .max(byte_start);
967
968 let mut flat_spans =
969 highlighter.highlight_range_with_injections_rope(rope, byte_start..byte_end, |name| {
970 directory.by_name(name)
971 });
972
973 let marker_pass = CommentMarkerPass::new();
974 marker_pass.apply_rope(&mut flat_spans, rope);
975 if colorizer {
976 let hex_color_pass = HexColorPass::new();
977 hex_color_pass.apply_range_rope(&mut flat_spans, rope, byte_start..byte_end);
978 }
979 if rainbow_brackets
980 && let (Some(tree), Some(grammar)) = (highlighter.tree(), highlighter.grammar())
981 {
982 let rb_spans = rainbow_spans_rope(tree, grammar, rope, byte_start..byte_end);
983 flat_spans.extend(rb_spans);
984 }
985
986 let _ = row_count; build_by_row_range(&flat_spans, rope_len, row_starts, seg_start..seg_end, theme)
993}
994
995fn build_by_row_range(
1001 flat_spans: &[hjkl_bonsai::HighlightSpan],
1002 source_len: usize,
1003 row_starts: &[usize],
1004 row_range: Range<usize>,
1005 theme: &dyn Theme,
1006) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1007 let seg_start = row_range.start;
1008 let seg_end = row_range.end.min(row_starts.len());
1009 if seg_end <= seg_start {
1010 return Vec::new();
1011 }
1012 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); seg_end - seg_start];
1013
1014 for span in flat_spans {
1015 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
1016 let bg = match span.metadata.get(HEX_BG_KEY) {
1017 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1018 _ => None,
1019 };
1020 let fg = match span.metadata.get(HEX_FG_KEY) {
1021 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1022 _ => None,
1023 };
1024 bg.map(|bg| StyleSpec {
1025 fg,
1026 bg: Some(bg),
1027 modifiers: hjkl_theme::Modifiers::default(),
1028 })
1029 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
1030 let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
1031 Some(MetaValue::Int(d)) => *d as usize,
1032 _ => 0,
1033 };
1034 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
1035 Some(StyleSpec {
1036 fg: Some(fg),
1037 bg: None,
1038 modifiers: hjkl_theme::Modifiers::default(),
1039 })
1040 } else {
1041 None
1042 };
1043
1044 let style: StyleSpec = if let Some(s) = hex_style {
1045 s
1046 } else {
1047 match theme.style(span.capture()) {
1048 Some(s) => *s,
1049 None => continue,
1050 }
1051 };
1052
1053 let span_start = span.byte_range.start;
1054 let span_end = span.byte_range.end;
1055
1056 let start_row = row_starts
1057 .partition_point(|&rs| rs <= span_start)
1058 .saturating_sub(1);
1059
1060 let mut row = start_row.max(seg_start);
1061 while row < seg_end {
1062 let row_byte_start = row_starts[row];
1063 let row_byte_end = row_starts
1064 .get(row + 1)
1065 .map(|&s| s.saturating_sub(1))
1066 .unwrap_or(source_len);
1067
1068 if row_byte_start >= span_end {
1069 break;
1070 }
1071
1072 let local_start = span_start.saturating_sub(row_byte_start);
1073 let local_end = span_end.min(row_byte_end) - row_byte_start;
1074
1075 if local_end > local_start {
1076 by_row[row - seg_start].push((local_start, local_end, style));
1077 }
1078
1079 row += 1;
1080 }
1081 }
1082
1083 by_row
1084}
1085
1086pub fn build_by_row(
1092 flat_spans: &[hjkl_bonsai::HighlightSpan],
1093 bytes: &[u8],
1094 row_starts: &[usize],
1095 row_count: usize,
1096 theme: &dyn Theme,
1097) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1098 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); row_count];
1099
1100 for span in flat_spans {
1101 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
1102 let bg = match span.metadata.get(HEX_BG_KEY) {
1103 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1104 _ => None,
1105 };
1106 let fg = match span.metadata.get(HEX_FG_KEY) {
1107 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1108 _ => None,
1109 };
1110 bg.map(|bg| StyleSpec {
1111 fg,
1112 bg: Some(bg),
1113 modifiers: hjkl_theme::Modifiers::default(),
1114 })
1115 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
1116 let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
1117 Some(MetaValue::Int(d)) => *d as usize,
1118 _ => 0,
1119 };
1120 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
1121 Some(StyleSpec {
1122 fg: Some(fg),
1123 bg: None,
1124 modifiers: hjkl_theme::Modifiers::default(),
1125 })
1126 } else {
1127 None
1128 };
1129
1130 let style: StyleSpec = if let Some(s) = hex_style {
1131 s
1132 } else {
1133 match theme.style(span.capture()) {
1134 Some(s) => *s,
1135 None => continue,
1136 }
1137 };
1138 let style = &style;
1139
1140 let span_start = span.byte_range.start;
1141 let span_end = span.byte_range.end;
1142
1143 let start_row = row_starts
1144 .partition_point(|&rs| rs <= span_start)
1145 .saturating_sub(1);
1146
1147 let mut row = start_row;
1148 while row < row_count {
1149 let row_byte_start = row_starts[row];
1150 let row_byte_end = row_starts
1151 .get(row + 1)
1152 .map(|&s| s.saturating_sub(1))
1153 .unwrap_or(bytes.len());
1154
1155 if row_byte_start >= span_end {
1156 break;
1157 }
1158
1159 let local_start = span_start.saturating_sub(row_byte_start);
1160 let local_end = span_end.min(row_byte_end) - row_byte_start;
1161
1162 if local_end > local_start {
1163 by_row[row].push((local_start, local_end, *style));
1164 }
1165
1166 row += 1;
1167 }
1168 }
1169
1170 by_row
1171}
1172
1173fn collect_diag_signs_range(
1178 h: &mut Highlighter,
1179 rope: &ropey::Rope,
1180 row_starts: &[usize],
1181 vp_top: usize,
1182 vp_end: usize,
1183) -> Vec<DiagSign> {
1184 let rope_len = rope.len_bytes();
1185 let byte_start = row_starts.get(vp_top).copied().unwrap_or(rope_len);
1186 let byte_end = row_starts.get(vp_end).copied().unwrap_or(rope_len);
1187 let window: String = if byte_start < byte_end && byte_end <= rope_len {
1191 rope.byte_slice(byte_start..byte_end).to_string()
1192 } else {
1193 String::new()
1194 };
1195 let errors = h.parse_errors_range(window.as_bytes(), 0..(byte_end - byte_start));
1197 let mut signs: Vec<DiagSign> = Vec::new();
1198 let mut last_row: Option<usize> = None;
1199 for err in &errors {
1200 let abs_start = err.byte_range.start + byte_start;
1202 let r = row_starts
1203 .partition_point(|&rs| rs <= abs_start)
1204 .saturating_sub(1);
1205 if last_row == Some(r) {
1206 continue;
1207 }
1208 last_row = Some(r);
1209 signs.push(DiagSign::new(r, 'E', 100));
1210 }
1211 signs
1212}
1213
1214pub fn layer_with_theme(
1220 theme: Arc<DotFallbackTheme>,
1221 directory: Arc<LanguageDirectory>,
1222) -> SyntaxLayer {
1223 SyntaxLayer::new(theme, directory)
1224}
1225
1226#[cfg(test)]
1228pub fn default_layer() -> SyntaxLayer {
1229 let directory = Arc::new(LanguageDirectory::new().expect("language directory"));
1230 SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()), directory)
1231}
1232
1233#[cfg(test)]
1238mod tests {
1239 use super::*;
1240 use hjkl_buffer::Buffer;
1241 use std::path::Path;
1242
1243 const TID: BufferId = 0;
1244
1245 #[test]
1248 fn diag_sign_new_roundtrip() {
1249 let s = DiagSign::new(7, 'W', 50);
1250 assert_eq!(s.row, 7);
1251 assert_eq!(s.ch, 'W');
1252 assert_eq!(s.priority, 50);
1253 }
1254
1255 #[test]
1256 fn diag_sign_default_is_sensible() {
1257 let s = DiagSign::default();
1258 assert_eq!(s.row, 0);
1259 assert_eq!(s.ch, 'E');
1260 assert_eq!(s.priority, 0);
1261 }
1262
1263 #[test]
1266 fn perf_breakdown_default_zeros() {
1267 let p = PerfBreakdown::new();
1268 assert_eq!(p.source_build_us, 0);
1269 assert_eq!(p.parse_us, 0);
1270 assert_eq!(p.highlight_us, 0);
1271 assert_eq!(p.by_row_us, 0);
1272 assert_eq!(p.diag_us, 0);
1273 }
1274
1275 #[test]
1278 fn set_language_outcome_is_known() {
1279 assert!(SetLanguageOutcome::Ready.is_known());
1280 assert!(SetLanguageOutcome::Loading("rust".to_string()).is_known());
1281 assert!(!SetLanguageOutcome::Unknown.is_known());
1282 }
1283
1284 #[test]
1287 fn render_output_new_roundtrip() {
1288 let out = RenderOutput::new(
1289 99,
1290 vec![vec![]],
1291 vec![DiagSign::new(0, 'E', 100)],
1292 (7, 0, 30),
1293 PerfBreakdown::new(),
1294 );
1295 assert_eq!(out.buffer_id, 99);
1296 assert_eq!(out.key, (7, 0, 30));
1297 assert_eq!(out.signs.len(), 1);
1298 }
1299
1300 #[test]
1301 fn render_output_partial_eq_same() {
1302 let a = RenderOutput::new(
1303 0,
1304 vec![vec![(0, 5, StyleSpec::default())]],
1305 vec![],
1306 (1, 0, 10),
1307 PerfBreakdown::default(),
1308 );
1309 let b = a.clone();
1310 assert_eq!(a, b);
1311 }
1312
1313 #[test]
1316 fn build_by_row_empty_spans_gives_empty_rows() {
1317 let by_row = build_by_row(
1318 &[],
1319 b"hello\nworld\n",
1320 &[0, 6, 12],
1321 2,
1322 &DotFallbackTheme::dark(),
1323 );
1324 assert_eq!(by_row.len(), 2);
1325 assert!(by_row[0].is_empty());
1326 assert!(by_row[1].is_empty());
1327 }
1328
1329 #[test]
1330 fn build_by_row_hex_color_uses_metadata_colors() {
1331 let bytes = b"--accent: #bb9af7;";
1332 let mut metadata = std::collections::HashMap::new();
1333 metadata.insert(
1334 HEX_BG_KEY.to_string(),
1335 MetaValue::Str("#bb9af7".to_string()),
1336 );
1337 metadata.insert(
1338 HEX_FG_KEY.to_string(),
1339 MetaValue::Str("#ffffff".to_string()),
1340 );
1341 let span = hjkl_bonsai::HighlightSpan {
1342 byte_range: 10..17,
1343 capture: HEX_COLOR_CAPTURE.to_string(),
1344 metadata,
1345 };
1346 let by_row = build_by_row(&[span], bytes, &[0], 1, &DotFallbackTheme::dark());
1347 assert_eq!(by_row.len(), 1);
1348 assert_eq!(by_row[0].len(), 1);
1349 let (_, _, style) = by_row[0][0];
1350 let bg = style.bg.expect("hex color must set background");
1351 assert_eq!((bg.r, bg.g, bg.b), (0xbb, 0x9a, 0xf7));
1352 let fg = style.fg.expect("hex color must set foreground");
1353 assert_eq!((fg.r, fg.g, fg.b), (0xff, 0xff, 0xff));
1354 }
1355
1356 #[test]
1357 fn build_by_row_hex_color_without_metadata_skips() {
1358 let span = hjkl_bonsai::HighlightSpan {
1359 byte_range: 0..3,
1360 capture: HEX_COLOR_CAPTURE.to_string(),
1361 metadata: std::collections::HashMap::new(),
1362 };
1363 let by_row = build_by_row(&[span], b"foo", &[0], 1, &DotFallbackTheme::dark());
1364 assert_eq!(by_row.len(), 1);
1365 assert!(by_row[0].is_empty());
1366 }
1367
1368 #[test]
1371 fn render_viewport_with_no_language_returns_none() {
1372 let buf = Buffer::from_str("hello world");
1373 let mut layer = default_layer();
1374 assert!(
1375 !layer
1376 .set_language_for_path(TID, Path::new("a.unknownext"))
1377 .is_known()
1378 );
1379 assert!(layer.render_viewport(TID, &buf, 0, 10).is_none());
1380 }
1381
1382 #[test]
1383 fn apply_edits_with_no_language_is_noop() {
1384 let mut layer = default_layer();
1385 let edits = vec![hjkl_engine::ContentEdit {
1386 start_byte: 0,
1387 old_end_byte: 0,
1388 new_end_byte: 1,
1389 start_position: (0, 0),
1390 old_end_position: (0, 0),
1391 new_end_position: (0, 1),
1392 }];
1393 layer.apply_edits(TID, &edits);
1394 }
1396
1397 #[test]
1398 fn set_language_for_path_returns_unknown_for_unrecognized_extension() {
1399 let mut layer = default_layer();
1400 let outcome = layer.set_language_for_path(TID, Path::new("a.zzznope_not_real"));
1401 assert!(!outcome.is_known());
1402 assert!(matches!(outcome, SetLanguageOutcome::Unknown));
1403 }
1404
1405 #[test]
1406 fn poll_pending_loads_drains_ready_handles() {
1407 let mut layer = default_layer();
1408 let events = layer.poll_pending_loads();
1409 assert!(
1410 events.is_empty(),
1411 "expected no events with no pending loads"
1412 );
1413 }
1414
1415 #[test]
1416 fn forget_removes_client_state() {
1417 let mut layer = default_layer();
1418 layer.set_language_for_path(TID, Path::new("a.zzz_unknown"));
1419 layer.forget(TID);
1420 assert!(!layer.clients.contains_key(&TID));
1421 }
1422
1423 #[test]
1442 fn extract_fold_ranges_returns_none_when_no_language_attached() {
1443 let buf =
1449 Buffer::from_str("fn hello() {\n let x = 1;\n x\n}\n\nfn world() {\n 2\n}\n");
1450 let mut layer = default_layer();
1451 layer.set_language_for_path(TID, Path::new("a.zzz_no_grammar_here"));
1453 let result = layer.extract_fold_ranges(TID, &buf);
1454 assert!(
1455 result.is_none(),
1456 "extract_fold_ranges must return None when no grammar is attached \
1457 (grammar still loading or unknown extension); got {result:?}"
1458 );
1459 }
1460
1461 #[test]
1462 fn extract_fold_ranges_returns_none_when_no_client_registered() {
1463 let buf = Buffer::from_str("fn foo() {}\n");
1465 let mut layer = default_layer();
1466 let result = layer.extract_fold_ranges(TID, &buf);
1468 assert!(
1469 result.is_none(),
1470 "extract_fold_ranges must return None when buffer has no syntax client; \
1471 got {result:?}"
1472 );
1473 }
1474
1475 #[test]
1478 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1479 fn parse_and_render_small_rust_buffer() {
1480 let buf = Buffer::from_str("fn main() { let x = 1; }\n");
1481 let mut layer = default_layer();
1482 assert!(
1483 layer
1484 .set_language_for_path(TID, Path::new("a.rs"))
1485 .is_known()
1486 );
1487 let out = layer
1488 .render_viewport(TID, &buf, 0, 10)
1489 .expect("render output");
1490 assert!(
1491 out.spans.iter().any(|r| !r.is_empty()),
1492 "expected at least one styled span"
1493 );
1494 }
1495
1496 #[test]
1497 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1498 fn diagnostics_emit_sign_for_syntax_error() {
1499 let buf = Buffer::from_str("fn main() {\nlet x = ;\n}\n");
1500 let mut layer = default_layer();
1501 layer.set_language_for_path(TID, Path::new("a.rs"));
1502 let out = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1503 assert!(
1504 !out.signs.is_empty(),
1505 "expected at least one diagnostic sign for `let x = ;`"
1506 );
1507 assert!(
1508 out.signs.iter().any(|s| s.row == 1 && s.ch == 'E'),
1509 "expected an 'E' sign on row 1; got {:?}",
1510 out.signs
1511 );
1512 }
1513
1514 #[test]
1515 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1516 fn incremental_path_matches_cold_for_small_edit() {
1517 let pre = Buffer::from_str("fn main() { let x = 1; }");
1518 let mut layer = default_layer();
1519 layer.set_language_for_path(TID, Path::new("a.rs"));
1520 let _ = layer.render_viewport(TID, &pre, 0, 10).unwrap();
1521 layer.apply_edits(
1522 TID,
1523 &[hjkl_engine::ContentEdit {
1524 start_byte: 3,
1525 old_end_byte: 3,
1526 new_end_byte: 4,
1527 start_position: (0, 3),
1528 old_end_position: (0, 3),
1529 new_end_position: (0, 4),
1530 }],
1531 );
1532 let post = Buffer::from_str("fn Ymain() { let x = 1; }");
1533 let inc = layer.render_viewport(TID, &post, 0, 10).unwrap();
1534 let mut cold_layer = default_layer();
1535 cold_layer.set_language_for_path(TID, Path::new("a.rs"));
1536 let cold = cold_layer.render_viewport(TID, &post, 0, 10).unwrap();
1537 assert_eq!(inc.spans, cold.spans);
1538 }
1539
1540 #[test]
1541 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1542 fn forget_drops_buffer_state() {
1543 let buf = Buffer::from_str("fn main() {}");
1544 let mut layer = default_layer();
1545 layer.set_language_for_path(TID, Path::new("a.rs"));
1546 let _ = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1547 assert!(layer.clients.contains_key(&TID));
1548 layer.forget(TID);
1549 assert!(!layer.clients.contains_key(&TID));
1550 }
1551}