use std::ops::Range;
use ropey::Rope;
use crate::theme::ActiveTheme as _;
use crate::{Context, HighlightStyle, Hsla};
use super::super::InputState;
use super::super::decorations::{TextDecoration, normalize};
const SCAN_BUDGET: usize = 100_000;
const PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
fn rainbow_palette(cx: &crate::App) -> [Hsla; 6] {
let theme = cx.theme();
[
theme.red,
theme.yellow,
theme.green,
theme.cyan,
theme.blue,
theme.magenta,
]
}
pub(crate) fn rainbow_bracket_depths(text: &Rope) -> Vec<(Range<usize>, usize)> {
let len = text.len();
let win_start = text.floor_char_boundary(0);
let win_end = text.ceil_char_boundary(len.min(SCAN_BUDGET)).min(len);
let window = text.slice(win_start..win_end).to_string();
let mut out = Vec::new();
let mut stack: Vec<char> = Vec::new();
let mut offset = win_start;
for c in window.chars() {
let next = offset + c.len_utf8();
if matching_closer(c).is_some() {
out.push((offset..next, stack.len()));
stack.push(c);
} else if let Some(open) = matching_opener(c) {
if stack.last() == Some(&open) {
stack.pop();
out.push((offset..next, stack.len()));
}
}
offset = next;
}
out
}
fn matching_closer(open: char) -> Option<char> {
PAIRS.iter().find(|(o, _)| *o == open).map(|(_, c)| *c)
}
fn matching_opener(close: char) -> Option<char> {
PAIRS.iter().find(|(_, c)| *c == close).map(|(o, _)| *o)
}
pub(crate) fn find_bracket_match(
text: &Rope,
cursor: usize,
) -> Option<(Range<usize>, Range<usize>)> {
let len = text.len();
let cursor = cursor.min(len);
let win_start = text.floor_char_boundary(cursor.saturating_sub(SCAN_BUDGET));
let win_end = text
.ceil_char_boundary((cursor + SCAN_BUDGET).min(len))
.min(len);
let window = text.slice(win_start..win_end).to_string();
let relative = cursor - win_start;
find_in_str(&window, relative).map(|(a, b)| {
(
a.start + win_start..a.end + win_start,
b.start + win_start..b.end + win_start,
)
})
}
fn find_in_str(text: &str, cursor: usize) -> Option<(Range<usize>, Range<usize>)> {
let before = text[..cursor]
.chars()
.next_back()
.map(|c| (cursor - c.len_utf8(), c));
let at = text[cursor..].chars().next().map(|c| (cursor, c));
if let Some((pos, ch)) = before {
if matching_closer(ch).is_some() {
return match_forward(text, pos, ch).map(|m| (pos..pos + ch.len_utf8(), m));
} else if matching_opener(ch).is_some() {
return match_backward(text, pos, ch).map(|m| (m, pos..pos + ch.len_utf8()));
}
}
if let Some((pos, ch)) = at {
if matching_closer(ch).is_some() {
return match_forward(text, pos, ch).map(|m| (pos..pos + ch.len_utf8(), m));
} else if matching_opener(ch).is_some() {
return match_backward(text, pos, ch).map(|m| (m, pos..pos + ch.len_utf8()));
}
}
None
}
fn match_forward(text: &str, open_pos: usize, open: char) -> Option<Range<usize>> {
let close = matching_closer(open)?;
let mut depth = 0;
let mut offset = open_pos;
for c in text[open_pos..].chars() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(offset..offset + c.len_utf8());
}
}
offset += c.len_utf8();
}
None
}
fn match_backward(text: &str, close_pos: usize, close: char) -> Option<Range<usize>> {
let open = matching_opener(close)?;
let mut depth = 0;
let mut offset = close_pos + close.len_utf8();
for c in text[..offset].chars().rev() {
offset -= c.len_utf8();
if c == close {
depth += 1;
} else if c == open {
depth -= 1;
if depth == 0 {
return Some(offset..offset + c.len_utf8());
}
}
}
None
}
fn bracket_color(cx: &crate::App) -> Hsla {
cx.theme().tokens.accent.color.opacity(0.25)
}
impl InputState {
pub(crate) fn refresh_bracket_match(&mut self, cx: &mut Context<Self>) {
if !self.bracket_match_enabled || !self.mode.is_multi_line() {
self.clear_bracket_match(cx);
return;
}
let style = HighlightStyle {
background_color: Some(bracket_color(cx)),
..Default::default()
};
let decorations: Vec<TextDecoration> = find_bracket_match(&self.core.text, self.cursor())
.into_iter()
.flat_map(|(a, b)| [a, b])
.map(|range| TextDecoration::new(range, style))
.collect();
if let Some(collection) = self.bracket_match_collection.clone() {
let decorations = normalize(&self.core.text, decorations);
if collection.set_in_place(&mut self.core.decorations, decorations) {
cx.notify();
}
} else if !decorations.is_empty() {
self.bracket_match_collection =
Some(self.create_decorations_collection(decorations, cx));
}
}
pub(crate) fn clear_bracket_match(&mut self, cx: &mut Context<Self>) {
if let Some(collection) = self.bracket_match_collection.clone() {
if collection.clear_in_place(&mut self.core.decorations) {
cx.notify();
}
}
}
pub(crate) fn refresh_bracket_rainbow(&mut self, cx: &mut Context<Self>) {
if !self.bracket_rainbow_enabled || !self.mode.is_multi_line() {
self.clear_bracket_rainbow(cx);
return;
}
let palette = rainbow_palette(cx);
let decorations: Vec<TextDecoration> = rainbow_bracket_depths(&self.core.text)
.into_iter()
.map(|(range, depth)| {
TextDecoration::new(
range,
HighlightStyle {
color: Some(palette[depth % palette.len()]),
..Default::default()
},
)
})
.collect();
if let Some(collection) = self.bracket_rainbow_collection.clone() {
let decorations = normalize(&self.core.text, decorations);
if collection.set_in_place(&mut self.core.decorations, decorations) {
cx.notify();
}
} else if !decorations.is_empty() {
self.bracket_rainbow_collection =
Some(self.create_decorations_collection(decorations, cx));
}
}
pub(crate) fn clear_bracket_rainbow(&mut self, cx: &mut Context<Self>) {
if let Some(collection) = self.bracket_rainbow_collection.clone() {
if collection.clear_in_place(&mut self.core.decorations) {
cx.notify();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AppContext as _;
use crate::{Entity, Window};
fn check(text: &str, cursor: usize, expected: Option<(&str, &str)>) {
let rope = Rope::from(text);
let found = find_bracket_match(&rope, cursor);
let actual = found.map(|(a, b)| (rope.slice(a).to_string(), rope.slice(b).to_string()));
let expected = expected.map(|(a, b)| (a.to_string(), b.to_string()));
assert_eq!(actual, expected, "text {text:?} cursor {cursor}");
}
#[test]
fn matches_adjacent_brackets() {
check("(a)", 1, Some(("(", ")")));
check("(a)", 2, Some(("(", ")")));
check("(a)", 0, Some(("(", ")")));
check("((x))", 2, Some(("(", ")")));
check("((x))", 3, Some(("(", ")")));
check("{a[b]}", 1, Some(("{", "}")));
check("{a[b]}", 3, Some(("[", "]")));
}
#[test]
fn no_match_cases() {
check("ab", 1, None);
check("(abc", 1, None);
check("abc)", 4, None);
check("", 0, None);
check("()", 99, Some(("(", ")")));
}
struct Probe {
state: Entity<InputState>,
}
impl crate::Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
#[rgpui::test]
fn refresh_highlights_match_on_cursor_move(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
let state = cx.new(|cx| InputState::new(window, cx).multi_line(true));
state.update(cx, |state, cx| state.replace("(a) b", window, cx));
Probe { state }
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(1..1, cx));
});
let ranges = state.read_with(cx, |state, cx| {
state
.bracket_match_collection
.as_ref()
.map(|collection| collection.get_ranges(cx))
.unwrap_or_default()
});
assert_eq!(ranges, vec![0..1, 2..3]);
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(5..5, cx));
});
let ranges = state.read_with(cx, |state, cx| {
state
.bracket_match_collection
.as_ref()
.map(|collection| collection.get_ranges(cx))
.unwrap_or_default()
});
assert!(ranges.is_empty());
}
#[test]
fn rainbow_depths_nested_and_stray() {
let depths = rainbow_bracket_depths(&Rope::from("((a){b})"));
let compact: Vec<(String, usize)> = depths
.into_iter()
.map(|(range, depth)| (Rope::from("((a){b})").slice(range).to_string(), depth))
.collect();
assert_eq!(
compact
.iter()
.map(|(text, depth)| (text.as_str(), *depth))
.collect::<Vec<_>>(),
vec![("(", 0), ("(", 1), (")", 1), ("{", 1), ("}", 1), (")", 0),]
);
assert!(rainbow_bracket_depths(&Rope::from("a)b")).is_empty());
assert_eq!(rainbow_bracket_depths(&Rope::from(")(")).len(), 1);
}
#[rgpui::test]
fn rainbow_toggle_refreshes_and_clears(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
let state = cx.new(|cx| {
InputState::new(window, cx)
.multi_line(true)
.bracket_rainbow(true)
});
state.update(cx, |state, cx| state.replace("(a(b))", window, cx));
Probe { state }
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
let ranges = state.read_with(cx, |state, cx| {
state
.bracket_rainbow_collection
.as_ref()
.map(|collection| collection.get_ranges(cx))
.unwrap_or_default()
});
assert_eq!(ranges.len(), 4);
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(1..1, cx));
});
let ranges_after_move = state.read_with(cx, |state, cx| {
state
.bracket_rainbow_collection
.as_ref()
.map(|collection| collection.get_ranges(cx))
.unwrap_or_default()
});
assert_eq!(ranges_after_move, ranges);
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_bracket_rainbow_enabled(false, cx));
});
let ranges = state.read_with(cx, |state, cx| {
state
.bracket_rainbow_collection
.as_ref()
.map(|collection| collection.get_ranges(cx))
.unwrap_or_default()
});
assert!(ranges.is_empty());
}
#[rgpui::test]
fn rulers_state_plumbing(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
let state = cx.new(|cx| InputState::new(window, cx).multi_line(true));
Probe { state }
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(state.read_with(cx, |state, _| state.rulers.is_empty()));
cx.update(|_, cx| {
state.update(cx, |state, cx| {
state.set_rulers(vec![80, 120], cx);
state.set_ruler_color(Some(crate::red_400()), cx);
});
});
let (rulers, color) =
state.read_with(cx, |state, _| (state.rulers.clone(), state.ruler_color));
assert_eq!(rulers, vec![80, 120]);
assert_eq!(color, Some(crate::red_400()));
}
}