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 Some(&row_byte_start) = row_starts.get(row) else {
1152 break;
1153 };
1154 let row_byte_end = row_starts
1155 .get(row + 1)
1156 .map(|&s| s.saturating_sub(1))
1157 .unwrap_or(bytes.len());
1158
1159 if row_byte_start >= span_end {
1160 break;
1161 }
1162
1163 let local_start = span_start.saturating_sub(row_byte_start);
1164 let local_end = span_end.min(row_byte_end) - row_byte_start;
1165
1166 if local_end > local_start {
1167 by_row[row].push((local_start, local_end, *style));
1168 }
1169
1170 row += 1;
1171 }
1172 }
1173
1174 by_row
1175}
1176
1177fn collect_diag_signs_range(
1182 h: &mut Highlighter,
1183 rope: &ropey::Rope,
1184 row_starts: &[usize],
1185 vp_top: usize,
1186 vp_end: usize,
1187) -> Vec<DiagSign> {
1188 let rope_len = rope.len_bytes();
1189 let byte_start = row_starts.get(vp_top).copied().unwrap_or(rope_len);
1190 let byte_end = row_starts.get(vp_end).copied().unwrap_or(rope_len);
1191 let source: Vec<u8> = if byte_start < byte_end && byte_end <= rope_len {
1200 let mut buf = vec![0u8; byte_end];
1201 let mut pos = byte_start;
1202 for chunk in rope.byte_slice(byte_start..byte_end).chunks() {
1203 buf[pos..pos + chunk.len()].copy_from_slice(chunk.as_bytes());
1204 pos += chunk.len();
1205 }
1206 buf
1207 } else {
1208 Vec::new()
1209 };
1210 let errors = h.parse_errors_range(&source, byte_start..byte_end);
1211 let mut signs: Vec<DiagSign> = Vec::new();
1212 let mut last_row: Option<usize> = None;
1213 for err in &errors {
1214 let abs_start = err.byte_range.start;
1216 let r = row_starts
1217 .partition_point(|&rs| rs <= abs_start)
1218 .saturating_sub(1);
1219 if last_row == Some(r) {
1220 continue;
1221 }
1222 last_row = Some(r);
1223 signs.push(DiagSign::new(r, 'E', 100));
1224 }
1225 signs
1226}
1227
1228pub fn layer_with_theme(
1234 theme: Arc<DotFallbackTheme>,
1235 directory: Arc<LanguageDirectory>,
1236) -> SyntaxLayer {
1237 SyntaxLayer::new(theme, directory)
1238}
1239
1240#[cfg(test)]
1242pub fn default_layer() -> SyntaxLayer {
1243 let directory = Arc::new(LanguageDirectory::new().expect("language directory"));
1244 SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()), directory)
1245}
1246
1247#[cfg(test)]
1252mod tests {
1253 use super::*;
1254 use hjkl_buffer::View;
1255 use std::path::Path;
1256
1257 const TID: BufferId = 0;
1258
1259 #[test]
1262 fn diag_sign_new_roundtrip() {
1263 let s = DiagSign::new(7, 'W', 50);
1264 assert_eq!(s.row, 7);
1265 assert_eq!(s.ch, 'W');
1266 assert_eq!(s.priority, 50);
1267 }
1268
1269 #[test]
1270 fn diag_sign_default_is_sensible() {
1271 let s = DiagSign::default();
1272 assert_eq!(s.row, 0);
1273 assert_eq!(s.ch, 'E');
1274 assert_eq!(s.priority, 0);
1275 }
1276
1277 #[test]
1280 fn perf_breakdown_default_zeros() {
1281 let p = PerfBreakdown::new();
1282 assert_eq!(p.source_build_us, 0);
1283 assert_eq!(p.parse_us, 0);
1284 assert_eq!(p.highlight_us, 0);
1285 assert_eq!(p.by_row_us, 0);
1286 assert_eq!(p.diag_us, 0);
1287 }
1288
1289 #[test]
1292 fn set_language_outcome_is_known() {
1293 assert!(SetLanguageOutcome::Ready.is_known());
1294 assert!(SetLanguageOutcome::Loading("rust".to_string()).is_known());
1295 assert!(!SetLanguageOutcome::Unknown.is_known());
1296 }
1297
1298 #[test]
1301 fn render_output_new_roundtrip() {
1302 let out = RenderOutput::new(
1303 99,
1304 vec![vec![]],
1305 vec![DiagSign::new(0, 'E', 100)],
1306 (7, 0, 30),
1307 PerfBreakdown::new(),
1308 );
1309 assert_eq!(out.buffer_id, 99);
1310 assert_eq!(out.key, (7, 0, 30));
1311 assert_eq!(out.signs.len(), 1);
1312 }
1313
1314 #[test]
1315 fn render_output_partial_eq_same() {
1316 let a = RenderOutput::new(
1317 0,
1318 vec![vec![(0, 5, StyleSpec::default())]],
1319 vec![],
1320 (1, 0, 10),
1321 PerfBreakdown::default(),
1322 );
1323 let b = a.clone();
1324 assert_eq!(a, b);
1325 }
1326
1327 #[test]
1330 fn build_by_row_empty_spans_gives_empty_rows() {
1331 let by_row = build_by_row(
1332 &[],
1333 b"hello\nworld\n",
1334 &[0, 6, 12],
1335 2,
1336 &DotFallbackTheme::dark(),
1337 );
1338 assert_eq!(by_row.len(), 2);
1339 assert!(by_row[0].is_empty());
1340 assert!(by_row[1].is_empty());
1341 }
1342
1343 #[test]
1344 fn build_by_row_hex_color_uses_metadata_colors() {
1345 let bytes = b"--accent: #bb9af7;";
1346 let mut metadata = std::collections::HashMap::new();
1347 metadata.insert(
1348 HEX_BG_KEY.to_string(),
1349 MetaValue::Str("#bb9af7".to_string()),
1350 );
1351 metadata.insert(
1352 HEX_FG_KEY.to_string(),
1353 MetaValue::Str("#ffffff".to_string()),
1354 );
1355 let span = hjkl_bonsai::HighlightSpan {
1356 byte_range: 10..17,
1357 capture: HEX_COLOR_CAPTURE.to_string(),
1358 metadata,
1359 };
1360 let by_row = build_by_row(&[span], bytes, &[0], 1, &DotFallbackTheme::dark());
1361 assert_eq!(by_row.len(), 1);
1362 assert_eq!(by_row[0].len(), 1);
1363 let (_, _, style) = by_row[0][0];
1364 let bg = style.bg.expect("hex color must set background");
1365 assert_eq!((bg.r, bg.g, bg.b), (0xbb, 0x9a, 0xf7));
1366 let fg = style.fg.expect("hex color must set foreground");
1367 assert_eq!((fg.r, fg.g, fg.b), (0xff, 0xff, 0xff));
1368 }
1369
1370 #[test]
1371 fn build_by_row_row_count_beyond_row_starts_no_panic() {
1372 let bytes = b"foo";
1375 let mut metadata = std::collections::HashMap::new();
1376 metadata.insert(
1377 HEX_BG_KEY.to_string(),
1378 MetaValue::Str("#112233".to_string()),
1379 );
1380 let span = hjkl_bonsai::HighlightSpan {
1381 byte_range: 0..3,
1382 capture: HEX_COLOR_CAPTURE.to_string(),
1383 metadata,
1384 };
1385 let by_row = build_by_row(&[span], bytes, &[0], 3, &DotFallbackTheme::dark());
1386 assert_eq!(by_row.len(), 3);
1387 assert_eq!(by_row[0].len(), 1);
1388 assert!(by_row[1].is_empty());
1389 assert!(by_row[2].is_empty());
1390 }
1391
1392 #[test]
1393 fn build_by_row_hex_color_without_metadata_skips() {
1394 let span = hjkl_bonsai::HighlightSpan {
1395 byte_range: 0..3,
1396 capture: HEX_COLOR_CAPTURE.to_string(),
1397 metadata: std::collections::HashMap::new(),
1398 };
1399 let by_row = build_by_row(&[span], b"foo", &[0], 1, &DotFallbackTheme::dark());
1400 assert_eq!(by_row.len(), 1);
1401 assert!(by_row[0].is_empty());
1402 }
1403
1404 #[test]
1407 fn render_viewport_with_no_language_returns_none() {
1408 let buf = View::from_str("hello world");
1409 let mut layer = default_layer();
1410 assert!(
1411 !layer
1412 .set_language_for_path(TID, Path::new("a.unknownext"))
1413 .is_known()
1414 );
1415 assert!(layer.render_viewport(TID, &buf, 0, 10).is_none());
1416 }
1417
1418 #[test]
1419 fn apply_edits_with_no_language_is_noop() {
1420 let mut layer = default_layer();
1421 let edits = vec![hjkl_engine::ContentEdit {
1422 start_byte: 0,
1423 old_end_byte: 0,
1424 new_end_byte: 1,
1425 start_position: (0, 0),
1426 old_end_position: (0, 0),
1427 new_end_position: (0, 1),
1428 }];
1429 layer.apply_edits(TID, &edits);
1430 }
1432
1433 #[test]
1434 fn set_language_for_path_returns_unknown_for_unrecognized_extension() {
1435 let mut layer = default_layer();
1436 let outcome = layer.set_language_for_path(TID, Path::new("a.zzznope_not_real"));
1437 assert!(!outcome.is_known());
1438 assert!(matches!(outcome, SetLanguageOutcome::Unknown));
1439 }
1440
1441 #[test]
1442 fn poll_pending_loads_drains_ready_handles() {
1443 let mut layer = default_layer();
1444 let events = layer.poll_pending_loads();
1445 assert!(
1446 events.is_empty(),
1447 "expected no events with no pending loads"
1448 );
1449 }
1450
1451 #[test]
1452 fn forget_removes_client_state() {
1453 let mut layer = default_layer();
1454 layer.set_language_for_path(TID, Path::new("a.zzz_unknown"));
1455 layer.forget(TID);
1456 assert!(!layer.clients.contains_key(&TID));
1457 }
1458
1459 #[test]
1478 fn extract_fold_ranges_returns_none_when_no_language_attached() {
1479 let buf =
1485 View::from_str("fn hello() {\n let x = 1;\n x\n}\n\nfn world() {\n 2\n}\n");
1486 let mut layer = default_layer();
1487 layer.set_language_for_path(TID, Path::new("a.zzz_no_grammar_here"));
1489 let result = layer.extract_fold_ranges(TID, &buf);
1490 assert!(
1491 result.is_none(),
1492 "extract_fold_ranges must return None when no grammar is attached \
1493 (grammar still loading or unknown extension); got {result:?}"
1494 );
1495 }
1496
1497 #[test]
1498 fn extract_fold_ranges_returns_none_when_no_client_registered() {
1499 let buf = View::from_str("fn foo() {}\n");
1501 let mut layer = default_layer();
1502 let result = layer.extract_fold_ranges(TID, &buf);
1504 assert!(
1505 result.is_none(),
1506 "extract_fold_ranges must return None when buffer has no syntax client; \
1507 got {result:?}"
1508 );
1509 }
1510
1511 #[test]
1514 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1515 fn parse_and_render_small_rust_buffer() {
1516 let buf = View::from_str("fn main() { let x = 1; }\n");
1517 let mut layer = default_layer();
1518 assert!(
1519 layer
1520 .set_language_for_path(TID, Path::new("a.rs"))
1521 .is_known()
1522 );
1523 let out = layer
1524 .render_viewport(TID, &buf, 0, 10)
1525 .expect("render output");
1526 assert!(
1527 out.spans.iter().any(|r| !r.is_empty()),
1528 "expected at least one styled span"
1529 );
1530 }
1531
1532 #[test]
1533 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1534 fn diagnostics_emit_sign_for_syntax_error() {
1535 let buf = View::from_str("fn main() {\nlet x = ;\n}\n");
1536 let mut layer = default_layer();
1537 layer.set_language_for_path(TID, Path::new("a.rs"));
1538 let out = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1539 assert!(
1540 !out.signs.is_empty(),
1541 "expected at least one diagnostic sign for `let x = ;`"
1542 );
1543 assert!(
1544 out.signs.iter().any(|s| s.row == 1 && s.ch == 'E'),
1545 "expected an 'E' sign on row 1; got {:?}",
1546 out.signs
1547 );
1548 }
1549
1550 #[test]
1551 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1552 fn diagnostics_signs_correct_when_scrolled() {
1553 let mut src = String::new();
1561 for i in 0..50 {
1562 src.push_str(&format!("fn f{i}() {{}}\n"));
1563 }
1564 src.push_str("fn broken() {\nlet x = ;\n}\n");
1565 let buf = View::from_str(&src);
1566 let mut layer = default_layer();
1567 layer.set_language_for_path(TID, Path::new("a.rs"));
1568 let out = layer.render_viewport(TID, &buf, 45, 20).unwrap();
1569 assert!(
1570 out.signs
1571 .iter()
1572 .any(|s| (50..=52).contains(&s.row) && s.ch == 'E'),
1573 "expected an 'E' sign on rows 50..=52; got {:?}",
1574 out.signs
1575 );
1576 }
1577
1578 #[test]
1579 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1580 fn incremental_path_matches_cold_for_small_edit() {
1581 let pre = View::from_str("fn main() { let x = 1; }");
1582 let mut layer = default_layer();
1583 layer.set_language_for_path(TID, Path::new("a.rs"));
1584 let _ = layer.render_viewport(TID, &pre, 0, 10).unwrap();
1585 layer.apply_edits(
1586 TID,
1587 &[hjkl_engine::ContentEdit {
1588 start_byte: 3,
1589 old_end_byte: 3,
1590 new_end_byte: 4,
1591 start_position: (0, 3),
1592 old_end_position: (0, 3),
1593 new_end_position: (0, 4),
1594 }],
1595 );
1596 let post = View::from_str("fn Ymain() { let x = 1; }");
1597 let inc = layer.render_viewport(TID, &post, 0, 10).unwrap();
1598 let mut cold_layer = default_layer();
1599 cold_layer.set_language_for_path(TID, Path::new("a.rs"));
1600 let cold = cold_layer.render_viewport(TID, &post, 0, 10).unwrap();
1601 assert_eq!(inc.spans, cold.spans);
1602 }
1603
1604 #[test]
1605 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1606 fn forget_drops_buffer_state() {
1607 let buf = View::from_str("fn main() {}");
1608 let mut layer = default_layer();
1609 layer.set_language_for_path(TID, Path::new("a.rs"));
1610 let _ = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1611 assert!(layer.clients.contains_key(&TID));
1612 layer.forget(TID);
1613 assert!(!layer.clients.contains_key(&TID));
1614 }
1615}