Make BufferSearch less wide (#10459)

This also adds some "responsiveness" so that UI elements are hidden
before everything has to be occluded

Release Notes:

- Improved search UI. It now works in narrower panes, and avoids
scrolling the editor on open.

<img width="899" alt="Screenshot 2024-04-11 at 21 33 17"
src="https://github.com/zed-industries/zed/assets/94272/44b95d4f-08d6-4c40-a175-0e594402ca01">
<img width="508" alt="Screenshot 2024-04-11 at 21 33 45"
src="https://github.com/zed-industries/zed/assets/94272/baf4638d-427b-43e6-ad67-13d43f0f18a2">
<img width="361" alt="Screenshot 2024-04-11 at 21 34 00"
src="https://github.com/zed-industries/zed/assets/94272/ff60b561-2f77-49c0-9df7-e26227fe9225">
<img width="348" alt="Screenshot 2024-04-11 at 21 37 03"
src="https://github.com/zed-industries/zed/assets/94272/a2a700a2-ce99-41bd-bf47-9b14d7082b0e">
This commit is contained in:
Conrad Irwin
2024-04-11 23:07:29 -06:00
committed by GitHub
parent f2d61f3ea5
commit 08786fa7bf
18 changed files with 326 additions and 482 deletions
+4
View File
@@ -471,6 +471,8 @@ pub struct Editor {
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
}
#[derive(Clone)]
@@ -1485,6 +1487,8 @@ impl Editor {
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
expect_bounds_change: None,
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
+2 -1
View File
@@ -3371,6 +3371,7 @@ impl Element for EditorElement {
let overscroll = size(em_width, px(0.));
snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds);
editor.gutter_width = gutter_dimensions.width;
editor.set_visible_line_count(bounds.size.height / line_height, cx);
@@ -3419,7 +3420,7 @@ impl Element for EditorElement {
let autoscroll_horizontally = self.editor.update(cx, |editor, cx| {
let autoscroll_horizontally =
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
editor.autoscroll_vertically(bounds, line_height, cx);
snapshot = editor.snapshot(cx);
autoscroll_horizontally
});
+4
View File
@@ -1167,6 +1167,10 @@ impl SearchableItem for Editor {
&self.buffer().read(cx).snapshot(cx),
)
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
self.expect_bounds_change = self.last_bounds;
}
}
pub fn active_match_index(
+15 -2
View File
@@ -1,6 +1,6 @@
use std::{cmp, f32};
use gpui::{px, Pixels, ViewContext};
use gpui::{px, Bounds, Pixels, ViewContext};
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
@@ -63,13 +63,23 @@ impl AutoscrollStrategy {
impl Editor {
pub fn autoscroll_vertically(
&mut self,
viewport_height: Pixels,
bounds: Bounds<Pixels>,
line_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> bool {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
if let Some(last_bounds) = self.expect_bounds_change.take() {
if scroll_position.y != 0. {
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
if scroll_position.y < 0. {
scroll_position.y = 0.;
}
}
}
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
} else {
@@ -77,6 +87,9 @@ impl Editor {
};
if scroll_position.y > max_scroll_top {
scroll_position.y = max_scroll_top;
}
if original_y != scroll_position.y {
self.set_scroll_position(scroll_position, cx);
}
+5
View File
@@ -2381,6 +2381,11 @@ impl ScrollHandle {
}
}
/// Return the bounds into which this child is painted
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.borrow().bounds
}
/// Get the bounds for a specific child.
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0.borrow().child_bounds.get(ix).cloned()
+148 -187
View File
@@ -1,24 +1,22 @@
mod registrar;
use crate::{
mode::{next_mode, SearchMode},
search_bar::render_nav_button,
ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, NextHistoryQuery,
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
};
use any_vec::AnyVec;
use collections::HashMap;
use editor::{
actions::{Tab, TabPrev},
Editor, EditorElement, EditorStyle,
DisplayPoint, Editor, EditorElement, EditorStyle,
};
use futures::channel::oneshot;
use gpui::{
actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView,
FontStyle, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyContext,
ParentElement as _, Render, Styled, Subscription, Task, TextStyle, View, ViewContext,
VisualContext as _, WhiteSpace, WindowContext,
ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View,
ViewContext, VisualContext as _, WhiteSpace, WindowContext,
};
use project::{
search::SearchQuery,
@@ -29,7 +27,7 @@ use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{h_flex, prelude::*, IconButton, IconName, ToggleButton, Tooltip};
use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX};
use util::ResultExt;
use workspace::{
item::ItemHandle,
@@ -40,7 +38,7 @@ use workspace::{
pub use registrar::DivRegistrar;
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
const MIN_INPUT_WIDTH_REMS: f32 = 15.;
const MIN_INPUT_WIDTH_REMS: f32 = 10.;
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
@@ -95,8 +93,10 @@ pub struct BufferSearchBar {
dismissed: bool,
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
current_mode: SearchMode,
replace_enabled: bool,
scroll_handle: ScrollHandle,
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
}
impl BufferSearchBar {
@@ -142,61 +142,29 @@ impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
impl Render for BufferSearchBar {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if self.dismissed {
return div();
return div().id("search_bar");
}
let narrow_mode =
self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
let hide_inline_icons = self.editor_needed_width
> self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
let supported_options = self.supported_options();
if self.query_editor.update(cx, |query_editor, cx| {
query_editor.placeholder_text(cx).is_none()
}) {
let query_focus_handle = self.query_editor.focus_handle(cx);
let up_keystrokes = cx
.bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle)
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let down_keystrokes = cx
.bindings_for_action_in(&NextHistoryQuery {}, &query_focus_handle)
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let placeholder_text =
up_keystrokes
.zip(down_keystrokes)
.map(|(up_keystrokes, down_keystrokes)| {
Arc::from(format!(
"Search ({}/{} for previous/next query)",
up_keystrokes.join(" "),
down_keystrokes.join(" ")
))
});
if let Some(placeholder_text) = placeholder_text {
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(placeholder_text, cx);
});
}
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 match_color = Color::Default;
let mut text_color = Color::Default;
let match_text = self
.active_searchable_item
.as_ref()
@@ -212,12 +180,11 @@ impl Render for BufferSearchBar {
if let Some(match_ix) = self.active_match_index {
Some(format!("{}/{}", match_ix + 1, matches_count))
} else {
match_color = Color::Error; // No matches found
text_color = Color::Error; // No matches found
None
}
})
.unwrap_or_else(|| "No matches".to_string());
let match_count = Label::new(match_text).color(match_color);
.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(cx);
@@ -233,111 +200,94 @@ impl Render for BufferSearchBar {
};
let search_line = h_flex()
.gap_2()
.child(
h_flex()
.id("editor-scroll")
.track_scroll(&self.editor_scroll_handle)
.flex_1()
.h_8()
.px_2()
.mr_2()
.py_1()
.border_1()
.border_color(editor_border)
.min_w(rems(MIN_INPUT_WIDTH_REMS))
.max_w(rems(MAX_INPUT_WIDTH_REMS))
.rounded_lg()
.child(self.render_text_input(&self.query_editor, match_color.color(cx), cx))
.children(supported_options.case.then(|| {
self.render_search_option_button(
SearchOptions::CASE_SENSITIVE,
cx.listener(|this, _, cx| {
this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
}),
)
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::WHOLE_WORD,
cx.listener(|this, _, cx| this.toggle_whole_word(&ToggleWholeWord, cx)),
)
})),
)
.child(
h_flex()
.gap_2()
.flex_none()
.child(
h_flex()
.child(
ToggleButton::new("search-mode-text", SearchMode::Text.label())
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.current_mode == SearchMode::Text)
.on_click(cx.listener(move |_, _event, cx| {
cx.dispatch_action(SearchMode::Text.action())
}))
.tooltip(|cx| {
Tooltip::for_action(
SearchMode::Text.tooltip(),
&*SearchMode::Text.action(),
cx,
)
})
.first(),
.child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
.when(!hide_inline_icons, |div| {
div.children(supported_options.case.then(|| {
self.render_search_option_button(
SearchOptions::CASE_SENSITIVE,
cx.listener(|this, _, cx| {
this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
}),
)
.child(
ToggleButton::new("search-mode-regex", SearchMode::Regex.label())
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.current_mode == SearchMode::Regex)
.on_click(cx.listener(move |_, _event, cx| {
cx.dispatch_action(SearchMode::Regex.action())
}))
.tooltip(|cx| {
Tooltip::for_action(
SearchMode::Regex.tooltip(),
&*SearchMode::Regex.action(),
cx,
)
})
.last(),
),
)
.when(supported_options.replacement, |this| {
this.child(
IconButton::new(
"buffer-search-bar-toggle-replace-button",
IconName::Replace,
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::WHOLE_WORD,
cx.listener(|this, _, cx| {
this.toggle_whole_word(&ToggleWholeWord, cx)
}),
)
.style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.tooltip(|cx| {
Tooltip::for_action("Toggle replace", &ToggleReplace, cx)
}),
)
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::REGEX,
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
)
}))
}),
)
.when(supported_options.replacement, |this| {
this.child(
IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace)
.style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(self.replace_enabled)
.size(ButtonSize::Compact)
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
)
})
.child(
h_flex()
.gap_2()
.flex_none()
.child(
IconButton::new("select-all", ui::IconName::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
.size(ButtonSize::Compact)
.tooltip(|cx| {
Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
}),
)
.child(div().min_w(rems(6.)).child(match_count))
.child(render_nav_button(
ui::IconName::ChevronLeft,
self.active_match_index.is_some(),
"Select previous match",
&SelectPrevMatch,
))
.when(!narrow_mode, |this| {
this.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(Label::new(match_text).color(
if self.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
},
)),
)
})
.child(render_nav_button(
ui::IconName::ChevronRight,
self.active_match_index.is_some(),
@@ -394,6 +344,8 @@ impl Render for BufferSearchBar {
});
v_flex()
.id("buffer_search")
.track_scroll(&self.scroll_handle)
.key_context(key_context)
.capture_action(cx.listener(Self::tab))
.capture_action(cx.listener(Self::tab_prev))
@@ -402,12 +354,6 @@ impl Render for BufferSearchBar {
.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, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}))
.when(self.supported_options().replacement, |this| {
this.on_action(cx.listener(Self::toggle_replace))
.when(in_replace, |this| {
@@ -421,15 +367,24 @@ impl Render for BufferSearchBar {
.when(self.supported_options().word, |this| {
this.on_action(cx.listener(Self::toggle_whole_word))
})
.when(self.supported_options().regex, |this| {
this.on_action(cx.listener(Self::toggle_regex))
})
.gap_2()
.child(
h_flex().child(search_line.w_full()).child(
IconButton::new(SharedString::from("Close"), IconName::Close)
.tooltip(move |cx| Tooltip::for_action("Close search bar", &Dismiss, cx))
.on_click(
cx.listener(|this, _: &ClickEvent, cx| this.dismiss(&Dismiss, cx)),
),
),
h_flex()
.child(search_line.w_full())
.when(!narrow_mode, |div| {
div.child(
IconButton::new(SharedString::from("Close"), IconName::Close)
.tooltip(move |cx| {
Tooltip::for_action("Close search bar", &Dismiss, cx)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.dismiss(&Dismiss, cx)
})),
)
}),
)
.children(replace_line)
}
@@ -504,21 +459,6 @@ impl BufferSearchBar {
this.toggle_replace(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}));
registrar.register_handler(ForDeployed(|this, action: &CycleMode, cx| {
if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling.
this.cycle_mode(action, cx)
}
}));
registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
this.select_next_match(action, cx);
}));
@@ -569,9 +509,11 @@ impl BufferSearchBar {
project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
),
search_history_cursor: Default::default(),
current_mode: SearchMode::default(),
active_search: None,
replace_enabled: false,
scroll_handle: ScrollHandle::new(),
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
}
}
@@ -589,6 +531,7 @@ impl BufferSearchBar {
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
active_editor.search_bar_visibility_changed(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@@ -630,10 +573,12 @@ impl BufferSearchBar {
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
let Some(handle) = self.active_searchable_item.as_ref() else {
return false;
}
};
self.dismissed = false;
handle.search_bar_visibility_changed(true, cx);
cx.notify();
cx.emit(Event::UpdateLocation);
cx.emit(ToolbarItemEvent::ChangeLocation(
@@ -740,14 +685,6 @@ impl BufferSearchBar {
let is_active = self.search_options.contains(option);
option.as_button(is_active, action)
}
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
if mode == self.current_mode {
return;
}
self.current_mode = mode;
let _ = self.update_matches(cx);
cx.notify();
}
pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_searchable_item.as_ref() {
@@ -763,6 +700,16 @@ impl BufferSearchBar {
cx.notify();
}
pub fn enable_search_option(
&mut self,
search_option: SearchOptions,
cx: &mut ViewContext<Self>,
) {
if !self.search_options.contains(search_option) {
self.toggle_search_option(search_option, cx)
}
}
pub fn set_search_options(
&mut self,
search_options: SearchOptions,
@@ -829,7 +776,7 @@ impl BufferSearchBar {
fn on_query_editor_event(
&mut self,
_: View<Editor>,
editor: View<Editor>,
event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
@@ -839,6 +786,17 @@ impl BufferSearchBar {
editor::EditorEvent::Edited => {
self.clear_matches(cx);
let search = self.update_matches(cx);
let width = editor.update(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
let snapshot = editor.snapshot(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(|this, mut cx| async move {
search.await?;
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
@@ -874,10 +832,15 @@ impl BufferSearchBar {
fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
}
fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
}
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::REGEX, cx)
}
fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
self.active_match_index = None;
@@ -917,7 +880,7 @@ impl BufferSearchBar {
let _ = done_tx.send(());
cx.notify();
} else {
let query: Arc<_> = if self.current_mode == SearchMode::Regex {
let query: Arc<_> = if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
query,
self.search_options.contains(SearchOptions::WHOLE_WORD),
@@ -1065,9 +1028,7 @@ impl BufferSearchBar {
let _ = self.search(&new_query, Some(self.search_options), cx);
}
}
fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
self.activate_search_mode(next_mode(&self.current_mode), cx);
}
fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
if let Some(_) = &self.active_searchable_item {
self.replace_enabled = !self.replace_enabled;
@@ -1910,8 +1871,7 @@ mod tests {
// Let's turn on regex mode.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("\\[([^\\]]+)\\]", None, cx)
search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
})
.await
.unwrap();
@@ -1934,8 +1894,11 @@ mod tests {
// Now with a whole-word twist.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
search_bar.search(
"a\\w+s",
Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
cx,
)
})
.await
.unwrap();
@@ -1963,7 +1926,6 @@ mod tests {
editor: &'a View<Editor>,
search_bar: &'a View<BufferSearchBar>,
cx: &'a mut VisualTestContext,
search_mode: SearchMode,
search_text: &'static str,
search_options: Option<SearchOptions>,
replacement_text: &'static str,
@@ -1975,7 +1937,9 @@ mod tests {
options
.search_bar
.update(options.cx, |search_bar, cx| {
search_bar.activate_search_mode(options.search_mode, cx);
if let Some(options) = options.search_options {
search_bar.set_search_options(options, cx);
}
search_bar.search(options.search_text, options.search_options, cx)
})
.await
@@ -2009,7 +1973,6 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Text,
search_text: "expression",
search_options: None,
replacement_text: r"\n",
@@ -2028,9 +1991,8 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Regex,
search_text: "or",
search_options: Some(SearchOptions::WHOLE_WORD),
search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
replacement_text: r"\\\n\\\\",
replace_all: false,
expected_text: r#"
@@ -2048,9 +2010,8 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Regex,
search_text: r"(that|used) ",
search_options: None,
search_options: Some(SearchOptions::REGEX),
replacement_text: r"$1\n",
replace_all: true,
expected_text: r#"
@@ -2079,7 +2040,7 @@ mod tests {
// Search using valid regexp
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.enable_search_option(SearchOptions::REGEX, cx);
search_bar.search("expression", None, cx)
})
.await
+97 -249
View File
@@ -1,8 +1,7 @@
use crate::{
mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch,
NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace,
ToggleWholeWord,
FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex,
ToggleReplace, ToggleWholeWord,
};
use anyhow::Context as _;
use collections::{HashMap, HashSet};
@@ -15,7 +14,7 @@ use editor::{
use gpui::{
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, Hsla,
InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, Point, Render,
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Point, Render,
SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
WeakModel, WeakView, WhiteSpace, WindowContext,
};
@@ -32,7 +31,7 @@ use std::{
use theme::ThemeSettings;
use ui::{
h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
Selectable, ToggleButton, Tooltip,
Selectable, Tooltip,
};
use util::paths::PathMatcher;
use workspace::{
@@ -72,18 +71,12 @@ pub fn init(cx: &mut AppContext) {
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, cx| {
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
});
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, cx);
});
register_workspace_action(workspace, move |search_bar, action: &ToggleReplace, cx| {
search_bar.toggle_replace(action, cx)
});
register_workspace_action(workspace, move |search_bar, _: &ActivateRegexMode, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx)
});
register_workspace_action(workspace, move |search_bar, _: &ActivateTextMode, cx| {
search_bar.activate_search_mode(SearchMode::Text, cx)
});
register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| {
search_bar.cycle_mode(action, cx)
});
register_workspace_action(
workspace,
move |search_bar, action: &SelectPrevMatch, cx| {
@@ -158,7 +151,6 @@ pub struct ProjectSearchView {
excluded_files_editor: View<Editor>,
filters_enabled: bool,
replace_enabled: bool,
current_mode: SearchMode,
_subscriptions: Vec<Subscription>,
}
@@ -166,7 +158,6 @@ pub struct ProjectSearchView {
struct ProjectSearchSettings {
search_options: SearchOptions,
filters_enabled: bool,
current_mode: SearchMode,
}
pub struct ProjectSearchBar {
@@ -302,7 +293,7 @@ impl Render for ProjectSearchView {
} else if has_no_results {
Label::new("No results")
} else {
Label::new(format!("{} search all files", self.current_mode.label()))
Label::new("Search all files")
};
let major_text = div().justify_center().max_w_96().child(major_text);
@@ -549,7 +540,6 @@ impl ProjectSearchView {
ProjectSearchSettings {
search_options: self.search_options,
filters_enabled: self.filters_enabled,
current_mode: self.current_mode,
}
}
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
@@ -562,39 +552,6 @@ impl ProjectSearchView {
});
}
fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
self.model.update(cx, |model, cx| {
model.pending_search = None;
model.no_results = None;
model.limit_reached = false;
model.match_ranges.clear();
model.excerpts.update(cx, |excerpts, cx| {
excerpts.clear(cx);
});
});
}
fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
let previous_mode = self.current_mode;
if previous_mode == mode {
return;
}
self.clear_search(cx);
self.current_mode = mode;
self.active_match_index = None;
self.search(cx);
cx.update_global(|state: &mut ActiveSettings, cx| {
state.0.insert(
self.model.read(cx).project.downgrade(),
self.current_settings(),
);
});
cx.notify();
}
fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
if self.model.read(cx).match_ranges.is_empty() {
return;
@@ -658,14 +615,10 @@ impl ProjectSearchView {
let mut subscriptions = Vec::new();
// Read in settings if available
let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
(
settings.search_options,
settings.current_mode,
settings.filters_enabled,
)
let (mut options, filters_enabled) = if let Some(settings) = settings {
(settings.search_options, settings.filters_enabled)
} else {
(SearchOptions::NONE, Default::default(), false)
(SearchOptions::NONE, false)
};
{
@@ -682,7 +635,7 @@ impl ProjectSearchView {
let query_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Text search all files", cx);
editor.set_placeholder_text("Search all files..", cx);
editor.set_text(query_text, cx);
editor
});
@@ -769,7 +722,6 @@ impl ProjectSearchView {
included_files_editor,
excluded_files_editor,
filters_enabled,
current_mode,
replace_enabled: false,
_subscriptions: subscriptions,
};
@@ -946,37 +898,8 @@ impl ProjectSearchView {
}
};
let current_mode = self.current_mode;
let query = match current_mode {
SearchMode::Regex => {
match SearchQuery::regex(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
Ok(query) => {
let should_unmark_error =
self.panels_with_errors.remove(&InputPanel::Query);
if should_unmark_error {
cx.notify();
}
Some(query)
}
Err(_e) => {
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
if should_mark_error {
cx.notify();
}
None
}
}
}
_ => match SearchQuery::text(
let query = if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
@@ -1000,7 +923,33 @@ impl ProjectSearchView {
None
}
},
}
} else {
match SearchQuery::text(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
Ok(query) => {
let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
if should_unmark_error {
cx.notify();
}
Some(query)
}
Err(_e) => {
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
if should_mark_error {
cx.notify();
}
None
}
}
};
if !self.panels_with_errors.is_empty() {
return None;
@@ -1116,10 +1065,9 @@ impl ProjectSearchView {
}
fn landing_text_minor(&self) -> SharedString {
match self.current_mode {
SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(),
}
"Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into()
}
fn border_color_for(&self, panel: InputPanel, cx: &WindowContext) -> Hsla {
if self.panels_with_errors.contains(&panel) {
Color::Error.color(cx)
@@ -1127,6 +1075,7 @@ impl ProjectSearchView {
cx.theme().colors().border
}
}
fn move_focus_to_results(&mut self, cx: &mut ViewContext<Self>) {
if !self.results_editor.focus_handle(cx).is_focused(cx)
&& !self.model.read(cx).match_ranges.is_empty()
@@ -1145,17 +1094,6 @@ impl ProjectSearchBar {
}
}
fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext<Self>) {
if let Some(view) = self.active_project_search.as_ref() {
view.update(cx, |this, cx| {
let new_mode = crate::mode::next_mode(&this.current_mode);
this.activate_search_mode(new_mode, cx);
let editor_handle = this.query_editor.focus_handle(cx);
cx.focus(&editor_handle);
});
}
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
@@ -1285,16 +1223,6 @@ impl ProjectSearchBar {
}
}
fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
// Update Current Mode
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
search_view.activate_search_mode(mode, cx);
});
cx.notify();
}
}
fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() {
search.read(cx).search_options.contains(option)
@@ -1372,48 +1300,6 @@ impl ProjectSearchBar {
}
}
fn new_placeholder_text(&self, cx: &mut ViewContext<Self>) -> Option<String> {
let previous_query_keystrokes = cx
.bindings_for_action(&PreviousHistoryQuery {})
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let next_query_keystrokes = cx
.bindings_for_action(&NextHistoryQuery {})
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
(Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!(
"Search ({}/{} for previous/next query)",
previous_query_keystrokes.join(" "),
next_query_keystrokes.join(" ")
)),
(None, Some(next_query_keystrokes)) => Some(format!(
"Search ({} for next query)",
next_query_keystrokes.join(" ")
)),
(Some(previous_query_keystrokes), None) => Some(format!(
"Search ({} for previous query)",
previous_query_keystrokes.join(" ")
)),
(None, None) => None,
};
new_placeholder_text
}
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -1451,19 +1337,12 @@ impl Render for ProjectSearchBar {
let Some(search) = self.active_project_search.clone() else {
return div();
};
let mut key_context = KeyContext::default();
key_context.add("ProjectSearchBar");
if let Some(placeholder_text) = self.new_placeholder_text(cx) {
search.update(cx, |search, cx| {
search.query_editor.update(cx, |this, cx| {
this.set_placeholder_text(placeholder_text, cx)
})
});
}
let search = search.read(cx);
let query_column = h_flex()
.flex_1()
.h_8()
.mr_2()
.px_2()
.py_1()
.border_1()
@@ -1477,79 +1356,39 @@ impl Render for ProjectSearchBar {
.child(self.render_text_input(&search.query_editor, cx))
.child(
h_flex()
.child(
IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
.unwrap_or_default(),
),
)
.child(
IconButton::new("project-search-case-sensitive", IconName::CaseSensitive)
.tooltip(|cx| {
Tooltip::for_action(
"Toggle case sensitive",
&ToggleCaseSensitive,
cx,
)
})
.selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
})),
)
.child(
IconButton::new("project-search-whole-word", IconName::WholeWord)
.tooltip(|cx| {
Tooltip::for_action("Toggle whole word", &ToggleWholeWord, cx)
})
.selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
})),
),
.child(SearchOptions::CASE_SENSITIVE.as_button(
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
}),
))
.child(SearchOptions::WHOLE_WORD.as_button(
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
}),
))
.child(SearchOptions::REGEX.as_button(
self.is_option_enabled(SearchOptions::REGEX, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::REGEX, cx);
}),
)),
);
let mode_column = v_flex().items_start().justify_start().child(
h_flex()
.gap_2()
.child(
h_flex()
.child(
ToggleButton::new("project-search-text-button", "Text")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Text)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Text, cx)
}))
.tooltip(|cx| {
Tooltip::for_action("Toggle text search", &ActivateTextMode, cx)
})
.first(),
)
.child(
ToggleButton::new("project-search-regex-button", "Regex")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Regex)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Regex, cx)
}))
.tooltip(|cx| {
Tooltip::for_action(
"Toggle regular expression search",
&ActivateRegexMode,
cx,
)
})
.last(),
IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
.unwrap_or_default(),
),
)
.child(
@@ -1557,6 +1396,12 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).replace_enabled)
.unwrap_or_default(),
)
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
),
);
@@ -1573,12 +1418,11 @@ impl Render for ProjectSearchBar {
None
}
})
.unwrap_or_else(|| "No matches".to_string());
.unwrap_or_else(|| "0/0".to_string());
let limit_reached = search.model.read(cx).limit_reached;
let matches_column = h_flex()
.child(div().min_w(rems(6.)).child(Label::new(match_text)))
.child(
IconButton::new("project-search-prev-match", IconName::ChevronLeft)
.disabled(search.active_match_index.is_none())
@@ -1593,6 +1437,20 @@ impl Render for ProjectSearchBar {
Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
}),
)
.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(
Label::new(match_text).color(if search.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
}),
),
)
.child(
IconButton::new("project-search-next-match", IconName::ChevronRight)
.disabled(search.active_match_index.is_none())
@@ -1614,7 +1472,6 @@ impl Render for ProjectSearchBar {
});
let search_line = h_flex()
.gap_2()
.flex_1()
.child(query_column)
.child(mode_column)
@@ -1705,17 +1562,11 @@ impl Render for ProjectSearchBar {
});
v_flex()
.key_context(key_context)
.key_context("ProjectSearchBar")
.on_action(cx.listener(|this, _: &ToggleFocus, cx| this.move_focus_to_results(cx)))
.on_action(cx.listener(|this, _: &ToggleFilters, cx| {
this.toggle_filters(cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx)
}))
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx)
}))
.capture_action(cx.listener(|this, action, cx| {
this.tab(action, cx);
cx.stop_propagation();
@@ -1725,9 +1576,6 @@ impl Render for ProjectSearchBar {
cx.stop_propagation();
}))
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
.on_action(cx.listener(|this, action, cx| {
this.cycle_mode(action, cx);
}))
.on_action(cx.listener(|this, action, cx| {
this.toggle_replace(action, cx);
}))
+9 -8
View File
@@ -1,14 +1,12 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::ProjectSearchView;
use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, IconButton};
pub mod buffer_search;
mod mode;
pub mod project_search;
pub(crate) mod search_bar;
@@ -21,19 +19,17 @@ pub fn init(cx: &mut AppContext) {
actions!(
search,
[
CycleMode,
FocusSearch,
ToggleWholeWord,
ToggleCaseSensitive,
ToggleIncludeIgnored,
ToggleRegex,
ToggleReplace,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
NextHistoryQuery,
PreviousHistoryQuery,
ActivateTextMode,
ActivateRegexMode,
ReplaceAll,
ReplaceNext,
]
@@ -46,15 +42,17 @@ bitflags! {
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
const REGEX = 0b1000;
}
}
impl SearchOptions {
pub fn label(&self) -> &'static str {
match *self {
SearchOptions::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case",
SearchOptions::INCLUDE_IGNORED => "Include ignored",
SearchOptions::WHOLE_WORD => "whole word",
SearchOptions::CASE_SENSITIVE => "match case",
SearchOptions::INCLUDE_IGNORED => "include Ignored",
SearchOptions::REGEX => "regular expression",
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@@ -64,6 +62,7 @@ impl SearchOptions {
SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit,
SearchOptions::REGEX => ui::IconName::Regex,
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@@ -73,6 +72,7 @@ impl SearchOptions {
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
SearchOptions::REGEX => Box::new(ToggleRegex),
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@@ -86,6 +86,7 @@ impl SearchOptions {
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options.set(SearchOptions::REGEX, query.is_regex());
options
}
+2
View File
@@ -98,6 +98,7 @@ pub enum IconName {
Plus,
Public,
Quote,
Regex,
Replace,
ReplaceAll,
ReplaceNext,
@@ -196,6 +197,7 @@ impl IconName {
IconName::Plus => "icons/plus.svg",
IconName::Public => "icons/public.svg",
IconName::Quote => "icons/quote.svg",
IconName::Regex => "icons/regex.svg",
IconName::Replace => "icons/replace.svg",
IconName::ReplaceAll => "icons/replace_all.svg",
IconName::ReplaceNext => "icons/replace_next.svg",
+1 -1
View File
@@ -1,7 +1,7 @@
use gpui::{rems, Length, Rems, WindowContext};
/// The base size of a rem, in pixels.
pub(crate) const BASE_REM_SIZE_IN_PX: f32 = 16.;
pub const BASE_REM_SIZE_IN_PX: f32 = 16.;
/// Returns a rem value derived from the provided pixel value and the base rem size (16px).
///
+9 -8
View File
@@ -1,5 +1,5 @@
use gpui::{actions, impl_actions, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
use search::{buffer_search, BufferSearchBar, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Workspace};
@@ -115,7 +115,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
if query.is_empty() {
search_bar.set_replacement(None, cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.set_search_options(SearchOptions::REGEX, cx);
}
vim.workspace_state.search = SearchState {
direction,
@@ -228,7 +228,7 @@ pub fn move_to_internal(
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
let options = SearchOptions::CASE_SENSITIVE;
let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
if !search_bar.show(cx) {
return None;
}
@@ -241,7 +241,6 @@ pub fn move_to_internal(
if whole_word {
query = format!(r"\b{}\b", query);
}
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&query, Some(options), cx))
});
@@ -288,8 +287,11 @@ fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewCo
query = search_bar.query(cx);
};
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
Some(search_bar.search(
&query,
Some(SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX),
cx,
))
});
let Some(search) = search else { return };
let search_bar = search_bar.downgrade();
@@ -326,7 +328,7 @@ fn replace_command(
return None;
}
let mut options = SearchOptions::default();
let mut options = SearchOptions::REGEX;
if replacement.is_case_sensitive {
options.set(SearchOptions::CASE_SENSITIVE, true)
}
@@ -337,7 +339,6 @@ fn replace_command(
};
search_bar.set_replacement(Some(&replacement.replacement), cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&search, Some(options), cx))
});
let Some(search) = search else { return };
+9
View File
@@ -55,6 +55,8 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
}
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
@@ -131,6 +133,7 @@ pub trait SearchableItemHandle: ItemHandle {
matches: &AnyVec<dyn Send>,
cx: &mut WindowContext,
) -> Option<usize>;
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
}
impl<T: SearchableItem> SearchableItemHandle for View<T> {
@@ -227,6 +230,12 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
let mat = mat.downcast_ref().unwrap();
self.update(cx, |this, cx| this.replace(mat, query, cx))
}
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.search_bar_visibility_changed(visible, cx)
});
}
}
impl From<Box<dyn SearchableItemHandle>> for AnyView {