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    /// Whether to show line numbers by default
303    pub show_line_numbers: bool,
304
305    /// Diagnostic indicators per line (displayed between line numbers and separator)
306    /// Maps line number to (symbol, color) tuple
307    diagnostic_indicators: BTreeMap<usize, (String, Color)>,
308
309    /// Marker list for tracking indicator positions through edits
310    /// Shared with the buffer's edit tracking
311    indicator_markers: MarkerList,
312
313    /// Line indicators stored by marker ID
314    /// Maps marker_id -> (namespace -> indicator)
315    /// The line number is computed at render time from the marker's byte position
316    line_indicators: BTreeMap<u64, BTreeMap<String, LineIndicator>>,
317}
318
319impl MarginManager {
320    /// Create a new margin manager with default settings
321    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    /// Create a margin manager with line numbers disabled
335    pub fn without_line_numbers() -> Self {
336        let mut manager = Self::new();
337        manager.show_line_numbers = false;
338        manager
339    }
340
341    // =========================================================================
342    // Edit Propagation - called when buffer content changes
343    // =========================================================================
344
345    /// Adjust all indicator markers after an insertion
346    /// Call this when text is inserted into the buffer
347    pub fn adjust_for_insert(&mut self, position: usize, length: usize) {
348        self.indicator_markers.adjust_for_insert(position, length);
349    }
350
351    /// Adjust all indicator markers after a deletion
352    /// Call this when text is deleted from the buffer
353    pub fn adjust_for_delete(&mut self, position: usize, length: usize) {
354        self.indicator_markers.adjust_for_delete(position, length);
355    }
356
357    /// Set a diagnostic indicator for a line
358    pub fn set_diagnostic_indicator(&mut self, line: usize, symbol: String, color: Color) {
359        self.diagnostic_indicators.insert(line, (symbol, color));
360    }
361
362    /// Remove diagnostic indicator for a line
363    pub fn remove_diagnostic_indicator(&mut self, line: usize) {
364        self.diagnostic_indicators.remove(&line);
365    }
366
367    /// Clear all diagnostic indicators
368    pub fn clear_diagnostic_indicators(&mut self) {
369        self.diagnostic_indicators.clear();
370    }
371
372    /// Get diagnostic indicator for a line
373    pub fn get_diagnostic_indicator(&self, line: usize) -> Option<&(String, Color)> {
374        self.diagnostic_indicators.get(&line)
375    }
376
377    /// Set a line indicator at a byte position for a specific namespace
378    ///
379    /// The indicator is anchored to the byte position and will automatically
380    /// shift when text is inserted or deleted before it.
381    ///
382    /// Returns the marker ID that can be used to remove or update the indicator.
383    pub fn set_line_indicator(
384        &mut self,
385        byte_offset: usize,
386        namespace: String,
387        mut indicator: LineIndicator,
388    ) -> MarkerId {
389        // Create a marker at this byte position (left affinity - stays before inserted text)
390        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    /// Remove line indicator for a specific namespace at a marker
402    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    /// Clear all line indicators for a specific namespace
413    pub fn clear_line_indicators_for_namespace(&mut self, namespace: &str) {
414        // Collect marker IDs to delete (can't modify while iterating)
415        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        // Delete empty marker entries and their markers
425        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    /// Get the line indicator for a specific line number
432    ///
433    /// This looks up all indicators whose markers resolve to the given line.
434    /// Returns the highest priority indicator if multiple exist on the same line.
435    ///
436    /// Note: This is O(n) in the number of indicators. For rendering, prefer
437    /// `get_indicators_in_viewport` which is more efficient.
438    pub fn get_line_indicator(
439        &self,
440        line: usize,
441        get_line_fn: impl Fn(usize) -> usize,
442    ) -> Option<&LineIndicator> {
443        // Find all indicators on this line
444        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                    // Found an indicator on this line, check if it's higher priority
451                    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    /// Get indicators within a viewport byte range
464    ///
465    /// Only queries markers within `viewport_start..viewport_end`, avoiding
466    /// iteration over the entire indicator set.
467    ///
468    /// Returns a map of line_number -> highest priority indicator for that line.
469    /// The `get_line_fn` converts byte offsets to line numbers.
470    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        // Query only markers within the viewport byte range
479        for (marker_id, byte_pos, _end) in self
480            .indicator_markers
481            .query_range(viewport_start, viewport_end)
482        {
483            // Look up the indicators for this marker
484            if let Some(indicators) = self.line_indicators.get(&marker_id.0) {
485                let line = get_line_fn(byte_pos);
486
487                // Get highest priority indicator for this marker
488                if let Some(indicator) = indicators.values().max_by_key(|ind| ind.priority) {
489                    // Check if this is higher priority than existing indicator on this line
490                    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    /// Add an annotation to a margin
505    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    /// Remove all annotations with a specific ID
518    pub fn remove_by_id(&mut self, id: &str) {
519        // Remove from left annotations
520        for annotations in self.left_annotations.values_mut() {
521            annotations.retain(|a| a.id.as_deref() != Some(id));
522        }
523
524        // Remove from right annotations
525        for annotations in self.right_annotations.values_mut() {
526            annotations.retain(|a| a.id.as_deref() != Some(id));
527        }
528
529        // Clean up empty entries
530        self.left_annotations.retain(|_, v| !v.is_empty());
531        self.right_annotations.retain(|_, v| !v.is_empty());
532    }
533
534    /// Remove all annotations at a specific line
535    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    /// Clear all annotations in a position
547    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    /// Clear all annotations
555    pub fn clear_all(&mut self) {
556        self.left_annotations.clear();
557        self.right_annotations.clear();
558    }
559
560    /// Get all annotations at a specific line
561    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    /// Get the content to render for a specific line in a margin
574    /// If show_line_numbers is true and position is Left, includes line number
575    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        // Get user annotations
587        let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
588
589        // For left margin, combine with line numbers if enabled
590        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            // Stack line number with user annotations
598            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    /// Update the left margin width based on buffer size
609    /// This should be called when the buffer grows significantly
610    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    /// Get the total width of the left margin (including separator)
622    /// The separator includes the diagnostic indicator when present
623    pub fn left_total_width(&self) -> usize {
624        self.left_config.total_width()
625    }
626
627    /// Get the total width of the right margin (including separator)
628    pub fn right_total_width(&self) -> usize {
629        self.right_config.total_width()
630    }
631
632    /// Enable or disable line numbers
633    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    /// Get the number of annotations in a position
647    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); // 1 (indicator) + 4 (line num) + 3 (separator)
687
688        config.show_separator = false;
689        assert_eq!(config.total_width(), 5); // 1 (indicator) + 4 (line num)
690
691        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        // Add annotation
711        let annotation = MarginAnnotation::line_number(5);
712        manager.add_annotation(annotation);
713
714        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
715
716        // Add annotation with ID
717        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        // Remove by ID
728        manager.remove_by_id("test-id");
729        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
730
731        // Clear all
732        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        // Without annotations, should render line number
742        let content = manager.render_line(5, MarginPosition::Left, 100);
743        let (rendered, _) = content.render(4);
744        assert!(rendered.contains("6")); // Line 5 is displayed as "6" (1-indexed)
745
746        // Add a breakpoint annotation
747        manager.add_annotation(MarginAnnotation::breakpoint(5));
748
749        // Should now render stacked content (line number + breakpoint)
750        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        // Small buffer
760        manager.update_width_for_buffer(99);
761        assert_eq!(manager.left_config.width, 4); // Minimum 4
762
763        // Medium buffer (4 digits)
764        manager.update_width_for_buffer(1000);
765        assert_eq!(manager.left_config.width, 4);
766
767        // Large buffer (5 digits)
768        manager.update_width_for_buffer(10000);
769        assert_eq!(manager.left_config.width, 5);
770
771        // Very large buffer (7 digits)
772        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    // Helper: simulates a buffer where each line is 10 bytes (9 chars + newline)
810    // Line 0 = bytes 0-9, Line 1 = bytes 10-19, etc.
811    fn byte_to_line(byte_offset: usize) -> usize {
812        byte_offset / 10
813    }
814
815    // Helper: get byte offset for start of a line
816    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        // Add a line indicator at byte offset 50 (line 5 in our simulated buffer)
825        let indicator = LineIndicator::new("│", Color::Green, 10);
826        manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
827
828        // Check it can be retrieved on line 5
829        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        // Non-existent line should return None
837        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        // Add indicators from different namespaces at the same byte position (line 5)
845        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        // Should return the highest priority indicator
856        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, "●"); // Breakpoint has higher priority
860        assert_eq!(retrieved.priority, 20);
861    }
862
863    #[test]
864    fn test_line_indicator_clear_namespace() {
865        let mut manager = MarginManager::new();
866
867        // Add indicators on multiple lines
868        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        // Clear git-gutter namespace
885        manager.clear_line_indicators_for_namespace("git-gutter");
886
887        // Git gutter indicators should be gone
888        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        // Breakpoint should still be there
892        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        // Add two indicators at the same byte position (line 5)
902        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        // Remove just the git-gutter indicator
914        manager.remove_line_indicator(git_marker, "git-gutter");
915
916        // Should still have the breakpoint indicator on line 5
917        let retrieved = manager.get_line_indicator(5, byte_to_line);
918        assert!(retrieved.is_some());
919        assert_eq!(retrieved.unwrap().symbol, "●");
920
921        // Remove the breakpoint indicator too
922        manager.remove_line_indicator(bp_marker, "breakpoints");
923
924        // Now no indicators on line 5
925        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        // Add indicator on line 5 (byte 50)
933        manager.set_line_indicator(
934            line_to_byte(5),
935            "git-gutter".to_string(),
936            LineIndicator::new("│", Color::Green, 10),
937        );
938
939        // Verify it's on line 5
940        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        // Insert 10 bytes (one line) at the beginning
944        manager.adjust_for_insert(0, 10);
945
946        // Now indicator should be on line 6 (shifted down by 1)
947        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        // Add indicator on line 5 (byte 50)
956        manager.set_line_indicator(
957            line_to_byte(5),
958            "git-gutter".to_string(),
959            LineIndicator::new("│", Color::Green, 10),
960        );
961
962        // Verify it's on line 5
963        assert!(manager.get_line_indicator(5, byte_to_line).is_some());
964
965        // Delete first 20 bytes (2 lines)
966        manager.adjust_for_delete(0, 20);
967
968        // Now indicator should be on line 3 (shifted up by 2)
969        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        // Add indicators on lines 3, 5, and 7
978        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        // Insert 2 lines (20 bytes) at byte 25 (middle of line 2)
995        // This should shift lines 3, 5, 7 -> lines 5, 7, 9
996        manager.adjust_for_insert(25, 20);
997
998        // Old positions should be empty
999        assert!(manager.get_line_indicator(3, byte_to_line).is_none());
1000
1001        // New positions should have indicators
1002        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}