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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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).
|
||||
///
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user