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:
parent
ed13e05855
commit
229e853874
9 changed files with 80 additions and 25 deletions
|
@ -54,7 +54,7 @@ use ui::{
|
||||||
Tooltip,
|
Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
use workspace::searchable::SearchableItemHandle;
|
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{self, FollowableItem, Item, ItemHandle},
|
item::{self, FollowableItem, Item, ItemHandle},
|
||||||
notifications::NotificationId,
|
notifications::NotificationId,
|
||||||
|
@ -3060,12 +3060,13 @@ impl SearchableItem for ContextEditor {
|
||||||
|
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
matches: &[Self::Match],
|
matches: &[Self::Match],
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.active_match_index(matches, window, cx)
|
editor.active_match_index(direction, matches, window, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1589,11 +1589,13 @@ impl SearchableItem for Editor {
|
||||||
|
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
matches: &[Range<Anchor>],
|
matches: &[Range<Anchor>],
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
active_match_index(
|
active_match_index(
|
||||||
|
direction,
|
||||||
matches,
|
matches,
|
||||||
&self.selections.newest_anchor().head(),
|
&self.selections.newest_anchor().head(),
|
||||||
&self.buffer().read(cx).snapshot(cx),
|
&self.buffer().read(cx).snapshot(cx),
|
||||||
|
@ -1606,6 +1608,7 @@ impl SearchableItem for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_match_index(
|
pub fn active_match_index(
|
||||||
|
direction: Direction,
|
||||||
ranges: &[Range<Anchor>],
|
ranges: &[Range<Anchor>],
|
||||||
cursor: &Anchor,
|
cursor: &Anchor,
|
||||||
buffer: &MultiBufferSnapshot,
|
buffer: &MultiBufferSnapshot,
|
||||||
|
@ -1613,7 +1616,7 @@ pub fn active_match_index(
|
||||||
if ranges.is_empty() {
|
if ranges.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
match ranges.binary_search_by(|probe| {
|
let r = ranges.binary_search_by(|probe| {
|
||||||
if probe.end.cmp(cursor, buffer).is_lt() {
|
if probe.end.cmp(cursor, buffer).is_lt() {
|
||||||
Ordering::Less
|
Ordering::Less
|
||||||
} else if probe.start.cmp(cursor, buffer).is_gt() {
|
} else if probe.start.cmp(cursor, buffer).is_gt() {
|
||||||
|
@ -1621,8 +1624,15 @@ pub fn active_match_index(
|
||||||
} else {
|
} else {
|
||||||
Ordering::Equal
|
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)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use std::{borrow::Cow, sync::Arc};
|
||||||
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
|
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemHandle},
|
item::{Item, ItemHandle},
|
||||||
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||||
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1170,12 +1170,14 @@ impl SearchableItem for LspLogView {
|
||||||
}
|
}
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
matches: &[Self::Match],
|
matches: &[Self::Match],
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
self.editor
|
self.editor.update(cx, |e, cx| {
|
||||||
.update(cx, |e, cx| e.active_match_index(matches, window, cx))
|
e.active_match_index(direction, matches, window, cx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1306,7 +1306,16 @@ impl BufferSearchBar {
|
||||||
done_rx
|
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>) {
|
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
|
let new_index = self
|
||||||
.active_searchable_item
|
.active_searchable_item
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -1314,7 +1323,7 @@ impl BufferSearchBar {
|
||||||
let matches = self
|
let matches = self
|
||||||
.searchable_items_with_matches
|
.searchable_items_with_matches
|
||||||
.get(&searchable_item.downgrade())?;
|
.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 {
|
if new_index != self.active_match_index {
|
||||||
self.active_match_index = new_index;
|
self.active_match_index = new_index;
|
||||||
|
|
|
@ -1240,6 +1240,7 @@ impl ProjectSearchView {
|
||||||
fn update_match_index(&mut self, cx: &mut Context<Self>) {
|
fn update_match_index(&mut self, cx: &mut Context<Self>) {
|
||||||
let results_editor = self.results_editor.read(cx);
|
let results_editor = self.results_editor.read(cx);
|
||||||
let new_index = active_match_index(
|
let new_index = active_match_index(
|
||||||
|
Direction::Next,
|
||||||
&self.entity.read(cx).match_ranges,
|
&self.entity.read(cx).match_ranges,
|
||||||
&results_editor.selections.newest_anchor().head(),
|
&results_editor.selections.newest_anchor().head(),
|
||||||
&results_editor.buffer().read(cx).snapshot(cx),
|
&results_editor.buffer().read(cx).snapshot(cx),
|
||||||
|
|
|
@ -47,6 +47,8 @@ bitflags! {
|
||||||
const CASE_SENSITIVE = 0b010;
|
const CASE_SENSITIVE = 0b010;
|
||||||
const INCLUDE_IGNORED = 0b100;
|
const INCLUDE_IGNORED = 0b100;
|
||||||
const REGEX = 0b1000;
|
const REGEX = 0b1000;
|
||||||
|
/// If set, reverse direction when finding the active match
|
||||||
|
const BACKWARDS = 0b10000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ use workspace::{
|
||||||
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
|
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
|
||||||
},
|
},
|
||||||
register_serializable_item,
|
register_serializable_item,
|
||||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||||
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
|
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
};
|
};
|
||||||
|
@ -1583,6 +1583,7 @@ impl SearchableItem for TerminalView {
|
||||||
/// Reports back to the search toolbar what the active match should be (the selection)
|
/// Reports back to the search toolbar what the active match should be (the selection)
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
matches: &[Self::Match],
|
matches: &[Self::Match],
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -1593,19 +1594,36 @@ impl SearchableItem for TerminalView {
|
||||||
let res = if !matches.is_empty() {
|
let res = if !matches.is_empty() {
|
||||||
if let Some(selection_head) = self.terminal().read(cx).selection_head {
|
if let Some(selection_head) = self.terminal().read(cx).selection_head {
|
||||||
// If selection head is contained in a match. Return that match
|
// If selection head is contained in a match. Return that match
|
||||||
if let Some(ix) = matches
|
match direction {
|
||||||
.iter()
|
Direction::Prev => {
|
||||||
.enumerate()
|
// If no selection before selection head, return the first match
|
||||||
.find(|(_, search_match)| {
|
Some(
|
||||||
search_match.contains(&selection_head)
|
matches
|
||||||
|| search_match.start() > &selection_head
|
.iter()
|
||||||
})
|
.enumerate()
|
||||||
.map(|(ix, _)| ix)
|
.rev()
|
||||||
{
|
.find(|(_, search_match)| {
|
||||||
Some(ix)
|
search_match.contains(&selection_head)
|
||||||
} else {
|
|| search_match.start() < &selection_head
|
||||||
// If no selection after selection head, return the last match
|
})
|
||||||
Some(matches.len().saturating_sub(1))
|
.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 {
|
} else {
|
||||||
// Matches found but no active selection, return the first last one (closest to cursor)
|
// Matches found but no active selection, return the first last one (closest to cursor)
|
||||||
|
|
|
@ -154,6 +154,9 @@ impl Vim {
|
||||||
if action.regex {
|
if action.regex {
|
||||||
options |= SearchOptions::REGEX;
|
options |= SearchOptions::REGEX;
|
||||||
}
|
}
|
||||||
|
if action.backwards {
|
||||||
|
options |= SearchOptions::BACKWARDS;
|
||||||
|
}
|
||||||
search_bar.set_search_options(options, cx);
|
search_bar.set_search_options(options, cx);
|
||||||
let prior_mode = if self.temp_mode {
|
let prior_mode = if self.temp_mode {
|
||||||
Mode::Insert
|
Mode::Insert
|
||||||
|
@ -198,7 +201,7 @@ impl Vim {
|
||||||
.last()
|
.last()
|
||||||
.map_or(true, |range| range.start != new_head);
|
.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)
|
count = count.saturating_sub(1)
|
||||||
}
|
}
|
||||||
self.search.count = 1;
|
self.search.count = 1;
|
||||||
|
@ -743,6 +746,12 @@ mod test {
|
||||||
cx.simulate_keystrokes("*");
|
cx.simulate_keystrokes("*");
|
||||||
cx.assert_state("one two ˇone", Mode::Normal);
|
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
|
// check that searching with unable search wrap
|
||||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
|
store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
|
||||||
|
|
|
@ -150,6 +150,7 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
||||||
) -> Task<Vec<Self::Match>>;
|
) -> Task<Vec<Self::Match>>;
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
matches: &[Self::Match],
|
matches: &[Self::Match],
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
@ -208,6 +209,7 @@ pub trait SearchableItemHandle: ItemHandle {
|
||||||
) -> Task<AnyVec<dyn Send>>;
|
) -> Task<AnyVec<dyn Send>>;
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&self,
|
&self,
|
||||||
|
direction: Direction,
|
||||||
matches: &AnyVec<dyn Send>,
|
matches: &AnyVec<dyn Send>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
@ -315,13 +317,14 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
|
||||||
}
|
}
|
||||||
fn active_match_index(
|
fn active_match_index(
|
||||||
&self,
|
&self,
|
||||||
|
direction: Direction,
|
||||||
matches: &AnyVec<dyn Send>,
|
matches: &AnyVec<dyn Send>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let matches = matches.downcast_ref()?;
|
let matches = matches.downcast_ref()?;
|
||||||
self.update(cx, |this, cx| {
|
self.update(cx, |this, cx| {
|
||||||
this.active_match_index(matches.as_slice(), window, cx)
|
this.active_match_index(direction, matches.as_slice(), window, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue