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 <conrad.irwin@gmail.com>
This commit is contained in:
parent
46645b552f
commit
3b823d4a0b
8 changed files with 137 additions and 19 deletions
|
@ -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:
|
||||
//
|
||||
|
|
|
@ -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<DoubleClickInMultibuffer>,
|
||||
/// Whether the editor search results will loop
|
||||
///
|
||||
/// Default: true
|
||||
pub search_wrap: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<Jupyter>,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<Self>) {
|
||||
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)
|
||||
});
|
||||
|
|
|
@ -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::<NotifType>();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(notification_id.clone(), "No more matches").autohide(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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::<EditorSettings>(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::<EditorSettings>(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);
|
||||
|
||||
// ?<enter> 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]
|
||||
|
|
|
@ -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>) {
|
||||
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<Self>) {
|
||||
|
|
|
@ -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<dyn Fn(&mut WindowContext)>)>,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue