Skip to content
Closed
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
4 changes: 4 additions & 0 deletions crates/edit/src/bin/edit/draw_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ fn draw_search(ctx: &mut Context, state: &mut State) {

pub enum SearchAction {
Search,
SearchPrev,
Replace,
ReplaceAll,
}
Expand All @@ -174,6 +175,9 @@ pub fn search_execute(ctx: &mut Context, state: &mut State, action: SearchAction
SearchAction::Search => {
doc.buffer.borrow_mut().find_and_select(&state.search_needle, state.search_options)
}
SearchAction::SearchPrev => {
doc.buffer.borrow_mut().find_and_select_prev(&state.search_needle, state.search_options)
}
SearchAction::Replace => doc.buffer.borrow_mut().find_and_replace(
&state.search_needle,
state.search_options,
Expand Down
2 changes: 2 additions & 0 deletions crates/edit/src/bin/edit/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ fn draw(tui: &mut Tui, input: Option<input::Input>, state: &mut State) {
state.wants_search.focus = true;
} else if key == vk::F3 {
search_execute(ctx, state, SearchAction::Search);
} else if key == kbmod::SHIFT | vk::F3 {
search_execute(ctx, state, SearchAction::SearchPrev);
} else {
return;
}
Expand Down
113 changes: 113 additions & 0 deletions crates/edit/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,25 @@ impl TextBuffer {

/// Find the next occurrence of the given `pattern` and select it.
pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> icu::Result<()> {
self.find_and_select_impl(pattern, options, false)
}

/// Find the previous occurrence of the given `pattern` and select it.
/// Wraps around to the end of the buffer if there's no match before the cursor.
pub fn find_and_select_prev(
&mut self,
pattern: &str,
options: SearchOptions,
) -> icu::Result<()> {
self.find_and_select_impl(pattern, options, true)
}

fn find_and_select_impl(
&mut self,
pattern: &str,
options: SearchOptions,
backwards: bool,
) -> icu::Result<()> {
if let Some(search) = &mut self.search {
let search = search.get_mut();
// When the search input changes we must reset the search.
Expand Down Expand Up @@ -1190,6 +1209,18 @@ impl TextBuffer {
return Ok(());
}

if backwards {
// Search backwards from the start of the selection, or otherwise the cursor.
let limit = match self.selection {
Some(TextBufferSelection { beg, end }) => {
self.cursor_move_to_logical_internal(self.cursor, beg.min(end)).offset
}
_ => self.cursor.offset,
};
self.find_select_prev(search, limit);
return Ok(());
}

// If the user moved the cursor since the last search, but the needle remained the same,
// we still need to move the start of the search to the new cursor position.
let next_search_offset = if self.selection_generation == search.selection_generation {
Expand Down Expand Up @@ -1352,6 +1383,57 @@ impl TextBuffer {
})
}

/// Selects the last match that starts before `limit`.
/// If there is none, the last match in the entire buffer is selected.
fn find_select_prev(
&mut self,
search: &mut ActiveSearch,
limit: usize,
) -> Option<Range<usize>> {
// Scan the entire buffer from the start. ICU has no backwards search.
if search.buffer_generation != self.buffer.generation() {
unsafe { search.regex.set_text(&mut search.text, 0) };
search.buffer_generation = self.buffer.generation();
} else {
search.regex.reset(0);
}

let mut before = None;
let mut last = None;
while let Some(range) = search.regex.next() {
if range.start < limit {
before = Some(range.clone());
} else if before.is_some() {
break;
}
last = Some(range);
}

let hit = before.or(last);

search.selection_generation = if let Some(range) = &hit {
// Continue forward searches from the end of this match.
search.next_search_offset = range.end;
search.regex.reset(range.end);

let beg = self.cursor_move_to_offset_internal(self.cursor, range.start);
let end = self.cursor_move_to_offset_internal(beg, range.end);

unsafe { self.set_cursor(end) };
self.make_cursor_visible();

self.set_selection(Some(TextBufferSelection {
beg: beg.logical_pos,
end: end.logical_pos,
}))
} else {
search.no_matches = true;
self.set_selection(None)
};

hit
}

fn find_select_next(
&mut self,
search: &mut ActiveSearch,
Expand Down Expand Up @@ -4190,4 +4272,35 @@ mod tests {

assert_eq!(buffer_stored_bytes(&buf), b"ax\nbx\nx\n");
}

// Searching backwards must step through matches from the cursor towards the start,
// wrap around to the last match, and interoperate with forward searching.
#[test]
fn test_find_prev() {
let mut buf = prep_buffer("ab ab ab");
let opts = SearchOptions::default();
buf.cursor_move_to_logical(Point { x: 8, y: 0 });

let sel_x = |buf: &TextBuffer| {
let s = buf.selection.unwrap();
(s.beg.x, s.end.x)
};

buf.find_and_select_prev("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (6, 8));
buf.find_and_select_prev("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (3, 5));
buf.find_and_select_prev("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (0, 2));
// Reached the start: wrap around to the end.
buf.find_and_select_prev("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (6, 8));
// Forward search continues from the current match and wraps to the start.
buf.find_and_select("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (0, 2));
buf.find_and_select("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (3, 5));
buf.find_and_select_prev("ab", opts).unwrap();
assert_eq!(sel_x(&buf), (0, 2));
}
}
6 changes: 6 additions & 0 deletions crates/edit/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,12 @@ impl<'input> Iterator for Stream<'_, '_, 'input> {
));
}
}
'P'..='S' => {
let key = vk::F1.value() + csi.final_byte as u32 - 'P' as u32;
return Some(Input::Keyboard(
InputKey::new(key) | Self::parse_modifiers(csi),
));
}
'Z' => return Some(Input::Keyboard(kbmod::SHIFT | vk::TAB)),
'~' => {
const LUT: [u8; 35] = [
Expand Down