fix: highlight all search matches before word navigation
This commit is contained in:
+65
-28
@@ -6490,6 +6490,18 @@ impl Editor {
|
||||
pub fn search_match(&self) -> Option<SearchMatch> {
|
||||
self.active.view.search_match
|
||||
}
|
||||
pub fn search_matches_on_row(&self, row: usize) -> impl Iterator<Item = SearchMatch> + '_ {
|
||||
let query = self.active.view.search_query.as_str();
|
||||
let line = &self.active.lines[row];
|
||||
let columns = grapheme_count(query);
|
||||
search_match_bytes(line, query, self.active.view.search_whole_word).map(move |byte| {
|
||||
let column = column_after_byte(line, byte);
|
||||
SearchMatch {
|
||||
start: Cursor { row, column },
|
||||
end_column: column + columns,
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Whole-row shape belongs to the active buffer, not the Visual binding context.
|
||||
pub fn visual_linewise(&self) -> bool {
|
||||
self.active.view.mode == Mode::Visual && self.active.view.linewise
|
||||
@@ -8412,7 +8424,7 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_word_search(&mut self) -> Option<Cursor> {
|
||||
fn set_word_search(&mut self) -> Option<(Cursor, bool)> {
|
||||
let cursor = self.cursor();
|
||||
let line = &self.active.lines[cursor.row];
|
||||
let origin = grapheme_byte(line, cursor.column);
|
||||
@@ -8438,25 +8450,41 @@ impl Editor {
|
||||
let end = tokens()
|
||||
.find(|(position, next)| *position > byte && *next != kind)
|
||||
.map_or(line.len(), |(position, _)| position);
|
||||
self.active.view.search_query = line[start..end].into();
|
||||
self.active.view.search_whole_word = kind == WordKind::Keyword;
|
||||
Some(Cursor {
|
||||
row: cursor.row,
|
||||
column: column_after_byte(line, start),
|
||||
})
|
||||
let query = &line[start..end];
|
||||
let whole_word = kind == WordKind::Keyword;
|
||||
let newly_active = self.active.view.search_query != query
|
||||
|| self.active.view.search_whole_word != whole_word;
|
||||
if newly_active {
|
||||
self.active.view.search_query = query.into();
|
||||
self.active.view.search_whole_word = whole_word;
|
||||
}
|
||||
Some((
|
||||
Cursor {
|
||||
row: cursor.row,
|
||||
column: column_after_byte(line, start),
|
||||
},
|
||||
newly_active,
|
||||
))
|
||||
}
|
||||
|
||||
fn seek_search(&mut self, action: Action, count: usize) {
|
||||
self.reveal_caret();
|
||||
let source = self.jump_location();
|
||||
let primary_origin = if action == Action::SearchWord {
|
||||
Some(match self.set_word_search() {
|
||||
Some(origin) => origin,
|
||||
None => return,
|
||||
})
|
||||
let Some((origin, newly_active)) = self.set_word_search() else {
|
||||
return;
|
||||
};
|
||||
if newly_active {
|
||||
self.active.view.search_match = Some(SearchMatch {
|
||||
start: origin,
|
||||
end_column: origin.column + grapheme_count(&self.active.view.search_query),
|
||||
});
|
||||
return;
|
||||
}
|
||||
Some(origin)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let source = self.jump_location();
|
||||
let forward = action != Action::SearchPrevious;
|
||||
self.active.view.search_match = None;
|
||||
if self.active.view.search_query.is_empty() {
|
||||
@@ -8470,22 +8498,7 @@ impl Editor {
|
||||
.navigable_rows(0, self.active.lines.len())
|
||||
.flat_map(|row| {
|
||||
let line = &self.active.lines[row];
|
||||
line.grapheme_indices(true).filter_map(move |(byte, _)| {
|
||||
(line[byte..].starts_with(query)
|
||||
&& GraphemeCursor::new(byte + query.len(), line.len(), true)
|
||||
.is_boundary(line, 0)
|
||||
.expect("complete line supplies grapheme context")
|
||||
&& (!whole_word
|
||||
|| (line[..byte]
|
||||
.graphemes(true)
|
||||
.next_back()
|
||||
.is_none_or(|g| word_kind(g, false) != WordKind::Keyword)
|
||||
&& line[byte + query.len()..]
|
||||
.graphemes(true)
|
||||
.next()
|
||||
.is_none_or(|g| word_kind(g, false) != WordKind::Keyword))))
|
||||
.then_some((row, byte))
|
||||
})
|
||||
search_match_bytes(line, query, whole_word).map(move |byte| (row, byte))
|
||||
})
|
||||
.collect();
|
||||
if matches.is_empty() {
|
||||
@@ -11927,6 +11940,30 @@ enum WordKind {
|
||||
EmptyLine,
|
||||
}
|
||||
|
||||
fn search_match_bytes<'a>(
|
||||
line: &'a str,
|
||||
query: &'a str,
|
||||
whole_word: bool,
|
||||
) -> impl Iterator<Item = usize> + 'a {
|
||||
let line = if query.is_empty() { "" } else { line };
|
||||
line.grapheme_indices(true).filter_map(move |(byte, _)| {
|
||||
(line[byte..].starts_with(query)
|
||||
&& GraphemeCursor::new(byte + query.len(), line.len(), true)
|
||||
.is_boundary(line, 0)
|
||||
.expect("complete line supplies grapheme context")
|
||||
&& (!whole_word
|
||||
|| (line[..byte]
|
||||
.graphemes(true)
|
||||
.next_back()
|
||||
.is_none_or(|g| word_kind(g, false) != WordKind::Keyword)
|
||||
&& line[byte + query.len()..]
|
||||
.graphemes(true)
|
||||
.next()
|
||||
.is_none_or(|g| word_kind(g, false) != WordKind::Keyword))))
|
||||
.then_some(byte)
|
||||
})
|
||||
}
|
||||
|
||||
fn word_kind(grapheme: &str, big: bool) -> WordKind {
|
||||
let ch = grapheme.chars().next().expect("graphemes are nonempty");
|
||||
if ch.is_whitespace() {
|
||||
|
||||
+68
-1
@@ -3122,7 +3122,7 @@ fn paint_editor_line(
|
||||
paint_grapheme_range(frame, area, left, line, start..end, style);
|
||||
}
|
||||
}
|
||||
if let Some(found) = editor.search_match().filter(|found| found.start.row == row) {
|
||||
for found in editor.search_matches_on_row(row) {
|
||||
paint_cells(
|
||||
frame,
|
||||
area,
|
||||
@@ -18877,6 +18877,73 @@ end
|
||||
assert_ne!(buffer[(6, 0)].bg, Color::Blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_highlights_all_occurrences_without_moving_on_first_star() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
for bar in [top, bottom] {
|
||||
bar.left.clear();
|
||||
bar.center.clear();
|
||||
bar.right.clear();
|
||||
}
|
||||
});
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"cat cat_cat cat\ncat".into(),
|
||||
)])
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('*'));
|
||||
assert_eq!(editor.cursor(), Cursor::default());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(30, 6)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
let mut syntax = SyntaxCache::new();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
&editor,
|
||||
&mut top,
|
||||
&mut left,
|
||||
&mut syntax,
|
||||
&mut DocumentViewport::default(),
|
||||
&mut WheelPanels::default(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (row, start, end) in [(0, 0, 3), (0, 12, 15), (1, 0, 3)] {
|
||||
for column in start..end {
|
||||
assert_eq!(buffer[(column + 2, row)].bg, Color::Yellow);
|
||||
}
|
||||
}
|
||||
assert_ne!(buffer[(6, 0)].bg, Color::Yellow);
|
||||
editor.handle(Input::Char('/'));
|
||||
for ch in "cat".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Enter);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
&editor,
|
||||
&mut top,
|
||||
&mut left,
|
||||
&mut syntax,
|
||||
&mut DocumentViewport::default(),
|
||||
&mut WheelPanels::default(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
for column in [0, 4, 8, 12] {
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(column + 2, 0)].bg,
|
||||
Color::Yellow
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_search_highlight_uses_display_cells_and_clips_with_horizontal_scroll() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user