diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24082a0debaf587bc0c84152bb8b7d09e65b2758..205fc5b199d934aeab5cbefb09ccc0aa3880ba50 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 de08bee29aded0ad561e09887f7eb2ca9cf73fa7..103214ae5f1a1caac92bc3bb7e21625cad12990f 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 0000000000000000000000000000000000000000..1b24398cc1d4bf03bc3b0b15f76476697f578736 --- /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 45a10bba42648ee0f6f9011f4386630609515e0c..78c3ee639939ecfb13d5da18ffcee8b53079c3a6 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 3ceeef29b1247fc6ec345b7cb6916b8b5cdfbc59..e285ef3384cac6a616d9f0dc265842c0d8d7e277 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 4d64cfb645356d6d4a0a291eb32123cfbc0e0e90..9aaa5776943c5acdceb5c1453d17ecdf1ea4fdeb 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 da51756d9aa70d6836cbae5bdce1dc9b80129ecc..e578fe57ee2597dba480149897b62e0c9f38ac41 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 b29708aac9909cb5f25c48513c505f6b7a25be26..7f242a91ec85e551879298960ce0d574e429a0d4 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 042661dff4720fa994f02ed84b7b82cd099e921a..30e3fbe25c935b2b66077208971764fcb6282abe 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 866eb2b0703ad01ca098ad3603679db9ceb517ea..b5708649cc3e6c022a32e687490ce28051fcb5f0 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 0d12f6edba6273128db157a07575199909137ede..b6c676cdb9bd446e01603dda340546f5cffbd3a6 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 34dcf602f6a4e13b3488476fb05584fa4812cd14..ae902b8a3309b70a17cc33d8142f3cfa22aef439 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 805c8cbe6ebb7925725a2b3de58bda116c67bf07..8f6b7917200df05bf82654a2dbe4316ee5393883 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,34 @@ 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) + 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), + 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(); } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { - cx.notify(); - } - None + 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( + } else { + match SearchQuery::text( text, self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::CASE_SENSITIVE), @@ -1000,7 +949,7 @@ impl ProjectSearchView { 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 3b710226d46dbaa55d0204af99cb8eaef7d7f07e..e086ebae51b8d4b8563a6a7e82d95932775ec8a6 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 11806f7c1d193d0f81d35bb5e6b875e076799d68..5992f8843896ff94140b9f3a476437897bfc282b 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 c20a3415a3a4242166f3909dca40f498aad2661d..31ff61acb79a267534926bcde03ddf80e0a9c20a 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 10c3a0181123cae149cde15a3b893319a05ae050..70b60703225f25d5d492cfa61214b7eadcc6e0b5 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 04413c1b9f43513cd863e254b7f8710ea1e69077..36df333fb37f1313b4be67306d1324e981bde6b8 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 {