Skip to main content

fresh/view/
bracket_highlight_overlay.rs

1//! Bracket matching highlight using the overlay system
2//!
3//! This module manages bracket pair highlighting through overlays.
4//! When the cursor is on a bracket, the matching bracket is highlighted.
5//! Optional rainbow colors can be applied based on nesting depth.
6
7use crate::model::buffer::Buffer;
8use crate::model::marker::MarkerList;
9use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, OverlayNamespace};
10use crate::view::theme::Theme;
11use ratatui::style::Color;
12
13/// Default rainbow bracket colors (cycle through these based on nesting depth)
14pub const DEFAULT_BRACKET_COLORS: [Color; 6] = [
15    Color::Rgb(255, 215, 0),   // Gold
16    Color::Rgb(218, 112, 214), // Orchid
17    Color::Rgb(50, 205, 50),   // Lime Green
18    Color::Rgb(30, 144, 255),  // Dodger Blue
19    Color::Rgb(255, 127, 80),  // Coral
20    Color::Rgb(147, 112, 219), // Medium Purple
21];
22
23/// Namespace for bracket highlight overlays
24pub fn bracket_highlight_namespace() -> OverlayNamespace {
25    OverlayNamespace::from_string("bracket-highlight".to_string())
26}
27
28/// Namespace for rainbow bracket colorization overlays
29pub fn bracket_colorization_namespace() -> OverlayNamespace {
30    OverlayNamespace::from_string("bracket-colorization".to_string())
31}
32
33/// Bracket types we match
34const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
35
36/// Maximum number of bytes to scan for bracket matching/nesting depth.
37/// Prevents O(n) scans on huge files from hanging the editor.
38pub(crate) const MAX_BRACKET_SEARCH_BYTES: usize = 1_000_000;
39
40/// Chunk size for bulk reads during bracket scanning.
41const BRACKET_SCAN_CHUNK: usize = 16 * 1024;
42
43/// Check if a character is an opening bracket
44fn is_opening_bracket(ch: char) -> bool {
45    BRACKET_PAIRS.iter().any(|(open, _)| *open == ch)
46}
47
48/// Check if a character is a closing bracket
49fn is_closing_bracket(ch: char) -> bool {
50    BRACKET_PAIRS.iter().any(|(_, close)| *close == ch)
51}
52
53/// Get the opening bracket for a closing bracket
54fn opening_for_closing(ch: char) -> Option<char> {
55    BRACKET_PAIRS
56        .iter()
57        .find_map(|(open, close)| if *close == ch { Some(*open) } else { None })
58}
59
60/// Get the matching bracket pair for a character
61fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
62    for &(open, close) in BRACKET_PAIRS {
63        if ch == open {
64            return Some((open, close, true)); // forward search
65        }
66        if ch == close {
67            return Some((open, close, false)); // backward search
68        }
69    }
70    None
71}
72
73/// Manager for bracket highlight overlays
74pub struct BracketHighlightOverlay {
75    /// Whether bracket highlighting is enabled
76    pub enabled: bool,
77    /// Whether to use rainbow colors based on nesting depth
78    pub rainbow_enabled: bool,
79    /// Colors to use for rainbow brackets (cycles through)
80    pub rainbow_colors: [Color; 6],
81    /// Default bracket match highlight color (when rainbow is disabled)
82    pub match_color: Color,
83    /// Last cursor position where we computed brackets
84    last_cursor_pos: Option<usize>,
85}
86
87impl BracketHighlightOverlay {
88    /// Create a new bracket highlight overlay manager
89    pub fn new() -> Self {
90        Self {
91            enabled: true,
92            rainbow_enabled: true,
93            rainbow_colors: DEFAULT_BRACKET_COLORS,
94            match_color: Color::Rgb(255, 215, 0), // Gold
95            last_cursor_pos: None,
96        }
97    }
98
99    /// Update bracket highlights based on cursor position
100    ///
101    /// Returns true if overlays were updated
102    pub fn update(
103        &mut self,
104        buffer: &Buffer,
105        overlays: &mut OverlayManager,
106        marker_list: &mut MarkerList,
107        theme: &Theme,
108        cursor_position: usize,
109        viewport_start: usize,
110        viewport_end: usize,
111    ) -> bool {
112        if !self.enabled && !self.rainbow_enabled {
113            return false;
114        }
115
116        let new_match_color = theme.bracket_match_fg;
117        let new_rainbow_colors = [
118            theme.bracket_rainbow_1,
119            theme.bracket_rainbow_2,
120            theme.bracket_rainbow_3,
121            theme.bracket_rainbow_4,
122            theme.bracket_rainbow_5,
123            theme.bracket_rainbow_6,
124        ];
125        let colors_changed =
126            self.match_color != new_match_color || self.rainbow_colors != new_rainbow_colors;
127        if colors_changed {
128            self.match_color = new_match_color;
129            self.rainbow_colors = new_rainbow_colors;
130        }
131
132        let mut updated = false;
133
134        // Update full rainbow bracket colorization
135        if self.rainbow_enabled {
136            updated |= self.update_colorization(
137                buffer,
138                overlays,
139                marker_list,
140                viewport_start,
141                viewport_end,
142            );
143        } else {
144            updated |= self.clear_colorization(overlays, marker_list);
145        }
146
147        // Check if cursor position changed
148        if !self.enabled {
149            return updated;
150        }
151
152        if self.last_cursor_pos == Some(cursor_position) && !colors_changed {
153            return updated;
154        }
155        self.last_cursor_pos = Some(cursor_position);
156        updated = true;
157
158        // Clear existing bracket overlays
159        let ns = bracket_highlight_namespace();
160        overlays.clear_namespace(&ns, marker_list);
161
162        // Check if cursor is on a bracket
163        let buf_len = buffer.len();
164        if cursor_position >= buf_len {
165            return true;
166        }
167
168        let bytes = buffer.slice_bytes(cursor_position..cursor_position + 1);
169        if bytes.is_empty() {
170            return true;
171        }
172
173        let ch = bytes[0] as char;
174
175        // Get bracket pair info
176        let (opening, closing, forward) = match get_bracket_pair(ch) {
177            Some(pair) => pair,
178            None => return true, // Not on a bracket
179        };
180
181        // Calculate nesting depth at cursor position for rainbow colors
182        let depth = if self.rainbow_enabled {
183            self.calculate_nesting_depth(buffer, cursor_position, forward)
184        } else {
185            0
186        };
187
188        // Find matching bracket
189        let matching_pos =
190            self.find_matching_bracket(buffer, cursor_position, opening, closing, forward);
191
192        // Determine color based on depth
193        let color = if self.rainbow_enabled {
194            self.rainbow_colors[depth % self.rainbow_colors.len()]
195        } else {
196            self.match_color
197        };
198
199        // Create overlay for the bracket at cursor
200        let cursor_face = OverlayFace::Foreground { color };
201        let cursor_overlay = Overlay::with_namespace(
202            marker_list,
203            cursor_position..cursor_position + 1,
204            cursor_face,
205            ns.clone(),
206        )
207        .with_priority_value(10);
208        overlays.add(cursor_overlay);
209
210        // Create overlay for the matching bracket if found
211        if let Some(match_pos) = matching_pos {
212            let match_face = OverlayFace::Foreground { color };
213            let match_overlay = Overlay::with_namespace(
214                marker_list,
215                match_pos..match_pos + 1,
216                match_face,
217                ns.clone(),
218            )
219            .with_priority_value(10);
220            overlays.add(match_overlay);
221        }
222
223        updated
224    }
225
226    /// Calculate the nesting depth of a bracket at a position
227    fn calculate_nesting_depth(&self, buffer: &Buffer, position: usize, is_opening: bool) -> usize {
228        // Track nesting depth across all bracket types so rainbow colors follow
229        // overall nesting level. Bound the scan to avoid O(n) work on huge files.
230        let scan_start = position.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
231        let mut stack: Vec<char> = Vec::new();
232        let mut pos = scan_start;
233
234        while pos < position {
235            let chunk_end = (pos + BRACKET_SCAN_CHUNK).min(position);
236            let chunk = buffer.slice_bytes(pos..chunk_end);
237            for &b in &chunk {
238                let c = b as char;
239                if is_opening_bracket(c) {
240                    stack.push(c);
241                } else if is_closing_bracket(c) {
242                    if let Some(expected_open) = opening_for_closing(c) {
243                        if stack.last() == Some(&expected_open) {
244                            stack.pop();
245                        }
246                    }
247                }
248            }
249            pos = chunk_end;
250        }
251
252        // For opening brackets, depth is the current stack size.
253        // For closing brackets, depth is the stack size minus one (matching opening).
254        if is_opening {
255            stack.len()
256        } else {
257            stack.len().saturating_sub(1)
258        }
259    }
260
261    /// Find the matching bracket (bounded to MAX_BRACKET_SEARCH_BYTES)
262    fn find_matching_bracket(
263        &self,
264        buffer: &Buffer,
265        position: usize,
266        opening: char,
267        closing: char,
268        forward: bool,
269    ) -> Option<usize> {
270        let buffer_len = buffer.len();
271        let open = opening as u8;
272        let close = closing as u8;
273        let mut depth: i32 = 1;
274
275        if forward {
276            let search_limit = (position + 1 + MAX_BRACKET_SEARCH_BYTES).min(buffer_len);
277            let mut pos = position + 1;
278            while pos < search_limit {
279                let chunk_end = (pos + BRACKET_SCAN_CHUNK).min(search_limit);
280                let chunk = buffer.slice_bytes(pos..chunk_end);
281                for (i, &b) in chunk.iter().enumerate() {
282                    if b == open {
283                        depth += 1;
284                    } else if b == close {
285                        depth -= 1;
286                        if depth == 0 {
287                            return Some(pos + i);
288                        }
289                    }
290                }
291                pos = chunk_end;
292            }
293        } else {
294            let search_limit = position.saturating_sub(MAX_BRACKET_SEARCH_BYTES);
295            let mut pos = position;
296            while pos > search_limit {
297                let chunk_start = pos.saturating_sub(BRACKET_SCAN_CHUNK).max(search_limit);
298                let chunk = buffer.slice_bytes(chunk_start..pos);
299                for (i, &b) in chunk.iter().enumerate().rev() {
300                    if b == close {
301                        depth += 1;
302                    } else if b == open {
303                        depth -= 1;
304                        if depth == 0 {
305                            return Some(chunk_start + i);
306                        }
307                    }
308                }
309                pos = chunk_start;
310            }
311        }
312
313        None
314    }
315
316    /// Force clear all highlights (e.g., when switching buffers)
317    pub fn clear(&mut self, overlays: &mut OverlayManager, marker_list: &mut MarkerList) {
318        let highlight_ns = bracket_highlight_namespace();
319        overlays.clear_namespace(&highlight_ns, marker_list);
320        let color_ns = bracket_colorization_namespace();
321        overlays.clear_namespace(&color_ns, marker_list);
322        self.last_cursor_pos = None;
323    }
324
325    /// Force recalculation on next update
326    pub fn invalidate(&mut self) {
327        self.last_cursor_pos = None;
328    }
329
330    fn clear_colorization(
331        &mut self,
332        overlays: &mut OverlayManager,
333        marker_list: &mut MarkerList,
334    ) -> bool {
335        let ns = bracket_colorization_namespace();
336        overlays.clear_namespace(&ns, marker_list);
337        true
338    }
339
340    fn update_colorization(
341        &mut self,
342        buffer: &Buffer,
343        overlays: &mut OverlayManager,
344        marker_list: &mut MarkerList,
345        viewport_start: usize,
346        viewport_end: usize,
347    ) -> bool {
348        if viewport_start >= viewport_end || buffer.len() == 0 {
349            return self.clear_colorization(overlays, marker_list);
350        }
351
352        let viewport_size = viewport_end.saturating_sub(viewport_start);
353        let scan_start = viewport_start.saturating_sub(viewport_size);
354        let scan_end = viewport_end.min(buffer.len());
355        if scan_start >= scan_end {
356            return self.clear_colorization(overlays, marker_list);
357        }
358
359        let bytes = buffer.slice_bytes(scan_start..scan_end);
360        if bytes.is_empty() {
361            return self.clear_colorization(overlays, marker_list);
362        }
363
364        let ns = bracket_colorization_namespace();
365        let mut stack: Vec<char> = Vec::new();
366        let mut new_overlays = Vec::new();
367
368        for (idx, byte) in bytes.iter().enumerate() {
369            let pos = scan_start + idx;
370            let c = *byte as char;
371
372            if is_opening_bracket(c) {
373                let depth = stack.len();
374                stack.push(c);
375                if pos >= viewport_start {
376                    let color = self.rainbow_colors[depth % self.rainbow_colors.len()];
377                    let face = OverlayFace::Foreground { color };
378                    let overlay =
379                        Overlay::with_namespace(marker_list, pos..pos + 1, face, ns.clone())
380                            .with_priority_value(6);
381                    new_overlays.push(overlay);
382                }
383                continue;
384            }
385
386            if is_closing_bracket(c) {
387                let depth = stack.len().saturating_sub(1);
388                if let Some(expected_open) = opening_for_closing(c) {
389                    if stack.last() == Some(&expected_open) {
390                        stack.pop();
391                    }
392                }
393                if pos >= viewport_start {
394                    let color = self.rainbow_colors[depth % self.rainbow_colors.len()];
395                    let face = OverlayFace::Foreground { color };
396                    let overlay =
397                        Overlay::with_namespace(marker_list, pos..pos + 1, face, ns.clone())
398                            .with_priority_value(6);
399                    new_overlays.push(overlay);
400                }
401            }
402        }
403
404        overlays.replace_range_in_namespace(&ns, &(0..buffer.len()), new_overlays, marker_list);
405        true
406    }
407}
408
409impl Default for BracketHighlightOverlay {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::model::buffer::Buffer;
419
420    #[test]
421    fn test_bracket_pair_detection() {
422        assert!(is_opening_bracket('('));
423        assert!(is_opening_bracket('['));
424        assert!(is_opening_bracket('{'));
425        assert!(!is_opening_bracket(')'));
426        assert!(!is_opening_bracket('a'));
427
428        assert!(is_closing_bracket(')'));
429        assert!(is_closing_bracket(']'));
430        assert!(is_closing_bracket('}'));
431        assert!(!is_closing_bracket('('));
432        assert!(!is_closing_bracket('a'));
433    }
434
435    #[test]
436    fn test_get_bracket_pair() {
437        assert_eq!(get_bracket_pair('('), Some(('(', ')', true)));
438        assert_eq!(get_bracket_pair(')'), Some(('(', ')', false)));
439        assert_eq!(get_bracket_pair('['), Some(('[', ']', true)));
440        assert_eq!(get_bracket_pair(']'), Some(('[', ']', false)));
441        assert_eq!(get_bracket_pair('a'), None);
442    }
443
444    #[test]
445    fn test_find_matching_bracket_forward() {
446        let buffer = Buffer::from_str_test("(hello)");
447        let overlay = BracketHighlightOverlay::new();
448
449        let result = overlay.find_matching_bracket(&buffer, 0, '(', ')', true);
450        assert_eq!(result, Some(6));
451    }
452
453    #[test]
454    fn test_find_matching_bracket_backward() {
455        let buffer = Buffer::from_str_test("(hello)");
456        let overlay = BracketHighlightOverlay::new();
457
458        let result = overlay.find_matching_bracket(&buffer, 6, '(', ')', false);
459        assert_eq!(result, Some(0));
460    }
461
462    #[test]
463    fn test_find_matching_bracket_nested() {
464        let buffer = Buffer::from_str_test("((inner))");
465        let overlay = BracketHighlightOverlay::new();
466
467        // Outer opening bracket should match outer closing
468        let result = overlay.find_matching_bracket(&buffer, 0, '(', ')', true);
469        assert_eq!(result, Some(8));
470
471        // Inner opening bracket should match inner closing
472        let result = overlay.find_matching_bracket(&buffer, 1, '(', ')', true);
473        assert_eq!(result, Some(7));
474    }
475
476    #[test]
477    fn test_nesting_depth() {
478        let buffer = Buffer::from_str_test("((()))");
479        let overlay = BracketHighlightOverlay::new();
480
481        // Outermost opening bracket: depth 0
482        assert_eq!(overlay.calculate_nesting_depth(&buffer, 0, true), 0);
483
484        // Second level opening bracket: depth 1
485        assert_eq!(overlay.calculate_nesting_depth(&buffer, 1, true), 1);
486
487        // Third level opening bracket: depth 2
488        assert_eq!(overlay.calculate_nesting_depth(&buffer, 2, true), 2);
489    }
490
491    #[test]
492    fn test_nesting_depth_mixed_types() {
493        let buffer = Buffer::from_str_test("({[]})");
494        let overlay = BracketHighlightOverlay::new();
495
496        assert_eq!(overlay.calculate_nesting_depth(&buffer, 0, true), 0);
497        assert_eq!(overlay.calculate_nesting_depth(&buffer, 1, true), 1);
498        assert_eq!(overlay.calculate_nesting_depth(&buffer, 2, true), 2);
499        assert_eq!(overlay.calculate_nesting_depth(&buffer, 3, false), 2);
500        assert_eq!(overlay.calculate_nesting_depth(&buffer, 4, false), 1);
501        assert_eq!(overlay.calculate_nesting_depth(&buffer, 5, false), 0);
502    }
503}