
This PR updates function signatures, docstrings, and gpui's other documentation to reflect it's new state following the merge of `Model` and `View` into `Entity` as well as the removal of `WindowContext`. Release Notes: - N/A
2792 lines
108 KiB
Rust
2792 lines
108 KiB
Rust
mod registrar;
|
|
|
|
use crate::{
|
|
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
|
|
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
|
|
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
|
|
};
|
|
use any_vec::AnyVec;
|
|
use collections::HashMap;
|
|
use editor::{
|
|
actions::{Tab, TabPrev},
|
|
DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
|
|
};
|
|
use futures::channel::oneshot;
|
|
use gpui::{
|
|
actions, div, impl_actions, Action, App, ClickEvent, Context, Entity, EventEmitter,
|
|
FocusHandle, Focusable, Hsla, InteractiveElement as _, IntoElement, KeyContext,
|
|
ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, Window,
|
|
};
|
|
use project::{
|
|
search::SearchQuery,
|
|
search_history::{SearchHistory, SearchHistoryCursor},
|
|
};
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use settings::Settings;
|
|
use std::sync::Arc;
|
|
use theme::ThemeSettings;
|
|
|
|
use ui::{
|
|
h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip,
|
|
BASE_REM_SIZE_IN_PX,
|
|
};
|
|
use util::ResultExt;
|
|
use workspace::{
|
|
item::ItemHandle,
|
|
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
|
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
|
};
|
|
|
|
pub use registrar::DivRegistrar;
|
|
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
|
|
|
|
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, JsonSchema)]
|
|
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,
|
|
}
|
|
|
|
impl_actions!(buffer_search, [Deploy]);
|
|
|
|
actions!(buffer_search, [DeployReplace, Dismiss, 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_contains_error: bool,
|
|
dismissed: bool,
|
|
search_history: SearchHistory,
|
|
search_history_cursor: SearchHistoryCursor,
|
|
replace_enabled: bool,
|
|
selection_search_enabled: bool,
|
|
scroll_handle: ScrollHandle,
|
|
editor_scroll_handle: ScrollHandle,
|
|
editor_needed_width: Pixels,
|
|
}
|
|
|
|
impl BufferSearchBar {
|
|
fn render_text_input(
|
|
&self,
|
|
editor: &Entity<Editor>,
|
|
color: Hsla,
|
|
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
let settings = ThemeSettings::get_global(cx);
|
|
let text_style = TextStyle {
|
|
color: if editor.read(cx).read_only(cx) {
|
|
cx.theme().colors().text_disabled
|
|
} else {
|
|
color
|
|
},
|
|
font_family: settings.buffer_font.family.clone(),
|
|
font_features: settings.buffer_font.features.clone(),
|
|
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
|
font_size: rems(0.875).into(),
|
|
font_weight: settings.buffer_font.weight,
|
|
line_height: relative(1.3),
|
|
..Default::default()
|
|
};
|
|
|
|
EditorElement::new(
|
|
editor,
|
|
EditorStyle {
|
|
background: cx.theme().colors().editor_background,
|
|
local_player: cx.theme().players().local(),
|
|
text: text_style,
|
|
..Default::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn query_editor_focused(&self) -> bool {
|
|
self.query_editor_focused
|
|
}
|
|
}
|
|
|
|
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 supported_options = self.supported_options(cx);
|
|
|
|
if self.query_editor.update(cx, |query_editor, _cx| {
|
|
query_editor.placeholder_text().is_none()
|
|
}) {
|
|
self.query_editor.update(cx, |editor, cx| {
|
|
editor.set_placeholder_text("Search…", cx);
|
|
});
|
|
}
|
|
|
|
self.replacement_editor.update(cx, |editor, cx| {
|
|
editor.set_placeholder_text("Replace with…", cx);
|
|
});
|
|
|
|
let mut text_color = Color::Default;
|
|
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 {
|
|
text_color = Color::Error; // No matches found
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "0/0".to_string());
|
|
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
|
|
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
|
|
|
|
let mut key_context = KeyContext::new_with_defaults();
|
|
key_context.add("BufferSearchBar");
|
|
if in_replace {
|
|
key_context.add("in_replace");
|
|
}
|
|
let editor_border = if self.query_contains_error {
|
|
Color::Error.color(cx)
|
|
} else {
|
|
cx.theme().colors().border
|
|
};
|
|
|
|
let container_width = window.viewport_size().width;
|
|
let input_width = SearchInputWidth::calc_width(container_width);
|
|
|
|
let input_base_styles = || {
|
|
h_flex()
|
|
.min_w_32()
|
|
.w(input_width)
|
|
.h_8()
|
|
.pl_2()
|
|
.pr_1()
|
|
.py_1()
|
|
.border_1()
|
|
.border_color(editor_border)
|
|
.rounded_lg()
|
|
};
|
|
|
|
let search_line = h_flex()
|
|
.gap_2()
|
|
.when(supported_options.find_in_results, |el| {
|
|
el.child(Label::new("Find in results").color(Color::Hint))
|
|
})
|
|
.child(
|
|
input_base_styles()
|
|
.id("editor-scroll")
|
|
.track_scroll(&self.editor_scroll_handle)
|
|
.child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
|
|
.when(!hide_inline_icons, |div| {
|
|
div.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.children(supported_options.case.then(|| {
|
|
self.render_search_option_button(
|
|
SearchOptions::CASE_SENSITIVE,
|
|
focus_handle.clone(),
|
|
cx.listener(|this, _, window, cx| {
|
|
this.toggle_case_sensitive(
|
|
&ToggleCaseSensitive,
|
|
window,
|
|
cx,
|
|
)
|
|
}),
|
|
)
|
|
}))
|
|
.children(supported_options.word.then(|| {
|
|
self.render_search_option_button(
|
|
SearchOptions::WHOLE_WORD,
|
|
focus_handle.clone(),
|
|
cx.listener(|this, _, window, cx| {
|
|
this.toggle_whole_word(&ToggleWholeWord, window, cx)
|
|
}),
|
|
)
|
|
}))
|
|
.children(supported_options.regex.then(|| {
|
|
self.render_search_option_button(
|
|
SearchOptions::REGEX,
|
|
focus_handle.clone(),
|
|
cx.listener(|this, _, window, cx| {
|
|
this.toggle_regex(&ToggleRegex, window, cx)
|
|
}),
|
|
)
|
|
})),
|
|
)
|
|
}),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_1()
|
|
.min_w_64()
|
|
.when(supported_options.replacement, |this| {
|
|
this.child(
|
|
IconButton::new(
|
|
"buffer-search-bar-toggle-replace-button",
|
|
IconName::Replace,
|
|
)
|
|
.style(ButtonStyle::Subtle)
|
|
.shape(IconButtonShape::Square)
|
|
.when(self.replace_enabled, |button| {
|
|
button.style(ButtonStyle::Filled)
|
|
})
|
|
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
|
this.toggle_replace(&ToggleReplace, window, cx);
|
|
}))
|
|
.toggle_state(self.replace_enabled)
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Replace",
|
|
&ToggleReplace,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.when(supported_options.selection, |this| {
|
|
this.child(
|
|
IconButton::new(
|
|
"buffer-search-bar-toggle-search-selection-button",
|
|
IconName::SearchSelection,
|
|
)
|
|
.style(ButtonStyle::Subtle)
|
|
.shape(IconButtonShape::Square)
|
|
.when(self.selection_search_enabled, |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)
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Search Selection",
|
|
&ToggleSelection,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.when(!supported_options.find_in_results, |el| {
|
|
el.child(
|
|
IconButton::new("select-all", ui::IconName::SelectAll)
|
|
.on_click(|_, window, cx| {
|
|
window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
|
|
})
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Select All Matches",
|
|
&SelectAllMatches,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.pl_2()
|
|
.ml_1()
|
|
.border_l_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.child(render_nav_button(
|
|
ui::IconName::ChevronLeft,
|
|
self.active_match_index.is_some(),
|
|
"Select Previous Match",
|
|
&SelectPrevMatch,
|
|
focus_handle.clone(),
|
|
))
|
|
.child(render_nav_button(
|
|
ui::IconName::ChevronRight,
|
|
self.active_match_index.is_some(),
|
|
"Select Next Match",
|
|
&SelectNextMatch,
|
|
focus_handle.clone(),
|
|
)),
|
|
)
|
|
.when(!narrow_mode, |this| {
|
|
this.child(h_flex().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
|
|
},
|
|
),
|
|
))
|
|
})
|
|
})
|
|
.when(supported_options.find_in_results, |el| {
|
|
el.child(
|
|
IconButton::new(SharedString::from("Close"), IconName::Close)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
|
|
})
|
|
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
|
this.dismiss(&Dismiss, window, cx)
|
|
})),
|
|
)
|
|
}),
|
|
);
|
|
|
|
let replace_line = should_show_replace_input.then(|| {
|
|
h_flex()
|
|
.gap_2()
|
|
.child(input_base_styles().child(self.render_text_input(
|
|
&self.replacement_editor,
|
|
cx.theme().colors().text,
|
|
cx,
|
|
)))
|
|
.child(
|
|
h_flex()
|
|
.min_w_64()
|
|
.gap_1()
|
|
.child(
|
|
IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Replace Next Match",
|
|
&ReplaceNext,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
})
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.replace_next(&ReplaceNext, window, cx)
|
|
})),
|
|
)
|
|
.child(
|
|
IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Replace All Matches",
|
|
&ReplaceAll,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
})
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.replace_all(&ReplaceAll, window, cx)
|
|
})),
|
|
),
|
|
)
|
|
});
|
|
|
|
v_flex()
|
|
.id("buffer_search")
|
|
.gap_2()
|
|
.py(px(1.0))
|
|
.track_scroll(&self.scroll_handle)
|
|
.key_context(key_context)
|
|
.capture_action(cx.listener(Self::tab))
|
|
.capture_action(cx.listener(Self::tab_prev))
|
|
.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))
|
|
.when(self.supported_options(cx).replacement, |this| {
|
|
this.on_action(cx.listener(Self::toggle_replace))
|
|
.when(in_replace, |this| {
|
|
this.on_action(cx.listener(Self::replace_next))
|
|
.on_action(cx.listener(Self::replace_all))
|
|
})
|
|
})
|
|
.when(self.supported_options(cx).case, |this| {
|
|
this.on_action(cx.listener(Self::toggle_case_sensitive))
|
|
})
|
|
.when(self.supported_options(cx).word, |this| {
|
|
this.on_action(cx.listener(Self::toggle_whole_word))
|
|
})
|
|
.when(self.supported_options(cx).regex, |this| {
|
|
this.on_action(cx.listener(Self::toggle_regex))
|
|
})
|
|
.when(self.supported_options(cx).selection, |this| {
|
|
this.on_action(cx.listener(Self::toggle_selection))
|
|
})
|
|
.child(h_flex().relative().child(search_line.w_full()).when(
|
|
!narrow_mode && !supported_options.find_in_results,
|
|
|div| {
|
|
div.child(
|
|
h_flex().absolute().right_0().child(
|
|
IconButton::new(SharedString::from("Close"), IconName::Close)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
|
|
})
|
|
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
|
this.dismiss(&Dismiss, window, cx)
|
|
})),
|
|
),
|
|
)
|
|
.w_full()
|
|
},
|
|
))
|
|
.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 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, 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 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: &SelectPrevMatch, 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, _: &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, deploy, window, cx| {
|
|
this.deploy(deploy, window, cx);
|
|
}))
|
|
}
|
|
|
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
|
|
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);
|
|
|
|
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_contains_error: false,
|
|
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: false,
|
|
scroll_handle: ScrollHandle::new(),
|
|
editor_scroll_handle: ScrollHandle::new(),
|
|
editor_needed_width: px(0.),
|
|
}
|
|
}
|
|
|
|
pub fn is_dismissed(&self) -> bool {
|
|
self.dismissed
|
|
}
|
|
|
|
pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.dismissed = true;
|
|
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 = false;
|
|
self.replace_enabled = false;
|
|
active_editor.search_bar_visibility_changed(false, window, cx);
|
|
active_editor.toggle_filtered_search_ranges(false, window, cx);
|
|
let handle = active_editor.item_focus_handle(cx);
|
|
self.focus(&handle, window, cx);
|
|
}
|
|
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 {
|
|
if self.show(window, cx) {
|
|
if let Some(active_item) = self.active_searchable_item.as_mut() {
|
|
active_item.toggle_filtered_search_ranges(
|
|
deploy.selection_search_enabled,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
self.search_suggested(window, cx);
|
|
self.smartcase(window, cx);
|
|
self.replace_enabled = deploy.replace_enabled;
|
|
self.selection_search_enabled = deploy.selection_search_enabled;
|
|
if deploy.focus {
|
|
let mut handle = self.query_editor.focus_handle(cx).clone();
|
|
let mut select_query = true;
|
|
if deploy.replace_enabled && handle.is_focused(window) {
|
|
handle = self.replacement_editor.focus_handle(cx).clone();
|
|
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;
|
|
};
|
|
|
|
self.configured_options =
|
|
SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
|
|
if self.dismissed && self.configured_options != self.default_options {
|
|
self.search_options = self.configured_options;
|
|
self.default_options = self.configured_options;
|
|
}
|
|
|
|
self.dismissed = false;
|
|
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), window, cx));
|
|
|
|
if let Some(search) = search {
|
|
cx.spawn_in(window, |this, mut cx| async move {
|
|
search.await?;
|
|
this.update_in(&mut cx, |this, window, cx| {
|
|
this.activate_current_match(window, cx)
|
|
})
|
|
})
|
|
.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 {
|
|
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
|
|
if 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([(0..len, replacement.unwrap())], None, cx);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn search(
|
|
&mut self,
|
|
query: &str,
|
|
options: Option<SearchOptions>,
|
|
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([(0..len, query)], None, cx);
|
|
});
|
|
});
|
|
self.search_options = options;
|
|
self.clear_matches(window, cx);
|
|
cx.notify();
|
|
}
|
|
self.update_matches(!updated, window, cx)
|
|
}
|
|
|
|
fn render_search_option_button(
|
|
&self,
|
|
option: SearchOptions,
|
|
focus_handle: FocusHandle,
|
|
action: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
) -> impl IntoElement {
|
|
let is_active = self.search_options.contains(option);
|
|
option.as_button(is_active, focus_handle, action)
|
|
}
|
|
|
|
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, window, 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_options(&mut self, search_options: SearchOptions, cx: &mut Context<Self>) {
|
|
self.search_options = search_options;
|
|
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,
|
|
_: &SelectPrevMatch,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_match(Direction::Prev, 1, window, cx);
|
|
}
|
|
|
|
fn select_all_matches(
|
|
&mut self,
|
|
_: &SelectAllMatches,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !self.dismissed && self.active_match_index.is_some() {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
|
if 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 {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
|
if 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, window, cx);
|
|
searchable_item.activate_match(new_match_index, 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() {
|
|
if 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, 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, 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, |this, mut cx| async move {
|
|
search.await?;
|
|
this.update_in(&mut cx, |this, window, cx| {
|
|
this.activate_current_match(window, cx)
|
|
})
|
|
})
|
|
.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, 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>,
|
|
) {
|
|
if let Some(active_item) = self.active_searchable_item.as_mut() {
|
|
self.selection_search_enabled = !self.selection_search_enabled;
|
|
active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx);
|
|
drop(self.update_matches(false, window, cx));
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
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,
|
|
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_contains_error = false;
|
|
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 {
|
|
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,
|
|
Default::default(),
|
|
Default::default(),
|
|
None,
|
|
) {
|
|
Ok(query) => query.with_replacement(self.replacement(cx)),
|
|
Err(_) => {
|
|
self.query_contains_error = true;
|
|
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,
|
|
Default::default(),
|
|
Default::default(),
|
|
None,
|
|
) {
|
|
Ok(query) => query.with_replacement(self.replacement(cx)),
|
|
Err(_) => {
|
|
self.query_contains_error = true;
|
|
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, |this, mut cx| async move {
|
|
let matches = matches.await;
|
|
|
|
this.update_in(&mut 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);
|
|
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, window, cx);
|
|
}
|
|
let _ = done_tx.send(());
|
|
}
|
|
cx.notify();
|
|
}
|
|
})
|
|
.log_err();
|
|
}));
|
|
}
|
|
}
|
|
done_rx
|
|
}
|
|
|
|
pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
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(matches, window, cx)
|
|
});
|
|
if new_index != self.active_match_index {
|
|
self.active_match_index = new_index;
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
|
// Search -> Replace -> Editor
|
|
let focus_handle = if self.replace_enabled && self.query_editor_focused {
|
|
self.replacement_editor.focus_handle(cx)
|
|
} else if let Some(item) = self.active_searchable_item.as_ref() {
|
|
item.item_focus_handle(cx)
|
|
} else {
|
|
return;
|
|
};
|
|
self.focus(&focus_handle, window, cx);
|
|
cx.stop_propagation();
|
|
}
|
|
|
|
fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
|
|
// Search -> Replace -> Search
|
|
let focus_handle = if self.replace_enabled && self.query_editor_focused {
|
|
self.replacement_editor.focus_handle(cx)
|
|
} else if self.replacement_editor_focused {
|
|
self.query_editor.focus_handle(cx)
|
|
} else {
|
|
return;
|
|
};
|
|
self.focus(&focus_handle, window, cx);
|
|
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), window, cx));
|
|
} else {
|
|
self.search_history_cursor.reset();
|
|
drop(self.search("", Some(self.search_options), window, cx));
|
|
}
|
|
}
|
|
|
|
fn previous_history_query(
|
|
&mut self,
|
|
_: &PreviousHistoryQuery,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.query(cx).is_empty() {
|
|
if let Some(new_query) = self
|
|
.search_history
|
|
.current(&mut self.search_history_cursor)
|
|
.map(str::to_string)
|
|
{
|
|
drop(self.search(&new_query, Some(self.search_options), 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), window, cx));
|
|
}
|
|
}
|
|
|
|
fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
|
|
cx.on_next_frame(window, |_, 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);
|
|
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() {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
|
if let Some(query) = self.active_search.as_ref() {
|
|
if 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;
|
|
self.focus_editor(&FocusEditor, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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() {
|
|
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
|
if let Some(query) = self.active_search.as_ref() {
|
|
if 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::ops::Range;
|
|
|
|
use super::*;
|
|
use editor::{display_map::DisplayRow, DisplayPoint, Editor, MultiBuffer, SearchSettings};
|
|
use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
|
|
use language::{Buffer, Point};
|
|
use project::Project;
|
|
use settings::SettingsStore;
|
|
use smol::stream::StreamExt as _;
|
|
use unindent::Unindent as _;
|
|
|
|
fn init_globals(cx: &mut TestAppContext) {
|
|
cx.update(|cx| {
|
|
let store = settings::SettingsStore::test(cx);
|
|
cx.set_global(store);
|
|
editor::init(cx);
|
|
|
|
language::init(cx);
|
|
Project::init_settings(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 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(window, cx);
|
|
search_bar.set_active_pane_item(Some(&editor), window, cx);
|
|
search_bar.show(window, cx);
|
|
search_bar
|
|
});
|
|
|
|
(editor, search_bar, cx)
|
|
}
|
|
|
|
#[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, 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, 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(None, 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(cx)),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.update(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(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.update(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(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.update(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(cx)),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.update(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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.update(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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.update(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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.update(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(None, 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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.update(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(None, 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(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)]
|
|
);
|
|
});
|
|
search_bar.update(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(None, 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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.update(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(None, 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(cx)),
|
|
[DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)]
|
|
);
|
|
});
|
|
search_bar.update(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(None, 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(&SelectPrevMatch, window, cx);
|
|
assert_eq!(
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
|
|
[DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)]
|
|
);
|
|
});
|
|
search_bar.update(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<_>>()
|
|
}
|
|
|
|
#[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), 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), 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
|
|
)
|
|
});
|
|
}
|
|
|
|
#[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.to_ascii_lowercase() == '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(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, 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(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(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(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(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(&SelectPrevMatch, 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 SelectPrevMatch"
|
|
);
|
|
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
let all_selections =
|
|
editor.update(cx, |editor, cx| editor.selections.display_ranges(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, 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(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();
|
|
}
|
|
|
|
#[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(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),
|
|
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(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),
|
|
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(cx));
|
|
assert_eq!(
|
|
all_selections.len(),
|
|
2,
|
|
"Should select all `edit(` in the buffer, but got: {all_selections:?}"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_search_query_history(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 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(window, cx);
|
|
search_bar.set_active_pane_item(Some(&editor), window, cx);
|
|
search_bar.show(window, cx);
|
|
search_bar
|
|
});
|
|
|
|
// Add 3 search items into the history.
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("a", None, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("b", None, window, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
search_bar
|
|
.update_in(cx, |search_bar, window, cx| {
|
|
search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), 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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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, 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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
search_bar.update(cx, |search_bar, cx| {
|
|
assert_eq!(search_bar.query(cx), "");
|
|
assert_eq!(search_bar.search_options, SearchOptions::NONE);
|
|
});
|
|
}
|
|
|
|
#[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, 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.update(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), 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.update(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), 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.update(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),
|
|
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.update(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()
|
|
);
|
|
}
|
|
|
|
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, 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
|
|
.update(options.cx, |this, cx| { this.text(cx) }),
|
|
options.expected_text
|
|
);
|
|
}
|
|
|
|
#[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;
|
|
}
|
|
|
|
#[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(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(None, 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, 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),
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[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, false, window, cx)
|
|
});
|
|
|
|
let search_bar = cx.new_window_entity(|window, cx| {
|
|
let mut search_bar = BufferSearchBar::new(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(None, 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, 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),
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
#[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, 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, 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(),
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_search_options_changes(cx: &mut TestAppContext) {
|
|
let (_editor, search_bar, cx) = init_test(cx);
|
|
update_search_settings(
|
|
SearchSettings {
|
|
whole_word: false,
|
|
case_sensitive: false,
|
|
include_ignored: false,
|
|
regex: 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.configured_options,
|
|
SearchOptions::NONE,
|
|
"Should have configured search options matching the settings"
|
|
);
|
|
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::NONE,
|
|
"After hiding and showing the search bar, default options should be used"
|
|
);
|
|
|
|
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 | SearchOptions::WHOLE_WORD,
|
|
"Should enable the options toggled"
|
|
);
|
|
assert!(
|
|
!search_bar.dismissed,
|
|
"Search bar should be present and visible"
|
|
);
|
|
});
|
|
|
|
update_search_settings(
|
|
SearchSettings {
|
|
whole_word: false,
|
|
case_sensitive: true,
|
|
include_ignored: false,
|
|
regex: 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.configured_options,
|
|
SearchOptions::CASE_SENSITIVE,
|
|
"Should have configured search options matching the settings"
|
|
);
|
|
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.search_options,
|
|
SearchOptions::CASE_SENSITIVE,
|
|
"After hiding and showing the search bar, default options should be used"
|
|
);
|
|
});
|
|
}
|
|
|
|
fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
|
|
cx.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
|
settings.search = Some(search_settings);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|