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 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 render_viewport(
627 &mut self,
628 id: BufferId,
629 buffer: &impl Query,
630 viewport_top: usize,
631 viewport_height: usize,
632 ) -> Option<RenderOutput> {
633 let client = self.clients.get_mut(&id)?;
634 if !client.has_language {
635 return None;
636 }
637 let dg = buffer.dirty_gen();
638 let row_count = buffer.line_count() as usize;
639 if row_count == 0 || viewport_height == 0 {
640 return None;
641 }
642
643 let vp_top = viewport_top.min(row_count);
644 let vp_end = (vp_top + viewport_height).min(row_count);
645 if vp_end <= vp_top {
646 return None;
647 }
648
649 if client.cache_dirty_gen != Some(dg) {
651 client.invalidate_cache();
652 }
653
654 let rope = buffer.rope();
658
659 let row_starts: Arc<Vec<usize>> = if client
663 .cache_row_starts
664 .as_ref()
665 .is_some_and(|(g, _)| *g == dg)
666 {
667 Arc::clone(&client.cache_row_starts.as_ref().unwrap().1)
668 } else {
669 let mut rs: Vec<usize> = Vec::with_capacity(row_count + 1);
672 rs.push(0);
673 let mut chunk_pos = 0usize;
674 for chunk in rope.chunks() {
675 for nl in memchr::memchr_iter(b'\n', chunk.as_bytes()) {
676 rs.push(chunk_pos + nl + 1);
677 }
678 chunk_pos += chunk.len();
679 }
680 let arc = Arc::new(rs);
681 client.cache_row_starts = Some((dg, Arc::clone(&arc)));
682 arc
683 };
684
685 let needs_reparse = client.parsed_dirty_gen != Some(dg);
689 {
690 let highlighter = client.highlighter.as_mut()?;
691 if highlighter.tree().is_none() {
692 highlighter.parse_initial_rope(&rope);
693 if highlighter.tree().is_some() {
694 client.parsed_dirty_gen = Some(dg);
695 }
696 } else if needs_reparse {
697 let ok = highlighter.parse_incremental_rope(&rope);
703 if ok && highlighter.tree().is_some() {
704 client.parsed_dirty_gen = Some(dg);
705 }
706 }
707 }
708
709 let colorizer_enabled = {
712 let c = self.clients.get(&id)?;
713 let lang_name = c.current_lang.as_ref().map(|g| g.name()).unwrap_or("");
714 self.colorizer
715 && (self.colorizer_filetypes.is_empty()
716 || self.colorizer_filetypes.iter().any(|ft| ft == lang_name))
717 };
718 let rainbow_brackets_enabled = self.rainbow_brackets;
719
720 let client = self.clients.get_mut(&id)?;
722 let highlighter = client.highlighter.as_mut()?;
723
724 highlighter.tree()?;
726
727 let theme = self.theme.as_ref();
728 let directory = Arc::clone(&self.directory);
729
730 if client.cache_rows.is_empty() {
732 client.cache_spans = walk_rows(
734 highlighter,
735 &rope,
736 &row_starts,
737 row_count,
738 vp_top,
739 vp_end,
740 theme,
741 &directory,
742 colorizer_enabled,
743 rainbow_brackets_enabled,
744 );
745 client.cache_rows = vp_top..vp_end;
746 client.cache_dirty_gen = Some(dg);
747 } else {
748 let cache_covers_overlap =
749 vp_top < client.cache_rows.end && vp_end > client.cache_rows.start;
750 if !cache_covers_overlap {
751 client.cache_spans = walk_rows(
753 highlighter,
754 &rope,
755 &row_starts,
756 row_count,
757 vp_top,
758 vp_end,
759 theme,
760 &directory,
761 colorizer_enabled,
762 rainbow_brackets_enabled,
763 );
764 client.cache_rows = vp_top..vp_end;
765 } else {
766 if vp_top < client.cache_rows.start {
768 let new_rows = walk_rows(
769 highlighter,
770 &rope,
771 &row_starts,
772 row_count,
773 vp_top,
774 client.cache_rows.start,
775 theme,
776 &directory,
777 colorizer_enabled,
778 rainbow_brackets_enabled,
779 );
780 let mut combined = new_rows;
781 combined.append(&mut client.cache_spans);
782 client.cache_spans = combined;
783 client.cache_rows.start = vp_top;
784 }
785 if vp_end > client.cache_rows.end {
787 let new_rows = walk_rows(
788 highlighter,
789 &rope,
790 &row_starts,
791 row_count,
792 client.cache_rows.end,
793 vp_end,
794 theme,
795 &directory,
796 colorizer_enabled,
797 rainbow_brackets_enabled,
798 );
799 client.cache_spans.extend(new_rows);
800 client.cache_rows.end = vp_end;
801 }
802 }
803 client.cache_dirty_gen = Some(dg);
804 }
805
806 let offset = vp_top - client.cache_rows.start;
808 let len = vp_end - vp_top;
809 let spans: Vec<Vec<(usize, usize, StyleSpec)>> =
810 client.cache_spans[offset..offset + len].to_vec();
811
812 let signs = if client
814 .cache_signs
815 .as_ref()
816 .is_some_and(|(g, t, e, _)| *g == dg && *t == vp_top && *e == vp_end)
817 {
818 client.cache_signs.as_ref().unwrap().3.clone()
819 } else {
820 let s = collect_diag_signs_range(highlighter, &rope, &row_starts, vp_top, vp_end);
821 client.cache_signs = Some((dg, vp_top, vp_end, s.clone()));
822 s
823 };
824
825 Some(RenderOutput {
826 buffer_id: id,
827 spans,
828 signs,
829 key: (dg, vp_top, viewport_height),
830 perf: PerfBreakdown::default(),
831 })
832 }
833
834 pub fn name_for_path(&self, path: &Path) -> Option<String> {
836 self.directory.name_for_path(path)
837 }
838
839 #[doc(hidden)]
841 pub fn has_client(&self, id: BufferId) -> bool {
842 self.clients.contains_key(&id)
843 }
844
845 pub fn dispatch_load_event(
865 event: &LoadEvent,
866 mut handler: impl FnMut(LoadEventKind<'_>),
867 ) -> bool {
868 #[allow(unreachable_patterns)]
869 match event {
870 LoadEvent::Ready { id, name } => {
871 handler(LoadEventKind::Ready { id: *id, name });
872 true
873 }
874 LoadEvent::Failed { id, name, error } => {
875 handler(LoadEventKind::Failed {
876 id: *id,
877 name,
878 error,
879 });
880 true
881 }
882 _ => false,
883 }
884 }
885}
886
887const RAINBOW_PALETTE: [Color; 7] = [
894 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), ];
902
903#[allow(clippy::too_many_arguments)]
908fn walk_rows(
909 highlighter: &mut Highlighter,
910 rope: &ropey::Rope,
911 row_starts: &[usize],
912 row_count: usize,
913 seg_start: usize,
914 seg_end: usize,
915 theme: &dyn Theme,
916 directory: &Arc<LanguageDirectory>,
917 colorizer: bool,
918 rainbow_brackets: bool,
919) -> Vec<Vec<(usize, usize, StyleSpec)>> {
920 let rope_len = rope.len_bytes();
921 let byte_start = row_starts.get(seg_start).copied().unwrap_or(rope_len);
922 let byte_end = row_starts
923 .get(seg_end)
924 .copied()
925 .unwrap_or(rope_len)
926 .min(rope_len)
927 .max(byte_start);
928
929 let mut flat_spans =
930 highlighter.highlight_range_with_injections_rope(rope, byte_start..byte_end, |name| {
931 directory.by_name(name)
932 });
933
934 let marker_pass = CommentMarkerPass::new();
935 marker_pass.apply_rope(&mut flat_spans, rope);
936 if colorizer {
937 let hex_color_pass = HexColorPass::new();
938 hex_color_pass.apply_range_rope(&mut flat_spans, rope, byte_start..byte_end);
939 }
940 if rainbow_brackets
941 && let (Some(tree), Some(grammar)) = (highlighter.tree(), highlighter.grammar())
942 {
943 let rb_spans = rainbow_spans_rope(tree, grammar, rope, byte_start..byte_end);
944 flat_spans.extend(rb_spans);
945 }
946
947 let _ = row_count; build_by_row_range(&flat_spans, rope_len, row_starts, seg_start..seg_end, theme)
954}
955
956fn build_by_row_range(
962 flat_spans: &[hjkl_bonsai::HighlightSpan],
963 source_len: usize,
964 row_starts: &[usize],
965 row_range: Range<usize>,
966 theme: &dyn Theme,
967) -> Vec<Vec<(usize, usize, StyleSpec)>> {
968 let seg_start = row_range.start;
969 let seg_end = row_range.end.min(row_starts.len());
970 if seg_end <= seg_start {
971 return Vec::new();
972 }
973 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); seg_end - seg_start];
974
975 for span in flat_spans {
976 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
977 let bg = match span.metadata.get(HEX_BG_KEY) {
978 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
979 _ => None,
980 };
981 let fg = match span.metadata.get(HEX_FG_KEY) {
982 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
983 _ => None,
984 };
985 bg.map(|bg| StyleSpec {
986 fg,
987 bg: Some(bg),
988 modifiers: hjkl_theme::Modifiers::default(),
989 })
990 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
991 let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
992 Some(MetaValue::Int(d)) => *d as usize,
993 _ => 0,
994 };
995 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
996 Some(StyleSpec {
997 fg: Some(fg),
998 bg: None,
999 modifiers: hjkl_theme::Modifiers::default(),
1000 })
1001 } else {
1002 None
1003 };
1004
1005 let style: StyleSpec = if let Some(s) = hex_style {
1006 s
1007 } else {
1008 match theme.style(span.capture()) {
1009 Some(s) => *s,
1010 None => continue,
1011 }
1012 };
1013
1014 let span_start = span.byte_range.start;
1015 let span_end = span.byte_range.end;
1016
1017 let start_row = row_starts
1018 .partition_point(|&rs| rs <= span_start)
1019 .saturating_sub(1);
1020
1021 let mut row = start_row.max(seg_start);
1022 while row < seg_end {
1023 let row_byte_start = row_starts[row];
1024 let row_byte_end = row_starts
1025 .get(row + 1)
1026 .map(|&s| s.saturating_sub(1))
1027 .unwrap_or(source_len);
1028
1029 if row_byte_start >= span_end {
1030 break;
1031 }
1032
1033 let local_start = span_start.saturating_sub(row_byte_start);
1034 let local_end = span_end.min(row_byte_end) - row_byte_start;
1035
1036 if local_end > local_start {
1037 by_row[row - seg_start].push((local_start, local_end, style));
1038 }
1039
1040 row += 1;
1041 }
1042 }
1043
1044 by_row
1045}
1046
1047pub fn build_by_row(
1053 flat_spans: &[hjkl_bonsai::HighlightSpan],
1054 bytes: &[u8],
1055 row_starts: &[usize],
1056 row_count: usize,
1057 theme: &dyn Theme,
1058) -> Vec<Vec<(usize, usize, StyleSpec)>> {
1059 let mut by_row: Vec<Vec<(usize, usize, StyleSpec)>> = vec![Vec::new(); row_count];
1060
1061 for span in flat_spans {
1062 let hex_style: Option<StyleSpec> = if span.capture() == HEX_COLOR_CAPTURE {
1063 let bg = match span.metadata.get(HEX_BG_KEY) {
1064 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1065 _ => None,
1066 };
1067 let fg = match span.metadata.get(HEX_FG_KEY) {
1068 Some(MetaValue::Str(s)) => hjkl_theme::Color::from_hex_str(s).ok(),
1069 _ => None,
1070 };
1071 bg.map(|bg| StyleSpec {
1072 fg,
1073 bg: Some(bg),
1074 modifiers: hjkl_theme::Modifiers::default(),
1075 })
1076 } else if span.capture() == RAINBOW_BRACKET_CAPTURE {
1077 let depth = match span.metadata.get(RAINBOW_DEPTH_KEY) {
1078 Some(MetaValue::Int(d)) => *d as usize,
1079 _ => 0,
1080 };
1081 let fg = RAINBOW_PALETTE[depth % RAINBOW_PALETTE.len()];
1082 Some(StyleSpec {
1083 fg: Some(fg),
1084 bg: None,
1085 modifiers: hjkl_theme::Modifiers::default(),
1086 })
1087 } else {
1088 None
1089 };
1090
1091 let style: StyleSpec = if let Some(s) = hex_style {
1092 s
1093 } else {
1094 match theme.style(span.capture()) {
1095 Some(s) => *s,
1096 None => continue,
1097 }
1098 };
1099 let style = &style;
1100
1101 let span_start = span.byte_range.start;
1102 let span_end = span.byte_range.end;
1103
1104 let start_row = row_starts
1105 .partition_point(|&rs| rs <= span_start)
1106 .saturating_sub(1);
1107
1108 let mut row = start_row;
1109 while row < row_count {
1110 let row_byte_start = row_starts[row];
1111 let row_byte_end = row_starts
1112 .get(row + 1)
1113 .map(|&s| s.saturating_sub(1))
1114 .unwrap_or(bytes.len());
1115
1116 if row_byte_start >= span_end {
1117 break;
1118 }
1119
1120 let local_start = span_start.saturating_sub(row_byte_start);
1121 let local_end = span_end.min(row_byte_end) - row_byte_start;
1122
1123 if local_end > local_start {
1124 by_row[row].push((local_start, local_end, *style));
1125 }
1126
1127 row += 1;
1128 }
1129 }
1130
1131 by_row
1132}
1133
1134fn collect_diag_signs_range(
1139 h: &mut Highlighter,
1140 rope: &ropey::Rope,
1141 row_starts: &[usize],
1142 vp_top: usize,
1143 vp_end: usize,
1144) -> Vec<DiagSign> {
1145 let rope_len = rope.len_bytes();
1146 let byte_start = row_starts.get(vp_top).copied().unwrap_or(rope_len);
1147 let byte_end = row_starts.get(vp_end).copied().unwrap_or(rope_len);
1148 let window: String = if byte_start < byte_end && byte_end <= rope_len {
1152 rope.byte_slice(byte_start..byte_end).to_string()
1153 } else {
1154 String::new()
1155 };
1156 let errors = h.parse_errors_range(window.as_bytes(), 0..(byte_end - byte_start));
1158 let mut signs: Vec<DiagSign> = Vec::new();
1159 let mut last_row: Option<usize> = None;
1160 for err in &errors {
1161 let abs_start = err.byte_range.start + byte_start;
1163 let r = row_starts
1164 .partition_point(|&rs| rs <= abs_start)
1165 .saturating_sub(1);
1166 if last_row == Some(r) {
1167 continue;
1168 }
1169 last_row = Some(r);
1170 signs.push(DiagSign::new(r, 'E', 100));
1171 }
1172 signs
1173}
1174
1175pub fn layer_with_theme(
1181 theme: Arc<DotFallbackTheme>,
1182 directory: Arc<LanguageDirectory>,
1183) -> SyntaxLayer {
1184 SyntaxLayer::new(theme, directory)
1185}
1186
1187#[cfg(test)]
1189pub fn default_layer() -> SyntaxLayer {
1190 let directory = Arc::new(LanguageDirectory::new().expect("language directory"));
1191 SyntaxLayer::new(Arc::new(DotFallbackTheme::dark()), directory)
1192}
1193
1194#[cfg(test)]
1199mod tests {
1200 use super::*;
1201 use hjkl_buffer::Buffer;
1202 use std::path::Path;
1203
1204 const TID: BufferId = 0;
1205
1206 #[test]
1209 fn diag_sign_new_roundtrip() {
1210 let s = DiagSign::new(7, 'W', 50);
1211 assert_eq!(s.row, 7);
1212 assert_eq!(s.ch, 'W');
1213 assert_eq!(s.priority, 50);
1214 }
1215
1216 #[test]
1217 fn diag_sign_default_is_sensible() {
1218 let s = DiagSign::default();
1219 assert_eq!(s.row, 0);
1220 assert_eq!(s.ch, 'E');
1221 assert_eq!(s.priority, 0);
1222 }
1223
1224 #[test]
1227 fn perf_breakdown_default_zeros() {
1228 let p = PerfBreakdown::new();
1229 assert_eq!(p.source_build_us, 0);
1230 assert_eq!(p.parse_us, 0);
1231 assert_eq!(p.highlight_us, 0);
1232 assert_eq!(p.by_row_us, 0);
1233 assert_eq!(p.diag_us, 0);
1234 }
1235
1236 #[test]
1239 fn set_language_outcome_is_known() {
1240 assert!(SetLanguageOutcome::Ready.is_known());
1241 assert!(SetLanguageOutcome::Loading("rust".to_string()).is_known());
1242 assert!(!SetLanguageOutcome::Unknown.is_known());
1243 }
1244
1245 #[test]
1248 fn render_output_new_roundtrip() {
1249 let out = RenderOutput::new(
1250 99,
1251 vec![vec![]],
1252 vec![DiagSign::new(0, 'E', 100)],
1253 (7, 0, 30),
1254 PerfBreakdown::new(),
1255 );
1256 assert_eq!(out.buffer_id, 99);
1257 assert_eq!(out.key, (7, 0, 30));
1258 assert_eq!(out.signs.len(), 1);
1259 }
1260
1261 #[test]
1262 fn render_output_partial_eq_same() {
1263 let a = RenderOutput::new(
1264 0,
1265 vec![vec![(0, 5, StyleSpec::default())]],
1266 vec![],
1267 (1, 0, 10),
1268 PerfBreakdown::default(),
1269 );
1270 let b = a.clone();
1271 assert_eq!(a, b);
1272 }
1273
1274 #[test]
1277 fn build_by_row_empty_spans_gives_empty_rows() {
1278 let by_row = build_by_row(
1279 &[],
1280 b"hello\nworld\n",
1281 &[0, 6, 12],
1282 2,
1283 &DotFallbackTheme::dark(),
1284 );
1285 assert_eq!(by_row.len(), 2);
1286 assert!(by_row[0].is_empty());
1287 assert!(by_row[1].is_empty());
1288 }
1289
1290 #[test]
1291 fn build_by_row_hex_color_uses_metadata_colors() {
1292 let bytes = b"--accent: #bb9af7;";
1293 let mut metadata = std::collections::HashMap::new();
1294 metadata.insert(
1295 HEX_BG_KEY.to_string(),
1296 MetaValue::Str("#bb9af7".to_string()),
1297 );
1298 metadata.insert(
1299 HEX_FG_KEY.to_string(),
1300 MetaValue::Str("#ffffff".to_string()),
1301 );
1302 let span = hjkl_bonsai::HighlightSpan {
1303 byte_range: 10..17,
1304 capture: HEX_COLOR_CAPTURE.to_string(),
1305 metadata,
1306 };
1307 let by_row = build_by_row(&[span], bytes, &[0], 1, &DotFallbackTheme::dark());
1308 assert_eq!(by_row.len(), 1);
1309 assert_eq!(by_row[0].len(), 1);
1310 let (_, _, style) = by_row[0][0];
1311 let bg = style.bg.expect("hex color must set background");
1312 assert_eq!((bg.r, bg.g, bg.b), (0xbb, 0x9a, 0xf7));
1313 let fg = style.fg.expect("hex color must set foreground");
1314 assert_eq!((fg.r, fg.g, fg.b), (0xff, 0xff, 0xff));
1315 }
1316
1317 #[test]
1318 fn build_by_row_hex_color_without_metadata_skips() {
1319 let span = hjkl_bonsai::HighlightSpan {
1320 byte_range: 0..3,
1321 capture: HEX_COLOR_CAPTURE.to_string(),
1322 metadata: std::collections::HashMap::new(),
1323 };
1324 let by_row = build_by_row(&[span], b"foo", &[0], 1, &DotFallbackTheme::dark());
1325 assert_eq!(by_row.len(), 1);
1326 assert!(by_row[0].is_empty());
1327 }
1328
1329 #[test]
1332 fn render_viewport_with_no_language_returns_none() {
1333 let buf = Buffer::from_str("hello world");
1334 let mut layer = default_layer();
1335 assert!(
1336 !layer
1337 .set_language_for_path(TID, Path::new("a.unknownext"))
1338 .is_known()
1339 );
1340 assert!(layer.render_viewport(TID, &buf, 0, 10).is_none());
1341 }
1342
1343 #[test]
1344 fn apply_edits_with_no_language_is_noop() {
1345 let mut layer = default_layer();
1346 let edits = vec![hjkl_engine::ContentEdit {
1347 start_byte: 0,
1348 old_end_byte: 0,
1349 new_end_byte: 1,
1350 start_position: (0, 0),
1351 old_end_position: (0, 0),
1352 new_end_position: (0, 1),
1353 }];
1354 layer.apply_edits(TID, &edits);
1355 }
1357
1358 #[test]
1359 fn set_language_for_path_returns_unknown_for_unrecognized_extension() {
1360 let mut layer = default_layer();
1361 let outcome = layer.set_language_for_path(TID, Path::new("a.zzznope_not_real"));
1362 assert!(!outcome.is_known());
1363 assert!(matches!(outcome, SetLanguageOutcome::Unknown));
1364 }
1365
1366 #[test]
1367 fn poll_pending_loads_drains_ready_handles() {
1368 let mut layer = default_layer();
1369 let events = layer.poll_pending_loads();
1370 assert!(
1371 events.is_empty(),
1372 "expected no events with no pending loads"
1373 );
1374 }
1375
1376 #[test]
1377 fn forget_removes_client_state() {
1378 let mut layer = default_layer();
1379 layer.set_language_for_path(TID, Path::new("a.zzz_unknown"));
1380 layer.forget(TID);
1381 assert!(!layer.clients.contains_key(&TID));
1382 }
1383
1384 #[test]
1387 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1388 fn parse_and_render_small_rust_buffer() {
1389 let buf = Buffer::from_str("fn main() { let x = 1; }\n");
1390 let mut layer = default_layer();
1391 assert!(
1392 layer
1393 .set_language_for_path(TID, Path::new("a.rs"))
1394 .is_known()
1395 );
1396 let out = layer
1397 .render_viewport(TID, &buf, 0, 10)
1398 .expect("render output");
1399 assert!(
1400 out.spans.iter().any(|r| !r.is_empty()),
1401 "expected at least one styled span"
1402 );
1403 }
1404
1405 #[test]
1406 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1407 fn diagnostics_emit_sign_for_syntax_error() {
1408 let buf = Buffer::from_str("fn main() {\nlet x = ;\n}\n");
1409 let mut layer = default_layer();
1410 layer.set_language_for_path(TID, Path::new("a.rs"));
1411 let out = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1412 assert!(
1413 !out.signs.is_empty(),
1414 "expected at least one diagnostic sign for `let x = ;`"
1415 );
1416 assert!(
1417 out.signs.iter().any(|s| s.row == 1 && s.ch == 'E'),
1418 "expected an 'E' sign on row 1; got {:?}",
1419 out.signs
1420 );
1421 }
1422
1423 #[test]
1424 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1425 fn incremental_path_matches_cold_for_small_edit() {
1426 let pre = Buffer::from_str("fn main() { let x = 1; }");
1427 let mut layer = default_layer();
1428 layer.set_language_for_path(TID, Path::new("a.rs"));
1429 let _ = layer.render_viewport(TID, &pre, 0, 10).unwrap();
1430 layer.apply_edits(
1431 TID,
1432 &[hjkl_engine::ContentEdit {
1433 start_byte: 3,
1434 old_end_byte: 3,
1435 new_end_byte: 4,
1436 start_position: (0, 3),
1437 old_end_position: (0, 3),
1438 new_end_position: (0, 4),
1439 }],
1440 );
1441 let post = Buffer::from_str("fn Ymain() { let x = 1; }");
1442 let inc = layer.render_viewport(TID, &post, 0, 10).unwrap();
1443 let mut cold_layer = default_layer();
1444 cold_layer.set_language_for_path(TID, Path::new("a.rs"));
1445 let cold = cold_layer.render_viewport(TID, &post, 0, 10).unwrap();
1446 assert_eq!(inc.spans, cold.spans);
1447 }
1448
1449 #[test]
1450 #[ignore = "network + compiler: needs tree-sitter-rust grammar"]
1451 fn forget_drops_buffer_state() {
1452 let buf = Buffer::from_str("fn main() {}");
1453 let mut layer = default_layer();
1454 layer.set_language_for_path(TID, Path::new("a.rs"));
1455 let _ = layer.render_viewport(TID, &buf, 0, 10).unwrap();
1456 assert!(layer.clients.contains_key(&TID));
1457 layer.forget(TID);
1458 assert!(!layer.clients.contains_key(&TID));
1459 }
1460}