Make buffer search aware of search direction (#24974)

This solves a couple of issues with Vim search by making the search
buffer and `SearchableItem` aware of the direction of the search. If
`SearchOptions::BACKWARDS` is set, all operations will be reversed. By
making `SearchableItem` aware of the direction, the correct active match
can be selected when searching backward.

Fixes #22506. This PR does not fix the last problem in that issue, but
that one is also tracked in #8049.

Release Notes:

- Fixes incorrect behavior of backward search in Vim mode
This commit is contained in:
Nico Lehmann 2025-03-04 18:27:37 -08:00 committed by GitHub
parent ed13e05855
commit 229e853874
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 80 additions and 25 deletions

View file

@ -54,7 +54,7 @@ use ui::{
Tooltip,
};
use util::{maybe, ResultExt};
use workspace::searchable::SearchableItemHandle;
use workspace::searchable::{Direction, SearchableItemHandle};
use workspace::{
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
@ -3060,12 +3060,13 @@ impl SearchableItem for ContextEditor {
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Self::Match],
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
self.editor.update(cx, |editor, cx| {
editor.active_match_index(matches, window, cx)
editor.active_match_index(direction, matches, window, cx)
})
}
}

View file

@ -1589,11 +1589,13 @@ impl SearchableItem for Editor {
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Range<Anchor>],
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
active_match_index(
direction,
matches,
&self.selections.newest_anchor().head(),
&self.buffer().read(cx).snapshot(cx),
@ -1606,6 +1608,7 @@ impl SearchableItem for Editor {
}
pub fn active_match_index(
direction: Direction,
ranges: &[Range<Anchor>],
cursor: &Anchor,
buffer: &MultiBufferSnapshot,
@ -1613,7 +1616,7 @@ pub fn active_match_index(
if ranges.is_empty() {
None
} else {
match ranges.binary_search_by(|probe| {
let r = ranges.binary_search_by(|probe| {
if probe.end.cmp(cursor, buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(cursor, buffer).is_gt() {
@ -1621,8 +1624,15 @@ pub fn active_match_index(
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
});
match direction {
Direction::Prev => match r {
Ok(i) => Some(i),
Err(i) => Some(i.saturating_sub(1)),
},
Direction::Next => match r {
Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
},
}
}
}

View file

@ -16,7 +16,7 @@ use std::{borrow::Cow, sync::Arc};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
};
@ -1170,12 +1170,14 @@ impl SearchableItem for LspLogView {
}
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Self::Match],
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
self.editor
.update(cx, |e, cx| e.active_match_index(matches, window, cx))
self.editor.update(cx, |e, cx| {
e.active_match_index(direction, matches, window, cx)
})
}
}

View file

@ -1306,7 +1306,16 @@ impl BufferSearchBar {
done_rx
}
fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction {
if self.search_options.contains(SearchOptions::BACKWARDS) {
direction.opposite()
} else {
direction
}
}
pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let direction = self.reverse_direction_if_backwards(Direction::Next);
let new_index = self
.active_searchable_item
.as_ref()
@ -1314,7 +1323,7 @@ impl BufferSearchBar {
let matches = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())?;
searchable_item.active_match_index(matches, window, cx)
searchable_item.active_match_index(direction, matches, window, cx)
});
if new_index != self.active_match_index {
self.active_match_index = new_index;

View file

@ -1240,6 +1240,7 @@ impl ProjectSearchView {
fn update_match_index(&mut self, cx: &mut Context<Self>) {
let results_editor = self.results_editor.read(cx);
let new_index = active_match_index(
Direction::Next,
&self.entity.read(cx).match_ranges,
&results_editor.selections.newest_anchor().head(),
&results_editor.buffer().read(cx).snapshot(cx),

View file

@ -47,6 +47,8 @@ bitflags! {
const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
const REGEX = 0b1000;
/// If set, reverse direction when finding the active match
const BACKWARDS = 0b10000;
}
}

View file

@ -41,7 +41,7 @@ use workspace::{
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
},
register_serializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
WorkspaceId,
};
@ -1583,6 +1583,7 @@ impl SearchableItem for TerminalView {
/// Reports back to the search toolbar what the active match should be (the selection)
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Self::Match],
_: &mut Window,
cx: &mut Context<Self>,
@ -1593,19 +1594,36 @@ impl SearchableItem for TerminalView {
let res = if !matches.is_empty() {
if let Some(selection_head) = self.terminal().read(cx).selection_head {
// If selection head is contained in a match. Return that match
if let Some(ix) = matches
.iter()
.enumerate()
.find(|(_, search_match)| {
search_match.contains(&selection_head)
|| search_match.start() > &selection_head
})
.map(|(ix, _)| ix)
{
Some(ix)
} else {
// If no selection after selection head, return the last match
Some(matches.len().saturating_sub(1))
match direction {
Direction::Prev => {
// If no selection before selection head, return the first match
Some(
matches
.iter()
.enumerate()
.rev()
.find(|(_, search_match)| {
search_match.contains(&selection_head)
|| search_match.start() < &selection_head
})
.map(|(ix, _)| ix)
.unwrap_or(0),
)
}
Direction::Next => {
// If no selection after selection head, return the last match
Some(
matches
.iter()
.enumerate()
.find(|(_, search_match)| {
search_match.contains(&selection_head)
|| search_match.start() > &selection_head
})
.map(|(ix, _)| ix)
.unwrap_or(matches.len().saturating_sub(1)),
)
}
}
} else {
// Matches found but no active selection, return the first last one (closest to cursor)

View file

@ -154,6 +154,9 @@ impl Vim {
if action.regex {
options |= SearchOptions::REGEX;
}
if action.backwards {
options |= SearchOptions::BACKWARDS;
}
search_bar.set_search_options(options, cx);
let prior_mode = if self.temp_mode {
Mode::Insert
@ -198,7 +201,7 @@ impl Vim {
.last()
.map_or(true, |range| range.start != new_head);
if is_different_head && self.search.direction == Direction::Next {
if is_different_head {
count = count.saturating_sub(1)
}
self.search.count = 1;
@ -743,6 +746,12 @@ mod test {
cx.simulate_keystrokes("*");
cx.assert_state("one two ˇone", Mode::Normal);
// check that a backward search after last match works correctly
cx.set_state("aa\naa\nbbˇ", Mode::Normal);
cx.simulate_keystrokes("? a a");
cx.simulate_keystrokes("enter");
cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
// check that searching with unable search wrap
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));

View file

@ -150,6 +150,7 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
) -> Task<Vec<Self::Match>>;
fn active_match_index(
&mut self,
direction: Direction,
matches: &[Self::Match],
window: &mut Window,
cx: &mut Context<Self>,
@ -208,6 +209,7 @@ pub trait SearchableItemHandle: ItemHandle {
) -> Task<AnyVec<dyn Send>>;
fn active_match_index(
&self,
direction: Direction,
matches: &AnyVec<dyn Send>,
window: &mut Window,
cx: &mut App,
@ -315,13 +317,14 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
}
fn active_match_index(
&self,
direction: Direction,
matches: &AnyVec<dyn Send>,
window: &mut Window,
cx: &mut App,
) -> Option<usize> {
let matches = matches.downcast_ref()?;
self.update(cx, |this, cx| {
this.active_match_index(matches.as_slice(), window, cx)
this.active_match_index(direction, matches.as_slice(), window, cx)
})
}