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
182#[derive(Debug)]
210pub struct RenderOutputRef<'a> {
211 pub buffer_id: BufferId,
213 pub spans: &'a [Vec<(usize, usize, StyleSpec)>],
215 pub signs: Vec<DiagSign>,
217 pub key: (u64, usize, usize),
219 pub perf: PerfBreakdown,
221}
222
223impl RenderOutputRef<'_> {
224 pub fn into_owned(self) -> RenderOutput {
242 RenderOutput {
243 buffer_id: self.buffer_id,
244 spans: self.spans.to_vec(),
245 signs: self.signs,
246 key: self.key,
247 perf: self.perf,
248 }
249 }
250}
251
252impl PartialEq for RenderOutput {
253 fn eq(&self, other: &Self) -> bool {
254 self.spans == other.spans
255 && self.signs.len() == other.signs.len()
256 && self
257 .signs
258 .iter()
259 .zip(other.signs.iter())
260 .all(|(a, b)| a.row == b.row && a.ch == b.ch && a.priority == b.priority)
261 }
262}
263
264#[non_exhaustive]
279pub enum SetLanguageOutcome {
280 Ready,
282 Loading(#[allow(dead_code)] String),
284 Unknown,
286}
287
288impl SetLanguageOutcome {
289 pub fn is_known(&self) -> bool {
291 matches!(self, Self::Ready | Self::Loading(_))
292 }
293}
294
295#[non_exhaustive]
309pub enum LoadEvent {
310 Ready { id: BufferId, name: String },
312 Failed {
314 id: BufferId,
315 name: String,
316 error: String,
317 },
318}
319
320#[derive(Debug)]
322pub enum LoadEventKind<'a> {
323 Ready { id: BufferId, name: &'a str },
325 Failed {
327 id: BufferId,
328 name: &'a str,
329 error: &'a str,
330 },
331}
332
333struct PendingLoad {
338 id: BufferId,
339 name: String,
340 handle: LoadHandle,
341}
342
343struct BufferClient {
349 has_language: bool,
350 current_lang: Option<Arc<Grammar>>,
351 highlighter: Option<Highlighter>,
353 cache_dirty_gen: Option<u64>,
355 cache_rows: Range<usize>,
357 cache_spans: Vec<Vec<(usize, usize, StyleSpec)>>,
359 cache_row_starts: Option<(u64, Arc<Vec<usize>>)>,
361 parsed_dirty_gen: Option<u64>,
363 cache_signs: Option<(u64, usize, usize, Vec<DiagSign>)>,
365}
366
367impl Default for BufferClient {
368 fn default() -> Self {
369 Self {
370 has_language: false,
371 current_lang: None,
372 highlighter: None,
373 cache_dirty_gen: None,
374 cache_rows: 0..0,
375 cache_spans: Vec::new(),
376 cache_row_starts: None,
377 parsed_dirty_gen: None,
378 cache_signs: None,
379 }
380 }
381}
382
383impl BufferClient {
384 fn invalidate_cache(&mut self) {
385 self.cache_dirty_gen = None;
386 self.cache_rows = 0..0;
387 self.cache_spans.clear();
388 self.cache_row_starts = None;
389 self.parsed_dirty_gen = None;
390 self.cache_signs = None;
391 }
392}
393
394pub struct SyntaxLayer {
414 pub directory: Arc<LanguageDirectory>,
416 theme: Arc<dyn Theme + Send + Sync>,
417 clients: HashMap<BufferId, BufferClient>,
418 pending_loads: Vec<PendingLoad>,
419 colorizer: bool,
421 colorizer_filetypes: Vec<String>,
423 rainbow_brackets: bool,
425}
426
427impl SyntaxLayer {
428 pub fn new(theme: Arc<dyn Theme + Send + Sync>, directory: Arc<LanguageDirectory>) -> Self {
443 Self {
444 directory,
445 theme,
446 clients: HashMap::new(),
447 pending_loads: Vec::new(),
448 colorizer: true,
449 colorizer_filetypes: vec![
450 "css".to_string(),
451 "scss".to_string(),
452 "sass".to_string(),
453 "less".to_string(),
454 "html".to_string(),
455 "vue".to_string(),
456 "svelte".to_string(),
457 "tailwindcss".to_string(),
458 "toml".to_string(),
459 "lua".to_string(),
460 "vim".to_string(),
461 ],
462 rainbow_brackets: true,
463 }
464 }
465
466 pub fn set_rainbow_brackets(&mut self, enabled: bool) {
470 if self.rainbow_brackets == enabled {
471 return;
472 }
473 self.rainbow_brackets = enabled;
474 for client in self.clients.values_mut() {
475 client.invalidate_cache();
476 }
477 }
478
479 pub fn set_colorizer(&mut self, enabled: bool, filetypes: Vec<String>) {
487 if self.colorizer == enabled && self.colorizer_filetypes == filetypes {
488 return;
489 }
490 self.colorizer = enabled;
491 self.colorizer_filetypes = filetypes;
492 for client in self.clients.values_mut() {
493 client.invalidate_cache();
494 }
495 }
496
497 pub fn directory(&self) -> &Arc<LanguageDirectory> {
499 &self.directory
500 }
501
502 fn client_mut(&mut self, id: BufferId) -> &mut BufferClient {
503 self.clients.entry(id).or_default()
504 }
505
506 pub fn set_language_for_path(&mut self, id: BufferId, path: &Path) -> SetLanguageOutcome {
529 match self.directory.request_for_path(path) {
530 GrammarRequest::Cached(grammar) => {
531 self.attach_grammar(id, grammar.clone());
532 let c = self.client_mut(id);
533 c.current_lang = Some(grammar);
534 c.has_language = true;
535 SetLanguageOutcome::Ready
536 }
537 GrammarRequest::Loading { name, handle } => {
538 let c = self.client_mut(id);
539 c.current_lang = None;
540 c.has_language = false;
541 c.highlighter = None;
542 c.invalidate_cache();
543 self.pending_loads.push(PendingLoad {
544 id,
545 name: name.clone(),
546 handle,
547 });
548 SetLanguageOutcome::Loading(name)
549 }
550 GrammarRequest::Unknown | _ => {
551 let c = self.client_mut(id);
552 c.current_lang = None;
553 c.has_language = false;
554 c.highlighter = None;
555 c.invalidate_cache();
556 SetLanguageOutcome::Unknown
557 }
558 }
559 }
560
561 fn attach_grammar(&mut self, id: BufferId, grammar: Arc<Grammar>) {
563 let c = self.clients.entry(id).or_default();
564 c.invalidate_cache();
565 match Highlighter::new(grammar) {
566 Ok(h) => {
567 c.highlighter = Some(h);
568 }
569 Err(e) => {
570 tracing::error!(buffer_id = id, error = %e, "failed to attach highlighter");
571 c.highlighter = None;
572 }
573 }
574 }
575
576 pub fn poll_pending_loads(&mut self) -> Vec<LoadEvent> {
580 let mut events = Vec::new();
581 let mut i = 0;
582 while i < self.pending_loads.len() {
583 match self.pending_loads[i].handle.try_recv() {
584 None => {
585 i += 1;
586 }
587 Some(Ok(lib_path)) => {
588 let name = self.pending_loads[i].name.clone();
589 let bid = self.pending_loads[i].id;
590 self.pending_loads.swap_remove(i);
591 match self.directory.complete_load(&name, &lib_path) {
592 Ok(grammar) => {
593 self.attach_grammar(bid, grammar.clone());
594 let c = self.client_mut(bid);
595 c.current_lang = Some(grammar);
596 c.has_language = true;
597 events.push(LoadEvent::Ready { id: bid, name });
598 }
599 Err(e) => {
600 events.push(LoadEvent::Failed {
601 id: bid,
602 name,
603 error: format!("{e:#}"),
604 });
605 }
606 }
607 }
608 Some(Err(err)) => {
609 let name = self.pending_loads[i].name.clone();
610 let bid = self.pending_loads[i].id;
611 self.pending_loads.swap_remove(i);
612 events.push(LoadEvent::Failed {
613 id: bid,
614 name,
615 error: err.to_string(),
616 });
617 }
618 }
619 }
620 events
621 }
622
623 pub fn forget(&mut self, id: BufferId) {
625 self.clients.remove(&id);
626 }
627
628 pub fn set_theme(&mut self, theme: Arc<dyn Theme + Send + Sync>) {
630 self.theme = theme;
631 for c in self.clients.values_mut() {
633 c.invalidate_cache();
634 }
635 }
636
637 pub fn apply_edits(&mut self, id: BufferId, edits: &[hjkl_engine::ContentEdit]) {
643 let c = match self.clients.get_mut(&id) {
644 Some(c) if c.has_language => c,
645 _ => return,
646 };
647 let Some(h) = c.highlighter.as_mut() else {
648 return;
649 };
650 for e in edits {
651 h.edit(&InputEdit {
652 start_byte: e.start_byte,
653 old_end_byte: e.old_end_byte,
654 new_end_byte: e.new_end_byte,
655 start_position: Point {
656 row: e.start_position.0 as usize,
657 column: e.start_position.1 as usize,
658 },
659 old_end_position: Point {
660 row: e.old_end_position.0 as usize,
661 column: e.old_end_position.1 as usize,
662 },
663 new_end_position: Point {
664 row: e.new_end_position.0 as usize,
665 column: e.new_end_position.1 as usize,
666 },
667 });
668 }
669 c.parsed_dirty_gen = None;
672 c.cache_row_starts = None;
673 c.cache_signs = None;
674 }
675
676 pub fn reset(&mut self, id: BufferId) {
680 if let Some(c) = self.clients.get_mut(&id) {
681 if let Some(h) = c.highlighter.as_mut() {
682 h.reset();
683 }
684 c.invalidate_cache();
685 }
686 }
687
688 pub fn extract_fold_ranges(
708 &mut self,
709 id: BufferId,
710 buffer: &impl hjkl_engine::Query,
711 ) -> Option<Vec<(usize, usize)>> {
712 let client = match self.clients.get_mut(&id) {
713 Some(c) if c.has_language => c,
714 _ => return None,
716 };
717 let highlighter = client.highlighter.as_mut()?;
719 let tree = highlighter.tree()?;
721 let grammar = highlighter.grammar()?;
722 let rope = buffer.rope();
723 Some(extract_fold_ranges_rope(tree, grammar, &rope))
725 }
726
727 pub fn render_viewport(
734 &mut self,
735 id: BufferId,
736 buffer: &impl Query,
737 viewport_top: usize,
738 viewport_height: usize,
739 ) -> Option<RenderOutput> {
740 Some(
741 self.render_viewport_ref(id, buffer, viewport_top, viewport_height)?
742 .into_owned(),
743 )
744 }
745
746 pub fn render_viewport_ref(
757 &mut self,
758 id: BufferId,
759 buffer: &impl Query,
760 viewport_top: usize,
761 viewport_height: usize,
762 ) -> Option<RenderOutputRef<'_>> {
763 let client = self.clients.get_mut(&id)?;
764 if !client.has_language {
765 return None;
766 }
767 let dg = buffer.dirty_gen();
768 let row_count = buffer.line_count() as usize;
769 if row_count == 0 || viewport_height == 0 {
770 return None;
771 }
772
773 let vp_top = viewport_top.min(row_count);
774 let vp_end = (vp_top + viewport_height).min(row_count);
775 if vp_end <= vp_top {
776 return None;
777 }
778
779 if client.cache_dirty_gen != Some(dg) {
781 client.invalidate_cache();
782 }
783
784 let rope = buffer.rope();
788
789 let row_starts: Arc<Vec<usize>> = if client
793 .cache_row_starts
794 .as_ref()
795 .is_some_and(|(g, _)| *g == dg)
796 {
797 Arc::clone(&client.cache_row_starts.as_ref().unwrap().1)
798 } else {
799 let mut rs: Vec<usize> = Vec::with_capacity(row_count + 1);
802 rs.push(0);
803 let mut chunk_pos = 0usize;
804 for chunk in rope.chunks() {
805 for nl in memchr::memchr_iter(b'\n', chunk.as_bytes()) {
806 rs.push(chunk_pos + nl + 1);
807 }
808 chunk_pos += chunk.len();
809 }
810 let arc = Arc::new(rs);
811 client.cache_row_starts = Some((dg, Arc::clone(&arc)));
812 arc
813 };
814
815 let needs_reparse = client.parsed_dirty_gen != Some(dg);
819 {
820 let highlighter = client.highlighter.as_mut()?;
821 if highlighter.tree().is_none() {
822 highlighter.parse_initial_rope(&rope);
823 if highlighter.tree().is_some() {
824 client.parsed_dirty_gen = Some(dg);
825 }
826 } else if needs_reparse {
827 let ok = highlighter.parse_incremental_rope(&rope);
833 if ok && highlighter.tree().is_some() {
834 client.parsed_dirty_gen = Some(dg);
835 }
836 }
837 }
838
839 let colorizer_enabled = {
842 let c = self.clients.get(&id)?;
843 let lang_name = c.current_lang.as_ref().map_or("", |g| g.name());
844 self.colorizer
845 && (self.colorizer_filetypes.is_empty()
846 || self.colorizer_filetypes.iter().any(|ft| ft == lang_name))
847 };
848 let rainbow_brackets_enabled = self.rainbow_brackets;
849
850 let client = self.clients.get_mut(&id)?;
852 let highlighter = client.highlighter.as_mut()?;
853
854 highlighter.tree()?;
856
857 let theme = self.theme.as_ref();
858 let directory = Arc::clone(&self.directory);
859
860 if client.cache_rows.is_empty() {
862 client.cache_spans = walk_rows(
864 highlighter,
865 &rope,
866 &row_starts,
867 row_count,
868 vp_top,
869 vp_end,
870 theme,
871 &directory,
872 colorizer_enabled,
873 rainbow_brackets_enabled,
874 );
875 client.cache_rows = vp_top..vp_end;
876 client.cache_dirty_gen = Some(dg);
877 } else {
878 let cache_covers_overlap =
879 vp_top < client.cache_rows.end && vp_end > client.cache_rows.start;
880 if !cache_covers_overlap {
881 client.cache_spans = walk_rows(
883 highlighter,
884 &rope,
885 &row_starts,
886 row_count,
887 vp_top,
888 vp_end,
889 theme,
890 &directory,
891 colorizer_enabled,
892 rainbow_brackets_enabled,
893 );
894 client.cache_rows = vp_top..vp_end;
895 } else {
896 if vp_top < client.cache_rows.start {
898 let new_rows = walk_rows(
899 highlighter,
900 &rope,
901 &row_starts,
902 row_count,
903 vp_top,
904 client.cache_rows.start,
905 theme,
906 &directory,
907 colorizer_enabled,
908 rainbow_brackets_enabled,
909 );
910 let mut combined = new_rows;
911 combined.append(&mut client.cache_spans);
912 client.cache_spans = combined;
913 client.cache_rows.start = vp_top;
914 }
915 if vp_end > client.cache_rows.end {
917 let new_rows = walk_rows(
918 highlighter,
919 &rope,
920 &row_starts,
921 row_count,
922 client.cache_rows.end,
923 vp_end,
924 theme,
925 &directory,
926 colorizer_enabled,
927 rainbow_brackets_enabled,
928 );
929 client.cache_spans.extend(new_rows);
930 client.cache_rows.end = vp_end;
931 }
932 }
933 client.cache_dirty_gen = Some(dg);
934 }
935
936 let offset = vp_top - client.cache_rows.start;
938 let len = vp_end - vp_top;
939
940 let signs = if client
944 .cache_signs
945 .as_ref()
946 .is_some_and(|(g, t, e, _)| *g == dg && *t == vp_top && *e == vp_end)
947 {
948 client.cache_signs.as_ref().unwrap().3.clone()
949 } else {
950 let s = collect_diag_signs_range(highlighter, &rope, &row_starts, vp_top, vp_end);
951 client.cache_signs = Some((dg, vp_top, vp_end, s.clone()));
952 s
953 };
954
955 let spans = &self.clients.get(&id)?.cache_spans[offset..offset + len];
957
958 Some(RenderOutputRef {
959 buffer_id: id,
960 spans,
961 signs,
962 key: (dg, vp_top, viewport_height),
963 perf: PerfBreakdown::default(),
964 })
965 }
966
967 pub fn name_for_path(&self, path: &Path) -> Option<String> {
969 self.directory.name_for_path(path)
970 }
971
972 #[doc(hidden)]
974 pub fn has_client(&self, id: BufferId) -> bool {
975 self.clients.contains_key(&id)
976 }
977
978 pub fn dispatch_load_event(
998 event: &LoadEvent,
999 mut handler: impl FnMut(LoadEventKind<'_>),
1000 ) -> bool {
1001 #[allow(unreachable_patterns)]
1002 match event {
1003 LoadEvent::Ready { id, name } => {
1004 handler(LoadEventKind::Ready { id: *id, name });
1005 true
1006 }
1007 LoadEvent::Failed { id, name, error } => {
1008 handler(LoadEventKind::Failed {
1009 id: *id,
1010 name,
1011 error,
1012 });
1013 true
1014 }
1015 _ => false,
1016 }
1017 }
1018}
1019
1020const RAINBOW_PALETTE: [Color; 7] = [
1027 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), ];
1035
1036#[allow(clippy::too_many_arguments)]
1041fn walk_rows(
1042 highlighter: &mut Highlighter,
1043 rope: &ropey::Rope,
1044 row_starts: &[usize],
1045 row_count: usize,
1046 seg_start: usize,
1047 seg_end: usize,
1048 theme: &dyn Theme,
1049 directory: &Arc<LanguageDirectory>,
1050 colorizer: bool,
1051 rainbow_brackets: bool,
1052) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1053 let rope_len = rope.len_bytes();
1054 let byte_start = row_starts.get(seg_start).copied().unwrap_or(rope_len);
1055 let byte_end = row_starts
1056 .get(seg_end)
1057 .copied()
1058 .unwrap_or(rope_len)
1059 .min(rope_len)
1060 .max(byte_start);
1061
1062 let mut flat_spans =
1063 highlighter.highlight_range_with_injections_rope(rope, byte_start..byte_end, |name| {
1064 directory.by_name(name)
1065 });
1066
1067 let marker_pass = CommentMarkerPass::new();
1068 marker_pass.apply_rope(&mut flat_spans, rope);
1069 if colorizer {
1070 let hex_color_pass = HexColorPass::new();
1071 hex_color_pass.apply_range_rope(&mut flat_spans, rope, byte_start..byte_end);
1072 }
1073 if rainbow_brackets
1074 && let (Some(tree), Some(grammar)) = (highlighter.tree(), highlighter.grammar())
1075 {
1076 let rb_spans = rainbow_spans_rope(tree, grammar, rope, byte_start..byte_end);
1077 flat_spans.extend(rb_spans);
1078 }
1079
1080 let _ = row_count; build_by_row_range(&flat_spans, rope_len, row_starts, seg_start..seg_end, theme)
1087}
1088
1089fn build_by_row_range(
1095 flat_spans: &[hjkl_bonsai::HighlightSpan],
1096 source_len: usize,
1097 row_starts: &[usize],
1098 row_range: Range<usize>,
1099 theme: &dyn Theme,
1100) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1101 let seg_start = row_range.start;
1102 let seg_end = row_range.end.min(row_starts.len());
1103 if seg_end <= seg_start {
1104 return Vec::new();
1105 }
1106 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); seg_end - seg_start];
1107
1108 for span in flat_spans {
1109 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
1110 let bg = match span.metadata().and_then(|m| m.get(HEX_BG_KEY)) {
1111 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1112 _ => None,
1113 };
1114 let fg = match span.metadata().and_then(|m| m.get(HEX_FG_KEY)) {
1115 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1116 _ => None,
1117 };
1118 bg.map(|bg| StyleSpec {
1119 fg,
1120 bg: Some(bg),
1121 modifiers: hjkl_theme::Modifiers::default(),
1122 })
1123 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
1124 let depth = match span.metadata().and_then(|m| m.get(RAINBOW_DEPTH_KEY)) {
1125 Some(MetaValue::Int(d)) => *d as usize,
1126 _ => 0,
1127 };
1128 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
1129 Some(StyleSpec {
1130 fg: Some(fg),
1131 bg: None,
1132 modifiers: hjkl_theme::Modifiers::default(),
1133 })
1134 } else {
1135 None
1136 };
1137
1138 let style: StyleSpec = if let Some(s) = hex_style {
1139 s
1140 } else {
1141 match theme.style(span.capture()) {
1142 Some(s) => *s,
1143 None => continue,
1144 }
1145 };
1146
1147 let span_start = span.byte_range.start;
1148 let span_end = span.byte_range.end;
1149
1150 let start_row = row_starts
1151 .partition_point(|&rs| rs <= span_start)
1152 .saturating_sub(1);
1153
1154 let mut row = start_row.max(seg_start);
1155 while row < seg_end {
1156 let row_byte_start = row_starts[row];
1157 let row_byte_end = row_starts
1158 .get(row + 1)
1159 .map_or(source_len, |&s| s.saturating_sub(1));
1160
1161 if row_byte_start >= span_end {
1162 break;
1163 }
1164
1165 let local_start = span_start.saturating_sub(row_byte_start);
1166 let local_end = span_end.min(row_byte_end) - row_byte_start;
1167
1168 if local_end > local_start {
1169 by_row[row - seg_start].push((local_start, local_end, style));
1170 }
1171
1172 row += 1;
1173 }
1174 }
1175
1176 by_row
1177}
1178
1179pub fn build_by_row(
1185 flat_spans: &[hjkl_bonsai::HighlightSpan],
1186 bytes: &[u8],
1187 row_starts: &[usize],
1188 row_count: usize,
1189 theme: &dyn Theme,
1190) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1191 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); row_count];
1192
1193 for span in flat_spans {
1194 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
1195 let bg = match span.metadata().and_then(|m| m.get(HEX_BG_KEY)) {
1196 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1197 _ => None,
1198 };
1199 let fg = match span.metadata().and_then(|m| m.get(HEX_FG_KEY)) {
1200 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1201 _ => None,
1202 };
1203 bg.map(|bg| StyleSpec {
1204 fg,
1205 bg: Some(bg),
1206 modifiers: hjkl_theme::Modifiers::default(),
1207 })
1208 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
1209 let depth = match span.metadata().and_then(|m| m.get(RAINBOW_DEPTH_KEY)) {
1210 Some(MetaValue::Int(d)) => *d as usize,
1211 _ => 0,
1212 };
1213 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
1214 Some(StyleSpec {
1215 fg: Some(fg),
1216 bg: None,
1217 modifiers: hjkl_theme::Modifiers::default(),
1218 })
1219 } else {
1220 None
1221 };
1222
1223 let style: StyleSpec = if let Some(s) = hex_style {
1224 s
1225 } else {
1226 match theme.style(span.capture()) {
1227 Some(s) => *s,
1228 None => continue,
1229 }
1230 };
1231 let style = &style;
1232
1233 let span_start = span.byte_range.start;
1234 let span_end = span.byte_range.end;
1235
1236 let start_row = row_starts
1237 .partition_point(|&rs| rs <= span_start)
1238 .saturating_sub(1);
1239
1240 let mut row = start_row;
1241 while row < row_count {
1242 let Some(&row_byte_start) = row_starts.get(row) else {
1245 break;
1246 };
1247 let row_byte_end = row_starts
1248 .get(row + 1)
1249 .map_or(bytes.len(), |&s| s.saturating_sub(1));
1250
1251 if row_byte_start >= span_end {
1252 break;
1253 }
1254
1255 let local_start = span_start.saturating_sub(row_byte_start);
1256 let local_end = span_end.min(row_byte_end) - row_byte_start;
1257
1258 if local_end > local_start {
1259 by_row[row].push((local_start, local_end, *style));
1260 }
1261
1262 row += 1;
1263 }
1264 }
1265
1266 by_row
1267}
1268
1269fn collect_diag_signs_range(
1274 h: &mut Highlighter,
1275 rope: &ropey::Rope,
1276 row_starts: &[usize],
1277 vp_top: usize,
1278 vp_end: usize,
1279) -> Vec<DiagSign> {
1280 let rope_len = rope.len_bytes();
1281 let byte_start = row_starts.get(vp_top).copied().unwrap_or(rope_len);
1282 let byte_end = row_starts.get(vp_end).copied().unwrap_or(rope_len);
1283 let source: Vec<u8> = if byte_start < byte_end && byte_end <= rope_len {
1292 let mut buf = vec![0u8; byte_end];
1293 let mut pos = byte_start;
1294 for chunk in rope.byte_slice(byte_start..byte_end).chunks() {
1295 buf[pos..pos + chunk.len()].copy_from_slice(chunk.as_bytes());
1296 pos += chunk.len();
1297 }
1298 buf
1299 } else {
1300 Vec::new()
1301 };
1302 let errors = h.parse_errors_range(&source, byte_start..byte_end);
1303 let mut signs: Vec<DiagSign> = Vec::new();
1304 let mut last_row: Option<usize> = None;
1305 for err in &errors {
1306 let abs_start = err.byte_range.start;
1308 let r = row_starts
1309 .partition_point(|&rs| rs <= abs_start)
1310 .saturating_sub(1);
1311 if last_row == Some(r) {
1312 continue;
1313 }
1314 last_row = Some(r);
1315 signs.push(DiagSign::new(r, 'E', 100));
1316 }
1317 signs
1318}
1319
1320pub fn layer_with_theme(
1326 theme: Arc<DotFallbackTheme>,
1327 directory: Arc<LanguageDirectory>,
1328) -> SyntaxLayer {
1329 SyntaxLayer::new(theme, directory)
1330}
1331
1332#[cfg(test)]
1334pub fn default_layer() -> SyntaxLayer {
1335 let directory = Arc::new(LanguageDirectory::new().expect("language directory"));
1336 SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()), directory)
1337}
1338
1339#[cfg(test)]
1344mod tests {
1345 use super::*;
1346 use hjkl_buffer::View;
1347 use std::fmt::Write as _;
1348 use std::path::Path;
1349
1350 const TID: BufferId = 0;
1351
1352 #[test]
1355 fn diag_sign_new_roundtrip() {
1356 let s = DiagSign::new(7, 'W', 50);
1357 assert_eq!(s.row, 7);
1358 assert_eq!(s.ch, 'W');
1359 assert_eq!(s.priority, 50);
1360 }
1361
1362 #[test]
1363 fn diag_sign_default_is_sensible() {
1364 let s = DiagSign::default();
1365 assert_eq!(s.row, 0);
1366 assert_eq!(s.ch, 'E');
1367 assert_eq!(s.priority, 0);
1368 }
1369
1370 #[test]
1373 fn perf_breakdown_default_zeros() {
1374 let p = PerfBreakdown::new();
1375 assert_eq!(p.source_build_us, 0);
1376 assert_eq!(p.parse_us, 0);
1377 assert_eq!(p.highlight_us, 0);
1378 assert_eq!(p.by_row_us, 0);
1379 assert_eq!(p.diag_us, 0);
1380 }
1381
1382 #[test]
1385 fn set_language_outcome_is_known() {
1386 assert!(SetLanguageOutcome::Ready.is_known());
1387 assert!(SetLanguageOutcome::Loading("rust".to_string()).is_known());
1388 assert!(!SetLanguageOutcome::Unknown.is_known());
1389 }
1390
1391 #[test]
1394 fn render_output_new_roundtrip() {
1395 let out = RenderOutput::new(
1396 99,
1397 vec![vec![]],
1398 vec![DiagSign::new(0, 'E', 100)],
1399 (7, 0, 30),
1400 PerfBreakdown::new(),
1401 );
1402 assert_eq!(out.buffer_id, 99);
1403 assert_eq!(out.key, (7, 0, 30));
1404 assert_eq!(out.signs.len(), 1);
1405 }
1406
1407 #[test]
1408 fn render_output_partial_eq_same() {
1409 let a = RenderOutput::new(
1410 0,
1411 vec![vec![(0, 5, StyleSpec::default())]],
1412 vec![],
1413 (1, 0, 10),
1414 PerfBreakdown::default(),
1415 );
1416 let b = a.clone();
1417 assert_eq!(a, b);
1418 }
1419
1420 #[test]
1423 fn build_by_row_empty_spans_gives_empty_rows() {
1424 let by_row = build_by_row(
1425 &[],
1426 b"hello\nworld\n",
1427 &[0, 6, 12],
1428 2,
1429 &DotFallbackTheme::dark(),
1430 );
1431 assert_eq!(by_row.len(), 2);
1432 assert!(by_row[0].is_empty());
1433 assert!(by_row[1].is_empty());
1434 }
1435
1436 #[test]
1437 fn build_by_row_hex_color_uses_metadata_colors() {
1438 let bytes = b"--accent: #bb9af7;";
1439 let mut metadata = std::collections::HashMap::new();
1440 metadata.insert(
1441 HEX_BG_KEY.to_string(),
1442 MetaValue::Str("#bb9af7".to_string()),
1443 );
1444 metadata.insert(
1445 HEX_FG_KEY.to_string(),
1446 MetaValue::Str("#ffffff".to_string()),
1447 );
1448 let span = hjkl_bonsai::HighlightSpan {
1449 byte_range: 10..17,
1450 capture: Arc::from(HEX_COLOR_CAPTURE),
1451 metadata: Some(Box::new(metadata)),
1452 };
1453 let by_row = build_by_row(&[span], bytes, &[0], 1, &DotFallbackTheme::dark());
1454 assert_eq!(by_row.len(), 1);
1455 assert_eq!(by_row[0].len(), 1);
1456 let (_, _, style) = by_row[0][0];
1457 let bg = style.bg.expect("hex color must set background");
1458 assert_eq!((bg.r, bg.g, bg.b), (0xbb, 0x9a, 0xf7));
1459 let fg = style.fg.expect("hex color must set foreground");
1460 assert_eq!((fg.r, fg.g, fg.b), (0xff, 0xff, 0xff));
1461 }
1462
1463 #[test]
1464 fn build_by_row_row_count_beyond_row_starts_no_panic() {
1465 let bytes = b"foo";
1468 let mut metadata = std::collections::HashMap::new();
1469 metadata.insert(
1470 HEX_BG_KEY.to_string(),
1471 MetaValue::Str("#112233".to_string()),
1472 );
1473 let span = hjkl_bonsai::HighlightSpan {
1474 byte_range: 0..3,
1475 capture: Arc::from(HEX_COLOR_CAPTURE),
1476 metadata: Some(Box::new(metadata)),
1477 };
1478 let by_row = build_by_row(&[span], bytes, &[0], 3, &DotFallbackTheme::dark());
1479 assert_eq!(by_row.len(), 3);
1480 assert_eq!(by_row[0].len(), 1);
1481 assert!(by_row[1].is_empty());
1482 assert!(by_row[2].is_empty());
1483 }
1484
1485 #[test]
1486 fn build_by_row_hex_color_without_metadata_skips() {
1487 let span = hjkl_bonsai::HighlightSpan {
1488 byte_range: 0..3,
1489 capture: Arc::from(HEX_COLOR_CAPTURE),
1490 metadata: None,
1491 };
1492 let by_row = build_by_row(&[span], b"foo", &[0], 1, &DotFallbackTheme::dark());
1493 assert_eq!(by_row.len(), 1);
1494 assert!(by_row[0].is_empty());
1495 }
1496
1497 #[test]
1500 fn render_viewport_with_no_language_returns_none() {
1501 let buf = View::from_str("hello world");
1502 let mut layer = default_layer();
1503 assert!(
1504 !layer
1505 .set_language_for_path(TID, Path::new("a.unknownext"))
1506 .is_known()
1507 );
1508 assert!(layer.render_viewport(TID, &buf, 0, 10).is_none());
1509 }
1510
1511 #[test]
1512 fn apply_edits_with_no_language_is_noop() {
1513 let mut layer = default_layer();
1514 let edits = vec![hjkl_engine::ContentEdit {
1515 start_byte: 0,
1516 old_end_byte: 0,
1517 new_end_byte: 1,
1518 start_position: (0, 0),
1519 old_end_position: (0, 0),
1520 new_end_position: (0, 1),
1521 }];
1522 layer.apply_edits(TID, &edits);
1523 }
1525
1526 #[test]
1527 fn set_language_for_path_returns_unknown_for_unrecognized_extension() {
1528 let mut layer = default_layer();
1529 let outcome = layer.set_language_for_path(TID, Path::new("a.zzznope_not_real"));
1530 assert!(!outcome.is_known());
1531 assert!(matches!(outcome, SetLanguageOutcome::Unknown));
1532 }
1533
1534 #[test]
1535 fn poll_pending_loads_drains_ready_handles() {
1536 let mut layer = default_layer();
1537 let events = layer.poll_pending_loads();
1538 assert!(
1539 events.is_empty(),
1540 "expected no events with no pending loads"
1541 );
1542 }
1543
1544 #[test]
1545 fn forget_removes_client_state() {
1546 let mut layer = default_layer();
1547 layer.set_language_for_path(TID, Path::new("a.zzz_unknown"));
1548 layer.forget(TID);
1549 assert!(!layer.clients.contains_key(&TID));
1550 }
1551
1552 #[test]
1571 fn extract_fold_ranges_returns_none_when_no_language_attached() {
1572 let buf =
1578 View::from_str("fn hello() {\n let x = 1;\n x\n}\n\nfn world() {\n 2\n}\n");
1579 let mut layer = default_layer();
1580 layer.set_language_for_path(TID, Path::new("a.zzz_no_grammar_here"));
1582 let result = layer.extract_fold_ranges(TID, &buf);
1583 assert!(
1584 result.is_none(),
1585 "extract_fold_ranges must return None when no grammar is attached \
1586 (grammar still loading or unknown extension); got {result:?}"
1587 );
1588 }
1589
1590 #[test]
1591 fn extract_fold_ranges_returns_none_when_no_client_registered() {
1592 let buf = View::from_str("fn foo() {}\n");
1594 let mut layer = default_layer();
1595 let result = layer.extract_fold_ranges(TID, &buf);
1597 assert!(
1598 result.is_none(),
1599 "extract_fold_ranges must return None when buffer has no syntax client; \
1600 got {result:?}"
1601 );
1602 }
1603
1604 #[test]
1607 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1608 fn parse_and_render_small_rust_buffer() {
1609 let buf = View::from_str("fn main() { let x = 1; }\n");
1610 let mut layer = default_layer();
1611 assert!(
1612 layer
1613 .set_language_for_path(TID, Path::new("a.rs"))
1614 .is_known()
1615 );
1616 let out = layer
1617 .render_viewport(TID, &buf, 0, 10)
1618 .expect("render output");
1619 assert!(
1620 out.spans.iter().any(|r| !r.is_empty()),
1621 "expected at least one styled span"
1622 );
1623 }
1624
1625 #[test]
1626 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1627 fn diagnostics_emit_sign_for_syntax_error() {
1628 let buf = View::from_str("fn main() {\nlet x = ;\n}\n");
1629 let mut layer = default_layer();
1630 layer.set_language_for_path(TID, Path::new("a.rs"));
1631 let out = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1632 assert!(
1633 !out.signs.is_empty(),
1634 "expected at least one diagnostic sign for `let x = ;`"
1635 );
1636 assert!(
1637 out.signs.iter().any(|s| s.row == 1 && s.ch == 'E'),
1638 "expected an 'E' sign on row 1; got {:?}",
1639 out.signs
1640 );
1641 }
1642
1643 #[test]
1644 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1645 fn diagnostics_signs_correct_when_scrolled() {
1646 let mut src = String::new();
1654 for i in 0..50 {
1655 let _ = writeln!(src, "fn f{i}() {{}}");
1656 }
1657 src.push_str("fn broken() {\nlet x = ;\n}\n");
1658 let buf = View::from_str(&src);
1659 let mut layer = default_layer();
1660 layer.set_language_for_path(TID, Path::new("a.rs"));
1661 let out = layer.render_viewport(TID, &buf, 45, 20).unwrap();
1662 assert!(
1663 out.signs
1664 .iter()
1665 .any(|s| (50..=52).contains(&s.row) && s.ch == 'E'),
1666 "expected an 'E' sign on rows 50..=52; got {:?}",
1667 out.signs
1668 );
1669 }
1670
1671 #[test]
1672 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1673 fn incremental_path_matches_cold_for_small_edit() {
1674 let pre = View::from_str("fn main() { let x = 1; }");
1675 let mut layer = default_layer();
1676 layer.set_language_for_path(TID, Path::new("a.rs"));
1677 let _ = layer.render_viewport(TID, &pre, 0, 10).unwrap();
1678 layer.apply_edits(
1679 TID,
1680 &[hjkl_engine::ContentEdit {
1681 start_byte: 3,
1682 old_end_byte: 3,
1683 new_end_byte: 4,
1684 start_position: (0, 3),
1685 old_end_position: (0, 3),
1686 new_end_position: (0, 4),
1687 }],
1688 );
1689 let post = View::from_str("fn Ymain() { let x = 1; }");
1690 let inc = layer.render_viewport(TID, &post, 0, 10).unwrap();
1691 let mut cold_layer = default_layer();
1692 cold_layer.set_language_for_path(TID, Path::new("a.rs"));
1693 let cold = cold_layer.render_viewport(TID, &post, 0, 10).unwrap();
1694 assert_eq!(inc.spans, cold.spans);
1695 }
1696
1697 #[test]
1698 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1699 fn forget_drops_buffer_state() {
1700 let buf = View::from_str("fn main() {}");
1701 let mut layer = default_layer();
1702 layer.set_language_for_path(TID, Path::new("a.rs"));
1703 let _ = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1704 assert!(layer.clients.contains_key(&TID));
1705 layer.forget(TID);
1706 assert!(!layer.clients.contains_key(&TID));
1707 }
1708}