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    /// Get the byte position of a line indicator's marker.
477    ///
478    /// Returns the current byte offset for the given marker ID, or None if the
479    /// marker doesn't exist. Useful for testing that marker positions survive
480    /// undo/redo correctly.
481    pub fn get_indicator_position(&self, marker_id: MarkerId) -> Option<usize> {
482        self.indicator_markers.get_position(marker_id)
483    }
484
485    /// Query indicator markers in a byte range.
486    /// Returns (MarkerId, start, end) tuples for markers in the range.
487    pub fn query_indicator_range(&self, start: usize, end: usize) -> Vec<(MarkerId, usize, usize)> {
488        self.indicator_markers.query_range(start, end)
489    }
490
491    /// Move a line indicator's marker to a new byte position.
492    ///
493    /// This is a no-op if the marker doesn't exist in the indicator markers.
494    /// Used to restore displaced markers after undo.
495    pub fn set_indicator_position(&mut self, marker_id: MarkerId, new_position: usize) {
496        // Only move if this marker is actually tracked by margins
497        if self.line_indicators.contains_key(&marker_id.0) {
498            self.indicator_markers.set_position(marker_id, new_position);
499        }
500    }
501
502    /// Add an annotation to a margin
503    pub fn add_annotation(&mut self, annotation: MarginAnnotation) {
504        let annotations = match annotation.position {
505            MarginPosition::Left => &mut self.left_annotations,
506            MarginPosition::Right => &mut self.right_annotations,
507        };
508
509        annotations
510            .entry(annotation.line)
511            .or_insert_with(Vec::new)
512            .push(annotation);
513    }
514
515    /// Remove all annotations with a specific ID
516    pub fn remove_by_id(&mut self, id: &str) {
517        // Remove from left annotations
518        for annotations in self.left_annotations.values_mut() {
519            annotations.retain(|a| a.id.as_deref() != Some(id));
520        }
521
522        // Remove from right annotations
523        for annotations in self.right_annotations.values_mut() {
524            annotations.retain(|a| a.id.as_deref() != Some(id));
525        }
526
527        // Clean up empty entries
528        self.left_annotations.retain(|_, v| !v.is_empty());
529        self.right_annotations.retain(|_, v| !v.is_empty());
530    }
531
532    /// Remove all annotations at a specific line
533    pub fn remove_at_line(&mut self, line: usize, position: MarginPosition) {
534        match position {
535            MarginPosition::Left => {
536                self.left_annotations.remove(&line);
537            }
538            MarginPosition::Right => {
539                self.right_annotations.remove(&line);
540            }
541        }
542    }
543
544    /// Clear all annotations in a position
545    pub fn clear_position(&mut self, position: MarginPosition) {
546        match position {
547            MarginPosition::Left => self.left_annotations.clear(),
548            MarginPosition::Right => self.right_annotations.clear(),
549        }
550    }
551
552    /// Clear all annotations
553    pub fn clear_all(&mut self) {
554        self.left_annotations.clear();
555        self.right_annotations.clear();
556    }
557
558    /// Get all annotations at a specific line
559    pub fn get_at_line(
560        &self,
561        line: usize,
562        position: MarginPosition,
563    ) -> Option<&[MarginAnnotation]> {
564        let annotations = match position {
565            MarginPosition::Left => &self.left_annotations,
566            MarginPosition::Right => &self.right_annotations,
567        };
568        annotations.get(&line).map(|v| v.as_slice())
569    }
570
571    /// Get the content to render for a specific line in a margin.
572    /// If `show_line_numbers` is true and position is Left, includes line number.
573    pub fn render_line(
574        &self,
575        line: usize,
576        position: MarginPosition,
577        _buffer_total_lines: usize,
578        show_line_numbers: bool,
579    ) -> MarginContent {
580        let annotations = match position {
581            MarginPosition::Left => &self.left_annotations,
582            MarginPosition::Right => &self.right_annotations,
583        };
584
585        // Get user annotations
586        let user_annotations = annotations.get(&line).cloned().unwrap_or_default();
587
588        // For left margin, combine with line numbers if enabled
589        if position == MarginPosition::Left && show_line_numbers {
590            let line_num = MarginContent::text(format!("{}", line + 1));
591
592            if user_annotations.is_empty() {
593                return line_num;
594            }
595
596            // Stack line number with user annotations
597            let mut stack = vec![line_num];
598            stack.extend(user_annotations.into_iter().map(|a| a.content));
599            MarginContent::Stacked(stack)
600        } else if let Some(annotation) = user_annotations.first() {
601            annotation.content.clone()
602        } else {
603            MarginContent::Empty
604        }
605    }
606
607    /// Update the left margin width based on buffer size.
608    /// Only adjusts width when `show_line_numbers` is true.
609    pub fn update_width_for_buffer(&mut self, buffer_total_lines: usize, show_line_numbers: bool) {
610        if show_line_numbers {
611            let digits = if buffer_total_lines == 0 {
612                1
613            } else {
614                ((buffer_total_lines as f64).log10().floor() as usize) + 1
615            };
616            self.left_config.width = digits.max(4);
617        }
618    }
619
620    /// Get the total width of the left margin (including separator)
621    /// The separator includes the diagnostic indicator when present
622    pub fn left_total_width(&self) -> usize {
623        self.left_config.total_width()
624    }
625
626    /// Get the total width of the right margin (including separator)
627    pub fn right_total_width(&self) -> usize {
628        self.right_config.total_width()
629    }
630
631    /// Configure left margin layout for line number visibility.
632    ///
633    /// This adjusts `left_config.enabled` and `left_config.width` so that
634    /// `left_total_width()` returns the correct gutter size for the given
635    /// `show_line_numbers` setting. Called at render time with the per-split
636    /// line number state.
637    pub fn configure_for_line_numbers(&mut self, show_line_numbers: bool) {
638        if !show_line_numbers {
639            self.left_config.width = 0;
640            self.left_config.enabled = false;
641        } else {
642            self.left_config.enabled = true;
643            if self.left_config.width == 0 {
644                self.left_config.width = 4;
645            }
646        }
647    }
648
649    /// Get the number of annotations in a position
650    pub fn annotation_count(&self, position: MarginPosition) -> usize {
651        match position {
652            MarginPosition::Left => self.left_annotations.values().map(|v| v.len()).sum(),
653            MarginPosition::Right => self.right_annotations.values().map(|v| v.len()).sum(),
654        }
655    }
656}
657
658impl Default for MarginManager {
659    fn default() -> Self {
660        Self::new()
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667
668    #[test]
669    fn test_margin_content_text() {
670        let content = MarginContent::text("123");
671        let (rendered, style) = content.render(5);
672        assert_eq!(rendered, "  123");
673        assert!(style.is_none());
674    }
675
676    #[test]
677    fn test_margin_content_symbol() {
678        let content = MarginContent::colored_symbol("●", Color::Red);
679        let (rendered, style) = content.render(3);
680        assert_eq!(rendered, "  ●");
681        assert!(style.is_some());
682    }
683
684    #[test]
685    fn test_margin_config_total_width() {
686        let mut config = MarginConfig::left_default();
687        config.width = 4;
688        config.separator = " │ ".to_string();
689        assert_eq!(config.total_width(), 8); // 1 (indicator) + 4 (line num) + 3 (separator)
690
691        config.show_separator = false;
692        assert_eq!(config.total_width(), 5); // 1 (indicator) + 4 (line num)
693
694        config.enabled = false;
695        assert_eq!(config.total_width(), 0);
696    }
697
698    #[test]
699    fn test_margin_annotation_helpers() {
700        let line_num = MarginAnnotation::line_number(5);
701        assert_eq!(line_num.line, 5);
702        assert_eq!(line_num.position, MarginPosition::Left);
703
704        let breakpoint = MarginAnnotation::breakpoint(10);
705        assert_eq!(breakpoint.line, 10);
706        assert_eq!(breakpoint.position, MarginPosition::Left);
707    }
708
709    #[test]
710    fn test_margin_manager_add_remove() {
711        let mut manager = MarginManager::new();
712
713        // Add annotation
714        let annotation = MarginAnnotation::line_number(5);
715        manager.add_annotation(annotation);
716
717        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
718
719        // Add annotation with ID
720        let annotation = MarginAnnotation::with_id(
721            10,
722            MarginPosition::Left,
723            MarginContent::text("test"),
724            "test-id".to_string(),
725        );
726        manager.add_annotation(annotation);
727
728        assert_eq!(manager.annotation_count(MarginPosition::Left), 2);
729
730        // Remove by ID
731        manager.remove_by_id("test-id");
732        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
733
734        // Clear all
735        manager.clear_all();
736        assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
737    }
738
739    #[test]
740    fn test_margin_manager_render_line() {
741        let mut manager = MarginManager::new();
742
743        // Without annotations, should render line number when show_line_numbers=true
744        let content = manager.render_line(5, MarginPosition::Left, 100, true);
745        let (rendered, _) = content.render(4);
746        assert!(rendered.contains("6")); // Line 5 is displayed as "6" (1-indexed)
747
748        // Add a breakpoint annotation
749        manager.add_annotation(MarginAnnotation::breakpoint(5));
750
751        // Should now render stacked content (line number + breakpoint)
752        let content = manager.render_line(5, MarginPosition::Left, 100, true);
753        assert!(matches!(content, MarginContent::Stacked(_)));
754    }
755
756    #[test]
757    fn test_margin_manager_update_width() {
758        let mut manager = MarginManager::new();
759
760        // Small buffer
761        manager.update_width_for_buffer(99, true);
762        assert_eq!(manager.left_config.width, 4); // Minimum 4
763
764        // Medium buffer (4 digits)
765        manager.update_width_for_buffer(1000, true);
766        assert_eq!(manager.left_config.width, 4);
767
768        // Large buffer (5 digits)
769        manager.update_width_for_buffer(10000, true);
770        assert_eq!(manager.left_config.width, 5);
771
772        // Very large buffer (7 digits)
773        manager.update_width_for_buffer(1000000, true);
774        assert_eq!(manager.left_config.width, 7);
775    }
776
777    #[test]
778    fn test_margin_manager_without_line_numbers() {
779        let manager = MarginManager::without_line_numbers();
780        assert!(!manager.left_config.enabled);
781
782        let content = manager.render_line(5, MarginPosition::Left, 100, false);
783        assert!(content.is_empty());
784    }
785
786    #[test]
787    fn test_margin_position_left_right() {
788        let mut manager = MarginManager::new();
789
790        manager.add_annotation(MarginAnnotation::new(
791            1,
792            MarginPosition::Left,
793            MarginContent::text("left"),
794        ));
795
796        manager.add_annotation(MarginAnnotation::new(
797            1,
798            MarginPosition::Right,
799            MarginContent::text("right"),
800        ));
801
802        assert_eq!(manager.annotation_count(MarginPosition::Left), 1);
803        assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
804
805        manager.clear_position(MarginPosition::Left);
806        assert_eq!(manager.annotation_count(MarginPosition::Left), 0);
807        assert_eq!(manager.annotation_count(MarginPosition::Right), 1);
808    }
809
810    // Helper: simulates a buffer where each line is 10 bytes (9 chars + newline)
811    // Line 0 = bytes 0-9, Line 1 = bytes 10-19, etc.
812    fn byte_to_line(byte_offset: usize) -> usize {
813        byte_offset / 10
814    }
815
816    // Helper: get byte offset for start of a line
817    fn line_to_byte(line: usize) -> usize {
818        line * 10
819    }
820
821    #[test]
822    fn test_line_indicator_basic() {
823        let mut manager = MarginManager::new();
824
825        // Add a line indicator at byte offset 50 (line 5 in our simulated buffer)
826        let indicator = LineIndicator::new("│", Color::Green, 10);
827        manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), indicator);
828
829        // Check it can be retrieved on line 5
830        let retrieved = manager.get_line_indicator(5, byte_to_line);
831        assert!(retrieved.is_some());
832        let retrieved = retrieved.unwrap();
833        assert_eq!(retrieved.symbol, "│");
834        assert_eq!(retrieved.color, Color::Green);
835        assert_eq!(retrieved.priority, 10);
836
837        // Non-existent line should return None
838        assert!(manager.get_line_indicator(10, byte_to_line).is_none());
839    }
840
841    #[test]
842    fn test_line_indicator_multiple_namespaces() {
843        let mut manager = MarginManager::new();
844
845        // Add indicators from different namespaces at the same byte position (line 5)
846        let git_indicator = LineIndicator::new("│", Color::Green, 10);
847        let breakpoint_indicator = LineIndicator::new("●", Color::Red, 20);
848
849        manager.set_line_indicator(line_to_byte(5), "git-gutter".to_string(), git_indicator);
850        manager.set_line_indicator(
851            line_to_byte(5),
852            "breakpoints".to_string(),
853            breakpoint_indicator,
854        );
855
856        // Should return the highest priority indicator
857        let retrieved = manager.get_line_indicator(5, byte_to_line);
858        assert!(retrieved.is_some());
859        let retrieved = retrieved.unwrap();
860        assert_eq!(retrieved.symbol, "●"); // Breakpoint has higher priority
861        assert_eq!(retrieved.priority, 20);
862    }
863
864    #[test]
865    fn test_line_indicator_clear_namespace() {
866        let mut manager = MarginManager::new();
867
868        // Add indicators on multiple lines
869        manager.set_line_indicator(
870            line_to_byte(1),
871            "git-gutter".to_string(),
872            LineIndicator::new("│", Color::Green, 10),
873        );
874        manager.set_line_indicator(
875            line_to_byte(2),
876            "git-gutter".to_string(),
877            LineIndicator::new("│", Color::Yellow, 10),
878        );
879        manager.set_line_indicator(
880            line_to_byte(3),
881            "breakpoints".to_string(),
882            LineIndicator::new("●", Color::Red, 20),
883        );
884
885        // Clear git-gutter namespace
886        manager.clear_line_indicators_for_namespace("git-gutter");
887
888        // Git gutter indicators should be gone
889        assert!(manager.get_line_indicator(1, byte_to_line).is_none());
890        assert!(manager.get_line_indicator(2, byte_to_line).is_none());
891
892        // Breakpoint should still be there
893        let breakpoint = manager.get_line_indicator(3, byte_to_line);
894        assert!(breakpoint.is_some());
895        assert_eq!(breakpoint.unwrap().symbol, "●");
896    }
897
898    #[test]
899    fn test_line_indicator_remove_specific() {
900        let mut manager = MarginManager::new();
901
902        // Add two indicators at the same byte position (line 5)
903        let git_marker = manager.set_line_indicator(
904            line_to_byte(5),
905            "git-gutter".to_string(),
906            LineIndicator::new("│", Color::Green, 10),
907        );
908        let bp_marker = manager.set_line_indicator(
909            line_to_byte(5),
910            "breakpoints".to_string(),
911            LineIndicator::new("●", Color::Red, 20),
912        );
913
914        // Remove just the git-gutter indicator
915        manager.remove_line_indicator(git_marker, "git-gutter");
916
917        // Should still have the breakpoint indicator on line 5
918        let retrieved = manager.get_line_indicator(5, byte_to_line);
919        assert!(retrieved.is_some());
920        assert_eq!(retrieved.unwrap().symbol, "●");
921
922        // Remove the breakpoint indicator too
923        manager.remove_line_indicator(bp_marker, "breakpoints");
924
925        // Now no indicators on line 5
926        assert!(manager.get_line_indicator(5, byte_to_line).is_none());
927    }
928
929    #[test]
930    fn test_line_indicator_shifts_on_insert() {
931        let mut manager = MarginManager::new();
932
933        // Add indicator on line 5 (byte 50)
934        manager.set_line_indicator(
935            line_to_byte(5),
936            "git-gutter".to_string(),
937            LineIndicator::new("│", Color::Green, 10),
938        );
939
940        // Verify it's on line 5
941        assert!(manager.get_line_indicator(5, byte_to_line).is_some());
942        assert!(manager.get_line_indicator(6, byte_to_line).is_none());
943
944        // Insert 10 bytes (one line) at the beginning
945        manager.adjust_for_insert(0, 10);
946
947        // Now indicator should be on line 6 (shifted down by 1)
948        assert!(manager.get_line_indicator(5, byte_to_line).is_none());
949        assert!(manager.get_line_indicator(6, byte_to_line).is_some());
950    }
951
952    #[test]
953    fn test_line_indicator_shifts_on_delete() {
954        let mut manager = MarginManager::new();
955
956        // Add indicator on line 5 (byte 50)
957        manager.set_line_indicator(
958            line_to_byte(5),
959            "git-gutter".to_string(),
960            LineIndicator::new("│", Color::Green, 10),
961        );
962
963        // Verify it's on line 5
964        assert!(manager.get_line_indicator(5, byte_to_line).is_some());
965
966        // Delete first 20 bytes (2 lines)
967        manager.adjust_for_delete(0, 20);
968
969        // Now indicator should be on line 3 (shifted up by 2)
970        assert!(manager.get_line_indicator(5, byte_to_line).is_none());
971        assert!(manager.get_line_indicator(3, byte_to_line).is_some());
972    }
973
974    #[test]
975    fn test_multiple_indicators_shift_together() {
976        let mut manager = MarginManager::new();
977
978        // Add indicators on lines 3, 5, and 7
979        manager.set_line_indicator(
980            line_to_byte(3),
981            "git-gutter".to_string(),
982            LineIndicator::new("│", Color::Green, 10),
983        );
984        manager.set_line_indicator(
985            line_to_byte(5),
986            "git-gutter".to_string(),
987            LineIndicator::new("│", Color::Yellow, 10),
988        );
989        manager.set_line_indicator(
990            line_to_byte(7),
991            "git-gutter".to_string(),
992            LineIndicator::new("│", Color::Red, 10),
993        );
994
995        // Insert 2 lines (20 bytes) at byte 25 (middle of line 2)
996        // This should shift lines 3, 5, 7 -> lines 5, 7, 9
997        manager.adjust_for_insert(25, 20);
998
999        // Old positions should be empty
1000        assert!(manager.get_line_indicator(3, byte_to_line).is_none());
1001
1002        // New positions should have indicators
1003        assert!(manager.get_line_indicator(5, byte_to_line).is_some());
1004        assert!(manager.get_line_indicator(7, byte_to_line).is_some());
1005        assert!(manager.get_line_indicator(9, byte_to_line).is_some());
1006    }
1007}