mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 19:15:53 +00:00
editor: Inherit whole word match from buffer search (#64137)
# Objective Make `editor: select next` / `editor: select previous` more powerful and conform to VSCode behaviour Partial fix for #15415 ## Solution When `editor: select next`/`editor: select previous`/`editor: select all` is used from an existing selection, it'll now inherit whole-word from the buffer search bar, while it's open. If those commands are used starting from a single caret, continue to always match whole words, with case sensitivity continuing to be configurable in that case. ## Testing Test both manually, as well as with unit tests: - Updated `editor::editor_tests::test_select_next` with new assertion for whole-word matching - Added `search::buffer_search::tests::test_set_active_pane_item_syncs_select_search_options` - Added `search::buffer_search::tests::test_search_option_change_during_selections` - Added `search::buffer_search::tests::test_show_syncs_select_search_options` ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Improved `editor: select next` and `editor: select previous` to respect the buffer search whole-word option. --------- Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
parent
a405fb91d5
commit
baee1ca6de
6 changed files with 315 additions and 51 deletions
|
|
@ -274,7 +274,7 @@ use workspace::{
|
|||
TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings,
|
||||
item::{ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions},
|
||||
notifications::{DetachAndPromptErr, NotificationId, NotifyResultExt, NotifyTaskExt},
|
||||
searchable::SearchEvent,
|
||||
searchable::{SearchEvent, SelectSearchOptions},
|
||||
};
|
||||
pub use zed_actions::editor::RevealInFileManager;
|
||||
use zed_actions::editor::{MoveDown, MoveUp};
|
||||
|
|
@ -1201,7 +1201,7 @@ pub struct Editor {
|
|||
refresh_folding_ranges_task: Task<()>,
|
||||
inlay_hints: Option<LspInlayHintData>,
|
||||
folding_newlines: Task<()>,
|
||||
select_next_is_case_sensitive: Option<bool>,
|
||||
select_next_options: Option<SelectSearchOptions>,
|
||||
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
|
||||
on_local_selections_changed:
|
||||
Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
|
||||
|
|
@ -2573,7 +2573,7 @@ impl Editor {
|
|||
selection_drag_state: SelectionDragState::None,
|
||||
folding_newlines: Task::ready(()),
|
||||
lookup_key: None,
|
||||
select_next_is_case_sensitive: None,
|
||||
select_next_options: None,
|
||||
on_local_selections_changed: None,
|
||||
suppress_selection_callback: false,
|
||||
applicable_language_settings: HashMap::default(),
|
||||
|
|
|
|||
|
|
@ -14881,6 +14881,18 @@ async fn test_select_next(cx: &mut TestAppContext) {
|
|||
e.select_next(&SelectNext::default(), window, cx).unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
|
||||
|
||||
// Enable whole word
|
||||
update_test_editor_settings(&mut cx, &|settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.whole_word = Some(true);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("abc\nabc «abcˇ»\ndefabc\nabc");
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\nabc «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ use workspace::{
|
|||
item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions},
|
||||
searchable::{
|
||||
Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItem,
|
||||
SearchableItemHandle,
|
||||
SearchableItemHandle, SelectSearchOptions,
|
||||
},
|
||||
};
|
||||
use workspace::{
|
||||
|
|
@ -2121,12 +2121,18 @@ impl SearchableItem for Editor {
|
|||
self.expect_bounds_change = self.last_bounds;
|
||||
}
|
||||
|
||||
fn set_search_is_case_sensitive(
|
||||
fn set_select_search_options(
|
||||
&mut self,
|
||||
case_sensitive: Option<bool>,
|
||||
select_search_options: Option<SelectSearchOptions>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.select_next_is_case_sensitive = case_sensitive;
|
||||
if self.select_next_options == select_search_options {
|
||||
return;
|
||||
}
|
||||
|
||||
self.select_next_options = select_search_options;
|
||||
self.select_next_state = None;
|
||||
self.select_prev_state = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
use super::*;
|
||||
use crate::columnar_selection::ColumnarSelectionRows;
|
||||
|
||||
/// How a [`crate::actions::SelectNext`] or [`crate::actions::SelectPrevious`]
|
||||
/// action originated.
|
||||
///
|
||||
/// When started from a caret, whole-word matching is always enabled, compared
|
||||
/// to when started from a selection, where the configured whole-word option is
|
||||
/// used instead.
|
||||
enum SelectNextOrigin {
|
||||
Caret,
|
||||
Selection,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn sync_selections(
|
||||
&mut self,
|
||||
|
|
@ -493,22 +504,23 @@ impl Editor {
|
|||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let is_empty = query.is_empty();
|
||||
let select_state = SelectNextState {
|
||||
query: self.build_query(&[query.chars().rev().collect::<String>()], cx)?,
|
||||
wordwise: true,
|
||||
done: is_empty,
|
||||
};
|
||||
let select_state = self.build_select_state(
|
||||
query.chars().rev().collect(),
|
||||
SelectNextOrigin::Caret,
|
||||
is_empty,
|
||||
cx,
|
||||
)?;
|
||||
self.select_prev_state = Some(select_state);
|
||||
} else {
|
||||
self.select_prev_state = None;
|
||||
}
|
||||
} else if let Some(selected_text) = selected_text {
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: self
|
||||
.build_query(&[selected_text.chars().rev().collect::<String>()], cx)?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_prev_state = Some(self.build_select_state(
|
||||
selected_text.chars().rev().collect(),
|
||||
SelectNextOrigin::Selection,
|
||||
false,
|
||||
cx,
|
||||
)?);
|
||||
self.select_previous(action, window, cx)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -2587,21 +2599,19 @@ impl Editor {
|
|||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let is_empty = query.is_empty();
|
||||
let select_state = SelectNextState {
|
||||
query: self.build_query(&[query], cx)?,
|
||||
wordwise: true,
|
||||
done: is_empty,
|
||||
};
|
||||
let select_state =
|
||||
self.build_select_state(query, SelectNextOrigin::Caret, is_empty, cx)?;
|
||||
self.select_next_state = Some(select_state);
|
||||
} else {
|
||||
self.select_next_state = None;
|
||||
}
|
||||
} else if let Some(selected_text) = selected_text {
|
||||
self.select_next_state = Some(SelectNextState {
|
||||
query: self.build_query(&[selected_text], cx)?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_next_state = Some(self.build_select_state(
|
||||
selected_text,
|
||||
SelectNextOrigin::Selection,
|
||||
false,
|
||||
cx,
|
||||
)?);
|
||||
self.select_next_match_internal(
|
||||
display_map,
|
||||
replace_newest,
|
||||
|
|
@ -2614,20 +2624,37 @@ impl Editor {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn build_query<I, P>(&self, patterns: I, cx: &Context<Self>) -> Result<AhoCorasick, BuildError>
|
||||
fn build_query<I, P>(patterns: I, case_sensitive: bool) -> Result<AhoCorasick, BuildError>
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
let case_sensitive = self
|
||||
.select_next_is_case_sensitive
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).search.case_sensitive);
|
||||
|
||||
let mut builder = AhoCorasickBuilder::new();
|
||||
builder.ascii_case_insensitive(!case_sensitive);
|
||||
builder.build(patterns)
|
||||
}
|
||||
|
||||
fn build_select_state(
|
||||
&self,
|
||||
query: String,
|
||||
origin: SelectNextOrigin,
|
||||
done: bool,
|
||||
cx: &App,
|
||||
) -> Result<SelectNextState, BuildError> {
|
||||
let search_options = self.select_next_options.unwrap_or_else(|| {
|
||||
let search_settings = EditorSettings::get_global(cx).search;
|
||||
SelectSearchOptions {
|
||||
case_sensitive: search_settings.case_sensitive,
|
||||
whole_word: search_settings.whole_word,
|
||||
}
|
||||
});
|
||||
Ok(SelectNextState {
|
||||
query: Self::build_query(&[query], search_options.case_sensitive)?,
|
||||
wordwise: matches!(origin, SelectNextOrigin::Caret) || search_options.whole_word,
|
||||
done,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_syntax_node_boundary(
|
||||
&self,
|
||||
selection_pos: MultiBufferOffset,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ use workspace::{
|
|||
item::{ItemBufferKind, ItemHandle},
|
||||
searchable::{
|
||||
Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItemHandle,
|
||||
WeakSearchableItemHandle,
|
||||
SelectSearchOptions, WeakSearchableItemHandle,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -528,12 +528,14 @@ impl ToolbarItemView for BufferSearchBar {
|
|||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_searchable_item_subscriptions.take();
|
||||
self.active_searchable_item.take();
|
||||
self.splittable_editor = None;
|
||||
self._splittable_editor_subscription = None;
|
||||
|
||||
self.pending_search.take();
|
||||
|
||||
if let Some(item) = self.active_searchable_item.take() {
|
||||
item.set_select_search_options(None, cx);
|
||||
}
|
||||
|
||||
if let Some(splittable_editor) = item
|
||||
.and_then(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
|
||||
.and_then(|entity| entity.downcast::<SplittableEditor>().ok())
|
||||
|
|
@ -608,6 +610,7 @@ impl ToolbarItemView for BufferSearchBar {
|
|||
|
||||
let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
|
||||
self.active_searchable_item = Some(searchable_item_handle);
|
||||
self.sync_select_search_options(cx);
|
||||
drop(self.update_matches(true, false, window, cx));
|
||||
if self.needs_expand_collapse_option(cx) && self.is_dismissed() {
|
||||
return ToolbarItemLocation::PrimaryLeft;
|
||||
|
|
@ -823,7 +826,7 @@ impl BufferSearchBar {
|
|||
self.dismissed = true;
|
||||
cx.emit(Event::Dismissed);
|
||||
self.query_error = None;
|
||||
self.sync_select_next_case_sensitivity(cx);
|
||||
self.sync_select_search_options(cx);
|
||||
|
||||
for searchable_item in self.searchable_items_with_matches.keys() {
|
||||
if let Some(searchable_item) =
|
||||
|
|
@ -877,7 +880,6 @@ impl BufferSearchBar {
|
|||
}
|
||||
self.search_suggested(seed_query_override, window, cx);
|
||||
self.smartcase(window, cx);
|
||||
self.sync_select_next_case_sensitivity(cx);
|
||||
self.replace_enabled |= deploy.replace_enabled;
|
||||
self.selection_search_enabled =
|
||||
self.selection_search_enabled
|
||||
|
|
@ -940,6 +942,7 @@ impl BufferSearchBar {
|
|||
self.search_options.remove(SearchOptions::BACKWARDS);
|
||||
|
||||
self.dismissed = false;
|
||||
self.sync_select_search_options(cx);
|
||||
self.adjust_query_regex_language(cx);
|
||||
handle.search_bar_visibility_changed(true, window, cx);
|
||||
cx.notify();
|
||||
|
|
@ -1210,7 +1213,7 @@ impl BufferSearchBar {
|
|||
self.default_options = self.search_options;
|
||||
drop(self.update_matches(false, false, window, cx));
|
||||
self.adjust_query_regex_language(cx);
|
||||
self.sync_select_next_case_sensitivity(cx);
|
||||
self.sync_select_search_options(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -1245,7 +1248,7 @@ impl BufferSearchBar {
|
|||
pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
|
||||
self.search_options = search_options;
|
||||
self.adjust_query_regex_language(cx);
|
||||
self.sync_select_next_case_sensitivity(cx);
|
||||
self.sync_select_search_options(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -1849,21 +1852,24 @@ impl BufferSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
/// Updates the searchable item's case sensitivity option to match the
|
||||
/// search bar's current case sensitivity setting. This ensures that
|
||||
/// Updates the searchable item's search options to match the
|
||||
/// search bar's current settings. This ensures that
|
||||
/// editor's `select_next`/ `select_previous` operations respect the buffer
|
||||
/// search bar's search options.
|
||||
///
|
||||
/// Clears the case sensitivity when the search bar is dismissed so that
|
||||
/// Clears them when the search bar is dismissed so that
|
||||
/// only the editor's settings are respected.
|
||||
fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
|
||||
let case_sensitive = match self.dismissed {
|
||||
fn sync_select_search_options(&self, cx: &mut Context<Self>) {
|
||||
let search_options = match self.dismissed {
|
||||
true => None,
|
||||
false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
|
||||
false => Some(SelectSearchOptions {
|
||||
case_sensitive: self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
whole_word: self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
||||
active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
|
||||
active_searchable_item.set_select_search_options(search_options, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4297,6 +4303,212 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_set_active_pane_item_syncs_select_search_options(cx: &mut TestAppContext) {
|
||||
init_globals(cx);
|
||||
|
||||
// Set up 2 editors, with both buffers containing a lowercase and
|
||||
// uppercase version of the same word, so we can confirm whether case
|
||||
// sensitivity is affecting `editor: select next` .
|
||||
let cx = cx.add_empty_window();
|
||||
let buffer_a = cx.new(|cx| Buffer::local("zed\nZED\n", cx));
|
||||
let buffer_b = cx.new(|cx| Buffer::local("zed\nZED\n", cx));
|
||||
let editor_a = cx
|
||||
.new_window_entity(|window, cx| Editor::for_buffer(buffer_a.clone(), None, window, cx));
|
||||
let editor_b = cx
|
||||
.new_window_entity(|window, cx| Editor::for_buffer(buffer_b.clone(), None, window, cx));
|
||||
|
||||
let search_bar = cx.new_window_entity(|window, cx| {
|
||||
let mut search_bar = BufferSearchBar::new(None, window, cx);
|
||||
search_bar.set_active_pane_item(Some(&editor_a), window, cx);
|
||||
search_bar.show(window, cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx)
|
||||
});
|
||||
|
||||
// Editor A should use the search bar's case sensitivity option.
|
||||
editor_a.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)
|
||||
]);
|
||||
});
|
||||
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("selecting next should succeed, even if selections are not changed");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)]
|
||||
);
|
||||
});
|
||||
|
||||
// Switching the active item should move the search bar's options to
|
||||
// Editor B.
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
search_bar.set_active_pane_item(Some(&editor_b), window, cx)
|
||||
});
|
||||
|
||||
editor_b.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)
|
||||
]);
|
||||
});
|
||||
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("selecting next should succeed, even if selections are not changed");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)]
|
||||
);
|
||||
});
|
||||
|
||||
// Editor A should now fall back to the default case insensitive search
|
||||
// setting.
|
||||
editor_a.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)
|
||||
]);
|
||||
});
|
||||
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("selecting next should succeed, even if selections are not changed");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 3)
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_option_change_during_selections(cx: &mut TestAppContext) {
|
||||
init_globals(cx);
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndefabc\nghiabc\nabc", cx));
|
||||
let editor =
|
||||
cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
|
||||
|
||||
let search_bar = cx.new_window_entity(|window, cx| {
|
||||
let mut search_bar = BufferSearchBar::new(None, window, cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), window, cx);
|
||||
search_bar.show(window, cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)
|
||||
]);
|
||||
});
|
||||
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("should be able to select next");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 6)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Toggle the "Whole Word" search option, so we can later check that
|
||||
// only the last line is now going to be selected.
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
search_bar.toggle_whole_word(&Default::default(), window, cx)
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("should be able to select next");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 6),
|
||||
DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 3)
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_show_syncs_select_search_options(cx: &mut TestAppContext) {
|
||||
init_globals(cx);
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
let buffer = cx.new(|cx| Buffer::local("zed\nzedfoo\nzed", cx));
|
||||
let editor =
|
||||
cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
|
||||
|
||||
let search_bar = cx.new_window_entity(|window, cx| {
|
||||
let mut search_bar = BufferSearchBar::new(None, window, cx);
|
||||
search_bar.set_active_pane_item(Some(&editor), window, cx);
|
||||
search_bar.show(window, cx);
|
||||
search_bar
|
||||
});
|
||||
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
search_bar.set_search_options(SearchOptions::WHOLE_WORD, cx);
|
||||
|
||||
// Reopening the search bar with the same options should sync them
|
||||
// back to the editor after dismissing cleared them from the editor.
|
||||
search_bar.dismiss(&Default::default(), window, cx);
|
||||
search_bar.show(window, cx)
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3)
|
||||
]);
|
||||
});
|
||||
|
||||
editor
|
||||
.select_next(&Default::default(), window, cx)
|
||||
.expect("should be able to select next");
|
||||
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.display_ranges(&editor.display_snapshot(cx)),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 3)
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ pub enum FilteredSearchRange {
|
|||
Default,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct SelectSearchOptions {
|
||||
pub case_sensitive: bool,
|
||||
pub whole_word: bool,
|
||||
}
|
||||
|
||||
pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
||||
type Match: Any + Sync + Send + Clone;
|
||||
|
||||
|
|
@ -205,7 +211,8 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize>;
|
||||
fn set_search_is_case_sensitive(&mut self, _: Option<bool>, _: &mut Context<Self>) {}
|
||||
fn set_select_search_options(&mut self, _: Option<SelectSearchOptions>, _: &mut Context<Self>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SearchableItemHandle: ItemHandle {
|
||||
|
|
@ -303,7 +310,7 @@ pub trait SearchableItemHandle: ItemHandle {
|
|||
cx: &mut App,
|
||||
);
|
||||
|
||||
fn set_search_is_case_sensitive(&self, is_case_sensitive: Option<bool>, cx: &mut App);
|
||||
fn set_select_search_options(&self, search_options: Option<SelectSearchOptions>, cx: &mut App);
|
||||
}
|
||||
|
||||
impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
|
||||
|
|
@ -507,9 +514,9 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
|
|||
this.toggle_filtered_search_ranges(enabled, window, cx)
|
||||
});
|
||||
}
|
||||
fn set_search_is_case_sensitive(&self, enabled: Option<bool>, cx: &mut App) {
|
||||
fn set_select_search_options(&self, search_options: Option<SelectSearchOptions>, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_search_is_case_sensitive(enabled, cx)
|
||||
this.set_select_search_options(search_options, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue