Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion crates/fresh-editor/plugins/theme_editor.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,20 @@
"field.line_number_bg_desc": "Line number gutter background",
"field.ruler_bg": "Ruler Background",
"field.ruler_bg_desc": "Vertical ruler line color",
"field.bracket_match_fg": "Bracket Match Foreground",
"field.bracket_match_fg_desc": "Matching bracket highlight color",
"field.bracket_rainbow_1": "Bracket Rainbow 1",
"field.bracket_rainbow_1_desc": "Rainbow bracket color (nesting level 1)",
"field.bracket_rainbow_2": "Bracket Rainbow 2",
"field.bracket_rainbow_2_desc": "Rainbow bracket color (nesting level 2)",
"field.bracket_rainbow_3": "Bracket Rainbow 3",
"field.bracket_rainbow_3_desc": "Rainbow bracket color (nesting level 3)",
"field.bracket_rainbow_4": "Bracket Rainbow 4",
"field.bracket_rainbow_4_desc": "Rainbow bracket color (nesting level 4)",
"field.bracket_rainbow_5": "Bracket Rainbow 5",
"field.bracket_rainbow_5_desc": "Rainbow bracket color (nesting level 5)",
"field.bracket_rainbow_6": "Bracket Rainbow 6",
"field.bracket_rainbow_6_desc": "Rainbow bracket color (nesting level 6)",
"field.diff_add_bg": "Diff Added Background",
"field.diff_add_bg_desc": "Diff added line background",
"field.diff_remove_bg": "Diff Removed Background",
Expand Down Expand Up @@ -4143,4 +4157,4 @@
"field.popup_selection_fg": "Primo piano selezione popup",
"field.popup_selection_fg_desc": "Colore del testo dell elemento selezionato nel popup"
}
}
}
220 changes: 176 additions & 44 deletions crates/fresh-editor/src/view/bracket_highlight_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use crate::model::buffer::Buffer;
use crate::model::marker::MarkerList;
use crate::view::overlay::{Overlay, OverlayFace, OverlayManager, OverlayNamespace};
use crate::view::theme::Theme;
use ratatui::style::Color;

/// Default rainbow bracket colors (cycle through these based on nesting depth)
Expand All @@ -24,21 +25,31 @@ pub fn bracket_highlight_namespace() -> OverlayNamespace {
OverlayNamespace::from_string("bracket-highlight".to_string())
}

/// Namespace for rainbow bracket colorization overlays
pub fn bracket_colorization_namespace() -> OverlayNamespace {
OverlayNamespace::from_string("bracket-colorization".to_string())
}

/// Bracket types we match
const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];

/// Check if a character is an opening bracket
#[cfg(test)]
fn is_opening_bracket(ch: char) -> bool {
BRACKET_PAIRS.iter().any(|(open, _)| *open == ch)
}

/// Check if a character is a closing bracket
#[cfg(test)]
fn is_closing_bracket(ch: char) -> bool {
BRACKET_PAIRS.iter().any(|(_, close)| *close == ch)
}

/// Get the opening bracket for a closing bracket
fn opening_for_closing(ch: char) -> Option<char> {
BRACKET_PAIRS
.iter()
.find_map(|(open, close)| if *close == ch { Some(*open) } else { None })
}

