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 add_annotation(&mut self, annotation: MarginAnnotation) {
478 let annotations = match annotation.position {
479 MarginPosition::Left => &mut self.left_annotations,
480 MarginPosition::Right => &mut self.right_annotations,
481 };
482
483 annotations
484 .entry(annotation.line)
485 .or_insert_with(Vec::new)
486 .push(annotation);
487 }
488
489 pub fn remove_by_id(&mut self, id: &str) {
491 for annotations in self.left_annotations.values_mut() {
493 annotations.retain(|a| a.id.as_deref() != Some(id));
494 }
495
496 for annotations in self.right_annotations.values_mut() {
498 annotations.retain(|a| a.id.as_deref() != Some(id));
499 }
500
501 self.left_annotations.retain(|_, v| !v.is_empty());
503 self.right_annotations.retain(|_, v| !v.is_empty());
504 }
505
506 pub fn remove_at_line(&mut self, line: usize, position: MarginPosition) {
508 match position {
509 MarginPosition::Left => {
510 self.left_annotations.remove(&line);
511 }
512 MarginPosition::Right => {
513 self.right_annotations.remove(&line);
514 }
515 }
516 }
517
518 pub fn clear_position(&mut self, position: MarginPosition) {
520 match position {
521 MarginPosition::Left => self.left_annotations.clear(),
522 MarginPosition::Right => self.right_annotations.clear(),
523 }
524 }
525
526 pub fn clear_all(&mut self) {
528 self.left_annotations.clear();
529 self.right_annotations.clear();
530 }
531
532 pub fn get_at_line(
534 &self,
535 line: usize,
536 position: MarginPosition,
537 ) -> Option<&[MarginAnnotation]> {
538 let annotations = match position {
539 MarginPosition::Left => &self.left_annotations,
540 MarginPosition::Right => &self.right_annotations,
541 };
542 annotations.get(&line).map(|v| v.as_slice())
543 }
544
545 pub fn render_line(
548 &self,
549 line: usize,
550 position: MarginPosition,
551 _buffer_total_lines: usize,
552 show_line_numbers: bool,
553 ) -> MarginContent {
554 let annotations = match position {
555 MarginPosition::Left => &self.left_annotations,
556 MarginPosition::Right => &self.right_annotations,
557 };
558
559 let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
561
562 if position == MarginPosition::Left && show_line_numbers {
564 let line_num = MarginContent::text(format!("{}", line + 1));
565
566 if user_annotations.is_empty() {
567 return line_num;
568 }
569
570 let mut stack = vec![line_num];
572 stack.extend(user_annotations.into_iter().map(|a| a.content));
573 MarginContent::Stacked(stack)
574 } else if let Some(annotation) = user_annotations.first() {
575 annotation.content.clone()
576 } else {
577 MarginContent::Empty
578 }
579 }
580
581 pub fn update_width_for_buffer(&mut self, buffer_total_lines: usize, show_line_numbers: bool) {
584 if show_line_numbers {
585 let digits = if buffer_total_lines == 0 {
586 1
587 } else {
588 ((buffer_total_lines as f64).log10().floor() as usize) + 1
589 };
590 self.left_config.width = digits.max(4);
591 }
592 }
593
594 pub fn left_total_width(&self) -> usize {
597 self.left_config.total_width()
598 }
599
600 pub fn right_total_width(&self) -> usize {
602 self.right_config.total_width()
603 }
604
605 pub fn configure_for_line_numbers(&mut self, show_line_numbers: bool) {
612 if !show_line_numbers {
613 self.left_config.width = 0;
614 self.left_config.enabled = false;
615 } else {
616 self.left_config.enabled = true;
617 if self.left_config.width == 0 {
618 self.left_config.width = 4;
619 }
620 }
621 }
622
623 pub fn annotation_count(&self, position: MarginPosition) -> usize {
625 match position {
626 MarginPosition::Left => self.left_annotations.values().map(|v| v.len()).sum(),
627 MarginPosition::Right => self.right_annotations.values().map(|v| v.len()).sum(),
628 }
629 }
630}
631
632impl Default for MarginManager {
633 fn default() -> Self {
634 Self::new()
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn test_margin_content_text() {
644 let content = MarginContent::text("123");
645 let (rendered, style) = content.render(5);
646 assert_eq!(rendered, " 123");
647 assert!(style.is_none());
648 }
649
650 #[test]
651 fn test_margin_content_symbol() {
652 let content = MarginContent::colored_symbol("●", Color::Red);
653 let (rendered, style) = content.render(3);
654 assert_eq!(rendered, " ●");
655 assert!(style.is_some());
656 }
657
658 #[test]
659 fn test_margin_config_total_width() {
660 let mut config = MarginConfig::left_default();
661 config.width = 4;
662 config.separator = " │ ".to_string();
663 assert_eq!(config.total_width(), 8); config.show_separator = false;
666 assert_eq!(config.total_width(), 5); config.enabled = false;
669 assert_eq!(config.total_width(), 0);
670 }
671
672 #[test]
673 fn test_margin_annotation_helpers() {
674 let line_num = MarginAnnotation::line_number(5);
675 assert_eq!(line_num.line, 5);
676 assert_eq!(line_num.position, MarginPosition::Left);
677
678 let breakpoint = MarginAnnotation::breakpoint(10);
679 assert_eq!(breakpoint.line, 10);
680 assert_eq!(breakpoint.position, MarginPosition::Left);
681 }
682
683 #[test]
684 fn test_margin_manager_add_remove() {
685 let mut manager = MarginManager::new();
686
687 let annotation = MarginAnnotation::line_number(5);
689 manager.add_annotation(annotation);
690
691 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
692
693 let annotation = MarginAnnotation::with_id(
695 10,
696 MarginPosition::Left,
697 MarginContent::text("test"),
698 "test-id".to_string(),
699 );
700 manager.add_annotation(annotation);
701
702 assert_eq!(manager.annotation_count(MarginPosition::Left), 2);
703
704 manager.remove_by_id("test-id");
706 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
707
708 manager.clear_all();
710 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
711 }
712
713 #[test]
714 fn test_margin_manager_render_line() {
715 let mut manager = MarginManager::new();
716
717 let content = manager.render_line(5, MarginPosition::Left, 100, true);
719 let (rendered, _) = content.render(4);
720 assert!(rendered.contains("6")); manager.add_annotation(MarginAnnotation::breakpoint(5));
724
725 let content = manager.render_line(5, MarginPosition::Left, 100, true);
727 assert!(matches!(content, MarginContent::Stacked(_)));
728 }
729
730 #[test]
731 fn test_margin_manager_update_width() {
732 let mut manager = MarginManager::new();
733
734 manager.update_width_for_buffer(99, true);
736 assert_eq!(manager.left_config.width, 4); manager.update_width_for_buffer(1000, true);
740 assert_eq!(manager.left_config.width, 4);
741
742 manager.update_width_for_buffer(10000, true);
744 assert_eq!(manager.left_config.width, 5);
745
746 manager.update_width_for_buffer(1000000, true);
748 assert_eq!(manager.left_config.width, 7);
749 }
750
751 #[test]
752 fn test_margin_manager_without_line_numbers() {
753 let manager = MarginManager::without_line_numbers();
754 assert!(!manager.left_config.enabled);
755
756 let content = manager.render_line(5, MarginPosition::Left, 100, false);
757 assert!(content.is_empty());
758 }
759
760 #[test]
761 fn test_margin_position_left_right() {
762 let mut manager = MarginManager::new();
763
764 manager.add_annotation(MarginAnnotation::new(
765 1,
766 MarginPosition::Left,
767 MarginContent::text("left"),
768 ));
769
770 manager.add_annotation(MarginAnnotation::new(
771 1,
772 MarginPosition::Right,
773 MarginContent::text("right"),
774 ));
775
776 assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
777 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
778
779 manager.clear_position(MarginPosition::Left);
780 assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
781 assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
782 }
783
784 fn byte_to_line(byte_offset: usize) -> usize {
787 byte_offset / 10
788 }
789
790 fn line_to_byte(line: usize) -> usize {
792 line * 10
793 }
794
795 #[test]
796 fn test_line_indicator_basic() {
797 let mut manager = MarginManager::new();
798
799 let indicator = LineIndicator::new("│", Color::Green, 10);
801 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
802
803 let retrieved = manager.get_line_indicator(5, byte_to_line);
805 assert!(retrieved.is_some());
806 let retrieved = retrieved.unwrap();
807 assert_eq!(retrieved.symbol, "│");
808 assert_eq!(retrieved.color, Color::Green);
809 assert_eq!(retrieved.priority, 10);
810
811 assert!(manager.get_line_indicator(10, byte_to_line).is_none());
813 }
814
815 #[test]
816 fn test_line_indicator_multiple_namespaces() {
817 let mut manager = MarginManager::new();
818
819 let git_indicator = LineIndicator::new("│", Color::Green, 10);
821 let breakpoint_indicator = LineIndicator::new("●", Color::Red, 20);
822
823 manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), git_indicator);
824 manager.set_line_indicator(
825 line_to_byte(5),
826 "breakpoints".to_string(),
827 breakpoint_indicator,
828 );
829
830 let retrieved = manager.get_line_indicator(5, byte_to_line);
832 assert!(retrieved.is_some());
833 let retrieved = retrieved.unwrap();
834 assert_eq!(retrieved.symbol, "●"); assert_eq!(retrieved.priority, 20);
836 }
837
838 #[test]
839 fn test_line_indicator_clear_namespace() {
840 let mut manager = MarginManager::new();
841
842 manager.set_line_indicator(
844 line_to_byte(1),
845 "git-gutter".to_string(),
846 LineIndicator::new("│", Color::Green, 10),
847 );
848 manager.set_line_indicator(
849 line_to_byte(2),
850 "git-gutter".to_string(),
851 LineIndicator::new("│", Color::Yellow, 10),
852 );
853 manager.set_line_indicator(
854 line_to_byte(3),
855 "breakpoints".to_string(),
856 LineIndicator::new("●", Color::Red, 20),
857 );
858
859 manager.clear_line_indicators_for_namespace("git-gutter");
861
862 assert!(manager.get_line_indicator(1, byte_to_line).is_none());
864 assert!(manager.get_line_indicator(2, byte_to_line).is_none());
865
866 let breakpoint = manager.get_line_indicator(3, byte_to_line);
868 assert!(breakpoint.is_some());
869 assert_eq!(breakpoint.unwrap().symbol, "●");
870 }
871
872 #[test]
873 fn test_line_indicator_remove_specific() {
874 let mut manager = MarginManager::new();
875
876 let git_marker = manager.set_line_indicator(
878 line_to_byte(5),
879 "git-gutter".to_string(),
880 LineIndicator::new("│", Color::Green, 10),
881 );
882 let bp_marker = manager.set_line_indicator(
883 line_to_byte(5),
884 "breakpoints".to_string(),
885 LineIndicator::new("●", Color::Red, 20),
886 );
887
888 manager.remove_line_indicator(git_marker, "git-gutter");
890
891 let retrieved = manager.get_line_indicator(5, byte_to_line);
893 assert!(retrieved.is_some());
894 assert_eq!(retrieved.unwrap().symbol, "●");
895
896 manager.remove_line_indicator(bp_marker, "breakpoints");
898
899 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
901 }
902
903 #[test]
904 fn test_line_indicator_shifts_on_insert() {
905 let mut manager = MarginManager::new();
906
907 manager.set_line_indicator(
909 line_to_byte(5),
910 "git-gutter".to_string(),
911 LineIndicator::new("│", Color::Green, 10),
912 );
913
914 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
916 assert!(manager.get_line_indicator(6, byte_to_line).is_none());
917
918 manager.adjust_for_insert(0, 10);
920
921 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
923 assert!(manager.get_line_indicator(6, byte_to_line).is_some());
924 }
925
926 #[test]
927 fn test_line_indicator_shifts_on_delete() {
928 let mut manager = MarginManager::new();
929
930 manager.set_line_indicator(
932 line_to_byte(5),
933 "git-gutter".to_string(),
934 LineIndicator::new("│", Color::Green, 10),
935 );
936
937 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
939
940 manager.adjust_for_delete(0, 20);
942
943 assert!(manager.get_line_indicator(5, byte_to_line).is_none());
945 assert!(manager.get_line_indicator(3, byte_to_line).is_some());
946 }
947
948 #[test]
949 fn test_multiple_indicators_shift_together() {
950 let mut manager = MarginManager::new();
951
952 manager.set_line_indicator(
954 line_to_byte(3),
955 "git-gutter".to_string(),
956 LineIndicator::new("│", Color::Green, 10),
957 );
958 manager.set_line_indicator(
959 line_to_byte(5),
960 "git-gutter".to_string(),
961 LineIndicator::new("│", Color::Yellow, 10),
962 );
963 manager.set_line_indicator(
964 line_to_byte(7),
965 "git-gutter".to_string(),
966 LineIndicator::new("│", Color::Red, 10),
967 );
968
969 manager.adjust_for_insert(25, 20);
972
973 assert!(manager.get_line_indicator(3, byte_to_line).is_none());
975
976 assert!(manager.get_line_indicator(5, byte_to_line).is_some());
978 assert!(manager.get_line_indicator(7, byte_to_line).is_some());
979 assert!(manager.get_line_indicator(9, byte_to_line).is_some());
980 }
981}