diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5957001a27..87a2c5132f 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -136,7 +136,7 @@ "ctrl-k z": "editor::ToggleSoftWrap", "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", - "ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }], + "ctrl-h": "buffer_search::DeployReplace", // "cmd-e": ["buffer_search::Deploy", { "focus": false }], "ctrl->": "assistant::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 0ea34140ab..86a761c0ec 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -145,7 +145,7 @@ "cmd-shift-enter": "editor::NewlineAbove", "cmd-k z": "editor::ToggleSoftWrap", "cmd-f": "buffer_search::Deploy", - "cmd-alt-f": ["buffer_search::Deploy", { "replace_enabled": true }], + "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], "cmd->": "assistant::QuoteSelection", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 491b9c44d3..d24eea221f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -728,6 +728,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, + in_project_search: bool, previous_search_ranges: Option]>>, breadcrumb_header: Option, focused_block: Option, @@ -1426,6 +1427,7 @@ impl Editor { ], tasks_update_task: None, linked_edit_ranges: Default::default(), + in_project_search: false, previous_search_ranges: None, breadcrumb_header: None, focused_block: None, @@ -1703,6 +1705,10 @@ impl Editor { self.collaboration_hub = Some(hub); } + pub fn set_in_project_search(&mut self, in_project_search: bool) { + self.in_project_search = in_project_search; + } + pub fn set_custom_context_menu( &mut self, f: impl 'static diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6568e1f5b1..2679df6a7c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -38,8 +38,11 @@ use text::{BufferId, Selection}; use theme::{Theme, ThemeSettings}; use ui::{h_flex, prelude::*, IconDecorationKind, Label}; use util::{paths::PathExt, ResultExt, TryFutureExt}; -use workspace::item::{BreadcrumbText, FollowEvent}; use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams}; +use workspace::{ + item::{BreadcrumbText, FollowEvent}, + searchable::SearchOptions, +}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, @@ -1324,6 +1327,28 @@ impl SearchableItem for Editor { } } + fn supported_options(&self) -> SearchOptions { + if self.in_project_search { + SearchOptions { + case: true, + word: true, + regex: true, + replacement: false, + selection: false, + find_in_results: true, + } + } else { + SearchOptions { + case: true, + word: true, + regex: true, + replacement: true, + selection: true, + find_in_results: false, + } + } + } + fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; let snapshot = &self.snapshot(window, cx).buffer_snapshot; diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 1ab9ea079b..9d4a236f5f 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1148,11 +1148,12 @@ impl SearchableItem for LspLogView { ) { // Since LSP Log is read-only, it doesn't make sense to support replace operation. } - fn supported_options() -> workspace::searchable::SearchOptions { + fn supported_options(&self) -> workspace::searchable::SearchOptions { workspace::searchable::SearchOptions { case: true, word: true, regex: true, + find_in_results: false, // LSP log is read-only. replacement: false, selection: false, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index cd7015a9a6..671ffb0702 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -55,7 +55,7 @@ pub struct Deploy { impl_actions!(buffer_search, [Deploy]); -actions!(buffer_search, [Dismiss, FocusEditor]); +actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]); impl Deploy { pub fn find() -> Self { @@ -65,6 +65,14 @@ impl Deploy { selection_search_enabled: false, } } + + pub fn replace() -> Self { + Self { + focus: true, + replace_enabled: true, + selection_search_enabled: false, + } + } } pub enum Event { @@ -156,7 +164,7 @@ impl Render for BufferSearchBar { let hide_inline_icons = self.editor_needed_width > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.; - let supported_options = self.supported_options(); + let supported_options = self.supported_options(cx); if self.query_editor.update(cx, |query_editor, _cx| { query_editor.placeholder_text().is_none() @@ -223,6 +231,9 @@ impl Render for BufferSearchBar { let search_line = h_flex() .gap_2() + .when(supported_options.find_in_results, |el| { + el.child(Label::new("Find in results").color(Color::Hint)) + }) .child( input_base_styles() .id("editor-scroll") @@ -328,56 +339,70 @@ impl Render for BufferSearchBar { }), ) }) - .child( - IconButton::new("select-all", ui::IconName::SelectAll) - .on_click(|_, window, cx| { - window.dispatch_action(SelectAllMatches.boxed_clone(), cx) - }) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Select All Matches", - &SelectAllMatches, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - h_flex() - .pl_2() - .ml_1() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child(render_nav_button( - ui::IconName::ChevronLeft, - self.active_match_index.is_some(), - "Select Previous Match", - &SelectPrevMatch, - focus_handle.clone(), + .when(!supported_options.find_in_results, |el| { + el.child( + IconButton::new("select-all", ui::IconName::SelectAll) + .on_click(|_, window, cx| { + window.dispatch_action(SelectAllMatches.boxed_clone(), cx) + }) + .shape(IconButtonShape::Square) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Select All Matches", + &SelectAllMatches, + &focus_handle, + window, + cx, + ) + } + }), + ) + .child( + h_flex() + .pl_2() + .ml_1() + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .child(render_nav_button( + ui::IconName::ChevronLeft, + self.active_match_index.is_some(), + "Select Previous Match", + &SelectPrevMatch, + focus_handle.clone(), + )) + .child(render_nav_button( + ui::IconName::ChevronRight, + self.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + focus_handle.clone(), + )), + ) + .when(!narrow_mode, |this| { + this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child( + Label::new(match_text).size(LabelSize::Small).color( + if self.active_match_index.is_some() { + Color::Default + } else { + Color::Disabled + }, + ), )) - .child(render_nav_button( - ui::IconName::ChevronRight, - self.active_match_index.is_some(), - "Select Next Match", - &SelectNextMatch, - focus_handle.clone(), - )), - ) - .when(!narrow_mode, |this| { - this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child( - Label::new(match_text).size(LabelSize::Small).color( - if self.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - ), - )) + }) + }) + .when(supported_options.find_in_results, |el| { + el.child( + IconButton::new(SharedString::from("Close"), IconName::Close) + .shape(IconButtonShape::Square) + .tooltip(move |window, cx| { + Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) + }) + .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { + this.dismiss(&Dismiss, window, cx) + })), + ) }), ); @@ -447,49 +472,43 @@ 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)) - .when(self.supported_options().replacement, |this| { + .when(self.supported_options(cx).replacement, |this| { this.on_action(cx.listener(Self::toggle_replace)) .when(in_replace, |this| { this.on_action(cx.listener(Self::replace_next)) .on_action(cx.listener(Self::replace_all)) }) }) - .when(self.supported_options().case, |this| { + .when(self.supported_options(cx).case, |this| { this.on_action(cx.listener(Self::toggle_case_sensitive)) }) - .when(self.supported_options().word, |this| { + .when(self.supported_options(cx).word, |this| { this.on_action(cx.listener(Self::toggle_whole_word)) }) - .when(self.supported_options().regex, |this| { + .when(self.supported_options(cx).regex, |this| { this.on_action(cx.listener(Self::toggle_regex)) }) - .when(self.supported_options().selection, |this| { + .when(self.supported_options(cx).selection, |this| { this.on_action(cx.listener(Self::toggle_selection)) }) - .child( - h_flex() - .relative() - .child(search_line.w_full()) - .when(!narrow_mode, |div| { - div.child( - h_flex().absolute().right_0().child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action( - "Close Search Bar", - &Dismiss, - window, - cx, - ) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.dismiss(&Dismiss, window, cx) - })), - ), - ) - }), - ) + .child(h_flex().relative().child(search_line.w_full()).when( + !narrow_mode && !supported_options.find_in_results, + |div| { + div.child( + h_flex().absolute().right_0().child( + IconButton::new(SharedString::from("Close"), IconName::Close) + .shape(IconButtonShape::Square) + .tooltip(move |window, cx| { + Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) + }) + .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { + this.dismiss(&Dismiss, window, cx) + })), + ), + ) + .w_full() + }, + )) .children(replace_line) } } @@ -531,10 +550,15 @@ impl ToolbarItemView for BufferSearchBar { }), )); + let is_project_search = searchable_item_handle.supported_options(cx).find_in_results; self.active_searchable_item = Some(searchable_item_handle); drop(self.update_matches(true, window, cx)); if !self.dismissed { - return ToolbarItemLocation::Secondary; + if is_project_search { + self.dismiss(&Default::default(), window, cx); + } else { + return ToolbarItemLocation::Secondary; + } } } ToolbarItemLocation::Hidden @@ -549,40 +573,56 @@ impl BufferSearchBar { })); registrar.register_handler(ForDeployed( |this, action: &ToggleCaseSensitive, window, cx| { - if this.supported_options().case { + if this.supported_options(cx).case { this.toggle_case_sensitive(action, window, cx); } }, )); registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| { - if this.supported_options().word { + if this.supported_options(cx).word { this.toggle_whole_word(action, window, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| { - if this.supported_options().regex { + if this.supported_options(cx).regex { this.toggle_regex(action, window, cx); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| { - if this.supported_options().selection { + if this.supported_options(cx).selection { this.toggle_selection(action, window, cx); + } else { + cx.propagate(); } })); registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| { - if this.supported_options().replacement { + if this.supported_options(cx).replacement { this.toggle_replace(action, window, cx); + } else { + cx.propagate(); } })); registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| { - this.select_next_match(action, window, cx); + if this.supported_options(cx).find_in_results { + cx.propagate(); + } else { + this.select_next_match(action, window, cx); + } })); registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, window, cx| { - this.select_prev_match(action, window, cx); + if this.supported_options(cx).find_in_results { + cx.propagate(); + } else { + this.select_prev_match(action, window, cx); + } })); registrar.register_handler(WithResults( |this, action: &SelectAllMatches, window, cx| { - this.select_all_matches(action, window, cx); + if this.supported_options(cx).find_in_results { + cx.propagate(); + } else { + this.select_all_matches(action, window, cx); + } }, )); registrar.register_handler(ForDeployed( @@ -599,6 +639,13 @@ impl BufferSearchBar { registrar.register_handler(ForDeployed(|this, deploy, window, cx| { this.deploy(deploy, window, cx); })); + registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| { + if this.supported_options(cx).find_in_results { + cx.propagate(); + } else { + this.deploy(&Deploy::replace(), window, cx); + } + })); registrar.register_handler(ForDismissed(|this, deploy, window, cx| { this.deploy(deploy, window, cx); })) @@ -735,10 +782,10 @@ impl BufferSearchBar { true } - fn supported_options(&self) -> workspace::searchable::SearchOptions { + fn supported_options(&self, cx: &mut Context) -> workspace::searchable::SearchOptions { self.active_searchable_item - .as_deref() - .map(SearchableItemHandle::supported_options) + .as_ref() + .map(|item| item.supported_options(cx)) .unwrap_or_default() } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7506bc6727..f6fb6de2ba 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -421,6 +421,9 @@ impl Item for ProjectSearchView { None } } + fn as_searchable(&self, _: &Entity) -> Option> { + Some(Box::new(self.results_editor.clone())) + } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { self.results_editor @@ -736,6 +739,7 @@ impl ProjectSearchView { let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), true, window, cx); editor.set_searchable(false); + editor.set_in_project_search(true); editor }); subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cde757a2c1..fb63c6f966 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1479,13 +1479,14 @@ impl SerializableItem for TerminalView { impl SearchableItem for TerminalView { type Match = RangeInclusive; - fn supported_options() -> SearchOptions { + fn supported_options(&self) -> SearchOptions { SearchOptions { case: false, word: false, regex: true, replacement: false, selection: false, + find_in_results: false, } } diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 599880cb8e..080307538b 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -42,18 +42,20 @@ pub struct SearchOptions { /// Specifies whether the supports search & replace. pub replacement: bool, pub selection: bool, + pub find_in_results: bool, } pub trait SearchableItem: Item + EventEmitter { type Match: Any + Sync + Send + Clone; - fn supported_options() -> SearchOptions { + fn supported_options(&self) -> SearchOptions { SearchOptions { case: true, word: true, regex: true, replacement: true, selection: true, + find_in_results: false, } } @@ -66,7 +68,7 @@ pub trait SearchableItem: Item + EventEmitter { } fn has_filtered_search_ranges(&mut self) -> bool { - Self::supported_options().selection + self.supported_options().selection } fn toggle_filtered_search_ranges( @@ -157,7 +159,7 @@ pub trait SearchableItem: Item + EventEmitter { pub trait SearchableItemHandle: ItemHandle { fn downgrade(&self) -> Box; fn boxed_clone(&self) -> Box; - fn supported_options(&self) -> SearchOptions; + fn supported_options(&self, cx: &App) -> SearchOptions; fn subscribe_to_search_events( &self, window: &mut Window, @@ -224,8 +226,8 @@ impl SearchableItemHandle for Entity { Box::new(self.clone()) } - fn supported_options(&self) -> SearchOptions { - T::supported_options() + fn supported_options(&self, cx: &App) -> SearchOptions { + self.read(cx).supported_options() } fn subscribe_to_search_events( diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 94722b10e5..9818e1a42b 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -82,7 +82,7 @@ impl Toolbar { } fn secondary_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { + self.items.iter().rev().filter_map(|(item, location)| { if *location == ToolbarItemLocation::Secondary { Some(item.as_ref()) } else { @@ -98,7 +98,7 @@ impl Render for Toolbar { return div(); } - let secondary_item = self.secondary_items().next().map(|item| item.to_any()); + let secondary_items = self.secondary_items().map(|item| item.to_any()); let has_left_items = self.left_items().count() > 0; let has_right_items = self.right_items().count() > 0; @@ -145,7 +145,7 @@ impl Render for Toolbar { }), ) }) - .children(secondary_item) + .children(secondary_items) } }