Skip to main content

fresh/view/
margin.rs

1use crate::model::marker::{MarkerId, MarkerList};
2use ratatui::style::{Color, Style};
3use std::collections::BTreeMap;
4
5/// Position of a margin in the editor
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum MarginPosition {
8    /// Left margin (before the text)
9    Left,
10    /// Right margin (after the text)
11    Right,
12}
13
14/// A line indicator displayed in the gutter's indicator column
15/// Can be used for git status, breakpoints, bookmarks, etc.
16///
17/// Indicators are anchored to byte positions via markers, so they automatically
18/// shift when text is inserted or deleted before them.
19#[derive(Debug, Clone, PartialEq)]
20pub struct LineIndicator {
21    /// The symbol to display (e.g., "│", "●", "★")
22    pub symbol: String,
23    /// The color of the indicator
24    pub color: Color,
25    /// Priority for display when multiple indicators exist (higher wins)
26    pub priority: i32,
27    /// Marker ID anchoring this indicator to a byte position
28    /// The line number is derived from this position at render time
29    pub marker_id: MarkerId,
30}
31
32impl LineIndicator {
33    /// Create a new line indicator (marker_id will be set when added to MarginManager)
34    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), // Placeholder, set by MarginManager
40        }
41    }
42
43    /// Create a line indicator with a specific marker ID
44    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/// Content type for a margin at a specific line
60#[derive(Debug, Clone, PartialEq)]
61pub enum MarginContent {
62    /// Simple text (e.g., line number)
63    Text(String),
64    /// Symbol with optional color (e.g., breakpoint, error indicator)
65    Symbol { text: String, style: Style },
66    /// Multiple items stacked (e.g., line number + breakpoint)
67    Stacked(Vec<MarginContent>),
68    /// Empty/cleared margin
69    Empty,
70}
71
72impl MarginContent {
73    /// Create a simple text margin content
74    pub fn text(text: impl Into<String>) -> Self {
75        Self::Text(text.into())
76    }
77
78    /// Create a symbol with styling
79    pub fn symbol(text: impl Into<String>, style: Style) -> Self {
80        Self::Symbol {
81            text: text.into(),
82            style,
83        }
84    }
85
86    /// Create a colored symbol
87    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    /// Check if this margin content is empty
95    pub fn is_empty(&self) -> bool {
96        matches!(self, Self::Empty)
97    }
98
99    /// Render this margin content to a string with width padding
100    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 stacked items, render the last non-empty one
112                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/// Configuration for a margin
125#[derive(Debug, Clone, PartialEq)]
126pub struct MarginConfig {
127    /// Position of the margin (left or right)
128    pub position: MarginPosition,
129
130    /// Width of the margin in characters
131    /// For left margin with line numbers, this is calculated dynamically
132    pub width: usize,
133
134    /// Whether this margin is enabled
135    pub enabled: bool,
136
137    /// Whether to show a separator (e.g., "│") after the margin
138    pub show_separator: bool,
139
140    /// Separator character(s)
141    pub separator: String,
142
143    /// Default style for the margin
144    pub style: Style,
145
146    /// Default separator style
147    pub separator_style: Style,
148}
149
150impl MarginConfig {
151    /// Create a default left margin config (for line numbers)
152    pub fn left_default() -> Self {
153        Self {
154            position: MarginPosition::Left,
155            width: 4, // Minimum 4 digits for line numbers
156            enabled: true,
157            show_separator: true,
158            separator: " │ ".to_string(), // Separator with spaces: " │ " (space before for indicators, space after for readability)
159            style: Style::default().fg(Color::DarkGray),
160            separator_style: Style::default().fg(Color::DarkGray),
161        }
162    }
163
164    /// Create a default right margin config
165    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    /// Calculate the total width including indicator column and separator
178    /// Format: [indicator (1 char)][line_number (N chars)][separator (3 chars)]
179    pub fn total_width(&self) -> usize {
180        if self.enabled {
181            // 1 char for indicator column + line number width + separator
182            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/// A margin annotation for a specific line
195#[derive(Debug, Clone)]
196pub struct MarginAnnotation {
197    /// The line number (0-indexed)
198    pub line: usize,
199
200    /// The margin position (left or right)
201    pub position: MarginPosition,
202
203    /// The content to display
204    pub content: MarginContent,
205
206    /// Optional ID for this annotation (for removal/updates)
207    pub id: Option<String>,
208}
209
210impl MarginAnnotation {
211    /// Create a new margin annotation
212    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    /// Create an annotation with an ID
222    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    /// Helper: Create a line number annotation for the left margin
237    pub fn line_number(line: usize) -> Self {
238        Self::new(
239            line,
240            MarginPosition::Left,
241            MarginContent::text(format!("{}", line + 1)), // 1-indexed display
242        )
243    }
244
245    /// Helper: Create a breakpoint indicator
246    pub fn breakpoint(line: usize) -> Self {
247        Self::new(
248            line,
249            MarginPosition::Left,
250            MarginContent::colored_symbol("●", Color::Red),
251        )
252    }
253
254    /// Helper: Create an error indicator
255    pub fn error(line: usize) -> Self {
256        Self::new(
257            line,
258            MarginPosition::Left,
259            MarginContent::colored_symbol("✗", Color::Red),
260        )
261    }
262
263    /// Helper: Create a warning indicator
264    pub fn warning(line: usize) -> Self {
265        Self::new(
266            line,
267            MarginPosition::Left,
268            MarginContent::colored_symbol("⚠", Color::Yellow),
269        )
270    }
271
272    /// Helper: Create an info indicator
273    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/// Manages margins and annotations for a buffer
283/// This is similar to OverlayManager - a general-purpose primitive for margin decorations
284///
285/// Line indicators use byte-position markers that automatically adjust when the buffer
286/// is edited. This ensures indicators stay anchored to the content they represent.
287#[derive(Debug)]
288pub struct MarginManager {
289    /// Configuration for left margin
290    pub left_config: MarginConfig,
291
292    /// Configuration for right margin
293    pub right_config: MarginConfig,
294
295    /// Annotations per line (left margin)
296    /// Uses BTreeMap for efficient range queries
297    left_annotations: BTreeMap<usize, Vec<MarginAnnotation>>,
298
299    /// Annotations per line (right margin)
300    right_annotations: BTreeMap<usize, Vec<MarginAnnotation>>,
301
302    /// Marker list for tracking indicator positions through edits
303    /// Shared with the buffer's edit tracking
304    indicator_markers: MarkerList,
305
306    /// Line indicators stored by marker ID
307    /// Maps marker_id -> (namespace -> indicator)
308    /// The line number is computed at render time from the marker's byte position
309    line_indicators: BTreeMap<u64, BTreeMap<String, LineIndicator>>,
310}
311
312impl MarginManager {
313    /// Create a new margin manager with default settings
314    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    /// Create a margin manager with line numbers disabled
326    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    // =========================================================================
334    // Edit Propagation - called when buffer content changes
335    // =========================================================================
336
337    /// Adjust all indicator markers after an insertion
338    /// Call this when text is inserted into the buffer
339    pub fn adjust_for_insert(&mut self, position: usize, length: usize) {
340        self.indicator_markers.adjust_for_insert(position, length);
341    }
342
343    /// Adjust all indicator markers after a deletion
344    /// Call this when text is deleted from the buffer
345    pub fn adjust_for_delete(&mut self, position: usize, length: usize) {
346        self.indicator_markers.adjust_for_delete(position, length);
347    }
348
349    /// Set a line indicator at a byte position for a specific namespace
350    ///
351    /// The indicator is anchored to the byte position and will automatically
352    /// shift when text is inserted or deleted before it.
353    ///
354    /// Returns the marker ID that can be used to remove or update the indicator.
355    pub fn set_line_indicator(
356        &mut self,
357        byte_offset: usize,
358        namespace: String,
359        mut indicator: LineIndicator,
360    ) -> MarkerId {
361        // Create a marker at this byte position (left affinity - stays before inserted text)
362        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    /// Remove line indicator for a specific namespace at a marker
374    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    /// Clear all line indicators for a specific namespace
385    pub fn clear_line_indicators_for_namespace(&mut self, namespace: &str) {
386        // Collect marker IDs to delete (can't modify while iterating)
387        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        // Delete empty marker entries and their markers
397        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    /// Get the line indicator for a specific line number
404    ///
405    /// This looks up all indicators whose markers resolve to the given line.
406    /// Returns the highest priority indicator if multiple exist on the same line.
407    ///
408    /// Note: This is O(n) in the number of indicators. For rendering, prefer
409    /// `get_indicators_in_viewport` which is more efficient.
410    pub fn get_line_indicator(
411        &self,
412        line: usize,
413        get_line_fn: impl Fn(usize) -> usize,
414    ) -> Option<&LineIndicator> {
415        // Find all indicators on this line
416        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                    // Found an indicator on this line, check if it's higher priority
423                    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    /// Get indicators within a viewport byte range
436    ///
437    /// Only queries markers within `viewport_start..viewport_end`, avoiding
438    /// iteration over the entire indicator set.
439    ///
440    /// Returns a map of line_number -> highest priority indicator for that line.
441    /// The `get_line_fn` converts byte offsets to line numbers.
442    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        // Query only markers within the viewport byte range
451        for (marker_id, byte_pos, _end) in self
452            .indicator_markers
453            .query_range(viewport_start, viewport_end)
454        {
455            // Look up the indicators for this marker
456            if let Some(indicators) = self.line_indicators.get(&marker_id.0) {
457                let line = get_line_fn(byte_pos);
458
459                // Get highest priority indicator for this marker
460                if let Some(indicator) = indicators.values().max_by_key(|ind| ind.priority) {
461                    // Check if this is higher priority than existing indicator on this line
462                    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    /// Add an annotation to a margin
477    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    /// Remove all annotations with a specific ID
490    pub fn remove_by_id(&mut self, id: &str) {
491        // Remove from left annotations
492        for annotations in self.left_annotations.values_mut() {
493            annotations.retain(|a| a.id.as_deref() != Some(id));
494        }
495
496        // Remove from right annotations
497        for annotations in self.right_annotations.values_mut() {
498            annotations.retain(|a| a.id.as_deref() != Some(id));
499        }
500
501        // Clean up empty entries
502        self.left_annotations.retain(|_, v| !v.is_empty());
503        self.right_annotations.retain(|_, v| !v.is_empty());
504    }
505
506    /// Remove all annotations at a specific line
507    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    /// Clear all annotations in a position
519    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    /// Clear all annotations
527    pub fn clear_all(&mut self) {
528        self.left_annotations.clear();
529        self.right_annotations.clear();
530    }
531
532    /// Get all annotations at a specific line
533    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    /// Get the content to render for a specific line in a margin.
546    /// If `show_line_numbers` is true and position is Left, includes line number.
547    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        // Get user annotations
560        let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
561
562        // For left margin, combine with line numbers if enabled
563        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            // Stack line number with user annotations
571            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    /// Update the left margin width based on buffer size.
582    /// Only adjusts width when `show_line_numbers` is true.
583    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    /// Get the total width of the left margin (including separator)
595    /// The separator includes the diagnostic indicator when present
596    pub fn left_total_width(&self) -> usize {
597        self.left_config.total_width()
598    }
599
600    /// Get the total width of the right margin (including separator)
601    pub fn right_total_width(&self) -> usize {
602        self.right_config.total_width()
603    }
604
605    /// Configure left margin layout for line number visibility.
606    ///
607    /// This adjusts `left_config.enabled` and `left_config.width` so that
608    /// `left_total_width()` returns the correct gutter size for the given
609    /// `show_line_numbers` setting. Called at render time with the per-split
610    /// line number state.
611    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    /// Get the number of annotations in a position
624    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); // 1 (indicator) + 4 (line num) + 3 (separator)
664
665        config.show_separator = false;
666        assert_eq!(config.total_width(), 5); // 1 (indicator) + 4 (line num)
667
668        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        // Add annotation
688        let annotation = MarginAnnotation::line_number(5);
689        manager.add_annotation(annotation);
690
691        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
692
693        // Add annotation with ID
694        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        // Remove by ID
705        manager.remove_by_id("test-id");
706        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
707
708        // Clear all
709        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        // Without annotations, should render line number when show_line_numbers=true
718        let content = manager.render_line(5, MarginPosition::Left, 100, true);
719        let (rendered, _) = content.render(4);
720        assert!(rendered.contains("6")); // Line 5 is displayed as "6" (1-indexed)
721
722        // Add a breakpoint annotation
723        manager.add_annotation(MarginAnnotation::breakpoint(5));
724
725        // Should now render stacked content (line number + breakpoint)
726        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        // Small buffer
735        manager.update_width_for_buffer(99, true);
736        assert_eq!(manager.left_config.width, 4); // Minimum 4
737
738        // Medium buffer (4 digits)
739        manager.update_width_for_buffer(1000, true);
740        assert_eq!(manager.left_config.width, 4);
741
742        // Large buffer (5 digits)
743        manager.update_width_for_buffer(10000, true);
744        assert_eq!(manager.left_config.width, 5);
745
746        // Very large buffer (7 digits)
747        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    // Helper: simulates a buffer where each line is 10 bytes (9 chars + newline)
785    // Line 0 = bytes 0-9, Line 1 = bytes 10-19, etc.
786    fn byte_to_line(byte_offset: usize) -> usize {
787        byte_offset / 10
788    }
789
790    // Helper: get byte offset for start of a line
791    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        // Add a line indicator at byte offset 50 (line 5 in our simulated buffer)
800        let indicator = LineIndicator::new("│", Color::Green, 10);
801        manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
802
803        // Check it can be retrieved on line 5
804        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        // Non-existent line should return None
812        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        // Add indicators from different namespaces at the same byte position (line 5)
820        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        // Should return the highest priority indicator
831        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, "●"); // Breakpoint has higher priority
835        assert_eq!(retrieved.priority, 20);
836    }
837
838    #[test]
839    fn test_line_indicator_clear_namespace() {
840        let mut manager = MarginManager::new();
841
842        // Add indicators on multiple lines
843        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        // Clear git-gutter namespace
860        manager.clear_line_indicators_for_namespace("git-gutter");
861
862        // Git gutter indicators should be gone
863        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        // Breakpoint should still be there
867        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        // Add two indicators at the same byte position (line 5)
877        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        // Remove just the git-gutter indicator
889        manager.remove_line_indicator(git_marker, "git-gutter");
890
891        // Should still have the breakpoint indicator on line 5
892        let retrieved = manager.get_line_indicator(5, byte_to_line);
893        assert!(retrieved.is_some());
894        assert_eq!(retrieved.unwrap().symbol, "●");
895
896        // Remove the breakpoint indicator too
897        manager.remove_line_indicator(bp_marker, "breakpoints");
898
899        // Now no indicators on line 5
900        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        // Add indicator on line 5 (byte 50)
908        manager.set_line_indicator(
909            line_to_byte(5),
910            "git-gutter".to_string(),
911            LineIndicator::new("│", Color::Green, 10),
912        );
913
914        // Verify it's on line 5
915        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        // Insert 10 bytes (one line) at the beginning
919        manager.adjust_for_insert(0, 10);
920
921        // Now indicator should be on line 6 (shifted down by 1)
922        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        // Add indicator on line 5 (byte 50)
931        manager.set_line_indicator(
932            line_to_byte(5),
933            "git-gutter".to_string(),
934            LineIndicator::new("│", Color::Green, 10),
935        );
936
937        // Verify it's on line 5
938        assert!(manager.get_line_indicator(5, byte_to_line).is_some());
939
940        // Delete first 20 bytes (2 lines)
941        manager.adjust_for_delete(0, 20);
942
943        // Now indicator should be on line 3 (shifted up by 2)
944        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        // Add indicators on lines 3, 5, and 7
953        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        // Insert 2 lines (20 bytes) at byte 25 (middle of line 2)
970        // This should shift lines 3, 5, 7 -> lines 5, 7, 9
971        manager.adjust_for_insert(25, 20);
972
973        // Old positions should be empty
974        assert!(manager.get_line_indicator(3, byte_to_line).is_none());
975
976        // New positions should have indicators
977        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}