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 9c4e72d27a2877130577aaddb03a849b50d4aff3..997c3468002c416785c93edafbd3e2af07288e22 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 28da3efa6aea7be8411195c8e88247940e241c60..fd7a21cd0ba6369a30f6f485310a6eccc20cbc55 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 ad737990fa0e146fe8fc3f0e2d4351e5507dc96a..d0669b4cfca52a66bd8bd6192b303ea9c83c23aa 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 6a546fbec9b850687370104cc073eafa28628a8e..82fc0a926cdcf935ca4a77c42f22a2b57c46a467 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 220863dfce64b3d2af4ef28f983ae389860c1448..6a6c60fd0792b45d8eb4806f390c5700ca42fe5b 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 c579d8ff7e322fb6540d365aef236025b9596536..ab69d9dbc64338a1d03ab69abfa21515f237b147 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 d54ec8bf9dd14dd2eaa95ba07dd1414b4f23aa06..f5935d49734b64f9e0b6d9a4df45f93d5422383f 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 fb7c3e45bc5af5f58c0e0da84e5b5e0be6708a68..636198bdacce4192011a8c0cd81bcee175f4b6d2 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,