From 3b823d4a0b934c60895de40a1ab3045eccfd14e2 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 11 Jul 2024 10:42:37 +0800 Subject: [PATCH] Add simple support for wrapscan (#13497) For: #13417 This is a simple version, I'm not sure if we just need to limit this feature to vim mode, or maybe in normal editor mode, which involves other logic like the location of the setting Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- assets/settings/default.json | 2 + crates/editor/src/editor_settings.rs | 5 ++ crates/search/src/buffer_search.rs | 11 ++++- crates/search/src/project_search.rs | 13 ++++- crates/search/src/search.rs | 20 ++++++++ crates/vim/src/normal/search.rs | 68 +++++++++++++++++++++++++-- crates/workspace/src/notifications.rs | 19 ++++++-- crates/workspace/src/workspace.rs | 18 ++++--- 8 files changed, 137 insertions(+), 19 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 9c4e72d27a..997c346800 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -262,6 +262,8 @@ // to both the horizontal and vertical delta values while scrolling. "scroll_sensitivity": 1.0, "relative_line_numbers": false, + // If 'search_wrap' is disabled, search result do not wrap around the end of the file. + "search_wrap": true, // When to populate a new search's query based on the text under the cursor. // This setting can take the following three values: // diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 28da3efa6a..fd7a21cd0b 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -25,6 +25,7 @@ pub struct EditorSettings { pub expand_excerpt_lines: u32, #[serde(default)] pub double_click_in_multibuffer: DoubleClickInMultibuffer, + pub search_wrap: bool, #[serde(default)] pub jupyter: Jupyter, } @@ -228,6 +229,10 @@ pub struct EditorSettingsContent { /// /// Default: select pub double_click_in_multibuffer: Option, + /// Whether the editor search results will loop + /// + /// Default: true + pub search_wrap: Option, /// Jupyter REPL settings. pub jupyter: Option, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ad737990fa..d0669b4cfc 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -9,7 +9,7 @@ use any_vec::AnyVec; use collections::HashMap; use editor::{ actions::{Tab, TabPrev}, - DisplayPoint, Editor, EditorElement, EditorStyle, + DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle, }; use futures::channel::oneshot; use gpui::{ @@ -777,6 +777,15 @@ impl BufferSearchBar { .get(&searchable_item.downgrade()) .filter(|matches| !matches.is_empty()) { + // If 'wrapscan' is disabled, searches do not wrap around the end of the file. + if !EditorSettings::get_global(cx).search_wrap { + if (direction == Direction::Next && index + count >= matches.len()) + || (direction == Direction::Prev && index < count) + { + crate::show_no_more_matches(cx); + return; + } + } let new_match_index = searchable_item .match_index_for_direction(matches, index, direction, count, cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6a546fbec9..82fc0a926c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -8,7 +8,8 @@ use editor::{ actions::SelectAll, items::active_match_index, scroll::{Autoscroll, Axis}, - Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN, + Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, + MAX_TAB_TITLE_LEN, }; use gpui::{ actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, @@ -968,6 +969,16 @@ impl ProjectSearchView { fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); + + if !EditorSettings::get_global(cx).search_wrap { + if (direction == Direction::Next && index + 1 >= match_ranges.len()) + || (direction == Direction::Prev && index == 0) + { + crate::show_no_more_matches(cx); + return; + } + } + let new_index = self.results_editor.update(cx, |editor, cx| { editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) }); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 220863dfce..6a6c60fd07 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -5,6 +5,8 @@ use project::search::SearchQuery; pub use project_search::ProjectSearchView; use ui::{prelude::*, Tooltip}; use ui::{ButtonStyle, IconButton}; +use workspace::notifications::NotificationId; +use workspace::{Toast, Workspace}; pub mod buffer_search; pub mod project_search; @@ -107,3 +109,21 @@ impl SearchOptions { }) } } + +pub(crate) fn show_no_more_matches(cx: &mut WindowContext) { + cx.defer(|cx| { + struct NotifType(); + let notification_id = NotificationId::unique::(); + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; + workspace + .update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(notification_id.clone(), "No more matches").autohide(), + cx, + ); + }) + .ok(); + }); +} diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index c579d8ff7e..ab69d9dbc6 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -526,14 +526,15 @@ fn parse_replace_all(query: &str) -> Replacement { mod test { use std::time::Duration; - use editor::{display_map::DisplayRow, DisplayPoint}; - use indoc::indoc; - use search::BufferSearchBar; - use crate::{ state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; + use editor::EditorSettings; + use editor::{display_map::DisplayRow, DisplayPoint}; + use indoc::indoc; + use search::BufferSearchBar; + use settings::SettingsStore; #[gpui::test] async fn test_move_to_next(cx: &mut gpui::TestAppContext) { @@ -572,6 +573,44 @@ mod test { cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); } + #[gpui::test] + async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| s.search_wrap = Some(false)); + }); + + cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes("*"); + cx.run_until_parked(); + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes("*"); + cx.run_until_parked(); + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes("#"); + cx.run_until_parked(); + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes("3 *"); + cx.run_until_parked(); + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes("g *"); + cx.run_until_parked(); + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes("n"); + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes("g #"); + cx.run_until_parked(); + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + } + #[gpui::test] async fn test_search(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; @@ -649,6 +688,27 @@ mod test { cx.assert_editor_state("«oneˇ» two one"); cx.simulate_keystrokes("*"); cx.assert_state("one two ˇone", Mode::Normal); + + // check that searching with unable search wrap + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| s.search_wrap = Some(false)); + }); + cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes("/ c c enter"); + + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + + // n to go to next/N to go to previous + cx.simulate_keystrokes("n"); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes("shift-n"); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + + // ? to go to previous + cx.simulate_keystrokes("? enter"); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes("? enter"); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); } #[gpui::test] diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index d54ec8bf9d..f5935d4973 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -7,7 +7,7 @@ use gpui::{ }; use language::DiagnosticSeverity; -use std::{any::TypeId, ops::DerefMut}; +use std::{any::TypeId, ops::DerefMut, time::Duration}; use ui::{prelude::*, Tooltip}; use util::ResultExt; @@ -174,7 +174,7 @@ impl Workspace { pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext) { self.dismiss_notification(&toast.id, cx); - self.show_notification(toast.id, cx, |cx| { + self.show_notification(toast.id.clone(), cx, |cx| { cx.new_view(|_cx| match toast.on_click.as_ref() { Some((click_msg, on_click)) => { let on_click = on_click.clone(); @@ -184,7 +184,20 @@ impl Workspace { } None => simple_message_notification::MessageNotification::new(toast.msg.clone()), }) - }) + }); + if toast.autohide { + cx.spawn(|workspace, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(5000)) + .await; + workspace + .update(&mut cx, |workspace, cx| { + workspace.dismiss_toast(&toast.id, cx) + }) + .ok(); + }) + .detach(); + } } pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut ViewContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fb7c3e45bc..636198bdac 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -218,9 +218,11 @@ impl_actions!( ] ); +#[derive(Clone)] pub struct Toast { id: NotificationId, msg: Cow<'static, str>, + autohide: bool, on_click: Option<(Cow<'static, str>, Arc)>, } @@ -230,6 +232,7 @@ impl Toast { id, msg: msg.into(), on_click: None, + autohide: false, } } @@ -241,6 +244,11 @@ impl Toast { self.on_click = Some((message.into(), Arc::new(on_click))); self } + + pub fn autohide(mut self) -> Self { + self.autohide = true; + self + } } impl PartialEq for Toast { @@ -251,16 +259,6 @@ impl PartialEq for Toast { } } -impl Clone for Toast { - fn clone(&self) -> Self { - Toast { - id: self.id.clone(), - msg: self.msg.clone(), - on_click: self.on_click.clone(), - } - } -} - #[derive(Debug, Default, Clone, Deserialize, PartialEq)] pub struct OpenTerminal { pub working_directory: PathBuf,