/// Get the matching bracket pair for a character
fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
for &(open, close) in BRACKET_PAIRS {
Expand All @@ -59,7 +70,7 @@ pub struct BracketHighlightOverlay {
/// Whether to use rainbow colors based on nesting depth
pub rainbow_enabled: bool,
/// Colors to use for rainbow brackets (cycles through)
pub rainbow_colors: Vec<Color>,
pub rainbow_colors: [Color; 6],
/// Default bracket match highlight color (when rainbow is disabled)
pub match_color: Color,
/// Last cursor position where we computed brackets
Expand All @@ -72,7 +83,7 @@ impl BracketHighlightOverlay {
Self {
enabled: true,
rainbow_enabled: true,
rainbow_colors: DEFAULT_BRACKET_COLORS.to_vec(),
rainbow_colors: DEFAULT_BRACKET_COLORS,
match_color: Color::Rgb(255, 215, 0), // Gold
last_cursor_pos: None,
}
Expand All @@ -86,17 +97,56 @@ impl BracketHighlightOverlay {
buffer: &Buffer,
overlays: &mut OverlayManager,
marker_list: &mut MarkerList,
theme: &Theme,
cursor_position: usize,
viewport_start: usize,
viewport_end: usize,
) -> bool {
if !self.enabled {
if !self.enabled && !self.rainbow_enabled {
return false;
}

let new_match_color = theme.bracket_match_fg;
let new_rainbow_colors = [
theme.bracket_rainbow_1,
theme.bracket_rainbow_2,
theme.bracket_rainbow_3,
theme.bracket_rainbow_4,
theme.bracket_rainbow_5,
theme.bracket_rainbow_6,
];
let colors_changed =
self.match_color != new_match_color || self.rainbow_colors != new_rainbow_colors;
if colors_changed {
self.match_color = new_match_color;
self.rainbow_colors = new_rainbow_colors;
}

let mut updated = false;

// Update full rainbow bracket colorization
if self.rainbow_enabled {
updated |= self.update_colorization(
buffer,
overlays,
marker_list,
viewport_start,
viewport_end,
);
} else {
updated |= self.clear_colorization(overlays, marker_list);
}

// Check if cursor position changed
if self.last_cursor_pos == Some(cursor_position) {
return false;
if !self.enabled {
return updated;
}

if self.last_cursor_pos == Some(cursor_position) && !colors_changed {
return updated;
}
self.last_cursor_pos = Some(cursor_position);
updated = true;

// Clear existing bracket overlays
let ns = bracket_highlight_namespace();
Expand All @@ -123,7 +173,7 @@ impl BracketHighlightOverlay {

// Calculate nesting depth at cursor position for rainbow colors
let depth = if self.rainbow_enabled {
self.calculate_nesting_depth(buffer, cursor_position, opening, closing, forward)
self.calculate_nesting_depth(buffer, cursor_position, forward)
} else {
0
};
Expand All @@ -133,7 +183,7 @@ impl BracketHighlightOverlay {
self.find_matching_bracket(buffer, cursor_position, opening, closing, forward);

// Determine color based on depth
let color = if self.rainbow_enabled && !self.rainbow_colors.is_empty() {
let color = if self.rainbow_enabled {
self.rainbow_colors[depth % self.rainbow_colors.len()]
} else {
self.match_color
Expand Down Expand Up @@ -163,41 +213,39 @@ impl BracketHighlightOverlay {
overlays.add(match_overlay);
}

true
updated
}

/// Calculate the nesting depth of a bracket at a position
fn calculate_nesting_depth(
&self,
buffer: &Buffer,
position: usize,
opening: char,
closing: char,
is_opening: bool,
) -> usize {
// Count how many unclosed opening brackets of the same type come before this position
let mut depth: usize = 0;
fn calculate_nesting_depth(&self, buffer: &Buffer, position: usize, is_opening: bool) -> usize {
// Track nesting depth across all bracket types (not just the current pair)
// so rainbow colors follow overall nesting level.
let mut stack: Vec<char> = Vec::new();
let mut pos = 0;

while pos < position {
let bytes = buffer.slice_bytes(pos..pos + 1);
if !bytes.is_empty() {
let c = bytes[0] as char;
if c == opening {
depth += 1;
} else if c == closing {
depth = depth.saturating_sub(1);
if let Some(&byte) = bytes.first() {
let c = byte as char;
if is_opening_bracket(c) {
Comment on lines 226 to +230
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculate_nesting_depth calls buffer.slice_bytes(pos..pos + 1) for every byte before position. slice_bytes allocates a new Vec<u8> each time, so this becomes extremely expensive (O(n) allocations) when the cursor is far into the file. Consider slicing once (e.g., buffer.slice_bytes(0..position)) and iterating that byte slice, or otherwise iterating without per-byte allocations.

Copilot uses AI. Check for mistakes.
stack.push(c);
} else if is_closing_bracket(c) {
if let Some(expected_open) = opening_for_closing(c) {
if stack.last() == Some(&expected_open) {
stack.pop();
}
}
}
}
pos += 1;
}

// For closing brackets, the depth is the current count
// For opening brackets, the depth is the current count (depth before this bracket)
// For opening brackets, depth is the current stack size.
// For closing brackets, depth is the stack size minus one (matching opening).
if is_opening {
depth
stack.len()
} else {
depth.saturating_sub(1)
stack.len().saturating_sub(1)
}
}

Expand Down Expand Up @@ -257,15 +305,95 @@ impl BracketHighlightOverlay {

/// Force clear all highlights (e.g., when switching buffers)
pub fn clear(&mut self, overlays: &mut OverlayManager, marker_list: &mut MarkerList) {
let ns = bracket_highlight_namespace();
overlays.clear_namespace(&ns, marker_list);
let highlight_ns = bracket_highlight_namespace();
overlays.clear_namespace(&highlight_ns, marker_list);
let color_ns = bracket_colorization_namespace();
overlays.clear_namespace(&color_ns, marker_list);
self.last_cursor_pos = None;
}

/// Force recalculation on next update
pub fn invalidate(&mut self) {
self.last_cursor_pos = None;
}

fn clear_colorization(
&mut self,
overlays: &mut OverlayManager,
marker_list: &mut MarkerList,
) -> bool {
let ns = bracket_colorization_namespace();
overlays.clear_namespace(&ns, marker_list);
true
}

fn update_colorization(
&mut self,
buffer: &Buffer,
overlays: &mut OverlayManager,
marker_list: &mut MarkerList,
viewport_start: usize,
viewport_end: usize,
) -> bool {
if viewport_start >= viewport_end || buffer.len() == 0 {
return self.clear_colorization(overlays, marker_list);
}

let viewport_size = viewport_end.saturating_sub(viewport_start);
let scan_start = viewport_start.saturating_sub(viewport_size);
let scan_end = viewport_end.min(buffer.len());
if scan_start >= scan_end {
return self.clear_colorization(overlays, marker_list);
}

let bytes = buffer.slice_bytes(scan_start..scan_end);
if bytes.is_empty() {
return self.clear_colorization(overlays, marker_list);
}

let ns = bracket_colorization_namespace();
let mut stack: Vec<char> = Vec::new();
let mut new_overlays = Vec::new();

for (idx, byte) in bytes.iter().enumerate() {
let pos = scan_start + idx;
let c = *byte as char;

if is_opening_bracket(c) {
let depth = stack.len();
stack.push(c);
if pos >= viewport_start {
let color = self.rainbow_colors[depth % self.rainbow_colors.len()];
let face = OverlayFace::Foreground { color };
let overlay =
Overlay::with_namespace(marker_list, pos..pos + 1, face, ns.clone())
.with_priority_value(6);
new_overlays.push(overlay);
}
continue;
}

if is_closing_bracket(c) {
let depth = stack.len().saturating_sub(1);
if let Some(expected_open) = opening_for_closing(c) {
if stack.last() == Some(&expected_open) {
stack.pop();
}
}
if pos >= viewport_start {
let color = self.rainbow_colors[depth % self.rainbow_colors.len()];
let face = OverlayFace::Foreground { color };
let overlay =
Overlay::with_namespace(marker_list, pos..pos + 1, face, ns.clone())
.with_priority_value(6);
new_overlays.push(overlay);
}
}
}

overlays.replace_range_in_namespace(&ns, &(0..buffer.len()), new_overlays, marker_list);
true
}
}

impl Default for BracketHighlightOverlay {
Expand Down Expand Up @@ -341,21 +469,25 @@ mod tests {
let overlay = BracketHighlightOverlay::new();

// Outermost opening bracket: depth 0
assert_eq!(
overlay.calculate_nesting_depth(&buffer, 0, '(', ')', true),
0
);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 0, true), 0);

// Second level opening bracket: depth 1
assert_eq!(
overlay.calculate_nesting_depth(&buffer, 1, '(', ')', true),
1
);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 1, true), 1);

// Third level opening bracket: depth 2
assert_eq!(
overlay.calculate_nesting_depth(&buffer, 2, '(', ')', true),
2
);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 2, true), 2);
}

#[test]
fn test_nesting_depth_mixed_types() {
let buffer = Buffer::from_str_test("({[]})");
let overlay = BracketHighlightOverlay::new();

assert_eq!(overlay.calculate_nesting_depth(&buffer, 0, true), 0);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 1, true), 1);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 2, true), 2);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 3, false), 2);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 4, false), 1);
assert_eq!(overlay.calculate_nesting_depth(&buffer, 5, false), 0);
}
}
Loading
Loading