Closes #28617 <img width="400" alt="image" src="https://github.com/user-attachments/assets/b1c2880c-5744-4bed-a687-5c5e7aa7fef5" /> Release Notes: - Improved visibility of the currently active match when browsing results in buffer or project search. --------- Co-authored-by: DarkMatter-999 <darkmatter999official@gmail.com>
3119 lines
118 KiB
Rust
3119 lines
118 KiB
Rust
mod registrar;
|
|
|
|
use crate::{
|
|
FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption,
|
|
SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch,
|
|
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
|
|
search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
|
|
};
|
|
use any_vec::AnyVec;
|
|
use anyhow::Context as _;
|
|
use collections::HashMap;
|
|
use editor::{
|
|
DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
|
|
actions::{Backtab, Tab},
|
|
};
|
|
use futures::channel::oneshot;
|
|
use gpui::{
|
|
Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
|
|
IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
|
|
Window, actions, div,
|
|
};
|
|
use language::{Language, LanguageRegistry};
|
|
use project::{
|
|
search::SearchQuery,
|
|
search_history::{SearchHistory, SearchHistoryCursor},
|
|
};
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use settings::Settings;
|
|
use std::sync::Arc;
|
|
use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
|
|
|
|
use ui::{
|
|
BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*,
|
|
utils::SearchInputWidth,
|
|
};
|
|
use util::{ResultExt, paths::PathMatcher};
|
|
use workspace::{
|
|
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
|
item::ItemHandle,
|
|
searchable::{
|
|
Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle,
|
|
},
|
|
};
|
|
|
|
pub use registrar::DivRegistrar;
|
|
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
|
|
|
|
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
|
|
|
|
/// Opens the buffer search interface with the specified configuration.
|
|
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
|
|
#[action(namespace = buffer_search)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Deploy {
|
|
#[serde(default = "util::serde::default_true")]
|
|
pub focus: bool,
|
|
#[serde(default)]
|
|
pub replace_enabled: bool,
|
|
#[serde(default)]
|
|
pub selection_search_enabled: bool,
|
|
}
|
|
|
|
actions!(
|
|
buffer_search,
|
|
[
|
|
/// Deploys the search and replace interface.
|
|
DeployReplace,
|
|
/// Dismisses the search bar.
|
|
Dismiss,
|
|
/// Focuses back on the editor.
|
|
FocusEditor
|
|
]
|
|
);
|
|
|
|
impl Deploy {
|
|
pub fn find() -> Self {
|
|
Self {
|
|
focus: true,
|
|
replace_enabled: false,
|
|
selection_search_enabled: false,
|
|
}
|
|
}
|
|
|
|
pub fn replace() -> Self {
|
|
Self {
|
|
focus: true,
|
|
replace_enabled: true,
|
|
selection_search_enabled: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum Event {
|
|
UpdateLocation,
|
|
}
|
|
|
|
pub fn init(cx: &mut App) {
|
|
cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace))
|
|
.detach();
|
|
}
|
|
|
|
pub struct BufferSearchBar {
|
|
query_editor: Entity<Editor>,
|
|
query_editor_focused: bool,
|
|
replacement_editor: Entity<Editor>,
|
|
replacement_editor_focused: bool,
|
|
active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
|
|
active_match_index: Option<usize>,
|
|
active_searchable_item_subscription: Option<Subscription>,
|
|
active_search: Option<Arc<SearchQuery>>,
|
|
searchable_items_with_matches: HashMap<Box<dyn WeakSearchableItemHandle>, AnyVec<dyn Send>>,
|
|
pending_search: Option<Task<()>>,
|
|
search_options: SearchOptions,
|
|
default_options: SearchOptions,
|
|
configured_options: SearchOptions,
|
|
query_error: Option<String>,
|
|
dismissed: bool,
|
|
search_history: SearchHistory,
|
|
search_history_cursor: SearchHistoryCursor,
|
|
replace_enabled: bool,
|
|
selection_search_enabled: Option<FilteredSearchRange>,
|
|
scroll_handle: ScrollHandle,
|
|
editor_scroll_handle: ScrollHandle,
|
|
editor_needed_width: Pixels,
|
|
regex_language: Option<Arc<Language>>,
|
|
}
|
|
|
|
impl EventEmitter<Event> for BufferSearchBar {}
|
|
impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
|
|
impl Render for BufferSearchBar {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
if self.dismissed {
|
|
return div().id("search_bar");
|
|
}
|
|
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let narrow_mode =
|
|
self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
|
|
let hide_inline_icons = self.editor_needed_width
|
|
> self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
|
|
|
|
let workspace::searchable::SearchOptions {
|
|
case,
|
|
word,
|
|
regex,
|
|
replacement,
|
|
selection,
|
|
find_in_results,
|
|
} = self.supported_options(cx);
|
|
|
|
self.query_editor.update(cx, |query_editor, cx| {
|
|
if query_editor.placeholder_text(cx).is_none() {
|
|
query_editor.set_placeholder_text("Search…", window, cx);
|
|
}
|
|
});
|
|
|
|
self.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_placeholder_text("Replace with…", window, cx);
|
|
});
|
|
|
|
let mut color_override = None;
|
|
let match_text = self
|
|
.active_searchable_item
|
|
.as_ref()
|
|
.and_then(|searchable_item| {
|
|
if self.query(cx).is_empty() {
|
|
return None;
|
|
}
|
|
let matches_count = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
.map(AnyVec::len)
|
|
.unwrap_or(0);
|
|
if let Some(match_ix) = self.active_match_index {
|
|
Some(format!("{}/{}", match_ix + 1, matches_count))
|
|
} else {
|
|
color_override = Some(Color::Error); // No matches found
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "0/0".to_string());
|
|
let should_show_replace_input = self.replace_enabled && replacement;
|
|
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
|
|
|
|
let theme_colors = cx.theme().colors();
|
|
let query_border = if self.query_error.is_some() {
|
|
Color::Error.color(cx)
|
|
} else {
|
|
theme_colors.border
|
|
};
|
|
let replacement_border = theme_colors.border;
|
|
|
|
let container_width = window.viewport_size().width;
|
|
let input_width = SearchInputWidth::calc_width(container_width);
|
|
|
|
let input_base_styles =
|
|
|border_color| input_base_styles(border_color, |div| div.w(input_width));
|
|
|
|
let query_column = input_base_styles(query_border)
|
|
.id("editor-scroll")
|
|
.track_scroll(&self.editor_scroll_handle)
|
|
.child(render_text_input(&self.query_editor, color_override, cx))
|
|
.when(!hide_inline_icons, |div| {
|
|
div.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.when(case, |div| {
|
|
div.child(SearchOption::CaseSensitive.as_button(
|
|
self.search_options,
|
|
SearchSource::Buffer,
|
|
focus_handle.clone(),
|
|
))
|
|
})
|
|
.when(word, |div| {
|
|
div.child(SearchOption::WholeWord.as_button(
|
|
self.search_options,
|
|
SearchSource::Buffer,
|
|
focus_handle.clone(),
|
|
))
|
|
})
|
|
.when(regex, |div| {
|
|
div.child(SearchOption::Regex.as_button(
|
|
self.search_options,
|
|
SearchSource::Buffer,
|
|
focus_handle.clone(),
|
|
))
|
|
}),
|
|
)
|
|
});
|
|
|
|
let mode_column = h_flex()
|
|
.gap_1()
|
|
.min_w_64()
|
|
.when(replacement, |this| {
|
|
this.child(render_action_button(
|
|
"buffer-search-bar-toggle",
|
|
IconName::Replace,
|
|
self.replace_enabled.then_some(ActionButtonState::Toggled),
|
|
"Toggle Replace",
|
|
&ToggleReplace,
|
|
focus_handle.clone(),
|
|
))
|
|
})
|
|
.when(selection, |this| {
|
|
this.child(
|
|
IconButton::new(
|
|
"buffer-search-bar-toggle-search-selection-button",
|
|
IconName::Quote,
|
|
)
|
|
.style(ButtonStyle::Subtle)
|
|
.shape(IconButtonShape::Square)
|
|
.when(self.selection_search_enabled.is_some(), |button| {
|
|
button.style(ButtonStyle::Filled)
|
|
})
|
|
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
|
this.toggle_selection(&ToggleSelection, window, cx);
|
|
}))
|
|
.toggle_state(self.selection_search_enabled.is_some())
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |_window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Search Selection",
|
|
&ToggleSelection,
|
|
&focus_handle,
|
|
cx,
|
|
)
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.when(!find_in_results, |el| {
|
|
let query_focus = self.query_editor.focus_handle(cx);
|
|
let matches_column = h_flex()
|
|
.pl_2()
|
|
.ml_2()
|
|
.border_l_1()
|
|
.border_color(theme_colors.border_variant)
|
|
.child(render_action_button(
|
|
"buffer-search-nav-button",
|
|
ui::IconName::ChevronLeft,
|
|
self.active_match_index
|
|
.is_none()
|
|
.then_some(ActionButtonState::Disabled),
|
|
"Select Previous Match",
|
|
&SelectPreviousMatch,
|
|
query_focus.clone(),
|
|
))
|
|
.child(render_action_button(
|
|
"buffer-search-nav-button",
|
|
ui::IconName::ChevronRight,
|
|
self.active_match_index
|
|
.is_none()
|
|
.then_some(ActionButtonState::Disabled),
|
|
"Select Next Match",
|
|
&SelectNextMatch,
|
|
query_focus.clone(),
|
|
))
|
|
.when(!narrow_mode, |this| {
|
|
this.child(div().ml_2().min_w(rems_from_px(40.)).child(
|
|
Label::new(match_text).size(LabelSize::Small).color(
|
|
if self.active_match_index.is_some() {
|
|
Color::Default
|
|
} else {
|
|
Color::Disabled
|
|
},
|
|
),
|
|
))
|
|
});
|
|
|
|
el.child(render_action_button(
|
|
"buffer-search-nav-button",
|
|
IconName::SelectAll,
|
|
Default::default(),
|
|
"Select All Matches",
|
|
&SelectAllMatches,
|
|
query_focus,
|
|
))
|
|
.child(matches_column)
|
|
})
|
|
.when(find_in_results, |el| {
|
|
el.child(render_action_button(
|
|
"buffer-search",
|
|
IconName::Close,
|
|
Default::default(),
|
|
"Close Search Bar",
|
|
&Dismiss,
|
|
focus_handle.clone(),
|
|
))
|
|
});
|
|
|
|
let search_line = h_flex()
|
|
.w_full()
|
|
.gap_2()
|
|
.when(find_in_results, |el| {
|
|
el.child(Label::new("Find in results").color(Color::Hint))
|
|
})
|
|
.child(query_column)
|
|
.child(mode_column);
|
|
|
|
let replace_line =
|
|
should_show_replace_input.then(|| {
|
|
let replace_column = input_base_styles(replacement_border)
|
|
.child(render_text_input(&self.replacement_editor, None, cx));
|
|
let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
|
|
|
|
let replace_actions = h_flex()
|
|
.min_w_64()
|
|
.gap_1()
|
|
.child(render_action_button(
|
|
"buffer-search-replace-button",
|
|
IconName::ReplaceNext,
|
|
Default::default(),
|
|
"Replace Next Match",
|
|
&ReplaceNext,
|
|
focus_handle.clone(),
|
|
))
|
|
.child(render_action_button(
|
|
"buffer-search-replace-button",
|
|
IconName::ReplaceAll,
|
|
Default::default(),
|
|
"Replace All Matches",
|
|
&ReplaceAll,
|
|
focus_handle,
|
|
));
|
|
h_flex()
|
|
.w_full()
|
|
.gap_2()
|
|
.child(replace_column)
|
|
.child(replace_actions)
|
|
});
|
|
|
|
let mut key_context = KeyContext::new_with_defaults();
|
|
key_context.add("BufferSearchBar");
|
|
if in_replace {
|
|
key_context.add("in_replace");
|
|
}
|
|
|
|
let query_error_line = self.query_error.as_ref().map(|error| {
|
|
Label::new(error)
|
|
.size(LabelSize::Small)
|
|
.color(Color::Error)
|
|
.mt_neg_1()
|
|
.ml_2()
|
|
});
|
|
|
|
let search_line =
|
|
h_flex()
|
|
.relative()
|
|
.child(search_line)
|
|
.when(!narrow_mode && !find_in_results, |div| {
|
|
div.child(h_flex().absolute().right_0().child(render_action_button(
|
|
"buffer-search",
|
|
IconName::Close,
|
|
Default::default(),
|
|
"Close Search Bar",
|
|
&Dismiss,
|
|
focus_handle.clone(),
|
|
)))
|
|
.w_full()
|
|
});
|
|
v_flex()
|
|
.id("buffer_search")
|
|
.gap_2()
|
|
.py(px(1.0))
|
|
.w_full()
|
|
.track_scroll(&self.scroll_handle)
|
|
.key_context(key_context)
|
|
.capture_action(cx.listener(Self::tab))
|
|
.capture_action(cx.listener(Self::backtab))
|
|
.on_action(cx.listener(Self::previous_history_query))
|
|
.on_action(cx.listener(Self::next_history_query))
|
|
.on_action(cx.listener(Self::dismiss))
|
|
.on_action(cx.listener(Self::select_next_match))
|
|
.on_action(cx.listener(Self::select_prev_match))
|
|
.on_action(cx.listener(|this, _: &ToggleOutline, window, cx| {
|
|
if let Some(active_searchable_item) = &mut this.active_searchable_item {
|
|
active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
|
|
}
|
|
}))
|
|
.on_action(cx.listener(|this, _: &CopyPath, window, cx| {
|
|
if let Some(active_searchable_item) = &mut this.active_searchable_item {
|
|
active_searchable_item.relay_action(Box::new(CopyPath), window, cx);
|
|
}
|
|
}))
|
|
.on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| {
|
|
if let Some(active_searchable_item) = &mut this.active_searchable_item {
|
|
active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx);
|
|
}
|
|
}))
|
|
.when(replacement, |this| {
|
|
this.on_action(cx.listener(Self::toggle_replace))
|
|
.on_action(cx.listener(Self::replace_next))
|
|
.on_action(cx.listener(Self::replace_all))
|
|
})
|
|
.when(case, |this| {
|
|
this.on_action(cx.listener(Self::toggle_case_sensitive))
|
|
})
|
|
.when(word, |this| {
|
|
this.on_action(cx.listener(Self::toggle_whole_word))
|
|
})
|
|
.when(regex, |this| {
|
|
this.on_action(cx.listener(Self::toggle_regex))
|
|
})
|
|
.when(selection, |this| {
|
|
this.on_action(cx.listener(Self::toggle_selection))
|
|
})
|
|
.child(search_line)
|
|
.children(query_error_line)
|
|
.children(replace_line)
|
|
}
|
|
}
|
|
|
|
impl Focusable for BufferSearchBar {
|
|
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
|
self.query_editor.focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl ToolbarItemView for BufferSearchBar {
|
|
fn contribute_context(&self, context: &mut KeyContext, _cx: &App) {
|
|
if !self.dismissed {
|
|
context.add("buffer_search_deployed");
|
|
}
|
|
}
|
|
|
|
fn set_active_pane_item(
|
|
&mut self,
|
|
item: Option<&dyn ItemHandle>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> ToolbarItemLocation {
|
|
cx.notify();
|
|
self.active_searchable_item_subscription.take();
|
|
self.active_searchable_item.take();
|
|
|
|
self.pending_search.take();
|
|
|
|
if let Some(searchable_item_handle) =
|
|
item.and_then(|item| item.to_searchable_item_handle(cx))
|
|
{
|
|
let this = cx.entity().downgrade();
|
|
|
|
self.active_searchable_item_subscription =
|
|
Some(searchable_item_handle.subscribe_to_search_events(
|
|
window,
|
|
cx,
|
|
Box::new(move |search_event, window, cx| {
|
|
if let Some(this) = this.upgrade() {
|
|
this.update(cx, |this, cx| {
|
|
this.on_active_searchable_item_event(search_event, window, cx)
|
|
});
|
|
}
|
|
}),
|
|
));
|
|
|
|
let is_project_search = searchable_item_handle.supported_options(cx).find_in_results;
|
|
self.active_searchable_item = Some(searchable_item_handle);
|
|
drop(self.update_matches(true, false, window, cx));
|
|
if !self.dismissed {
|
|
if is_project_search {
|
|
self.dismiss(&Default::default(), window, cx);
|
|
} else {
|
|
return ToolbarItemLocation::Secondary;
|
|
}
|
|
}
|
|
}
|
|
ToolbarItemLocation::Hidden
|
|
}
|
|
}
|
|
|
|
impl BufferSearchBar {
|
|
pub fn query_editor_focused(&self) -> bool {
|
|
self.query_editor_focused
|
|
}
|
|
|
|
pub fn register(registrar: &mut impl SearchActionsRegistrar) {
|
|
registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
|
|
this.query_editor.focus_handle(cx).focus(window);
|
|
this.select_query(window, cx);
|
|
}));
|
|
registrar.register_handler(ForDeployed(
|
|
|this, action: &ToggleCaseSensitive, window, cx| {
|
|
if this.supported_options(cx).case {
|
|
this.toggle_case_sensitive(action, window, cx);
|
|
}
|
|
},
|
|
));
|
|
registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| {
|
|
if this.supported_options(cx).word {
|
|
this.toggle_whole_word(action, window, cx);
|
|
}
|
|
}));
|
|
registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| {
|
|
if this.supported_options(cx).regex {
|
|
this.toggle_regex(action, window, cx);
|
|
}
|
|
}));
|
|
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| {
|
|
if this.supported_options(cx).selection {
|
|
this.toggle_selection(action, window, cx);
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}));
|
|
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| {
|
|
if this.supported_options(cx).replacement {
|
|
this.toggle_replace(action, window, cx);
|
|
} else {
|
|
cx.propagate();
|
|
}
|
|
}));
|
|
registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| {
|
|
if this.supported_options(cx).find_in_results {
|
|
cx.propagate();
|
|
} else {
|
|
this.select_next_match(action, window, cx);
|
|
}
|
|
}));
|
|
registrar.register_handler(WithResults(
|
|
|this, action: &SelectPreviousMatch, window, cx| {
|
|
if this.supported_options(cx).find_in_results {
|
|
cx.propagate();
|
|
} else {
|
|
this.select_prev_match(action, window, cx);
|
|
}
|
|
},
|
|
));
|
|
registrar.register_handler(WithResults(
|
|
|this, action: &SelectAllMatches, window, cx| {
|
|
if this.supported_options(cx).find_in_results {
|
|
cx.propagate();
|
|
} else {
|
|
this.select_all_matches(action, window, cx);
|
|
}
|
|
},
|
|
));
|
|
registrar.register_handler(ForDeployed(
|
|
|this, _: &editor::actions::Cancel, window, cx| {
|
|
this.dismiss(&Dismiss, window, cx);
|
|
},
|
|
));
|
|
registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| {
|
|
this.dismiss(&Dismiss, window, cx);
|
|
}));
|
|
|
|
// register deploy buffer search for both search bar states, since we want to focus into the search bar
|
|
// when the deploy action is triggered in the buffer.
|
|
registrar.register_handler(ForDeployed(|this, deploy, window, cx| {
|
|
this.deploy(deploy, window, cx);
|
|
}));
|
|
registrar.register_handler(ForDismissed(|this, deploy, window, cx| {
|
|
this.deploy(deploy, window, cx);
|
|
}));
|
|
registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| {
|
|
if this.supported_options(cx).find_in_results {
|
|
cx.propagate();
|
|
} else {
|
|
this.deploy(&Deploy::replace(), window, cx);
|
|
}
|
|
}));
|
|
registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| {
|
|
if this.supported_options(cx).find_in_results {
|
|
cx.propagate();
|
|
} else {
|
|
this.deploy(&Deploy::replace(), window, cx);
|
|
}
|
|
}));
|
|
}
|
|
|
|
pub fn new(
|
|
languages: Option<Arc<LanguageRegistry>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let query_editor = cx.new(|cx| {
|
|
let mut editor = Editor::single_line(window, cx);
|
|
editor.set_use_autoclose(false);
|
|
editor
|
|
});
|
|
cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
|
|
.detach();
|
|
let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
|
|
cx.subscribe(&replacement_editor, Self::on_replacement_editor_event)
|
|
.detach();
|
|
|
|
let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
|
|
if let Some(languages) = languages {
|
|
let query_buffer = query_editor
|
|
.read(cx)
|
|
.buffer()
|
|
.read(cx)
|
|
.as_singleton()
|
|
.expect("query editor should be backed by a singleton buffer");
|
|
query_buffer
|
|
.read(cx)
|
|
.set_language_registry(languages.clone());
|
|
|
|
cx.spawn(async move |buffer_search_bar, cx| {
|
|
let regex_language = languages
|
|
.language_for_name("regex")
|
|
.await
|
|
.context("loading regex language")?;
|
|
buffer_search_bar
|
|
.update(cx, |buffer_search_bar, cx| {
|
|
buffer_search_bar.regex_language = Some(regex_language);
|
|
buffer_search_bar.adjust_query_regex_language(cx);
|
|
})
|
|
.ok();
|
|
anyhow::Ok(())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
Self {
|
|
query_editor,
|
|
query_editor_focused: false,
|
|
replacement_editor,
|
|
replacement_editor_focused: false,
|
|
active_searchable_item: None,
|
|
active_searchable_item_subscription: None,
|
|
active_match_index: None,
|
|
searchable_items_with_matches: Default::default(),
|
|
default_options: search_options,
|
|
configured_options: search_options,
|
|
search_options,
|
|
pending_search: None,
|
|
query_error: None,
|
|
dismissed: true,
|
|
search_history: SearchHistory::new(
|
|
Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
|
|
project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
|
|
),
|
|
search_history_cursor: Default::default(),
|
|
active_search: None,
|
|
replace_enabled: false,
|
|
selection_search_enabled: None,
|
|
scroll_handle: ScrollHandle::new(),
|
|
editor_scroll_handle: ScrollHandle::new(),
|
|
editor_needed_width: px(0.),
|
|
regex_language: None,
|
|
}
|
|
}
|
|
|
|
pub fn is_dismissed(&self) -> bool {
|
|
self.dismissed
|
|
}
|
|
|
|
pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.dismissed = true;
|
|
self.query_error = None;
|
|
self.sync_select_next_case_sensitivity(cx);
|
|
|
|
for searchable_item in self.searchable_items_with_matches.keys() {
|
|
if let Some(searchable_item) =
|
|
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
|
{
|
|
searchable_item.clear_matches(window, cx);
|
|
}
|
|
}
|
|
if let Some(active_editor) = self.active_searchable_item.as_mut() {
|
|
self.selection_search_enabled = None;
|
|
self.replace_enabled = false;
|
|
active_editor.search_bar_visibility_changed(false, window, cx);
|
|
active_editor.toggle_filtered_search_ranges(None, window, cx);
|
|
let handle = active_editor.item_focus_handle(cx);
|
|
self.focus(&handle, window);
|
|
}
|
|
|
|
cx.emit(Event::UpdateLocation);
|
|
cx.emit(ToolbarItemEvent::ChangeLocation(
|
|
ToolbarItemLocation::Hidden,
|
|
));
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
|
let filtered_search_range = if deploy.selection_search_enabled {
|
|
Some(FilteredSearchRange::Default)
|
|
} else {
|
|
None
|
|
};
|
|
if self.show(window, cx) {
|
|
if let Some(active_item) = self.active_searchable_item.as_mut() {
|
|
active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx);
|
|
}
|
|
self.search_suggested(window, cx);
|
|
self.smartcase(window, cx);
|
|
self.sync_select_next_case_sensitivity(cx);
|
|
self.replace_enabled = deploy.replace_enabled;
|
|
self.selection_search_enabled = if deploy.selection_search_enabled {
|
|
Some(FilteredSearchRange::Default)
|
|
} else {
|
|
None
|
|
};
|
|
if deploy.focus {
|
|
let mut handle = self.query_editor.focus_handle(cx);
|
|
let mut select_query = true;
|
|
if deploy.replace_enabled && handle.is_focused(window) {
|
|
handle = self.replacement_editor.focus_handle(cx);
|
|
select_query = false;
|
|
};
|
|
|
|
if select_query {
|
|
self.select_query(window, cx);
|
|
}
|
|
|
|
window.focus(&handle);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
cx.propagate();
|
|
false
|
|
}
|
|
|
|
pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.is_dismissed() {
|
|
self.deploy(action, window, cx);
|
|
} else {
|
|
self.dismiss(&Dismiss, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
|
let Some(handle) = self.active_searchable_item.as_ref() else {
|
|
return false;
|
|
};
|
|
|
|
let configured_options =
|
|
SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
|
|
let settings_changed = configured_options != self.configured_options;
|
|
|
|
if self.dismissed && settings_changed {
|
|
// Only update configuration options when search bar is dismissed,
|
|
// so we don't miss updates even after calling show twice
|
|
self.configured_options = configured_options;
|
|
self.search_options = configured_options;
|
|
self.default_options = configured_options;
|
|
}
|
|
|
|
self.dismissed = false;
|
|
self.adjust_query_regex_language(cx);
|
|
handle.search_bar_visibility_changed(true, window, cx);
|
|
cx.notify();
|
|
cx.emit(Event::UpdateLocation);
|
|
cx.emit(ToolbarItemEvent::ChangeLocation(
|
|
ToolbarItemLocation::Secondary,
|
|
));
|
|
true
|
|
}
|
|
|
|
fn supported_options(&self, cx: &mut Context<Self>) -> workspace::searchable::SearchOptions {
|
|
self.active_searchable_item
|
|
.as_ref()
|
|
.map(|item| item.supported_options(cx))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let search = self.query_suggestion(window, cx).map(|suggestion| {
|
|
self.search(&suggestion, Some(self.default_options), true, window, cx)
|
|
});
|
|
|
|
if let Some(search) = search {
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
if search.await.is_ok() {
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.activate_current_match(window, cx)
|
|
})
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
}
|
|
|
|
pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(match_ix) = self.active_match_index
|
|
&& let Some(active_searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&active_searchable_item.downgrade())
|
|
{
|
|
active_searchable_item.activate_match(match_ix, matches, window, cx)
|
|
}
|
|
}
|
|
|
|
pub fn select_query(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.query_editor.update(cx, |query_editor, cx| {
|
|
query_editor.select_all(&Default::default(), window, cx);
|
|
});
|
|
}
|
|
|
|
pub fn query(&self, cx: &App) -> String {
|
|
self.query_editor.read(cx).text(cx)
|
|
}
|
|
|
|
pub fn replacement(&self, cx: &mut App) -> String {
|
|
self.replacement_editor.read(cx).text(cx)
|
|
}
|
|
|
|
pub fn query_suggestion(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<String> {
|
|
self.active_searchable_item
|
|
.as_ref()
|
|
.map(|searchable_item| searchable_item.query_suggestion(window, cx))
|
|
.filter(|suggestion| !suggestion.is_empty())
|
|
}
|
|
|
|
pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context<Self>) {
|
|
if replacement.is_none() {
|
|
self.replace_enabled = false;
|
|
return;
|
|
}
|
|
self.replace_enabled = true;
|
|
self.replacement_editor
|
|
.update(cx, |replacement_editor, cx| {
|
|
replacement_editor
|
|
.buffer()
|
|
.update(cx, |replacement_buffer, cx| {
|
|
let len = replacement_buffer.len(cx);
|
|
replacement_buffer.edit(
|
|
[(MultiBufferOffset(0)..len, replacement.unwrap())],
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.focus(&self.replacement_editor.focus_handle(cx), window);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn search(
|
|
&mut self,
|
|
query: &str,
|
|
options: Option<SearchOptions>,
|
|
add_to_history: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<()> {
|
|
let options = options.unwrap_or(self.default_options);
|
|
let updated = query != self.query(cx) || self.search_options != options;
|
|
if updated {
|
|
self.query_editor.update(cx, |query_editor, cx| {
|
|
query_editor.buffer().update(cx, |query_buffer, cx| {
|
|
let len = query_buffer.len(cx);
|
|
query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx);
|
|
});
|
|
});
|
|
self.set_search_options(options, cx);
|
|
self.clear_matches(window, cx);
|
|
cx.notify();
|
|
}
|
|
self.update_matches(!updated, add_to_history, window, cx)
|
|
}
|
|
|
|
pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
|
let handle = active_editor.item_focus_handle(cx);
|
|
window.focus(&handle);
|
|
}
|
|
}
|
|
|
|
pub fn toggle_search_option(
|
|
&mut self,
|
|
search_option: SearchOptions,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.search_options.toggle(search_option);
|
|
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);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool {
|
|
self.search_options.contains(search_option)
|
|
}
|
|
|
|
pub fn enable_search_option(
|
|
&mut self,
|
|
search_option: SearchOptions,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !self.search_options.contains(search_option) {
|
|
self.toggle_search_option(search_option, window, cx)
|
|
}
|
|
}
|
|
|
|
pub fn set_search_within_selection(
|
|
&mut self,
|
|
search_within_selection: Option<FilteredSearchRange>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<oneshot::Receiver<()>> {
|
|
let active_item = self.active_searchable_item.as_mut()?;
|
|
self.selection_search_enabled = search_within_selection;
|
|
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
|
|
cx.notify();
|
|
Some(self.update_matches(false, false, window, cx))
|
|
}
|
|
|
|
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);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn clear_search_within_ranges(
|
|
&mut self,
|
|
search_options: SearchOptions,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.search_options = search_options;
|
|
self.adjust_query_regex_language(cx);
|
|
cx.notify();
|
|
}
|
|
|
|
fn select_next_match(
|
|
&mut self,
|
|
_: &SelectNextMatch,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_match(Direction::Next, 1, window, cx);
|
|
}
|
|
|
|
fn select_prev_match(
|
|
&mut self,
|
|
_: &SelectPreviousMatch,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_match(Direction::Prev, 1, window, cx);
|
|
}
|
|
|
|
pub fn select_all_matches(
|
|
&mut self,
|
|
_: &SelectAllMatches,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !self.dismissed
|
|
&& self.active_match_index.is_some()
|
|
&& let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
searchable_item.select_matches(matches, window, cx);
|
|
self.focus_editor(&FocusEditor, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn select_match(
|
|
&mut self,
|
|
direction: Direction,
|
|
count: usize,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(index) = self.active_match_index
|
|
&& let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
.filter(|matches| !matches.is_empty())
|
|
{
|
|
// If 'wrapscan' is disabled, searches do not wrap around the end of the file.
|
|
if !EditorSettings::get_global(cx).search_wrap
|
|
&& ((direction == Direction::Next && index + count >= matches.len())
|
|
|| (direction == Direction::Prev && index < count))
|
|
{
|
|
crate::show_no_more_matches(window, cx);
|
|
return;
|
|
}
|
|
let new_match_index = searchable_item
|
|
.match_index_for_direction(matches, index, direction, count, window, cx);
|
|
|
|
searchable_item.update_matches(matches, Some(new_match_index), window, cx);
|
|
searchable_item.activate_match(new_match_index, matches, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
if matches.is_empty() {
|
|
return;
|
|
}
|
|
searchable_item.update_matches(matches, Some(0), window, cx);
|
|
searchable_item.activate_match(0, matches, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
if matches.is_empty() {
|
|
return;
|
|
}
|
|
let new_match_index = matches.len() - 1;
|
|
searchable_item.update_matches(matches, Some(new_match_index), window, cx);
|
|
searchable_item.activate_match(new_match_index, matches, window, cx);
|
|
}
|
|
}
|
|
|
|
fn on_query_editor_event(
|
|
&mut self,
|
|
editor: &Entity<Editor>,
|
|
event: &editor::EditorEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
editor::EditorEvent::Focused => self.query_editor_focused = true,
|
|
editor::EditorEvent::Blurred => self.query_editor_focused = false,
|
|
editor::EditorEvent::Edited { .. } => {
|
|
self.smartcase(window, cx);
|
|
self.clear_matches(window, cx);
|
|
let search = self.update_matches(false, true, window, cx);
|
|
|
|
let width = editor.update(cx, |editor, cx| {
|
|
let text_layout_details = editor.text_layout_details(window);
|
|
let snapshot = editor.snapshot(window, cx).display_snapshot;
|
|
|
|
snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
|
|
- snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
|
|
});
|
|
self.editor_needed_width = width;
|
|
cx.notify();
|
|
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
if search.await.is_ok() {
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.activate_current_match(window, cx)
|
|
})
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn on_replacement_editor_event(
|
|
&mut self,
|
|
_: Entity<Editor>,
|
|
event: &editor::EditorEvent,
|
|
_: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
editor::EditorEvent::Focused => self.replacement_editor_focused = true,
|
|
editor::EditorEvent::Blurred => self.replacement_editor_focused = false,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn on_active_searchable_item_event(
|
|
&mut self,
|
|
event: &SearchEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
SearchEvent::MatchesInvalidated => {
|
|
drop(self.update_matches(false, false, window, cx));
|
|
}
|
|
SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx),
|
|
}
|
|
}
|
|
|
|
fn toggle_case_sensitive(
|
|
&mut self,
|
|
_: &ToggleCaseSensitive,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx)
|
|
}
|
|
|
|
fn toggle_whole_word(
|
|
&mut self,
|
|
_: &ToggleWholeWord,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
|
|
}
|
|
|
|
fn toggle_selection(
|
|
&mut self,
|
|
_: &ToggleSelection,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.set_search_within_selection(
|
|
if let Some(_) = self.selection_search_enabled {
|
|
None
|
|
} else {
|
|
Some(FilteredSearchRange::Default)
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.toggle_search_option(SearchOptions::REGEX, window, cx)
|
|
}
|
|
|
|
fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) {
|
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
|
self.active_match_index = None;
|
|
self.searchable_items_with_matches
|
|
.remove(&active_searchable_item.downgrade());
|
|
active_searchable_item.clear_matches(window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn has_active_match(&self) -> bool {
|
|
self.active_match_index.is_some()
|
|
}
|
|
|
|
fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let mut active_item_matches = None;
|
|
for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
|
|
if let Some(searchable_item) =
|
|
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
|
{
|
|
if Some(&searchable_item) == self.active_searchable_item.as_ref() {
|
|
active_item_matches = Some((searchable_item.downgrade(), matches));
|
|
} else {
|
|
searchable_item.clear_matches(window, cx);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.searchable_items_with_matches
|
|
.extend(active_item_matches);
|
|
}
|
|
|
|
fn update_matches(
|
|
&mut self,
|
|
reuse_existing_query: bool,
|
|
add_to_history: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> oneshot::Receiver<()> {
|
|
let (done_tx, done_rx) = oneshot::channel();
|
|
let query = self.query(cx);
|
|
self.pending_search.take();
|
|
|
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
|
self.query_error = None;
|
|
if query.is_empty() {
|
|
self.clear_active_searchable_item_matches(window, cx);
|
|
let _ = done_tx.send(());
|
|
cx.notify();
|
|
} else {
|
|
let query: Arc<_> = if let Some(search) =
|
|
self.active_search.take().filter(|_| reuse_existing_query)
|
|
{
|
|
search
|
|
} else {
|
|
// Value doesn't matter, we only construct empty matchers with it
|
|
|
|
if self.search_options.contains(SearchOptions::REGEX) {
|
|
match SearchQuery::regex(
|
|
query,
|
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
|
false,
|
|
self.search_options
|
|
.contains(SearchOptions::ONE_MATCH_PER_LINE),
|
|
PathMatcher::default(),
|
|
PathMatcher::default(),
|
|
false,
|
|
None,
|
|
) {
|
|
Ok(query) => query.with_replacement(self.replacement(cx)),
|
|
Err(e) => {
|
|
self.query_error = Some(e.to_string());
|
|
self.clear_active_searchable_item_matches(window, cx);
|
|
cx.notify();
|
|
return done_rx;
|
|
}
|
|
}
|
|
} else {
|
|
match SearchQuery::text(
|
|
query,
|
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
|
false,
|
|
PathMatcher::default(),
|
|
PathMatcher::default(),
|
|
false,
|
|
None,
|
|
) {
|
|
Ok(query) => query.with_replacement(self.replacement(cx)),
|
|
Err(e) => {
|
|
self.query_error = Some(e.to_string());
|
|
self.clear_active_searchable_item_matches(window, cx);
|
|
cx.notify();
|
|
return done_rx;
|
|
}
|
|
}
|
|
}
|
|
.into()
|
|
};
|
|
|
|
self.active_search = Some(query.clone());
|
|
let query_text = query.as_str().to_string();
|
|
|
|
let matches = active_searchable_item.find_matches(query, window, cx);
|
|
|
|
let active_searchable_item = active_searchable_item.downgrade();
|
|
self.pending_search = Some(cx.spawn_in(window, async move |this, cx| {
|
|
let matches = matches.await;
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
if let Some(active_searchable_item) =
|
|
WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
|
|
{
|
|
this.searchable_items_with_matches
|
|
.insert(active_searchable_item.downgrade(), matches);
|
|
|
|
this.update_match_index(window, cx);
|
|
if add_to_history {
|
|
this.search_history
|
|
.add(&mut this.search_history_cursor, query_text);
|
|
}
|
|
if !this.dismissed {
|
|
let matches = this
|
|
.searchable_items_with_matches
|
|
.get(&active_searchable_item.downgrade())
|
|
.unwrap();
|
|
if matches.is_empty() {
|
|
active_searchable_item.clear_matches(window, cx);
|
|
} else {
|
|
active_searchable_item.update_matches(
|
|
matches,
|
|
this.active_match_index,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
let _ = done_tx.send(());
|
|
}
|
|
cx.notify();
|
|
}
|
|
})
|
|
.log_err();
|
|
}));
|
|
}
|
|
}
|
|
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()
|
|
.and_then(|searchable_item| {
|
|
let matches = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())?;
|
|
searchable_item.active_match_index(direction, matches, window, cx)
|
|
});
|
|
if new_index != self.active_match_index {
|
|
self.active_match_index = new_index;
|
|
if !self.dismissed {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
|
if let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
if !matches.is_empty() {
|
|
searchable_item.update_matches(matches, new_index, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.cycle_field(Direction::Next, window, cx);
|
|
}
|
|
|
|
fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.cycle_field(Direction::Prev, window, cx);
|
|
}
|
|
fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
|
|
let mut handles = vec![self.query_editor.focus_handle(cx)];
|
|
if self.replace_enabled {
|
|
handles.push(self.replacement_editor.focus_handle(cx));
|
|
}
|
|
if let Some(item) = self.active_searchable_item.as_ref() {
|
|
handles.push(item.item_focus_handle(cx));
|
|
}
|
|
let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
|
|
Some(index) => index,
|
|
None => return,
|
|
};
|
|
|
|
let new_index = match direction {
|
|
Direction::Next => (current_index + 1) % handles.len(),
|
|
Direction::Prev if current_index == 0 => handles.len() - 1,
|
|
Direction::Prev => (current_index - 1) % handles.len(),
|
|
};
|
|
let next_focus_handle = &handles[new_index];
|
|
self.focus(next_focus_handle, window);
|
|
cx.stop_propagation();
|
|
}
|
|
|
|
fn next_history_query(
|
|
&mut self,
|
|
_: &NextHistoryQuery,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(new_query) = self
|
|
.search_history
|
|
.next(&mut self.search_history_cursor)
|
|
.map(str::to_string)
|
|
{
|
|
drop(self.search(&new_query, Some(self.search_options), false, window, cx));
|
|
} else {
|
|
self.search_history_cursor.reset();
|
|
drop(self.search("", Some(self.search_options), false, window, cx));
|
|
}
|
|
}
|
|
|
|
fn previous_history_query(
|
|
&mut self,
|
|
_: &PreviousHistoryQuery,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.query(cx).is_empty()
|
|
&& let Some(new_query) = self
|
|
.search_history
|
|
.current(&self.search_history_cursor)
|
|
.map(str::to_string)
|
|
{
|
|
drop(self.search(&new_query, Some(self.search_options), false, window, cx));
|
|
return;
|
|
}
|
|
|
|
if let Some(new_query) = self
|
|
.search_history
|
|
.previous(&mut self.search_history_cursor)
|
|
.map(str::to_string)
|
|
{
|
|
drop(self.search(&new_query, Some(self.search_options), false, window, cx));
|
|
}
|
|
}
|
|
|
|
fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
|
|
window.invalidate_character_coordinates();
|
|
window.focus(handle);
|
|
}
|
|
|
|
fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.active_searchable_item.is_some() {
|
|
self.replace_enabled = !self.replace_enabled;
|
|
let handle = if self.replace_enabled {
|
|
self.replacement_editor.focus_handle(cx)
|
|
} else {
|
|
self.query_editor.focus_handle(cx)
|
|
};
|
|
self.focus(&handle, window);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) {
|
|
let mut should_propagate = true;
|
|
if !self.dismissed
|
|
&& self.active_search.is_some()
|
|
&& let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(query) = self.active_search.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
if let Some(active_index) = self.active_match_index {
|
|
let query = query
|
|
.as_ref()
|
|
.clone()
|
|
.with_replacement(self.replacement(cx));
|
|
searchable_item.replace(matches.at(active_index), &query, window, cx);
|
|
self.select_next_match(&SelectNextMatch, window, cx);
|
|
}
|
|
should_propagate = false;
|
|
}
|
|
if !should_propagate {
|
|
cx.stop_propagation();
|
|
}
|
|
}
|
|
|
|
pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) {
|
|
if !self.dismissed
|
|
&& self.active_search.is_some()
|
|
&& let Some(searchable_item) = self.active_searchable_item.as_ref()
|
|
&& let Some(query) = self.active_search.as_ref()
|
|
&& let Some(matches) = self
|
|
.searchable_items_with_matches
|
|
.get(&searchable_item.downgrade())
|
|
{
|
|
let query = query
|
|
.as_ref()
|
|
.clone()
|
|
.with_replacement(self.replacement(cx));
|
|
searchable_item.replace_all(&mut matches.iter(), &query, window, cx);
|
|
}
|
|
}
|
|
|
|
pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
|
self.update_match_index(window, cx);
|
|
self.active_match_index.is_some()
|
|
}
|
|
|
|
pub fn should_use_smartcase_search(&mut self, cx: &mut Context<Self>) -> bool {
|
|
EditorSettings::get_global(cx).use_smartcase_search
|
|
}
|
|
|
|
pub fn is_contains_uppercase(&mut self, str: &String) -> bool {
|
|
str.chars().any(|c| c.is_uppercase())
|
|
}
|
|
|
|
fn smartcase(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.should_use_smartcase_search(cx) {
|
|
let query = self.query(cx);
|
|
if !query.is_empty() {
|
|
let is_case = self.is_contains_uppercase(&query);
|
|
if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case {
|
|
self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn adjust_query_regex_language(&self, cx: &mut App) {
|
|
let enable = self.search_options.contains(SearchOptions::REGEX);
|
|
let query_buffer = self
|
|
.query_editor
|
|
.read(cx)
|
|
.buffer()
|
|
.read(cx)
|
|
.as_singleton()
|
|
.expect("query editor should be backed by a singleton buffer");
|
|
|
|
if enable {
|
|
if let Some(regex_language) = self.regex_language.clone() {
|
|
query_buffer.update(cx, |query_buffer, cx| {
|
|
query_buffer.set_language(Some(regex_language), cx);
|
|
})
|
|
}
|
|
} else {
|
|
query_buffer.update(cx, |query_buffer, cx| {
|
|
query_buffer.set_language(None, cx);
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Updates the searchable item's case sensitivity option to match the
|
|
/// search bar's current case sensitivity setting. 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
|
|
/// only the editor's settings are respected.
|
|
fn sync_select_next_case_sensitivity(&self, cx: &mut Context<Self>) {
|
|
let case_sensitive = match self.dismissed {
|
|
true => None,
|
|
false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)),
|
|
};
|
|
|
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
|
active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::ops::Range;
|
|
|
|
use super::*;
|
|
use editor::{
|
|
DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
|
|
display_map::DisplayRow, test::editor_test_context::EditorTestContext,
|
|
};
|
|
use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
|
|
use language::{Buffer, Point};
|
|
use settings::{SearchSettingsContent, SettingsStore};
|
|
use smol::stream::StreamExt as _;
|
|
use unindent::Unindent as _;
|
|
use util_macros::perf;
|
|
|
|
fn init_globals(cx: &mut TestAppContext) {
|
|
cx.update(|cx| {
|
|
let store = settings::SettingsStore::test(cx);
|
|
cx.set_global(store);
|
|
editor::init(cx);
|
|
|
|
theme::init(theme::LoadThemes::JustBase, cx);
|
|
crate::init(cx);
|
|
});
|
|
}
|
|
|
|
fn init_test(
|
|
cx: &mut TestAppContext,
|
|
) -> (
|
|
Entity<Editor>,
|
|
Entity<BufferSearchBar>,
|
|
&mut VisualTestContext,
|
|
) {
|
|
init_globals(cx);
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
r#"
|
|
A regular expression (shortened as regex or regexp;[1] also referred to as
|
|
rational expression[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent(),
|
|
cx,
|
|
)
|
|
});
|
|
let mut editor = None;
|
|
let window = cx.add_window(|window, cx| {
|
|
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
|
"keymaps/default-macos.json",
|
|
cx,
|
|
)
|
|
.unwrap();
|
|
cx.bind_keys(default_key_bindings);
|
|
editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx)));
|
|
let mut search_bar = BufferSearchBar::new(None, window, cx);
|
|
search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx);
|
|
search_bar.show(window, cx);
|
|
search_bar
|
|
});
|
|
let search_bar = window.root(cx).unwrap();
|
|
|
|
let cx = VisualTestContext::from_window(*window, cx).into_mut();
|
|
|
|
(editor.unwrap(), search_bar, cx)
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_simple(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
|
|
background_highlights
|
|
.into_iter()
|
|
.map(|(range, _)| range)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
// Search for a string that appears with different casing.
|
|
// By default, search is case-insensitive.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("us", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[
|
|
DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19),
|
|
DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),
|
|
]
|
|
);
|
|
});
|
|
|
|
// Switch to a case sensitive search.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
|
|
});
|
|
let mut editor_notifications = cx.notifications(&editor);
|
|
editor_notifications.next().await;
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
|
|
);
|
|
});
|
|
|
|
// Search for a string that appears both as a whole word and
|
|
// within other words. By default, all results are found.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("or", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[
|
|
DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26),
|
|
DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
|
|
DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73),
|
|
DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3),
|
|
DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
|
|
DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
|
|
DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62),
|
|
]
|
|
);
|
|
});
|
|
|
|
// Switch to a whole word search.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
|
});
|
|
let mut editor_notifications = cx.notifications(&editor);
|
|
editor_notifications.next().await;
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[
|
|
DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43),
|
|
DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13),
|
|
DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58),
|
|
]
|
|
);
|
|
});
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(1));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(1));
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
});
|
|
|
|
// Park the cursor in between matches and ensure that going to the previous match selects
|
|
// the closest match to the left.
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(1));
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
});
|
|
|
|
// Park the cursor in between matches and ensure that going to the next match selects the
|
|
// closest match to the right.
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(1));
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(1));
|
|
});
|
|
|
|
// Park the cursor after the last match and ensure that going to the previous match selects
|
|
// the last match.
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
});
|
|
|
|
// Park the cursor after the last match and ensure that going to the next match selects the
|
|
// first match.
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
});
|
|
|
|
// Park the cursor before the first match and ensure that going to the previous match
|
|
// selects the last match.
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_display_ranges([
|
|
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
|
|
])
|
|
});
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.read_with(cx, |search_bar, _| {
|
|
assert_eq!(search_bar.active_match_index, Some(2));
|
|
});
|
|
}
|
|
|
|
fn display_points_of(
|
|
background_highlights: Vec<(Range<DisplayPoint>, Hsla)>,
|
|
) -> Vec<Range<DisplayPoint>> {
|
|
background_highlights
|
|
.into_iter()
|
|
.map(|(range, _)| range)
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_option_handling(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
|
|
// show with options should make current search case sensitive
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.show(window, cx);
|
|
search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),]
|
|
);
|
|
});
|
|
|
|
// search_suggested should restore default options
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search_suggested(window, cx);
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE)
|
|
});
|
|
|
|
// toggling a search option should update the defaults
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search(
|
|
"regex",
|
|
Some(SearchOptions::CASE_SENSITIVE),
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx)
|
|
});
|
|
let mut editor_notifications = cx.notifications(&editor);
|
|
editor_notifications.next().await;
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),]
|
|
);
|
|
});
|
|
|
|
// defaults should still include whole word
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search_suggested(window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
|
|
)
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_select_all_matches(cx: &mut TestAppContext) {
|
|
init_globals(cx);
|
|
let buffer_text = r#"
|
|
A regular expression (shortened as regex or regexp;[1] also referred to as
|
|
rational expression[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent();
|
|
let expected_query_matches_count = buffer_text
|
|
.chars()
|
|
.filter(|c| c.eq_ignore_ascii_case(&'a'))
|
|
.count();
|
|
assert!(
|
|
expected_query_matches_count > 1,
|
|
"Should pick a query with multiple results"
|
|
);
|
|
let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
|
|
let window = cx.add_window(|_, _| gpui::Empty);
|
|
|
|
let editor = window.build_entity(cx, |window, cx| {
|
|
Editor::for_buffer(buffer.clone(), None, window, cx)
|
|
});
|
|
|
|
let search_bar = window.build_entity(cx, |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
|
|
});
|
|
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
search_bar.search("a", None, true, window, cx)
|
|
})
|
|
})
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
let initial_selections = window
|
|
.update(cx, |_, window, cx| {
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let handle = search_bar.query_editor.focus_handle(cx);
|
|
window.focus(&handle);
|
|
search_bar.activate_current_match(window, cx);
|
|
});
|
|
assert!(
|
|
!editor.read(cx).is_focused(window),
|
|
"Initially, the editor should not be focused"
|
|
);
|
|
let initial_selections = editor.update(cx, |editor, cx| {
|
|
let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx));
|
|
assert_eq!(
|
|
initial_selections.len(), 1,
|
|
"Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
|
|
);
|
|
initial_selections
|
|
});
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.active_match_index, Some(0));
|
|
let handle = search_bar.query_editor.focus_handle(cx);
|
|
window.focus(&handle);
|
|
search_bar.select_all_matches(&SelectAllMatches, window, cx);
|
|
});
|
|
assert!(
|
|
editor.read(cx).is_focused(window),
|
|
"Should focus editor after successful SelectAllMatches"
|
|
);
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections =
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
expected_query_matches_count,
|
|
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
|
|
);
|
|
assert_eq!(
|
|
search_bar.active_match_index,
|
|
Some(0),
|
|
"Match index should not change after selecting all matches"
|
|
);
|
|
});
|
|
|
|
search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx));
|
|
initial_selections
|
|
}).unwrap();
|
|
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
assert!(
|
|
editor.read(cx).is_focused(window),
|
|
"Should still have editor focused after SelectNextMatch"
|
|
);
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))
|
|
});
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
1,
|
|
"On next match, should deselect items and select the next match"
|
|
);
|
|
assert_ne!(
|
|
all_selections, initial_selections,
|
|
"Next match should be different from the first selection"
|
|
);
|
|
assert_eq!(
|
|
search_bar.active_match_index,
|
|
Some(1),
|
|
"Match index should be updated to the next one"
|
|
);
|
|
let handle = search_bar.query_editor.focus_handle(cx);
|
|
window.focus(&handle);
|
|
search_bar.select_all_matches(&SelectAllMatches, window, cx);
|
|
});
|
|
})
|
|
.unwrap();
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
assert!(
|
|
editor.read(cx).is_focused(window),
|
|
"Should focus editor after successful SelectAllMatches"
|
|
);
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections =
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
expected_query_matches_count,
|
|
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
|
|
);
|
|
assert_eq!(
|
|
search_bar.active_match_index,
|
|
Some(1),
|
|
"Match index should not change after selecting all matches"
|
|
);
|
|
});
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
|
});
|
|
})
|
|
.unwrap();
|
|
let last_match_selections = window
|
|
.update(cx, |_, window, cx| {
|
|
assert!(
|
|
editor.read(cx).is_focused(window),
|
|
"Should still have editor focused after SelectPreviousMatch"
|
|
);
|
|
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))
|
|
});
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
1,
|
|
"On previous match, should deselect items and select the previous item"
|
|
);
|
|
assert_eq!(
|
|
all_selections, initial_selections,
|
|
"Previous match should be the same as the first selection"
|
|
);
|
|
assert_eq!(
|
|
search_bar.active_match_index,
|
|
Some(0),
|
|
"Match index should be updated to the previous one"
|
|
);
|
|
all_selections
|
|
})
|
|
})
|
|
.unwrap();
|
|
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let handle = search_bar.query_editor.focus_handle(cx);
|
|
window.focus(&handle);
|
|
search_bar.search("abas_nonexistent_match", None, true, window, cx)
|
|
})
|
|
})
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
search_bar.select_all_matches(&SelectAllMatches, window, cx);
|
|
});
|
|
assert!(
|
|
editor.update(cx, |this, _cx| !this.is_focused(window)),
|
|
"Should not switch focus to editor if SelectAllMatches does not find any matches"
|
|
);
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections =
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx)));
|
|
assert_eq!(
|
|
all_selections, last_match_selections,
|
|
"Should not select anything new if there are no matches"
|
|
);
|
|
assert!(
|
|
search_bar.active_match_index.is_none(),
|
|
"For no matches, there should be no active match index"
|
|
);
|
|
});
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
|
|
init_globals(cx);
|
|
let buffer_text = r#"
|
|
self.buffer.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
edits,
|
|
Some(AutoindentMode::Block {
|
|
original_indent_columns,
|
|
}),
|
|
cx,
|
|
)
|
|
});
|
|
|
|
this.buffer.update(cx, |buffer, cx| {
|
|
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
|
|
});
|
|
"#
|
|
.unindent();
|
|
let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
|
|
let cx = cx.add_empty_window();
|
|
|
|
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.search(
|
|
"edit\\(",
|
|
Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_all_matches(&SelectAllMatches, window, cx);
|
|
});
|
|
search_bar.update(cx, |_, cx| {
|
|
let all_selections = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))
|
|
});
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
2,
|
|
"Should select all `edit(` in the buffer, but got: {all_selections:?}"
|
|
);
|
|
});
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search(
|
|
"edit(",
|
|
Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE),
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.select_all_matches(&SelectAllMatches, window, cx);
|
|
});
|
|
search_bar.update(cx, |_, cx| {
|
|
let all_selections = editor.update(cx, |editor, cx| {
|
|
editor
|
|
.selections
|
|
.display_ranges(&editor.display_snapshot(cx))
|
|
});
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
2,
|
|
"Should select all `edit(` in the buffer, but got: {all_selections:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_query_history(cx: &mut TestAppContext) {
|
|
let (_editor, search_bar, cx) = init_test(cx);
|
|
|
|
// Add 3 search items into the history.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("a", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("b", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
// Ensure that the latest search is active.
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "c");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
// Next history query after the latest should set the query to the empty string.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
// First previous query for empty current query should set the query to the latest.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "c");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
// Further previous items should go over the history in reverse order.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "b");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
// Previous items should never go behind the first history item.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "a");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "a");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
// Next items should go over the history in the original order.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "b");
|
|
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
|
|
});
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("ba", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "ba");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
|
|
// New search input should add another entry to history and move the selection to the end of the history.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "c");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.previous_history_query(&PreviousHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "b");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "c");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "ba");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.next_history_query(&NextHistoryQuery, window, cx);
|
|
});
|
|
cx.background_executor.run_until_parked();
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_replace_simple(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("expression", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
// We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
|
|
editor.set_text("expr$1", window, cx);
|
|
});
|
|
search_bar.replace_all(&ReplaceAll, window, cx)
|
|
});
|
|
assert_eq!(
|
|
editor.read_with(cx, |this, cx| { this.text(cx) }),
|
|
r#"
|
|
A regular expr$1 (shortened as regex or regexp;[1] also referred to as
|
|
rational expr$1[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent()
|
|
);
|
|
|
|
// Search for word boundaries and replace just a single one.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_text("banana", window, cx);
|
|
});
|
|
search_bar.replace_next(&ReplaceNext, window, cx)
|
|
});
|
|
// Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
|
|
assert_eq!(
|
|
editor.read_with(cx, |this, cx| { this.text(cx) }),
|
|
r#"
|
|
A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
|
|
rational expr$1[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent()
|
|
);
|
|
// Let's turn on regex mode.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search(
|
|
"\\[([^\\]]+)\\]",
|
|
Some(SearchOptions::REGEX),
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_text("${1}number", window, cx);
|
|
});
|
|
search_bar.replace_all(&ReplaceAll, window, cx)
|
|
});
|
|
assert_eq!(
|
|
editor.read_with(cx, |this, cx| { this.text(cx) }),
|
|
r#"
|
|
A regular expr$1 (shortened as regex banana regexp;1number also referred to as
|
|
rational expr$12number3number) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent()
|
|
);
|
|
// Now with a whole-word twist.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search(
|
|
"a\\w+s",
|
|
Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_text("things", window, cx);
|
|
});
|
|
search_bar.replace_all(&ReplaceAll, window, cx)
|
|
});
|
|
// The only word affected by this edit should be `algorithms`, even though there's a bunch
|
|
// of words in this text that would match this regex if not for WHOLE_WORD.
|
|
assert_eq!(
|
|
editor.read_with(cx, |this, cx| { this.text(cx) }),
|
|
r#"
|
|
A regular expr$1 (shortened as regex banana regexp;1number also referred to as
|
|
rational expr$12number3number) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching things
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent()
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_replace_focus(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.set_text("What a bad day!", window, cx)
|
|
});
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("bad", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
// Calling `toggle_replace` in the search bar ensures that the "Replace
|
|
// *" buttons are rendered, so we can then simulate clicking the
|
|
// buttons.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.toggle_replace(&ToggleReplace, window, cx)
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_text("great", window, cx);
|
|
});
|
|
});
|
|
|
|
// Focus on the editor instead of the search bar, as we want to ensure
|
|
// that pressing the "Replace Next Match" button will work, even if the
|
|
// search bar is not focused.
|
|
cx.focus(&editor);
|
|
|
|
// We'll not simulate clicking the "Replace Next Match " button, asserting that
|
|
// the replacement was done.
|
|
let button_bounds = cx
|
|
.debug_bounds("ICON-ReplaceNext")
|
|
.expect("'Replace Next Match' button should be visible");
|
|
cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
|
|
|
|
assert_eq!(
|
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
|
"What a great day!"
|
|
);
|
|
}
|
|
|
|
struct ReplacementTestParams<'a> {
|
|
editor: &'a Entity<Editor>,
|
|
search_bar: &'a Entity<BufferSearchBar>,
|
|
cx: &'a mut VisualTestContext,
|
|
search_text: &'static str,
|
|
search_options: Option<SearchOptions>,
|
|
replacement_text: &'static str,
|
|
replace_all: bool,
|
|
expected_text: String,
|
|
}
|
|
|
|
async fn run_replacement_test(options: ReplacementTestParams<'_>) {
|
|
options
|
|
.search_bar
|
|
.update_in(options.cx, |search_bar, window, cx| {
|
|
if let Some(options) = options.search_options {
|
|
search_bar.set_search_options(options, cx);
|
|
}
|
|
search_bar.search(
|
|
options.search_text,
|
|
options.search_options,
|
|
true,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
options
|
|
.search_bar
|
|
.update_in(options.cx, |search_bar, window, cx| {
|
|
search_bar.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_text(options.replacement_text, window, cx);
|
|
});
|
|
|
|
if options.replace_all {
|
|
search_bar.replace_all(&ReplaceAll, window, cx)
|
|
} else {
|
|
search_bar.replace_next(&ReplaceNext, window, cx)
|
|
}
|
|
});
|
|
|
|
assert_eq!(
|
|
options
|
|
.editor
|
|
.read_with(options.cx, |this, cx| { this.text(cx) }),
|
|
options.expected_text
|
|
);
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_replace_special_characters(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
|
|
run_replacement_test(ReplacementTestParams {
|
|
editor: &editor,
|
|
search_bar: &search_bar,
|
|
cx,
|
|
search_text: "expression",
|
|
search_options: None,
|
|
replacement_text: r"\n",
|
|
replace_all: true,
|
|
expected_text: r#"
|
|
A regular \n (shortened as regex or regexp;[1] also referred to as
|
|
rational \n[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent(),
|
|
})
|
|
.await;
|
|
|
|
run_replacement_test(ReplacementTestParams {
|
|
editor: &editor,
|
|
search_bar: &search_bar,
|
|
cx,
|
|
search_text: "or",
|
|
search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
|
|
replacement_text: r"\\\n\\\\",
|
|
replace_all: false,
|
|
expected_text: r#"
|
|
A regular \n (shortened as regex \
|
|
\\ regexp;[1] also referred to as
|
|
rational \n[2][3]) is a sequence of characters that specifies a search
|
|
pattern in text. Usually such patterns are used by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent(),
|
|
})
|
|
.await;
|
|
|
|
run_replacement_test(ReplacementTestParams {
|
|
editor: &editor,
|
|
search_bar: &search_bar,
|
|
cx,
|
|
search_text: r"(that|used) ",
|
|
search_options: Some(SearchOptions::REGEX),
|
|
replacement_text: r"$1\n",
|
|
replace_all: true,
|
|
expected_text: r#"
|
|
A regular \n (shortened as regex \
|
|
\\ regexp;[1] also referred to as
|
|
rational \n[2][3]) is a sequence of characters that
|
|
specifies a search
|
|
pattern in text. Usually such patterns are used
|
|
by string-searching algorithms
|
|
for "find" or "find and replace" operations on strings, or for input validation.
|
|
"#
|
|
.unindent(),
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
init_globals(cx);
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
r#"
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
"#
|
|
.unindent(),
|
|
cx,
|
|
)
|
|
});
|
|
let cx = cx.add_empty_window();
|
|
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, |s| {
|
|
s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
|
|
})
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
let deploy = Deploy {
|
|
focus: true,
|
|
replace_enabled: false,
|
|
selection_search_enabled: true,
|
|
};
|
|
search_bar.deploy(&deploy, window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("aaa", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
assert_eq!(
|
|
editor.search_background_highlights(cx),
|
|
&[
|
|
Point::new(1, 0)..Point::new(1, 3),
|
|
Point::new(1, 8)..Point::new(1, 11),
|
|
Point::new(2, 0)..Point::new(2, 3),
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
init_globals(cx);
|
|
let text = r#"
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
aaa bbb aaa ccc
|
|
"#
|
|
.unindent();
|
|
|
|
let cx = cx.add_empty_window();
|
|
let editor = cx.new_window_entity(|window, cx| {
|
|
let multibuffer = MultiBuffer::build_multi(
|
|
[
|
|
(
|
|
&text,
|
|
vec![
|
|
Point::new(0, 0)..Point::new(2, 0),
|
|
Point::new(4, 0)..Point::new(5, 0),
|
|
],
|
|
),
|
|
(&text, vec![Point::new(9, 0)..Point::new(11, 0)]),
|
|
],
|
|
cx,
|
|
);
|
|
Editor::for_multibuffer(multibuffer, 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, |s| {
|
|
s.select_ranges(vec![
|
|
Point::new(1, 0)..Point::new(1, 4),
|
|
Point::new(5, 3)..Point::new(6, 4),
|
|
])
|
|
})
|
|
});
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
let deploy = Deploy {
|
|
focus: true,
|
|
replace_enabled: false,
|
|
selection_search_enabled: true,
|
|
};
|
|
search_bar.deploy(&deploy, window, cx);
|
|
});
|
|
|
|
cx.run_until_parked();
|
|
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("aaa", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
assert_eq!(
|
|
editor.search_background_highlights(cx),
|
|
&[
|
|
Point::new(1, 0)..Point::new(1, 3),
|
|
Point::new(5, 8)..Point::new(5, 11),
|
|
Point::new(6, 0)..Point::new(6, 3),
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
// Search using valid regexp
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.enable_search_option(SearchOptions::REGEX, window, cx);
|
|
search_bar.search("expression", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert_eq!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)),
|
|
&[
|
|
DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20),
|
|
DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19),
|
|
],
|
|
);
|
|
});
|
|
|
|
// Now, the expression is invalid
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("expression (", None, true, window, cx)
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
assert!(
|
|
display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(),
|
|
);
|
|
});
|
|
}
|
|
|
|
#[perf]
|
|
#[gpui::test]
|
|
async fn test_search_options_changes(cx: &mut TestAppContext) {
|
|
let (_editor, search_bar, cx) = init_test(cx);
|
|
update_search_settings(
|
|
SearchSettings {
|
|
button: true,
|
|
whole_word: false,
|
|
case_sensitive: false,
|
|
include_ignored: false,
|
|
regex: false,
|
|
center_on_match: false,
|
|
},
|
|
cx,
|
|
);
|
|
|
|
let deploy = Deploy {
|
|
focus: true,
|
|
replace_enabled: false,
|
|
selection_search_enabled: true,
|
|
};
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::NONE,
|
|
"Should have no search options enabled by default"
|
|
);
|
|
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::WHOLE_WORD,
|
|
"Should enable the option toggled"
|
|
);
|
|
assert!(
|
|
!search_bar.dismissed,
|
|
"Search bar should be present and visible"
|
|
);
|
|
search_bar.deploy(&deploy, window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::WHOLE_WORD,
|
|
"After (re)deploying, the option should still be enabled"
|
|
);
|
|
|
|
search_bar.dismiss(&Dismiss, window, cx);
|
|
search_bar.deploy(&deploy, window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::WHOLE_WORD,
|
|
"After hiding and showing the search bar, search options should be preserved"
|
|
);
|
|
|
|
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
|
|
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::REGEX,
|
|
"Should enable the options toggled"
|
|
);
|
|
assert!(
|
|
!search_bar.dismissed,
|
|
"Search bar should be present and visible"
|
|
);
|
|
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
|
});
|
|
|
|
update_search_settings(
|
|
SearchSettings {
|
|
button: true,
|
|
whole_word: false,
|
|
case_sensitive: true,
|
|
include_ignored: false,
|
|
regex: false,
|
|
center_on_match: false,
|
|
},
|
|
cx,
|
|
);
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
|
|
"Should have no search options enabled by default"
|
|
);
|
|
|
|
search_bar.deploy(&deploy, window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::REGEX | SearchOptions::WHOLE_WORD,
|
|
"Toggling a non-dismissed search bar with custom options should not change the default options"
|
|
);
|
|
search_bar.dismiss(&Dismiss, window, cx);
|
|
search_bar.deploy(&deploy, window, cx);
|
|
assert_eq!(
|
|
search_bar.configured_options,
|
|
SearchOptions::CASE_SENSITIVE,
|
|
"After a settings update and toggling the search bar, configured options should be updated"
|
|
);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::CASE_SENSITIVE,
|
|
"After a settings update and toggling the search bar, configured options should be used"
|
|
);
|
|
});
|
|
|
|
update_search_settings(
|
|
SearchSettings {
|
|
button: true,
|
|
whole_word: true,
|
|
case_sensitive: true,
|
|
include_ignored: false,
|
|
regex: false,
|
|
center_on_match: false,
|
|
},
|
|
cx,
|
|
);
|
|
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.deploy(&deploy, window, cx);
|
|
search_bar.dismiss(&Dismiss, window, cx);
|
|
search_bar.show(window, cx);
|
|
assert_eq!(
|
|
search_bar.search_options,
|
|
SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD,
|
|
"Calling deploy on an already deployed search bar should not prevent settings updates from being detected"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) {
|
|
let (editor, search_bar, cx) = init_test(cx);
|
|
let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await;
|
|
|
|
// Start with case sensitive search settings.
|
|
let mut search_settings = SearchSettings::default();
|
|
search_settings.case_sensitive = true;
|
|
update_search_settings(search_settings, cx);
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let mut search_options = search_bar.search_options;
|
|
search_options.insert(SearchOptions::CASE_SENSITIVE);
|
|
search_bar.set_search_options(search_options, cx);
|
|
});
|
|
|
|
editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
|
|
editor_cx.update_editor(|e, window, cx| {
|
|
e.select_next(&Default::default(), window, cx).unwrap();
|
|
});
|
|
editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
|
|
|
|
// Update the search bar's case sensitivite toggle, so we can later
|
|
// confirm that `select_next` will now be case-insensitive.
|
|
editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.toggle_case_sensitive(&Default::default(), window, cx);
|
|
});
|
|
editor_cx.update_editor(|e, window, cx| {
|
|
e.select_next(&Default::default(), window, cx).unwrap();
|
|
});
|
|
editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
|
|
|
|
// Confirm that, after dismissing the search bar, only the editor's
|
|
// search settings actually affect the behavior of `select_next`.
|
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.dismiss(&Default::default(), window, cx);
|
|
});
|
|
editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
|
|
editor_cx.update_editor(|e, window, cx| {
|
|
e.select_next(&Default::default(), window, cx).unwrap();
|
|
});
|
|
editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
|
|
|
|
// Update the editor's search settings, disabling case sensitivity, to
|
|
// check that the value is respected.
|
|
let mut search_settings = SearchSettings::default();
|
|
search_settings.case_sensitive = false;
|
|
update_search_settings(search_settings, cx);
|
|
editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
|
|
editor_cx.update_editor(|e, window, cx| {
|
|
e.select_next(&Default::default(), window, cx).unwrap();
|
|
});
|
|
editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo");
|
|
}
|
|
|
|
fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
|
|
cx.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |settings| {
|
|
settings.editor.search = Some(SearchSettingsContent {
|
|
button: Some(search_settings.button),
|
|
whole_word: Some(search_settings.whole_word),
|
|
case_sensitive: Some(search_settings.case_sensitive),
|
|
include_ignored: Some(search_settings.include_ignored),
|
|
regex: Some(search_settings.regex),
|
|
center_on_match: Some(search_settings.center_on_match),
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|