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 indicator_markers: MarkerList,
305
306 line_indicators: BTreeMap<u64, BTreeMap<String, LineIndicator>>,
310}
311
312impl MarginManager {
313 pub fn new() -> Self {
315 Self {
316 left_config: MarginConfig::left_default(),
317 right_config: MarginConfig::right_default(),
318 left_annotations: BTreeMap::new(),
319 right_annotations: BTreeMap::new(),
320 indicator_markers: MarkerList::new(),
321 line_indicators: BTreeMap::new(),
322 }
323 }
324
325 pub fn without_line_numbers() -> Self {
327 let mut manager = Self::new();
328 manager.left_config.width = 0;
329 manager.left_config.enabled = false;
330 manager
331 }
332
333 pub fn adjust_for_insert(&mut self, position: usize, length: usize) {
340 self.indicator_markers.adjust_for_insert(position, length);
341 }
342
343 pub fn adjust_for_delete(&mut self, position: usize, length: usize) {
346 self.indicator_markers.adjust_for_delete(position, length);
347 }
348
349 pub fn set_line_indicator(
356 &mut self,
357 byte_offset: usize,
358 namespace: String,
359 mut indicator: LineIndicator,
360 ) -> MarkerId {
361 let marker_id = self.indicator_markers.create(byte_offset, true);
363 indicator.marker_id = marker_id;
364
365 self.line_indicators
366 .entry(marker_id.0)
367 .or_default()
368 .insert(namespace, indicator);
369
370 marker_id
371 }
372
373 pub fn remove_line_indicator(&mut self, marker_id: MarkerId, namespace: &str) {
375 if let Some(indicators) = self.line_indicators.get_mut(&marker_id.0) {
376 indicators.remove(namespace);
377 if indicators.is_empty() {
378 self.line_indicators.remove(&marker_id.0);
379 self.indicator_markers.delete(marker_id);
380 }
381 }
382 }
383
384 pub fn clear_line_indicators_for_namespace(&mut self, namespace: &str) {
386 let mut markers_to_delete = Vec::new();
388
389 for (&marker_id, indicators) in self.line_indicators.iter_mut() {
390 indicators.remove(namespace);
391 if indicators.is_empty() {
392 markers_to_delete.push(marker_id);
393 }
394 }
395
396 for marker_id in markers_to_delete {
398 self.line_indicators.remove(&marker_id);
399 self.indicator_markers.delete(MarkerId(marker_id));
400 }
401 }
402
403 pub fn get_line_indicator(
411 &self,
412 line: usize,
413 get_line_fn: impl Fn(usize) -> usize,
414 ) -> Option<&LineIndicator> {
415 let mut best: Option<&LineIndicator> = None;
417
418 for (&marker_id, indicators) in &self.line_indicators {
419 if let Some(byte_pos) = self.indicator_markers.get_position(MarkerId(marker_id)) {
420 let indicator_line = get_line_fn(byte_pos);
421 if indicator_line == line {
422 for indicator in indicators.values() {
424 if best.is_none() || indicator.priority > best.unwrap().priority {
425 best = Some(indicator);
426 }
427 }
428 }
429 }
430 }
431
432 best
433 }
434
435 pub fn get_indicators_for_viewport(
443 &self,
444 viewport_start: usize,
445 viewport_end: usize,
446 get_line_fn: impl Fn(usize) -> usize,
447 ) -> BTreeMap<usize, LineIndicator> {
448 let mut by_line: BTreeMap<usize, LineIndicator> = BTreeMap::new();
449
450 for (marker_id, byte_pos, _end) in self
452 .indicator_markers
453 .query_range(viewport_start, viewport_end)
454 {
455 if let Some(indicators) = self.line_indicators.get(&marker_id.0) {
457 let line = get_line_fn(byte_pos);
458
459 if let Some(indicator) = indicators.values().max_by_key(|ind| ind.priority) {
461 if let Some(existing) = by_line.get(&line) {
463 if indicator.priority > existing.priority {
464 by_line.insert(line, indicator.clone());
465 }
466 } else {
467 by_line.insert(line, indicator.clone());
468 }
469 }
470 }
471 }
472
473 by_line
474 }
475
476 pub fn get_indicator_position(&self, marker_id: MarkerId) -> Option<usize> {
482 self.indicator_markers.get_position(marker_id)
483 }
484
485 pub fn query_indicator_range(&self, start: usize, end: usize) -> Vec<(MarkerId, usize, usize)> {
488 self.indicator_markers.query_range(start, end)
489 }
490
491 pub fn set_indicator_position(&mut self, marker_id: MarkerId, new_position: usize) {
496 if self.line_indicators.contains_key(&marker_id.0) {
498 self.indicator_markers.set_position(marker_id, new_position);
499 }
500 }
501
502 pub fn add_annotation(&mut self, annotation: MarginAnnotation) {
504 let annotations = match annotation.position {
505 MarginPosition::Left => &mut self.left_annotations,
506 MarginPosition::Right => &mut self.right_annotations,
507 };
508
509 annotations
510 .entry(annotation.line)
511 .or_insert_with(Vec::new)
512 .push(annotation);
513 }
514
515 pub fn remove_by_id(&mut self, id: &str) {
517 for annotations in self.left_annotations.values_mut() {
519 annotations.retain(|a| a.id.as_deref() != Some(id));
520 }
521
522 for annotations in self.right_annotations.values_mut() {
524 annotations.retain(|a| a.id.as_deref() != Some(id));
525 }
526
527 self.left_annotations.retain(|_, v| !v.is_empty());
529 self.right_annotations.retain(|_, v| !v.is_empty());
530 }
531
532 pub fn remove_at_line(&mut self, line: usize, position: MarginPosition) {
534 match position {
535 MarginPosition::Left => {
536 self.left_annotations.remove(&line);
537 }
538 MarginPosition::Right => {
539 self.right_annotations.remove(&line);
540 }
541 }
542 }
543
544 pub fn clear_position(&mut self, position: MarginPosition) {
546 match position {
547 MarginPosition::Left => self.left_annotations.clear(),
548 MarginPosition::Right => self.right_annotations.clear(),
549 }
550 }
551
552 pub fn clear_all(&mut self) {
554 self.left_annotations.clear();
555 self.right_annotations.clear();
556 }
557
558 pub fn get_at_line(
560 &self,
561 line: usize,
562 position: MarginPosition,
563 ) -> Option<&[MarginAnnotation]> {
564 let annotations = match position {
565 MarginPosition::Left => &self.left_annotations,
566 MarginPosition::Right => &self.right_annotations,
567 };
568 annotations.get(&line).map(|v| v.as_slice())
569 }
570
571 pub fn render_line(
574 &self,
575 line: usize,
576 position: MarginPosition,
577 _buffer_total_lines: usize,
578 show_line_numbers: bool,
579 ) -> MarginContent {
580 let annotations = match position {
581 MarginPosition::Left => &self.left_annotations,
582 MarginPosition::Right => &self.right_annotations,
583 };
584
585 let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
587
588 if position == MarginPosition::Left && show_line_numbers {
590 let line_num = MarginContent::text(format!("{}", line + 1));
591
592 if user_annotations.is_empty() {
593 return line_num;
594 }
595
596 let mut stack = vec![line_num];
598 stack.extend(user_annotations.into_iter().map(|a| a.content));
599 MarginContent::Stacked(stack)
600 } else if let Some(annotation) = user_annotations.first() {
601 annotation.content.clone()
602 } else {
603 MarginContent::Empty
604 }
605 }
606
607 pub fn update_width_for_buffer(&mut self, buffer_total_lines: usize, show_line_numbers: bool) {
610 if show_line_numbers {
611 let digits = if buffer_total_lines == 0 {
612 1
613 } else {
614 ((buffer_total_lines as f64).log10().floor() as usize) + 1
615 };
616 self.left_config.width = digits.max(4);
617 }
618 }
619
620 pub fn left_total_width(&self) -> usize {
623 self.left_config.total_width()
624 }
625
626 pub fn right_total_width(&self) -> usize {
628 self.right_config.total_width()
629 }
630
631 pub fn configure_for_line_numbers(&mut self, show_line_numbers: bool) {
638 if !show_line_numbers {
639 self.left_config.width = 0;
640 self.left_config.enabled = false;
641 } else {
642 self.left_config.enabled = true;
643 if self.left_config.width == 0 {
644 self.left_config.width = 4;
645 }
646 }
647 }
648
649 pub fn annotation_count(&self, position: MarginPosition) -> usize {
651 match position {
652 MarginPosition::Left => self.left_annotations.values().map(|v| v.len()).sum(),
653 MarginPosition::Right => self.right_annotations.values().map(|v| v.len()).sum(),
654 }
655 }
656}
657
658impl Default for MarginManager {
659 fn default() -> Self {
660 Self::new()
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
669 fn test_margin_content_text() {
670 let content = MarginContent::text("123");
671 let (rendered, style) = content.render(5);
672 assert_eq!(rendered, " 123");
673 assert!(style.is_none());
674 }
675
676 #[test]
677 fn test_margin_content_symbol() {
678 let content = MarginContent::colored_symbol("●", Color::Red);
679 let (rendered, style) = content.render(3);
680 assert_eq!(rendered, " ●");
681 assert!(style.is_some());
682 }
683
684 #[test]
685 fn test_margin_config_total_width() {
686 let mut config = MarginConfig::left_default();
687 config.width = 4;
688 config.separator = " │ ".to_string();
689 assert_eq!(config.total_width(), 8); config.show_separator = false;
692 assert_eq!(config.total_width(), 5); config.enabled = false;
695 assert_eq!(config.total_width(), 0);
696 }
697
698 #[test]
699 fn test_margin_annotation_helpers() {
700 let line_num = MarginAnnotation::line_number(5);
701 assert_eq!(line_num.line, 5);
702 assert_eq!(line_num.position, MarginPosition::Left);
703
704 let breakpoint = MarginAnnotation::breakpoint(10);
705 assert_eq!(breakpoint.line, 10);
706 assert_eq!(breakpoint.position, MarginPosition::Left);
707 }
708
709 #[test]
710 fn test_margin_manager_add_remove() {
711 let mut manager = MarginManager::new();
712
713 let annotation = MarginAnnotation::line_number(5);
715 manager.add_annotation(annotation);
716
717 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
718
719 let annotation = MarginAnnotation::with_id(
721 10,
722 MarginPosition::Left,
723 MarginContent::text("test"),
724 "test-id".to_string(),
725 );
726 manager.add_annotation(annotation);
727
728 assert_eq!(manager.annotation_count(MarginPosition::Left), 2);
729
730 manager.remove_by_id("test-id");
732 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
733
734 manager.clear_all();
736 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
737 }
738
739 #[test]
740 fn test_margin_manager_render_line() {
741 let mut manager = MarginManager::new();
742
743 let content = manager.render_line(5, MarginPosition::Left, 100, true);
745 let (rendered, _) = content.render(4);
746 assert!(rendered.contains("6")); manager.add_annotation(MarginAnnotation::breakpoint(5));
750
751 let content = manager.render_line(5, MarginPosition::Left, 100, true);
753 assert!(matches!(content, MarginContent::Stacked(_)));
754 }
755
756 #[test]
757 fn test_margin_manager_update_width() {
758 let mut manager = MarginManager::new();
759
760 manager.update_width_for_buffer(99, true);
762 assert_eq!(manager.left_config.width, 4); manager.update_width_for_buffer(1000, true);
766 assert_eq!(manager.left_config.width, 4);
767
768 manager.update_width_for_buffer(10000, true);
770 assert_eq!(manager.left_config.width, 5);
771
772 manager.update_width_for_buffer(1000000, true);
774 assert_eq!(manager.left_config.width, 7);
775 }
776
777 #[test]
778 fn test_margin_manager_without_line_numbers() {
779 let manager = MarginManager::without_line_numbers();
780 assert!(!manager.left_config.enabled);
781
782 let content = manager.render_line(5, MarginPosition::Left, 100, false);
783 assert!(content.is_empty());
784 }
785
786 #[test]
787 fn test_margin_position_left_right() {
788 let mut manager = MarginManager::new();
789
790 manager.add_annotation(MarginAnnotation::new(
791 1,
792 MarginPosition::Left,
793 MarginContent::text("left"),
794 ));
795
796 manager.add_annotation(MarginAnnotation::new(
797 1,
798 MarginPosition::Right,
799 MarginContent::text("right"),
800 ));
801
802 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
803 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
804
805 manager.clear_position(MarginPosition::Left);
806 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
807 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
808 }
809
810 fn byte_to_line(byte_offset: usize) -> usize {
813 byte_offset / 10
814 }
815
816 fn line_to_byte(line: usize) -> usize {
818 line * 10
819 }
820
821 #[test]
822 fn test_line_indicator_basic() {
823 let mut manager = MarginManager::new();
824
825 let indicator = LineIndicator::new("│", Color::Green, 10);
827 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
828
829 let retrieved = manager.get_line_indicator(5, byte_to_line);
831 assert!(retrieved.is_some());
832 let retrieved = retrieved.unwrap();
833 assert_eq!(retrieved.symbol, "│");
834 assert_eq!(retrieved.color, Color::Green);
835 assert_eq!(retrieved.priority, 10);
836
837 assert!(manager.get_line_indicator(10, byte_to_line).is_none());
839 }
840
841 #[test]
842 fn test_line_indicator_multiple_namespaces() {
843 let mut manager = MarginManager::new();
844
845 let git_indicator = LineIndicator::new("│", Color::Green, 10);
847 let breakpoint_indicator = LineIndicator::new("●", Color::Red, 20);
848
849 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), git_indicator);
850 manager.set_line_indicator(
851 line_to_byte(5),
852 "breakpoints".to_string(),
853 breakpoint_indicator,
854 );
855
856 let retrieved = manager.get_line_indicator(5, byte_to_line);
858 assert!(retrieved.is_some());
859 let retrieved = retrieved.unwrap();
860 assert_eq!(retrieved.symbol, "●"); assert_eq!(retrieved.priority, 20);
862 }
863
864 #[test]
865 fn test_line_indicator_clear_namespace() {
866 let mut manager = MarginManager::new();
867
868 manager.set_line_indicator(
870 line_to_byte(1),
871 "git-gutter".to_string(),
872 LineIndicator::new("│", Color::Green, 10),
873 );
874 manager.set_line_indicator(
875 line_to_byte(2),
876 "git-gutter".to_string(),
877 LineIndicator::new("│", Color::Yellow, 10),
878 );
879 manager.set_line_indicator(
880 line_to_byte(3),
881 "breakpoints".to_string(),
882 LineIndicator::new("●", Color::Red, 20),
883 );
884
885 manager.clear_line_indicators_for_namespace("git-gutter");
887
888 assert!(manager.get_line_indicator(1, byte_to_line).is_none());
890 assert!(manager.get_line_indicator(2, byte_to_line).is_none());
891
892 let breakpoint = manager.get_line_indicator(3, byte_to_line);
894 assert!(breakpoint.is_some());
895 assert_eq!(breakpoint.unwrap().symbol, "●");
896 }
897
898 #[test]
899 fn test_line_indicator_remove_specific() {
900 let mut manager = MarginManager::new();
901
902 let git_marker = manager.set_line_indicator(
904 line_to_byte(5),
905 "git-gutter".to_string(),
906 LineIndicator::new("│", Color::Green, 10),
907 );
908 let bp_marker = manager.set_line_indicator(
909 line_to_byte(5),
910 "breakpoints".to_string(),
911 LineIndicator::new("●", Color::Red, 20),
912 );
913
914 manager.remove_line_indicator(git_marker, "git-gutter");
916
917 let retrieved = manager.get_line_indicator(5, byte_to_line);
919 assert!(retrieved.is_some());
920 assert_eq!(retrieved.unwrap().symbol, "●");
921
922 manager.remove_line_indicator(bp_marker, "breakpoints");
924
925 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
927 }
928
929 #[test]
930 fn test_line_indicator_shifts_on_insert() {
931 let mut manager = MarginManager::new();
932
933 manager.set_line_indicator(
935 line_to_byte(5),
936 "git-gutter".to_string(),
937 LineIndicator::new("│", Color::Green, 10),
938 );
939
940 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
942 assert!(manager.get_line_indicator(6, byte_to_line).is_none());
943
944 manager.adjust_for_insert(0, 10);
946
947 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
949 assert!(manager.get_line_indicator(6, byte_to_line).is_some());
950 }
951
952 #[test]
953 fn test_line_indicator_shifts_on_delete() {
954 let mut manager = MarginManager::new();
955
956 manager.set_line_indicator(
958 line_to_byte(5),
959 "git-gutter".to_string(),
960 LineIndicator::new("│", Color::Green, 10),
961 );
962
963 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
965
966 manager.adjust_for_delete(0, 20);
968
969 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
971 assert!(manager.get_line_indicator(3, byte_to_line).is_some());
972 }
973
974 #[test]
975 fn test_multiple_indicators_shift_together() {
976 let mut manager = MarginManager::new();
977
978 manager.set_line_indicator(
980 line_to_byte(3),
981 "git-gutter".to_string(),
982 LineIndicator::new("│", Color::Green, 10),
983 );
984 manager.set_line_indicator(
985 line_to_byte(5),
986 "git-gutter".to_string(),
987 LineIndicator::new("│", Color::Yellow, 10),
988 );
989 manager.set_line_indicator(
990 line_to_byte(7),
991 "git-gutter".to_string(),
992 LineIndicator::new("│", Color::Red, 10),
993 );
994
995 manager.adjust_for_insert(25, 20);
998
999 assert!(manager.get_line_indicator(3, byte_to_line).is_none());
1001
1002 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
1004 assert!(manager.get_line_indicator(7, byte_to_line).is_some());
1005 assert!(manager.get_line_indicator(9, byte_to_line).is_some());
1006 }
1007}