From 08786fa7bfa6b391b28c18205e6909193458f4fb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 11 Apr 2024 23:07:29 -0600 Subject: [PATCH] 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. Screenshot 2024-04-11 at 21 33 17 Screenshot 2024-04-11 at 21 33 45 Screenshot 2024-04-11 at 21 34 00 Screenshot 2024-04-11 at 21 37 03 --- CONTRIBUTING.md | 2 - README.md | 2 +- assets/icons/regex.svg | 4 + assets/icons/select_all.svg | 6 +- assets/keymaps/default-linux.json | 17 +- assets/keymaps/default-macos.json | 16 +- crates/editor/src/editor.rs | 4 + crates/editor/src/element.rs | 3 +- crates/editor/src/items.rs | 4 + crates/editor/src/scroll/autoscroll.rs | 17 +- crates/gpui/src/elements/div.rs | 5 + crates/search/src/buffer_search.rs | 335 +++++++++++------------- crates/search/src/project_search.rs | 346 +++++++------------------ crates/search/src/search.rs | 17 +- crates/ui/src/components/icon.rs | 2 + crates/ui/src/styles/units.rs | 2 +- crates/vim/src/normal/search.rs | 17 +- crates/workspace/src/searchable.rs | 9 + 18 files changed, 326 insertions(+), 482 deletions(-) create mode 100644 assets/icons/regex.svg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24082a0deb..205fc5b199 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,6 @@ Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor! -We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome. - All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged. ## Contribution ideas diff --git a/README.md b/README.md index de08bee29a..103214ae5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Zed -[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) +[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/ze34actions/workflows/ci.yml) Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg new file mode 100644 index 0000000000..1b24398cc1 --- /dev/null +++ b/assets/icons/regex.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg index 45a10bba42..78c3ee6399 100644 --- a/assets/icons/select_all.svg +++ b/assets/icons/select_all.svg @@ -1,5 +1,5 @@ - - + + + - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3ceeef29b1..e285ef3384 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -212,7 +212,6 @@ "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", "alt-enter": "search::SelectAllMatches", - "alt-tab": "search::CycleMode", "ctrl-f": "search::FocusSearch", "ctrl-h": "search::ToggleReplace" } @@ -235,11 +234,10 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode", "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", - "alt-ctrl-g": "search::ActivateRegexMode", - "alt-ctrl-x": "search::ActivateTextMode" + "alt-ctrl-g": "search::ToggleRegex", + "alt-ctrl-x": "search::ToggleRegex" } }, { @@ -260,10 +258,9 @@ "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode", "ctrl-shift-h": "search::ToggleReplace", - "alt-ctrl-g": "search::ActivateRegexMode", - "alt-ctrl-x": "search::ActivateTextMode" + "alt-ctrl-g": "search::ToggleRegex", + "alt-ctrl-x": "search::ToggleRegex" } }, { @@ -283,10 +280,10 @@ "alt-enter": "search::SelectAllMatches", "alt-c": "search::ToggleCaseSensitive", "alt-w": "search::ToggleWholeWord", - "alt-r": "search::CycleMode", + "alt-r": "search::ToggleRegex", "alt-ctrl-f": "project_search::ToggleFilters", - "ctrl-alt-shift-r": "search::ActivateRegexMode", - "ctrl-alt-shift-x": "search::ActivateTextMode" + "ctrl-alt-shift-r": "search::ToggleRegex", + "ctrl-alt-shift-x": "search::ToggleRegex" } }, // Bindings from VS Code diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 4d64cfb645..9aaa577694 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -233,7 +233,6 @@ "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", "alt-enter": "search::SelectAllMatches", - "alt-tab": "search::CycleMode", "cmd-f": "search::FocusSearch", "cmd-alt-f": "search::ToggleReplace" } @@ -256,11 +255,10 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode", "cmd-shift-f": "search::FocusSearch", "cmd-shift-h": "search::ToggleReplace", - "alt-cmd-g": "search::ActivateRegexMode", - "alt-cmd-x": "search::ActivateTextMode" + "alt-cmd-g": "search::ToggleRegex", + "alt-cmd-x": "search::ToggleRegex" } }, { @@ -281,10 +279,9 @@ "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "search::CycleMode", "cmd-shift-h": "search::ToggleReplace", - "alt-cmd-g": "search::ActivateRegexMode", - "alt-cmd-x": "search::ActivateTextMode" + "alt-cmd-g": "search::ToggleRegex", + "alt-cmd-x": "search::ToggleRegex" } }, { @@ -306,10 +303,9 @@ "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-tab": "search::CycleMode", "alt-cmd-f": "project_search::ToggleFilters", - "alt-cmd-g": "search::ActivateRegexMode", - "alt-cmd-x": "search::ActivateTextMode" + "alt-cmd-g": "search::ToggleRegex", + "alt-cmd-x": "search::ToggleRegex" } }, // Bindings from VS Code diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index da51756d9a..e578fe57ee 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -471,6 +471,8 @@ pub struct Editor { + Fn(&mut Self, DisplayPoint, &mut ViewContext) -> Option>, >, >, + last_bounds: Option>, + expect_bounds_change: Option>, } #[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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b29708aac9..7f242a91ec 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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 }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 042661dff4..30e3fbe25c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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.expect_bounds_change = self.last_bounds; + } } pub fn active_match_index( diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 866eb2b070..b5708649cc 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -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, line_height: Pixels, cx: &mut ViewContext, ) -> 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); } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 0d12f6edba..b6c676cdb9 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2381,6 +2381,11 @@ impl ScrollHandle { } } + /// Return the bounds into which this child is painted + pub fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + /// Get the bounds for a specific child. pub fn bounds_for_item(&self, ix: usize) -> Option> { self.0.borrow().child_bounds.get(ix).cloned() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 34dcf602f6..ae902b8a33 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -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 for BufferSearchBar {} impl Render for BufferSearchBar { fn render(&mut self, cx: &mut ViewContext) -> 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::>() - }); - 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::>() - }); - - 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) -> 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) { - 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) { 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, + ) { + 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: View, event: &editor::EditorEvent, cx: &mut ViewContext, ) { @@ -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.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) } + fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext) { self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) } + fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext) { + 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.activate_search_mode(next_mode(&self.current_mode), cx); - } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { 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, search_bar: &'a View, cx: &'a mut VisualTestContext, - search_mode: SearchMode, search_text: &'static str, search_options: Option, 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 diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 805c8cbe6e..8f6b791720 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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, filters_enabled: bool, replace_enabled: bool, - current_mode: SearchMode, _subscriptions: Vec, } @@ -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) { @@ -562,39 +552,6 @@ impl ProjectSearchView { }); } - fn clear_search(&mut self, cx: &mut ViewContext) { - 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) { - 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) { 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) { 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) { - 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) { 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) { - // 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) -> Option { - let previous_query_keystrokes = cx - .bindings_for_action(&PreviousHistoryQuery {}) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let next_query_keystrokes = cx - .bindings_for_action(&NextHistoryQuery {}) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - 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, cx: &ViewContext) -> 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); })) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 3b710226d4..e086ebae51 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -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 } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 11806f7c1d..5992f88438 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -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", diff --git a/crates/ui/src/styles/units.rs b/crates/ui/src/styles/units.rs index c20a3415a3..31ff61acb7 100644 --- a/crates/ui/src/styles/units.rs +++ b/crates/ui/src/styles/units.rs @@ -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). /// diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 10c3a01811..70b6070322 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -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() { 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 }; diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 04413c1b9f..36df333fb3 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -55,6 +55,8 @@ pub trait SearchableItem: Item + EventEmitter { } } + fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext) {} + fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; @@ -131,6 +133,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &AnyVec, cx: &mut WindowContext, ) -> Option; + fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext); } impl SearchableItemHandle for View { @@ -227,6 +230,12 @@ impl SearchableItemHandle for View { 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> for AnyView {