1use crate::model::marker::{MarkerId, MarkerList};
2use ratatui::style::{Color, Style};
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum MarginPosition {
8 Left,
10 Right,
12}
13
14#[derive(Debug, Clone, PartialEq)]
20pub struct LineIndicator {
21 pub symbol: String,
23 pub color: Color,
25 pub priority: i32,
27 pub marker_id: MarkerId,
30}
31
32impl LineIndicator {
33 pub fn new(symbol: impl Into<String>, color: Color, priority: i32) -> Self {
35 Self {
36 symbol: symbol.into(),
37 color,
38 priority,
39 marker_id: MarkerId(0), }
41 }
42
43 pub fn with_marker(
45 symbol: impl Into<String>,
46 color: Color,
47 priority: i32,
48 marker_id: MarkerId,
49 ) -> Self {
50 Self {
51 symbol: symbol.into(),
52 color,
53 priority,
54 marker_id,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum MarginContent {
62 Text(String),
64 Symbol { text: String, style: Style },
66 Stacked(Vec<MarginContent>),
68 Empty,
70}
71
72impl MarginContent {
73 pub fn text(text: impl Into<String>) -> Self {
75 Self::Text(text.into())
76 }
77
78 pub fn symbol(text: impl Into<String>, style: Style) -> Self {
80 Self::Symbol {
81 text: text.into(),
82 style,
83 }
84 }
85
86 pub fn colored_symbol(text: impl Into<String>, color: Color) -> Self {
88 Self::Symbol {
89 text: text.into(),
90 style: Style::default().fg(color),
91 }
92 }
93
94 pub fn is_empty(&self) -> bool {
96 matches!(self, Self::Empty)
97 }
98
99 pub fn render(&self, width: usize) -> (String, Option<Style>) {
101 match self {
102 Self::Text(text) => {
103 let padded = format!("{:>width$}", text, width = width);
104 (padded, None)
105 }
106 Self::Symbol { text, style } => {
107 let padded = format!("{:>width$}", text, width = width);
108 (padded, Some(*style))
109 }
110 Self::Stacked(items) => {
111 for item in items.iter().rev() {
113 if !item.is_empty() {
114 return item.render(width);
115 }
116 }
117 (format!("{:>width$}", "", width = width), None)
118 }
119 Self::Empty => (format!("{:>width$}", "", width = width), None),
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub struct MarginConfig {
127 pub position: MarginPosition,
129
130 pub width: usize,
133
134 pub enabled: bool,
136
137 pub show_separator: bool,
139
140 pub separator: String,
142
143 pub style: Style,
145
146 pub separator_style: Style,
148}
149
150impl MarginConfig {
151 pub fn left_default() -> Self {
153 Self {
154 position: MarginPosition::Left,
155 width: 4, enabled: true,
157 show_separator: true,
158 separator: " │ ".to_string(), style: Style::default().fg(Color::DarkGray),
160 separator_style: Style::default().fg(Color::DarkGray),
161 }
162 }
163
164 pub fn right_default() -> Self {
166 Self {
167 position: MarginPosition::Right,
168 width: 0,
169 enabled: false,
170 show_separator: false,
171 separator: String::new(),
172 style: Style::default(),
173 separator_style: Style::default(),
174 }
175 }
176
177 pub fn total_width(&self) -> usize {
180 if self.enabled {
181 1 + self.width
183 + if self.show_separator {
184 self.separator.chars().count()
185 } else {
186 0
187 }
188 } else {
189 0
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct MarginAnnotation {
197 pub line: usize,
199
200 pub position: MarginPosition,
202
203 pub content: MarginContent,
205
206 pub id: Option<String>,
208}
209
210impl MarginAnnotation {
211 pub fn new(line: usize, position: MarginPosition, content: MarginContent) -> Self {
213 Self {
214 line,
215 position,
216 content,
217 id: None,
218 }
219 }
220
221 pub fn with_id(
223 line: usize,
224 position: MarginPosition,
225 content: MarginContent,
226 id: String,
227 ) -> Self {
228 Self {
229 line,
230 position,
231 content,
232 id: Some(id),
233 }
234 }
235
236 pub fn line_number(line: usize) -> Self {
238 Self::new(
239 line,
240 MarginPosition::Left,
241 MarginContent::text(format!("{}", line + 1)), )
243 }
244
245 pub fn breakpoint(line: usize) -> Self {
247 Self::new(
248 line,
249 MarginPosition::Left,
250 MarginContent::colored_symbol("●", Color::Red),
251 )
252 }
253
254 pub fn error(line: usize) -> Self {
256 Self::new(
257 line,
258 MarginPosition::Left,
259 MarginContent::colored_symbol("✗", Color::Red),
260 )
261 }
262
263 pub fn warning(line: usize) -> Self {
265 Self::new(
266 line,
267 MarginPosition::Left,
268 MarginContent::colored_symbol("⚠", Color::Yellow),
269 )
270 }
271
272 pub fn info(line: usize) -> Self {
274 Self::new(
275 line,
276 MarginPosition::Left,
277 MarginContent::colored_symbol("ℹ", Color::Blue),
278 )
279 }
280}
281
282#[derive(Debug)]
288pub struct MarginManager {
289 pub left_config: MarginConfig,
291
292 pub right_config: MarginConfig,
294
295 left_annotations: BTreeMap<usize, Vec<MarginAnnotation>>,
298
299 right_annotations: BTreeMap<usize, Vec<MarginAnnotation>>,
301
302 pub show_line_numbers: bool,
304
305 diagnostic_indicators: BTreeMap<usize, (String, Color)>,
308
309 indicator_markers: MarkerList,
312
313 line_indicators: BTreeMap<u64, BTreeMap<String, LineIndicator>>,
317}
318
319impl MarginManager {
320 pub fn new() -> Self {
322 Self {
323 left_config: MarginConfig::left_default(),
324 right_config: MarginConfig::right_default(),
325 left_annotations: BTreeMap::new(),
326 right_annotations: BTreeMap::new(),
327 show_line_numbers: true,
328 diagnostic_indicators: BTreeMap::new(),
329 indicator_markers: MarkerList::new(),
330 line_indicators: BTreeMap::new(),
331 }
332 }
333
334 pub fn without_line_numbers() -> Self {
336 let mut manager = Self::new();
337 manager.show_line_numbers = false;
338 manager
339 }
340
341 pub fn adjust_for_insert(&mut self, position: usize, length: usize) {
348 self.indicator_markers.adjust_for_insert(position, length);
349 }
350
351 pub fn adjust_for_delete(&mut self, position: usize, length: usize) {
354 self.indicator_markers.adjust_for_delete(position, length);
355 }
356
357 pub fn set_diagnostic_indicator(&mut self, line: usize, symbol: String, color: Color) {
359 self.diagnostic_indicators.insert(line, (symbol, color));
360 }
361
362 pub fn remove_diagnostic_indicator(&mut self, line: usize) {
364 self.diagnostic_indicators.remove(&line);
365 }
366
367 pub fn clear_diagnostic_indicators(&mut self) {
369 self.diagnostic_indicators.clear();
370 }
371
372 pub fn get_diagnostic_indicator(&self, line: usize) -> Option<&(String, Color)> {
374 self.diagnostic_indicators.get(&line)
375 }
376
377 pub fn set_line_indicator(
384 &mut self,
385 byte_offset: usize,
386 namespace: String,
387 mut indicator: LineIndicator,
388 ) -> MarkerId {
389 let marker_id = self.indicator_markers.create(byte_offset, true);
391 indicator.marker_id = marker_id;
392
393 self.line_indicators
394 .entry(marker_id.0)
395 .or_default()
396 .insert(namespace, indicator);
397
398 marker_id
399 }
400
401 pub fn remove_line_indicator(&mut self, marker_id: MarkerId, namespace: &str) {
403 if let Some(indicators) = self.line_indicators.get_mut(&marker_id.0) {
404 indicators.remove(namespace);
405 if indicators.is_empty() {
406 self.line_indicators.remove(&marker_id.0);
407 self.indicator_markers.delete(marker_id);
408 }
409 }
410 }
411
412 pub fn clear_line_indicators_for_namespace(&mut self, namespace: &str) {
414 let mut markers_to_delete = Vec::new();
416
417 for (&marker_id, indicators) in self.line_indicators.iter_mut() {
418 indicators.remove(namespace);
419 if indicators.is_empty() {
420 markers_to_delete.push(marker_id);
421 }
422 }
423
424 for marker_id in markers_to_delete {
426 self.line_indicators.remove(&marker_id);
427 self.indicator_markers.delete(MarkerId(marker_id));
428 }
429 }
430
431 pub fn get_line_indicator(
439 &self,
440 line: usize,
441 get_line_fn: impl Fn(usize) -> usize,
442 ) -> Option<&LineIndicator> {
443 let mut best: Option<&LineIndicator> = None;
445
446 for (&marker_id, indicators) in &self.line_indicators {
447 if let Some(byte_pos) = self.indicator_markers.get_position(MarkerId(marker_id)) {
448 let indicator_line = get_line_fn(byte_pos);
449 if indicator_line == line {
450 for indicator in indicators.values() {
452 if best.is_none() || indicator.priority > best.unwrap().priority {
453 best = Some(indicator);
454 }
455 }
456 }
457 }
458 }
459
460 best
461 }
462
463 pub fn get_indicators_for_viewport(
471 &self,
472 viewport_start: usize,
473 viewport_end: usize,
474 get_line_fn: impl Fn(usize) -> usize,
475 ) -> BTreeMap<usize, LineIndicator> {
476 let mut by_line: BTreeMap<usize, LineIndicator> = BTreeMap::new();
477
478 for (marker_id, byte_pos, _end) in self
480 .indicator_markers
481 .query_range(viewport_start, viewport_end)
482 {
483 if let Some(indicators) = self.line_indicators.get(&marker_id.0) {
485 let line = get_line_fn(byte_pos);
486
487 if let Some(indicator) = indicators.values().max_by_key(|ind| ind.priority) {
489 if let Some(existing) = by_line.get(&line) {
491 if indicator.priority > existing.priority {
492 by_line.insert(line, indicator.clone());
493 }
494 } else {
495 by_line.insert(line, indicator.clone());
496 }
497 }
498 }
499 }
500
501 by_line
502 }
503
504 pub fn add_annotation(&mut self, annotation: MarginAnnotation) {
506 let annotations = match annotation.position {
507 MarginPosition::Left => &mut self.left_annotations,
508 MarginPosition::Right => &mut self.right_annotations,
509 };
510
511 annotations
512 .entry(annotation.line)
513 .or_insert_with(Vec::new)
514 .push(annotation);
515 }
516
517 pub fn remove_by_id(&mut self, id: &str) {
519 for annotations in self.left_annotations.values_mut() {
521 annotations.retain(|a| a.id.as_deref() != Some(id));
522 }
523
524 for annotations in self.right_annotations.values_mut() {
526 annotations.retain(|a| a.id.as_deref() != Some(id));
527 }
528
529 self.left_annotations.retain(|_, v| !v.is_empty());
531 self.right_annotations.retain(|_, v| !v.is_empty());
532 }
533
534 pub fn remove_at_line(&mut self, line: usize, position: MarginPosition) {
536 match position {
537 MarginPosition::Left => {
538 self.left_annotations.remove(&line);
539 }
540 MarginPosition::Right => {
541 self.right_annotations.remove(&line);
542 }
543 }
544 }
545
546 pub fn clear_position(&mut self, position: MarginPosition) {
548 match position {
549 MarginPosition::Left => self.left_annotations.clear(),
550 MarginPosition::Right => self.right_annotations.clear(),
551 }
552 }
553
554 pub fn clear_all(&mut self) {
556 self.left_annotations.clear();
557 self.right_annotations.clear();
558 }
559
560 pub fn get_at_line(
562 &self,
563 line: usize,
564 position: MarginPosition,
565 ) -> Option<&[MarginAnnotation]> {
566 let annotations = match position {
567 MarginPosition::Left => &self.left_annotations,
568 MarginPosition::Right => &self.right_annotations,
569 };
570 annotations.get(&line).map(|v| v.as_slice())
571 }
572
573 pub fn render_line(
576 &self,
577 line: usize,
578 position: MarginPosition,
579 _buffer_total_lines: usize,
580 ) -> MarginContent {
581 let annotations = match position {
582 MarginPosition::Left => &self.left_annotations,
583 MarginPosition::Right => &self.right_annotations,
584 };
585
586 let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
588
589 if position == MarginPosition::Left && self.show_line_numbers {
591 let line_num = MarginContent::text(format!("{}", line + 1));
592
593 if user_annotations.is_empty() {
594 return line_num;
595 }
596
597 let mut stack = vec![line_num];
599 stack.extend(user_annotations.into_iter().map(|a| a.content));
600 MarginContent::Stacked(stack)
601 } else if let Some(annotation) = user_annotations.first() {
602 annotation.content.clone()
603 } else {
604 MarginContent::Empty
605 }
606 }
607
608 pub fn update_width_for_buffer(&mut self, buffer_total_lines: usize) {
611 if self.show_line_numbers {
612 let digits = if buffer_total_lines == 0 {
613 1
614 } else {
615 ((buffer_total_lines as f64).log10().floor() as usize) + 1
616 };
617 self.left_config.width = digits.max(4);
618 }
619 }
620
621 pub fn left_total_width(&self) -> usize {
624 self.left_config.total_width()
625 }
626
627 pub fn right_total_width(&self) -> usize {
629 self.right_config.total_width()
630 }
631
632 pub fn set_line_numbers(&mut self, enabled: bool) {
634 self.show_line_numbers = enabled;
635 if !enabled {
636 self.left_config.width = 0;
637 self.left_config.enabled = false;
638 } else {
639 self.left_config.enabled = true;
640 if self.left_config.width == 0 {
641 self.left_config.width = 4;
642 }
643 }
644 }
645
646 pub fn annotation_count(&self, position: MarginPosition) -> usize {
648 match position {
649 MarginPosition::Left => self.left_annotations.values().map(|v| v.len()).sum(),
650 MarginPosition::Right => self.right_annotations.values().map(|v| v.len()).sum(),
651 }
652 }
653}
654
655impl Default for MarginManager {
656 fn default() -> Self {
657 Self::new()
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn test_margin_content_text() {
667 let content = MarginContent::text("123");
668 let (rendered, style) = content.render(5);
669 assert_eq!(rendered, " 123");
670 assert!(style.is_none());
671 }
672
673 #[test]
674 fn test_margin_content_symbol() {
675 let content = MarginContent::colored_symbol("●", Color::Red);
676 let (rendered, style) = content.render(3);
677 assert_eq!(rendered, " ●");
678 assert!(style.is_some());
679 }
680
681 #[test]
682 fn test_margin_config_total_width() {
683 let mut config = MarginConfig::left_default();
684 config.width = 4;
685 config.separator = " │ ".to_string();
686 assert_eq!(config.total_width(), 8); config.show_separator = false;
689 assert_eq!(config.total_width(), 5); config.enabled = false;
692 assert_eq!(config.total_width(), 0);
693 }
694
695 #[test]
696 fn test_margin_annotation_helpers() {
697 let line_num = MarginAnnotation::line_number(5);
698 assert_eq!(line_num.line, 5);
699 assert_eq!(line_num.position, MarginPosition::Left);
700
701 let breakpoint = MarginAnnotation::breakpoint(10);
702 assert_eq!(breakpoint.line, 10);
703 assert_eq!(breakpoint.position, MarginPosition::Left);
704 }
705
706 #[test]
707 fn test_margin_manager_add_remove() {
708 let mut manager = MarginManager::new();
709
710 let annotation = MarginAnnotation::line_number(5);
712 manager.add_annotation(annotation);
713
714 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
715
716 let annotation = MarginAnnotation::with_id(
718 10,
719 MarginPosition::Left,
720 MarginContent::text("test"),
721 "test-id".to_string(),
722 );
723 manager.add_annotation(annotation);
724
725 assert_eq!(manager.annotation_count(MarginPosition::Left), 2);
726
727 manager.remove_by_id("test-id");
729 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
730
731 manager.clear_all();
733 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
734 }
735
736 #[test]
737 fn test_margin_manager_render_line() {
738 let mut manager = MarginManager::new();
739 manager.show_line_numbers = true;
740
741 let content = manager.render_line(5, MarginPosition::Left, 100);
743 let (rendered, _) = content.render(4);
744 assert!(rendered.contains("6")); manager.add_annotation(MarginAnnotation::breakpoint(5));
748
749 let content = manager.render_line(5, MarginPosition::Left, 100);
751 assert!(matches!(content, MarginContent::Stacked(_)));
752 }
753
754 #[test]
755 fn test_margin_manager_update_width() {
756 let mut manager = MarginManager::new();
757 manager.show_line_numbers = true;
758
759 manager.update_width_for_buffer(99);
761 assert_eq!(manager.left_config.width, 4); manager.update_width_for_buffer(1000);
765 assert_eq!(manager.left_config.width, 4);
766
767 manager.update_width_for_buffer(10000);
769 assert_eq!(manager.left_config.width, 5);
770
771 manager.update_width_for_buffer(1000000);
773 assert_eq!(manager.left_config.width, 7);
774 }
775
776 #[test]
777 fn test_margin_manager_without_line_numbers() {
778 let manager = MarginManager::without_line_numbers();
779 assert!(!manager.show_line_numbers);
780
781 let content = manager.render_line(5, MarginPosition::Left, 100);
782 assert!(content.is_empty());
783 }
784
785 #[test]
786 fn test_margin_position_left_right() {
787 let mut manager = MarginManager::new();
788
789 manager.add_annotation(MarginAnnotation::new(
790 1,
791 MarginPosition::Left,
792 MarginContent::text("left"),
793 ));
794
795 manager.add_annotation(MarginAnnotation::new(
796 1,
797 MarginPosition::Right,
798 MarginContent::text("right"),
799 ));
800
801 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
802 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
803
804 manager.clear_position(MarginPosition::Left);
805 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
806 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
807 }
808
809 fn byte_to_line(byte_offset: usize) -> usize {
812 byte_offset / 10
813 }
814
815 fn line_to_byte(line: usize) -> usize {
817 line * 10
818 }
819
820 #[test]
821 fn test_line_indicator_basic() {
822 let mut manager = MarginManager::new();
823
824 let indicator = LineIndicator::new("│", Color::Green, 10);
826 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
827
828 let retrieved = manager.get_line_indicator(5, byte_to_line);
830 assert!(retrieved.is_some());
831 let retrieved = retrieved.unwrap();
832 assert_eq!(retrieved.symbol, "│");
833 assert_eq!(retrieved.color, Color::Green);
834 assert_eq!(retrieved.priority, 10);
835
836 assert!(manager.get_line_indicator(10, byte_to_line).is_none());
838 }
839
840 #[test]
841 fn test_line_indicator_multiple_namespaces() {
842 let mut manager = MarginManager::new();
843
844 let git_indicator = LineIndicator::new("│", Color::Green, 10);
846 let breakpoint_indicator = LineIndicator::new("●", Color::Red, 20);
847
848 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), git_indicator);
849 manager.set_line_indicator(
850 line_to_byte(5),
851 "breakpoints".to_string(),
852 breakpoint_indicator,
853 );
854
855 let retrieved = manager.get_line_indicator(5, byte_to_line);
857 assert!(retrieved.is_some());
858 let retrieved = retrieved.unwrap();
859 assert_eq!(retrieved.symbol, "●"); assert_eq!(retrieved.priority, 20);
861 }
862
863 #[test]
864 fn test_line_indicator_clear_namespace() {
865 let mut manager = MarginManager::new();
866
867 manager.set_line_indicator(
869 line_to_byte(1),
870 "git-gutter".to_string(),
871 LineIndicator::new("│", Color::Green, 10),
872 );
873 manager.set_line_indicator(
874 line_to_byte(2),
875 "git-gutter".to_string(),
876 LineIndicator::new("│", Color::Yellow, 10),
877 );
878 manager.set_line_indicator(
879 line_to_byte(3),
880 "breakpoints".to_string(),
881 LineIndicator::new("●", Color::Red, 20),
882 );
883
884 manager.clear_line_indicators_for_namespace("git-gutter");
886
887 assert!(manager.get_line_indicator(1, byte_to_line).is_none());
889 assert!(manager.get_line_indicator(2, byte_to_line).is_none());
890
891 let breakpoint = manager.get_line_indicator(3, byte_to_line);
893 assert!(breakpoint.is_some());
894 assert_eq!(breakpoint.unwrap().symbol, "●");
895 }
896
897 #[test]
898 fn test_line_indicator_remove_specific() {
899 let mut manager = MarginManager::new();
900
901 let git_marker = manager.set_line_indicator(
903 line_to_byte(5),
904 "git-gutter".to_string(),
905 LineIndicator::new("│", Color::Green, 10),
906 );
907 let bp_marker = manager.set_line_indicator(
908 line_to_byte(5),
909 "breakpoints".to_string(),
910 LineIndicator::new("●", Color::Red, 20),
911 );
912
913 manager.remove_line_indicator(git_marker, "git-gutter");
915
916 let retrieved = manager.get_line_indicator(5, byte_to_line);
918 assert!(retrieved.is_some());
919 assert_eq!(retrieved.unwrap().symbol, "●");
920
921 manager.remove_line_indicator(bp_marker, "breakpoints");
923
924 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
926 }
927
928 #[test]
929 fn test_line_indicator_shifts_on_insert() {
930 let mut manager = MarginManager::new();
931
932 manager.set_line_indicator(
934 line_to_byte(5),
935 "git-gutter".to_string(),
936 LineIndicator::new("│", Color::Green, 10),
937 );
938
939 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
941 assert!(manager.get_line_indicator(6, byte_to_line).is_none());
942
943 manager.adjust_for_insert(0, 10);
945
946 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
948 assert!(manager.get_line_indicator(6, byte_to_line).is_some());
949 }
950
951 #[test]
952 fn test_line_indicator_shifts_on_delete() {
953 let mut manager = MarginManager::new();
954
955 manager.set_line_indicator(
957 line_to_byte(5),
958 "git-gutter".to_string(),
959 LineIndicator::new("│", Color::Green, 10),
960 );
961
962 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
964
965 manager.adjust_for_delete(0, 20);
967
968 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
970 assert!(manager.get_line_indicator(3, byte_to_line).is_some());
971 }
972
973 #[test]
974 fn test_multiple_indicators_shift_together() {
975 let mut manager = MarginManager::new();
976
977 manager.set_line_indicator(
979 line_to_byte(3),
980 "git-gutter".to_string(),
981 LineIndicator::new("│", Color::Green, 10),
982 );
983 manager.set_line_indicator(
984 line_to_byte(5),
985 "git-gutter".to_string(),
986 LineIndicator::new("│", Color::Yellow, 10),
987 );
988 manager.set_line_indicator(
989 line_to_byte(7),
990 "git-gutter".to_string(),
991 LineIndicator::new("│", Color::Red, 10),
992 );
993
994 manager.adjust_for_insert(25, 20);
997
998 assert!(manager.get_line_indicator(3, byte_to_line).is_none());
1000
1001 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
1003 assert!(manager.get_line_indicator(7, byte_to_line).is_some());
1004 assert!(manager.get_line_indicator(9, byte_to_line).is_some());
1005 }
1006}