From 20d8a2a1ec8f01f71fc1402b04f5d14014b0c688 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 27 Jun 2023 11:52:04 -0600 Subject: [PATCH 01/11] vim: indent in visual mode uses only one < Fixes: zed-industries/community#1562 --- assets/keymaps/vim.json | 4 ++-- crates/vim/src/test.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 639daef614332a7103b2195e298fe0fcf8f2eef3..84f9689c2cbb346e7b89d03dea1e0389c5ca6632 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -314,8 +314,8 @@ "vim::SwitchMode", "Normal" ], - "> >": "editor::Indent", - "< <": "editor::Outdent" + ">": "editor::Indent", + "<": "editor::Outdent" } }, { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index a6efbd4da1de34447d7165040bd6da32a79faa9c..95962f85b892b1dfa376bb081dd5e1c22dd9d5fc 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -137,7 +137,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("aa\nbˇb\ncc"); // works in visuial mode - cx.simulate_keystrokes(["shift-v", "down", ">", ">"]); + cx.simulate_keystrokes(["shift-v", "down", ">"]); cx.assert_editor_state("aa\n b«b\n cˇ»c"); } From 75fe77c11dc85a2f8ecbe42e9c6039eecc3f5f6c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 27 Jun 2023 21:46:08 -0600 Subject: [PATCH 02/11] search: Allow running a search with different options Refactor search options to use bitflags so that we can represent the entire set of settings in one place. --- Cargo.lock | 1 + crates/search/Cargo.toml | 1 + crates/search/src/buffer_search.rs | 181 +++++++++++++++++++++------- crates/search/src/project_search.rs | 70 ++++------- crates/search/src/search.rs | 43 ++++--- 5 files changed, 196 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60ed830683a5f91d0c8f4806b5d6758b2afbc31e..c6f5e70c9f0be82bb5d355ebc2a23015495e0f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6428,6 +6428,7 @@ name = "search" version = "0.1.0" dependencies = [ "anyhow", + "bitflags", "client", "collections", "editor", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 7ef388f7c087638c1ee3f5c2002ab3d2c3371dc7..b37d0a46adb7c5a7142e5b1c4e9ee5516bee3072 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -9,6 +9,7 @@ path = "src/search.rs" doctest = false [dependencies] +bitflags = "1" collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 59d25c2659f5ebbcad2b0a7ca4825c8a5bbf0d37..140f5accabe7180ad978c77006cc2f3f170602d7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,5 +1,5 @@ use crate::{ - SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use collections::HashMap; @@ -42,12 +42,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::select_next_match_on_pane); cx.add_action(BufferSearchBar::select_prev_match_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); - add_toggle_option_action::(SearchOption::CaseSensitive, cx); - add_toggle_option_action::(SearchOption::WholeWord, cx); - add_toggle_option_action::(SearchOption::Regex, cx); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_option_action::(SearchOptions::REGEX, cx); } -fn add_toggle_option_action(option: SearchOption, cx: &mut AppContext) { +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) { @@ -69,9 +69,8 @@ pub struct BufferSearchBar { seachable_items_with_matches: HashMap, Vec>>, pending_search: Option>, - case_sensitive: bool, - whole_word: bool, - regex: bool, + search_options: SearchOptions, + default_options: SearchOptions, query_contains_error: bool, dismissed: bool, } @@ -153,19 +152,19 @@ impl View for BufferSearchBar { .with_children(self.render_search_option( supported_options.case, "Case", - SearchOption::CaseSensitive, + SearchOptions::CASE_SENSITIVE, cx, )) .with_children(self.render_search_option( supported_options.word, "Word", - SearchOption::WholeWord, + SearchOptions::WHOLE_WORD, cx, )) .with_children(self.render_search_option( supported_options.regex, "Regex", - SearchOption::Regex, + SearchOptions::REGEX, cx, )) .contained() @@ -250,9 +249,8 @@ impl BufferSearchBar { active_searchable_item_subscription: None, active_match_index: None, seachable_items_with_matches: Default::default(), - case_sensitive: false, - whole_word: false, - regex: false, + default_options: SearchOptions::NONE, + search_options: SearchOptions::NONE, pending_search: None, query_contains_error: false, dismissed: true, @@ -280,6 +278,17 @@ impl BufferSearchBar { } pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { + self.show_with_options(focus, suggest_query, self.default_options, cx) + } + + pub fn show_with_options( + &mut self, + focus: bool, + suggest_query: bool, + search_option: SearchOptions, + cx: &mut ViewContext, + ) -> bool { + self.search_options = search_option; let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -320,7 +329,7 @@ impl BufferSearchBar { &self, option_supported: bool, icon: &'static str, - option: SearchOption, + option: SearchOptions, cx: &mut ViewContext, ) -> Option> { if !option_supported { @@ -328,9 +337,9 @@ impl BufferSearchBar { } let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.is_search_option_enabled(option); + let is_active = self.search_options.contains(option); Some( - MouseEventHandler::::new(option as usize, cx, |state, cx| { + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -346,7 +355,7 @@ impl BufferSearchBar { }) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( - option as usize, + option.bits as usize, format!("Toggle {}", option.label()), Some(option.to_toggle_action()), tooltip_style, @@ -461,21 +470,10 @@ impl BufferSearchBar { } } - fn is_search_option_enabled(&self, search_option: SearchOption) -> bool { - match search_option { - SearchOption::WholeWord => self.whole_word, - SearchOption::CaseSensitive => self.case_sensitive, - SearchOption::Regex => self.regex, - } - } + fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(search_option); + self.default_options = self.search_options; - fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext) { - let value = match search_option { - SearchOption::WholeWord => &mut self.whole_word, - SearchOption::CaseSensitive => &mut self.case_sensitive, - SearchOption::Regex => &mut self.regex, - }; - *value = !*value; self.update_matches(false, cx); cx.notify(); } @@ -571,11 +569,11 @@ impl BufferSearchBar { self.active_match_index.take(); active_searchable_item.clear_matches(cx); } else { - let query = if self.regex { + let query = if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( query, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), Vec::new(), Vec::new(), ) { @@ -589,8 +587,8 @@ impl BufferSearchBar { } else { SearchQuery::text( query, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), Vec::new(), Vec::new(), ) @@ -656,8 +654,7 @@ mod tests { use language::Buffer; use unindent::Unindent as _; - #[gpui::test] - async fn test_search_simple(cx: &mut TestAppContext) { + fn init_test(cx: &mut TestAppContext) -> (ViewHandle, ViewHandle) { crate::project_search::tests::init_test(cx); let buffer = cx.add_model(|cx| { @@ -684,6 +681,13 @@ mod tests { search_bar }); + (editor, search_bar) + } + + #[gpui::test] + async fn test_search_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + // Search for a string that appears with different casing. // By default, search is case-insensitive. search_bar.update(cx, |search_bar, cx| { @@ -708,7 +712,7 @@ mod tests { // Switch to a case sensitive search. search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOption::CaseSensitive, cx); + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -765,7 +769,7 @@ mod tests { // Switch to a whole word search. search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOption::WholeWord, cx); + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -966,4 +970,99 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(2)); }); } + + #[gpui::test] + async fn test_search_with_options(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + // show with options should make current search case sensitive + search_bar.update(cx, |search_bar, cx| { + search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); + search_bar.set_query("us", cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // show should return to the default options (case insensitive) + search_bar.update(cx, |search_bar, cx| { + search_bar.show(true, true, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ) + ] + ); + }); + + // toggling a search option (even in show_with_options mode) should update the defaults + search_bar.update(cx, |search_bar, cx| { + search_bar.set_query("regex", cx); + search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ),] + ); + }); + + // defaults should still include whole word + search_bar.update(cx, |search_bar, cx| { + search_bar.show(true, true, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ),] + ); + }); + + // removing whole word changes the search again + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[ + ( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ), + ( + DisplayPoint::new(0, 44)..DisplayPoint::new(0, 49), + Color::red() + ) + ] + ); + }); + } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 135194df6a2f5a22c1ed001a725488ae2c003437..4e485eaaabb3d03906e62a48804468303cc832fd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,5 +1,5 @@ use crate::{ - SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use anyhow::Result; @@ -51,12 +51,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::select_prev_match); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); - add_toggle_option_action::(SearchOption::CaseSensitive, cx); - add_toggle_option_action::(SearchOption::WholeWord, cx); - add_toggle_option_action::(SearchOption::Regex, cx); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_option_action::(SearchOptions::REGEX, cx); } -fn add_toggle_option_action(option: SearchOption, cx: &mut AppContext) { +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if search_bar.update(cx, |search_bar, cx| { @@ -89,9 +89,7 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, - case_sensitive: bool, - whole_word: bool, - regex: bool, + search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, search_id: usize, @@ -408,9 +406,7 @@ impl ProjectSearchView { let project; let excerpts; let mut query_text = String::new(); - let mut regex = false; - let mut case_sensitive = false; - let mut whole_word = false; + let mut options = SearchOptions::NONE; { let model = model.read(cx); @@ -418,9 +414,7 @@ impl ProjectSearchView { excerpts = model.excerpts.clone(); if let Some(active_query) = model.active_query.as_ref() { query_text = active_query.as_str().to_string(); - regex = active_query.is_regex(); - case_sensitive = active_query.case_sensitive(); - whole_word = active_query.whole_word(); + options = SearchOptions::from_query(active_query); } } cx.observe(&model, |this, _, cx| this.model_changed(cx)) @@ -496,9 +490,7 @@ impl ProjectSearchView { model, query_editor, results_editor, - case_sensitive, - whole_word, - regex, + search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, query_editor_was_focused: false, @@ -594,11 +586,11 @@ impl ProjectSearchView { return None; } }; - if self.regex { + if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( text, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, ) { @@ -615,8 +607,8 @@ impl ProjectSearchView { } else { Some(SearchQuery::text( text, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, )) @@ -765,9 +757,7 @@ impl ProjectSearchBar { search_view.query_editor.update(cx, |editor, cx| { editor.set_text(old_query.as_str(), cx); }); - search_view.regex = old_query.is_regex(); - search_view.whole_word = old_query.whole_word(); - search_view.case_sensitive = old_query.case_sensitive(); + search_view.search_options = SearchOptions::from_query(&old_query); } } new_query @@ -855,15 +845,10 @@ impl ProjectSearchBar { }); } - fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext) -> bool { + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - let value = match option { - SearchOption::WholeWord => &mut search_view.whole_word, - SearchOption::CaseSensitive => &mut search_view.case_sensitive, - SearchOption::Regex => &mut search_view.regex, - }; - *value = !*value; + search_view.search_options.toggle(option); search_view.search(cx); }); cx.notify(); @@ -920,12 +905,12 @@ impl ProjectSearchBar { fn render_option_button( &self, icon: &'static str, - option: SearchOption, + option: SearchOptions, cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::::new(option as usize, cx, |state, cx| { + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -941,7 +926,7 @@ impl ProjectSearchBar { }) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( - option as usize, + option.bits as usize, format!("Toggle {}", option.label()), Some(option.to_toggle_action()), tooltip_style, @@ -950,14 +935,9 @@ impl ProjectSearchBar { .into_any() } - fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool { + fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - match option { - SearchOption::WholeWord => search.whole_word, - SearchOption::CaseSensitive => search.case_sensitive, - SearchOption::Regex => search.regex, - } + search.read(cx).search_options.contains(option) } else { false } @@ -1048,17 +1028,17 @@ impl View for ProjectSearchBar { Flex::row() .with_child(self.render_option_button( "Case", - SearchOption::CaseSensitive, + SearchOptions::CASE_SENSITIVE, cx, )) .with_child(self.render_option_button( "Word", - SearchOption::WholeWord, + SearchOptions::WHOLE_WORD, cx, )) .with_child(self.render_option_button( "Regex", - SearchOption::Regex, + SearchOptions::REGEX, cx, )) .contained() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 90ea508cc61ad59b64606bede16c35882452d2b7..efec4c2516fcffdcc3326f1f32774dc5c2626450 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,5 +1,7 @@ +use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; +use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub mod buffer_search; @@ -21,27 +23,40 @@ actions!( ] ); -#[derive(Clone, Copy, PartialEq)] -pub enum SearchOption { - WholeWord, - CaseSensitive, - Regex, +bitflags! { + #[derive(Default)] + pub struct SearchOptions: u8 { + const NONE = 0b000; + const WHOLE_WORD = 0b001; + const CASE_SENSITIVE = 0b010; + const REGEX = 0b100; + } } -impl SearchOption { +impl SearchOptions { pub fn label(&self) -> &'static str { - match self { - SearchOption::WholeWord => "Match Whole Word", - SearchOption::CaseSensitive => "Match Case", - SearchOption::Regex => "Use Regular Expression", + match *self { + SearchOptions::WHOLE_WORD => "Match Whole Word", + SearchOptions::CASE_SENSITIVE => "Match Case", + SearchOptions::REGEX => "Use Regular Expression", + _ => panic!("{:?} is not a named SearchOption", self), } } pub fn to_toggle_action(&self) -> Box { - match self { - SearchOption::WholeWord => Box::new(ToggleWholeWord), - SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive), - SearchOption::Regex => Box::new(ToggleRegex), + match *self { + SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), + SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + SearchOptions::REGEX => Box::new(ToggleRegex), + _ => panic!("{:?} is not a named SearchOption", self), } } + + pub fn from_query(query: &SearchQuery) -> SearchOptions { + let mut options = SearchOptions::NONE; + options.set(SearchOptions::WHOLE_WORD, query.whole_word()); + options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); + options.set(SearchOptions::REGEX, query.is_regex()); + options + } } From 2ffce24ef09715e754e078f61f4cba6ffdc29f9a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 11:47:14 -0600 Subject: [PATCH 03/11] vim: Don't enter visual mode in search/go to definition Fixes: zed-industries/community#1514 Contributes: zed-industries/community#1284 --- crates/editor/src/editor.rs | 15 +++++++++++++++ crates/editor/src/items.rs | 5 ++--- crates/vim/src/vim.rs | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d7b8ffad6d2804a59affb0935d06306695693e3..259037c3ff93de0212e794ee58397866cf6ae37f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -544,6 +544,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + collapse_matches: bool, workspace: Option<(WeakViewHandle, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, @@ -1376,6 +1377,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + collapse_matches: false, workspace: None, keymap_context_layers: Default::default(), input_enabled: true, @@ -1515,6 +1517,17 @@ impl Editor { cx.notify(); } + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } + + fn range_for_match(&self, range: &Range) -> Range { + if self.collapse_matches { + return range.start..range.start; + } + range.clone() + } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { if self.display_map.read(cx).clip_at_line_ends != clip { self.display_map @@ -6233,6 +6246,7 @@ impl Editor { .to_offset(definition.target.buffer.read(cx)); if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { + let range = self.range_for_match(&range); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); @@ -6245,6 +6259,7 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); + let range = target_editor.range_for_match(&range); target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 74b8e0ddb68d81f3d00fafa518cfbff4bc4c71b6..86f26bcf944dc9675efa253783f9638d9a9915a0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -936,9 +936,8 @@ impl SearchableItem for Editor { cx: &mut ViewContext, ) { self.unfold_ranges([matches[index].clone()], false, true, cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([matches[index].clone()]) - }); + let range = self.range_for_match(&matches[index]); + self.change_selections(Some(Autoscroll::fit()), cx, |s| s.select_ranges([range])); } fn match_index_for_direction( diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2bcc2254eed1d75dd88ec446428c306cd1d677ba..ada8f2c1defdd5014d673239b9c3f57d5e8124d9 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -295,11 +295,15 @@ impl Vim { if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); } else { + // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur, + // but we need collapse_matches to persist when the search bar is focused. + editor.set_collapse_matches(false); Self::unhook_vim_settings(editor, cx); } }); From 96ce0bb78338d185bf0e2c223696f748e3c319a1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 12:48:15 -0600 Subject: [PATCH 04/11] vim: Enter/n/N to navigate search results --- assets/keymaps/vim.json | 8 ++++++++ crates/vim/src/test.rs | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 84f9689c2cbb346e7b89d03dea1e0389c5ca6632..72aa7194080f20a1215867822f4cba90f6129523 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -60,6 +60,8 @@ "ignorePunctuation": true } ], + "n": "search::SelectNextMatch", + "shift-n": "search::SelectPrevMatch", "%": "vim::Matching", "f": [ "vim::PushOperator", @@ -335,5 +337,11 @@ "Normal" ] } + }, + { + "context": "BufferSearchBar", + "bindings": { + "enter": "buffer_search::FocusEditor" + } } ] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 95962f85b892b1dfa376bb081dd5e1c22dd9d5fc..d9d24ec30eeb19577cadf743c7ba84db874210ff 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -5,6 +5,7 @@ mod vim_binding_test_context; mod vim_test_context; use command_palette::CommandPalette; +use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; pub use vim_binding_test_context::*; @@ -153,3 +154,44 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { assert!(!cx.workspace(|workspace, _| workspace.modal::().is_some())); cx.assert_state("aˇbc\n", Mode::Insert); } + +#[gpui::test] +async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["/", "c", "c"]); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + }); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.update_editor(|editor, cx| { + let highlights = editor.all_background_highlights(cx); + assert_eq!(3, highlights.len()); + assert_eq!( + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), + highlights[0].0 + ) + }); + cx.simulate_keystrokes(["enter"]); + + cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["n"]); + cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["shift-n"]); + cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); +} From dbec2ed1f137dc36dfd359f548da2f10d768dfeb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 13:13:02 -0600 Subject: [PATCH 05/11] vim: add */#/g*/g# for jumping to next word As in vim, this toggles the normal search experience. --- assets/keymaps/vim.json | 16 ++++- crates/search/src/buffer_search.rs | 28 +++++++- crates/vim/src/normal.rs | 4 ++ crates/vim/src/normal/search.rs | 108 +++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 crates/vim/src/normal/search.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 72aa7194080f20a1215867822f4cba90f6129523..20b831b0e525e3abbf3d53352ba187ceccce3367 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,6 +101,8 @@ "vim::SwitchMode", "Normal" ], + "*": "vim::MoveToNext", + "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -240,7 +242,19 @@ "vim::SwitchMode", "Normal" ], - "d": "editor::GoToDefinition" + "d": "editor::GoToDefinition", + "*": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "#": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ] } }, { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 140f5accabe7180ad978c77006cc2f3f170602d7..fb4d5da7648dd387a4904b28ec95ab28c7fefdb3 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -65,6 +65,7 @@ pub struct BufferSearchBar { pub query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, + pending_match_direction: Option, active_searchable_item_subscription: Option, seachable_items_with_matches: HashMap, Vec>>, @@ -252,6 +253,7 @@ impl BufferSearchBar { default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, + pending_match_direction: None, query_contains_error: false, dismissed: true, } @@ -285,10 +287,10 @@ impl BufferSearchBar { &mut self, focus: bool, suggest_query: bool, - search_option: SearchOptions, + search_options: SearchOptions, cx: &mut ViewContext, ) -> bool { - self.search_options = search_option; + self.search_options = search_options; let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -486,6 +488,17 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } + pub fn select_word_under_cursor( + &mut self, + direction: Direction, + options: SearchOptions, + cx: &mut ViewContext, + ) { + self.active_match_index = None; + self.pending_match_direction = Some(direction); + self.show_with_options(false, true, options, cx); + } + pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { @@ -567,6 +580,7 @@ impl BufferSearchBar { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); + self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); } else { let query = if self.search_options.contains(SearchOptions::REGEX) { @@ -614,7 +628,15 @@ impl BufferSearchBar { .unwrap(); active_searchable_item.update_matches(matches, cx); if select_closest_match { - if let Some(match_ix) = this.active_match_index { + if let Some(mut match_ix) = this.active_match_index { + if let Some(direction) = this.pending_match_direction.take() + { + match_ix += match direction { + Direction::Next => 1, + Direction::Prev => matches.len() - 1, + }; + match_ix = match_ix % matches.len(); + } active_searchable_item .activate_match(match_ix, matches, cx); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1227afbb85e4f8d78b5dab54d9b9f5410f588863..c382a08b5c03ea173cb6dd27a70ab2abe7301644 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,6 +2,7 @@ mod case; mod change; mod delete; mod scroll; +mod search; mod substitute; mod yank; @@ -27,6 +28,7 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, + search::{move_to_next, move_to_prev}, substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -57,6 +59,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + cx.add_action(move_to_next); + cx.add_action(move_to_prev); cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..49eb97c9acb595d52e1aa8f962b5313d2cba1c70 --- /dev/null +++ b/crates/vim/src/normal/search.rs @@ -0,0 +1,108 @@ +use gpui::{impl_actions, ViewContext}; +use search::{BufferSearchBar, SearchOptions}; +use serde_derive::Deserialize; +use workspace::{searchable::Direction, Workspace}; + +use crate::Vim; + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToNext { + #[serde(default)] + partial_word: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToPrev { + #[serde(default)] + partial_word: bool, +} + +impl_actions!(vim, [MoveToNext, MoveToPrev]); + +pub(crate) fn move_to_next( + workspace: &mut Workspace, + action: &MoveToNext, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Next, !action.partial_word, cx) +} + +pub(crate) fn move_to_prev( + workspace: &mut Workspace, + action: &MoveToPrev, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Prev, !action.partial_word, cx) +} + +fn move_to_internal( + workspace: &mut Workspace, + direction: Direction, + whole_word: bool, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let mut options = SearchOptions::CASE_SENSITIVE; + options.set(SearchOptions::WHOLE_WORD, whole_word); + search_bar.select_word_under_cursor(direction, options, cx); + }); + } + }); + vim.clear_operator(cx); + }); +} + +#[cfg(test)] +mod test { + use search::BufferSearchBar; + + use crate::{state::Mode, test::VimTestContext}; + + #[gpui::test] + async fn test_move_to_next(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["g", "*"]); + search_bar.next_notification(&cx).await; + 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", "#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + } +} From d70f415e8ea81c40083623b976cec2120cee44c7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 13:17:48 -0600 Subject: [PATCH 06/11] vim: add gD to go to type definition --- assets/keymaps/vim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 20b831b0e525e3abbf3d53352ba187ceccce3367..b9497394ab99c5fcc3f43913a9aabfa863362233 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -243,6 +243,7 @@ "Normal" ], "d": "editor::GoToDefinition", + "shift-d": "editor::GoToTypeDefinition", "*": [ "vim::MoveToNext", { From 6cf13c62d11334dbe145c248d816a1831f08be87 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 6 Jul 2023 13:21:01 -0600 Subject: [PATCH 07/11] vim: ? to search backwards, and / to repeat search --- assets/keymaps/vim.json | 10 ++- crates/vim/src/normal.rs | 4 +- crates/vim/src/normal/search.rs | 110 +++++++++++++++++++++--- crates/vim/src/test.rs | 2 +- crates/vim/src/test/vim_test_context.rs | 1 + 5 files changed, 106 insertions(+), 21 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b9497394ab99c5fcc3f43913a9aabfa863362233..4a215dcef3116d3609a3445c8f5be34ad9f2b7d9 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -197,10 +197,11 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "/": [ - "buffer_search::Deploy", + "/": "vim::Search", + "?": [ + "vim::Search", { - "focus": true + "backwards": true, } ], "ctrl-f": "vim::PageDown", @@ -356,7 +357,8 @@ { "context": "BufferSearchBar", "bindings": { - "enter": "buffer_search::FocusEditor" + "enter": "buffer_search::FocusEditor", + "escape": "buffer_search::Dismiss" } } ] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index c382a08b5c03ea173cb6dd27a70ab2abe7301644..8dcaa5008ef60f1e184ff2b251569d20a5b68943 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -28,7 +28,6 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, - search::{move_to_next, move_to_prev}, substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -59,8 +58,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); - cx.add_action(move_to_next); - cx.add_action(move_to_prev); + search::init(cx); cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 49eb97c9acb595d52e1aa8f962b5313d2cba1c70..5f0ed9e7b989e954fa15a646fb8cdb6befc61843 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,4 +1,4 @@ -use gpui::{impl_actions, ViewContext}; +use gpui::{impl_actions, AppContext, ViewContext}; use search::{BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Workspace}; @@ -19,25 +19,47 @@ pub(crate) struct MoveToPrev { partial_word: bool, } -impl_actions!(vim, [MoveToNext, MoveToPrev]); +#[derive(Clone, Deserialize, PartialEq)] +pub(crate) struct Search { + #[serde(default)] + backwards: bool, +} -pub(crate) fn move_to_next( - workspace: &mut Workspace, - action: &MoveToNext, - cx: &mut ViewContext, -) { +impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); + +pub(crate) fn init(cx: &mut AppContext) { + cx.add_action(move_to_next); + cx.add_action(move_to_prev); + cx.add_action(search); +} + +fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { move_to_internal(workspace, Direction::Next, !action.partial_word, cx) } -pub(crate) fn move_to_prev( - workspace: &mut Workspace, - action: &MoveToPrev, - cx: &mut ViewContext, -) { +fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext) { move_to_internal(workspace, Direction::Prev, !action.partial_word, cx) } -fn move_to_internal( +fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + search_bar.select_match(direction, cx); + search_bar.show_with_options(true, false, options, cx); + }) + } + }) +} + +pub fn move_to_internal( workspace: &mut Workspace, direction: Direction, whole_word: bool, @@ -60,6 +82,7 @@ fn move_to_internal( #[cfg(test)] mod test { + use editor::DisplayPoint; use search::BufferSearchBar; use crate::{state::Mode, test::VimTestContext}; @@ -105,4 +128,65 @@ mod test { search_bar.next_notification(&cx).await; 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; + + cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "c", "c"]); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + }); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.update_editor(|editor, cx| { + let highlights = editor.all_background_highlights(cx); + assert_eq!(3, highlights.len()); + assert_eq!( + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), + highlights[0].0 + ) + }); + + cx.simulate_keystrokes(["enter"]); + + // n to go to next/N to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["n"]); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["shift-n"]); + + // ? to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + + // / to go to next + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + + // ?{search} to search backwards + cx.simulate_keystrokes(["?", "b", "enter"]); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index d9d24ec30eeb19577cadf743c7ba84db874210ff..8ed649e61bef510d8cf9fa76eb8d2edf3eca4e69 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -97,7 +97,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); + assert_eq!(bar.query_editor.read(cx).text(cx), ""); }) } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index f9ba577231f5de6317f1194e14a6ad4cf139e06b..56ca654644c5277fc8eca5e8fcd769815a6a356c 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> { self.cx.set_state(text) } + #[track_caller] pub fn assert_state(&mut self, text: &str, mode: Mode) { self.assert_editor_state(text); assert_eq!(self.mode(), mode, "{}", self.assertion_context()); From dea728a7e58f746d5a1429894451286febd85d66 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 09:10:11 -0600 Subject: [PATCH 08/11] Better waiting in tests --- crates/vim/src/normal/search.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 5f0ed9e7b989e954fa15a646fb8cdb6befc61843..bb57045add719ff42a834fbdcde513ab000b21ed 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -82,13 +82,18 @@ pub fn move_to_internal( #[cfg(test)] mod test { + use std::sync::Arc; + use editor::DisplayPoint; use search::BufferSearchBar; use crate::{state::Mode, test::VimTestContext}; #[gpui::test] - async fn test_move_to_next(cx: &mut gpui::TestAppContext) { + async fn test_move_to_next( + cx: &mut gpui::TestAppContext, + deterministic: Arc, + ) { let mut cx = VimTestContext::new(cx, true).await; let search_bar = cx.workspace(|workspace, cx| { workspace @@ -102,30 +107,30 @@ mod test { cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); cx.simulate_keystrokes(["#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["g", "*"]); - search_bar.next_notification(&cx).await; + deterministic.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", "#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); } From 232d14a3ae6a86f027edcf907fbf74f3bd44e8e1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 10:55:32 -0600 Subject: [PATCH 09/11] Make search less magic Co-Authored-By: Antonio --- crates/ai/src/assistant.rs | 18 ++- crates/search/src/buffer_search.rs | 189 ++++++++++++++++------------- crates/vim/src/normal/search.rs | 13 +- 3 files changed, 129 insertions(+), 91 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4d300230e16109558ac7f2d8850132912c76f67e..3cc97468c32f177e037af618da98da14d7e7f32e 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -298,12 +298,22 @@ impl AssistantPanel { } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) { - return; - } + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false + } + }); + } + if propagate_action { + cx.propagate_action(); } - cx.propagate_action(); } fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index fb4d5da7648dd387a4904b28ec95ab28c7fefdb3..c8d1c58b6f265e0a15201fc1033b61eea58b5de6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -4,12 +4,14 @@ use crate::{ }; use collections::HashMap; use editor::Editor; +use futures::channel::oneshot; use gpui::{ actions, elements::*, impl_actions, platform::{CursorStyle, MouseButton}, Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, + WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -50,12 +52,11 @@ pub fn init(cx: &mut AppContext) { fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) { - search_bar.update(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { search_bar.toggle_search_option(option, cx); - }); - return; - } + } + }); } cx.propagate_action(); }); @@ -209,7 +210,7 @@ impl ToolbarItemView for BufferSearchBar { )); self.active_searchable_item = Some(searchable_item_handle); - self.update_matches(false, cx); + let _ = self.update_matches(cx); if !self.dismissed { return ToolbarItemLocation::Secondary; } @@ -279,54 +280,75 @@ impl BufferSearchBar { cx.notify(); } - pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { - self.show_with_options(focus, suggest_query, self.default_options, cx) - } - - pub fn show_with_options( - &mut self, - focus: bool, - suggest_query: bool, - search_options: SearchOptions, - cx: &mut ViewContext, - ) -> bool { - self.search_options = search_options; + pub fn show(&mut self, cx: &mut ViewContext) -> bool { let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { return false; }; - if suggest_query { - let text = searchable_item.query_suggestion(cx); - if !text.is_empty() { - self.set_query(&text, cx); - } - } - - if focus { - let query_editor = self.query_editor.clone(); - query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&editor::SelectAll, cx); - }); - cx.focus_self(); - } - self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); true } - fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + pub fn search_suggested(&mut self, cx: &mut ViewContext) { + let search = self + .query_suggestion(cx) + .map(|suggestion| self.search(&suggestion, self.default_options, cx)); + + if let Some(search) = search { + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| { + if let Some(match_ix) = this.active_match_index { + if let Some(active_searchable_item) = this.active_searchable_item.as_ref() { + if let Some(matches) = this + .seachable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx); + } + } + } + }) + }) + .detach_and_log_err(cx); + } + } + + pub fn select_query(&mut self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); + query_editor.select_all(&Default::default(), cx); }); } + pub fn query_suggestion(&self, cx: &mut ViewContext) -> Option { + Some(self.active_searchable_item.as_ref()?.query_suggestion(cx)) + } + + fn search( + &mut self, + query: &str, + options: SearchOptions, + cx: &mut ViewContext, + ) -> oneshot::Receiver<()> { + if query != self.query_editor.read(cx).text(cx) || self.search_options != options { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + self.search_options = options; + self.query_contains_error = false; + self.clear_matches(cx); + cx.notify(); + } + self.update_matches(cx) + } + fn render_search_option( &self, option_supported: bool, @@ -448,12 +470,23 @@ impl BufferSearchBar { } fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) { - return; - } + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false; + } + }); + } + + if propagate_action { + cx.propagate_action(); } - cx.propagate_action(); } fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { @@ -475,8 +508,7 @@ impl BufferSearchBar { fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { self.search_options.toggle(search_option); self.default_options = self.search_options; - - self.update_matches(false, cx); + let _ = self.update_matches(cx); cx.notify(); } @@ -488,17 +520,6 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } - pub fn select_word_under_cursor( - &mut self, - direction: Direction, - options: SearchOptions, - cx: &mut ViewContext, - ) { - self.active_match_index = None; - self.pending_match_direction = Some(direction); - self.show_with_options(false, true, options, cx); - } - pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { @@ -541,17 +562,26 @@ impl BufferSearchBar { event: &editor::Event, cx: &mut ViewContext, ) { - if let editor::Event::BufferEdited { .. } = event { + if let editor::Event::Edited { .. } = event { + let query = self.query_editor.read(cx).text(cx); + let search = self.search(&query, self.search_options, cx); self.query_contains_error = false; self.clear_matches(cx); - self.update_matches(true, cx); - cx.notify(); + let search = self.update_matches(cx); + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.select_match(Direction::Next, cx))?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { match event { - SearchEvent::MatchesInvalidated => self.update_matches(false, cx), + SearchEvent::MatchesInvalidated => { + let _ = self.update_matches(cx); + } SearchEvent::ActiveMatchChanged => self.update_match_index(cx), } } @@ -574,7 +604,8 @@ impl BufferSearchBar { .extend(active_item_matches); } - fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { + fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { + let (done_tx, done_rx) = oneshot::channel(); let query = self.query_editor.read(cx).text(cx); self.pending_search.take(); if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { @@ -582,6 +613,7 @@ impl BufferSearchBar { self.active_match_index.take(); self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); + let _ = done_tx.send(()); } else { let query = if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( @@ -595,7 +627,7 @@ impl BufferSearchBar { Err(_) => { self.query_contains_error = true; cx.notify(); - return; + return done_rx; } } } else { @@ -627,20 +659,7 @@ impl BufferSearchBar { .get(&active_searchable_item.downgrade()) .unwrap(); active_searchable_item.update_matches(matches, cx); - if select_closest_match { - if let Some(mut match_ix) = this.active_match_index { - if let Some(direction) = this.pending_match_direction.take() - { - match_ix += match direction { - Direction::Next => 1, - Direction::Prev => matches.len() - 1, - }; - match_ix = match_ix % matches.len(); - } - active_searchable_item - .activate_match(match_ix, matches, cx); - } - } + let _ = done_tx.send(()); } cx.notify(); } @@ -649,6 +668,7 @@ impl BufferSearchBar { })); } } + done_rx } fn update_match_index(&mut self, cx: &mut ViewContext) { @@ -699,7 +719,7 @@ mod tests { let search_bar = cx.add_view(window_id, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(false, true, cx); + search_bar.show(cx); search_bar }); @@ -712,10 +732,11 @@ mod tests { // Search for a string that appears with different casing. // By default, search is case-insensitive. - search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("us", cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("us", search_bar.default_options, cx) + }) + .await; editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -750,7 +771,7 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("or", cx); + search_bar.search("or", search_bar.default_options, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -993,6 +1014,7 @@ mod tests { }); } + /* #[gpui::test] async fn test_search_with_options(cx: &mut TestAppContext) { let (editor, search_bar) = init_test(cx); @@ -1000,7 +1022,7 @@ mod tests { // show with options should make current search case sensitive search_bar.update(cx, |search_bar, cx| { search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); - search_bar.set_query("us", cx); + search_bar.search("us", cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -1036,7 +1058,7 @@ mod tests { // toggling a search option (even in show_with_options mode) should update the defaults search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("regex", cx); + search_bar.search("regex", cx); search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); @@ -1087,4 +1109,5 @@ mod tests { ); }); } + */ } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index bb57045add719ff42a834fbdcde513ab000b21ed..f23de53c37be20e93b6d90e7344cd3ff2a6ee8db 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -53,7 +53,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::CASE_SENSITIVE; - options.set(SearchOptions::WHOLE_WORD, whole_word); - search_bar.select_word_under_cursor(direction, options, cx); + // let mut options = SearchOptions::CASE_SENSITIVE; + // options.set(SearchOptions::WHOLE_WORD, whole_word); + // search_bar.show(false, false, cx); + // let word = search_bar.query_suggestion(); + // search_bar.show() + // search_bar.search(word, options) + + // search_bar.select_word_under_cursor(direction, options, cx); }); } }); From b4b0f622de531a58a6fa6b1f8c80743105e6ec32 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 11:33:15 -0600 Subject: [PATCH 10/11] Rebuild vim search experience on refactored code --- assets/keymaps/vim.json | 2 +- crates/ai/src/assistant.rs | 4 +- crates/editor/src/items.rs | 22 ++-- crates/search/src/buffer_search.rs | 99 +++++++++------- crates/search/src/project_search.rs | 2 +- crates/vim/src/normal/search.rs | 170 +++++++++++++++++++++------- crates/vim/src/state.rs | 18 +++ crates/workspace/src/searchable.rs | 23 ++-- 8 files changed, 225 insertions(+), 115 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4a215dcef3116d3609a3445c8f5be34ad9f2b7d9..40ebe13558b22991c511840ba02d4c99e8bbb855 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -357,7 +357,7 @@ { "context": "BufferSearchBar", "bindings": { - "enter": "buffer_search::FocusEditor", + "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" } } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 3cc97468c32f177e037af618da98da14d7e7f32e..4ca771ebcb8027f2ba5aa3aa9cca24fe160a64e6 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -330,13 +330,13 @@ impl AssistantPanel { fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, None, cx)); } } fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, None, cx)); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 86f26bcf944dc9675efa253783f9638d9a9915a0..6b2cdacaa2746964266a2c1f8721188ce719b51a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -937,7 +937,9 @@ impl SearchableItem for Editor { ) { self.unfold_ranges([matches[index].clone()], false, true, cx); let range = self.range_for_match(&matches[index]); - self.change_selections(Some(Autoscroll::fit()), cx, |s| s.select_ranges([range])); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) } fn match_index_for_direction( @@ -945,11 +947,12 @@ impl SearchableItem for Editor { matches: &Vec>, mut current_index: usize, direction: Direction, + count: Option, cx: &mut ViewContext, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); let cursor = self.selections.newest_anchor().head(); - if matches[current_index].start.cmp(&cursor, &buffer).is_gt() { + if count.is_none() && matches[current_index].start.cmp(&cursor, &buffer).is_gt() { if direction == Direction::Prev { if current_index == 0 { current_index = matches.len() - 1; @@ -957,22 +960,19 @@ impl SearchableItem for Editor { current_index -= 1; } } - } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() { + } else if count.is_none() && matches[current_index].end.cmp(&cursor, &buffer).is_lt() { if direction == Direction::Next { current_index = 0; } } else if direction == Direction::Prev { - if current_index == 0 { - current_index = matches.len() - 1; + let count = count.unwrap_or(1) % matches.len(); + if current_index >= count { + current_index = current_index - count; } else { - current_index -= 1; + current_index = matches.len() - (count - current_index); } } else if direction == Direction::Next { - if current_index == matches.len() - 1 { - current_index = 0 - } else { - current_index += 1; - } + current_index = (current_index + count.unwrap_or(1)) % matches.len() }; current_index } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c8d1c58b6f265e0a15201fc1033b61eea58b5de6..2bd765f8bbd780b3c5177252ad1dbe5862a055c5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -66,7 +66,6 @@ pub struct BufferSearchBar { pub query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, - pending_match_direction: Option, active_searchable_item_subscription: Option, seachable_items_with_matches: HashMap, Vec>>, @@ -254,7 +253,6 @@ impl BufferSearchBar { default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, - pending_match_direction: None, query_contains_error: false, dismissed: true, } @@ -281,12 +279,9 @@ impl BufferSearchBar { } pub fn show(&mut self, cx: &mut ViewContext) -> bool { - let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { - SearchableItemHandle::boxed_clone(searchable_item.as_ref()) - } else { + if self.active_searchable_item.is_none() { return false; - }; - + } self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); @@ -296,44 +291,53 @@ impl BufferSearchBar { pub fn search_suggested(&mut self, cx: &mut ViewContext) { let search = self .query_suggestion(cx) - .map(|suggestion| self.search(&suggestion, self.default_options, cx)); + .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); if let Some(search) = search { cx.spawn(|this, mut cx| async move { search.await?; - this.update(&mut cx, |this, cx| { - if let Some(match_ix) = this.active_match_index { - if let Some(active_searchable_item) = this.active_searchable_item.as_ref() { - if let Some(matches) = this - .seachable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, cx); - } - } - } - }) + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) }) .detach_and_log_err(cx); } } + pub fn activate_current_match(&mut self, cx: &mut ViewContext) { + if let Some(match_ix) = self.active_match_index { + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .seachable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx) + } + } + } + } + pub fn select_query(&mut self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { query_editor.select_all(&Default::default(), cx); }); } - pub fn query_suggestion(&self, cx: &mut ViewContext) -> Option { - Some(self.active_searchable_item.as_ref()?.query_suggestion(cx)) + pub fn query(&self, cx: &WindowContext) -> String { + self.query_editor.read(cx).text(cx) } - fn search( + pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { + self.active_searchable_item + .as_ref() + .map(|searchable_item| searchable_item.query_suggestion(cx)) + } + + pub fn search( &mut self, query: &str, - options: SearchOptions, + options: Option, cx: &mut ViewContext, ) -> oneshot::Receiver<()> { + let options = options.unwrap_or(self.default_options); if query != self.query_editor.read(cx).text(cx) || self.search_options != options { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { @@ -499,7 +503,7 @@ impl BufferSearchBar { cx.propagate_action(); } - fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { if let Some(active_editor) = self.active_searchable_item.as_ref() { cx.focus(active_editor.as_any()); } @@ -512,23 +516,37 @@ impl BufferSearchBar { cx.notify(); } + pub fn set_search_options( + &mut self, + search_options: SearchOptions, + cx: &mut ViewContext, + ) { + self.search_options = search_options; + cx.notify(); + } + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { - self.select_match(Direction::Next, cx); + self.select_match(Direction::Next, None, cx); } fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { - self.select_match(Direction::Prev, cx); + self.select_match(Direction::Prev, None, cx); } - pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn select_match( + &mut self, + direction: Direction, + count: Option, + cx: &mut ViewContext, + ) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self .seachable_items_with_matches .get(&searchable_item.downgrade()) { - let new_match_index = - searchable_item.match_index_for_direction(matches, index, direction, cx); + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, cx); searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); } @@ -563,15 +581,12 @@ impl BufferSearchBar { cx: &mut ViewContext, ) { if let editor::Event::Edited { .. } = event { - let query = self.query_editor.read(cx).text(cx); - let search = self.search(&query, self.search_options, cx); self.query_contains_error = false; self.clear_matches(cx); let search = self.update_matches(cx); cx.spawn(|this, mut cx| async move { search.await?; - this.update(&mut cx, |this, cx| this.select_match(Direction::Next, cx))?; - anyhow::Ok(()) + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) }) .detach_and_log_err(cx); } @@ -611,7 +626,6 @@ impl BufferSearchBar { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); - self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); } else { @@ -733,10 +747,9 @@ mod tests { // Search for a string that appears with different casing. // By default, search is case-insensitive. search_bar - .update(cx, |search_bar, cx| { - search_bar.search("us", search_bar.default_options, cx) - }) - .await; + .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -770,10 +783,10 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. - search_bar.update(cx, |search_bar, cx| { - search_bar.search("or", search_bar.default_options, cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4e485eaaabb3d03906e62a48804468303cc832fd..76350f1812fd1b574ef13544739dd9c7c5022063 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -627,7 +627,7 @@ impl ProjectSearchView { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, cx) + editor.match_index_for_direction(&match_ranges, index, direction, None, cx) }); let range_to_select = match_ranges[new_index].clone(); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index f23de53c37be20e93b6d90e7344cd3ff2a6ee8db..70e397bcb0b88081fdbb1c86f2b881302af27f90 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,9 +1,9 @@ -use gpui::{impl_actions, AppContext, ViewContext}; -use search::{BufferSearchBar, SearchOptions}; +use gpui::{actions, impl_actions, AppContext, ViewContext}; +use search::{buffer_search, BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; -use workspace::{searchable::Direction, Workspace}; +use workspace::{searchable::Direction, Pane, Workspace}; -use crate::Vim; +use crate::{state::SearchState, Vim}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -26,11 +26,14 @@ pub(crate) struct Search { } impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); +actions!(vim, [SearchSubmit]); pub(crate) fn init(cx: &mut AppContext) { cx.add_action(move_to_next); cx.add_action(move_to_prev); cx.add_action(search); + cx.add_action(search_submit); + cx.add_action(search_deploy); } fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { @@ -43,19 +46,68 @@ fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewCon fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { let pane = workspace.active_pane().clone(); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - search_bar.select_match(direction, cx); - // search_bar.show_with_options(true, false, options, cx); - }) - } + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + Vim::update(cx, |vim, cx| { + let count = vim.pop_number_operator(cx).unwrap_or(1); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return; + } + let query = search_bar.query(cx); + + search_bar.select_query(cx); + cx.focus_self(); + + if query.is_empty() { + search_bar.set_search_options( + SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX, + cx, + ); + } + vim.state.search = SearchState { + direction, + count, + initial_query: query, + }; + }); + } + }) + }) +} + +fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { + Vim::update(cx, |vim, _| vim.state.search = Default::default()); + cx.propagate_action(); +} + +fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let mut state = &mut vim.state.search; + let mut count = state.count; + + // in the case that the query has changed, the search bar + // will have selected the next match already. + if (search_bar.query(cx) != state.initial_query) + && state.direction == Direction::Next + { + count = count.saturating_sub(1); + } + search_bar.select_match(state.direction, Some(count), cx); + state.count = 1; + search_bar.focus_editor(&Default::default(), cx); + }); + } + }); }) } @@ -67,18 +119,32 @@ pub fn move_to_internal( ) { Vim::update(cx, |vim, cx| { let pane = workspace.active_pane().clone(); + let count = vim.pop_number_operator(cx).unwrap_or(1); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - // let mut options = SearchOptions::CASE_SENSITIVE; - // options.set(SearchOptions::WHOLE_WORD, whole_word); - // search_bar.show(false, false, cx); - // let word = search_bar.query_suggestion(); - // search_bar.show() - // search_bar.search(word, options) - - // search_bar.select_word_under_cursor(direction, options, cx); + let search = search_bar.update(cx, |search_bar, cx| { + let mut options = SearchOptions::CASE_SENSITIVE; + options.set(SearchOptions::WHOLE_WORD, whole_word); + if search_bar.show(cx) { + search_bar + .query_suggestion(cx) + .map(|query| search_bar.search(&query, Some(options), cx)) + } else { + None + } }); + + if let Some(search) = search { + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(direction, Some(count), cx) + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } }); vim.clear_operator(cx); @@ -100,15 +166,6 @@ mod test { deterministic: Arc, ) { let mut cx = VimTestContext::new(cx, true).await; - let search_bar = cx.workspace(|workspace, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); @@ -127,6 +184,10 @@ mod test { deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + cx.simulate_keystrokes(["2", "*"]); + deterministic.run_until_parked(); + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + cx.simulate_keystrokes(["g", "*"]); deterministic.run_until_parked(); cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); @@ -140,7 +201,10 @@ mod test { } #[gpui::test] - async fn test_search(cx: &mut gpui::TestAppContext) { + async fn test_search( + cx: &mut gpui::TestAppContext, + deterministic: Arc, + ) { let mut cx = VimTestContext::new(cx, true).await; cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); @@ -160,8 +224,7 @@ mod test { assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); }); - // wait for the query editor change event to fire. - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.update_editor(|editor, cx| { let highlights = editor.all_background_highlights(cx); @@ -173,30 +236,49 @@ mod test { }); cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); // n to go to next/N to go to previous - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); 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.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["?", "enter"]); + deterministic.run_until_parked(); cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); cx.simulate_keystrokes(["?", "enter"]); + deterministic.run_until_parked(); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); // / to go to next - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["/", "enter"]); + deterministic.run_until_parked(); cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); // ?{search} to search backwards cx.simulate_keystrokes(["?", "b", "enter"]); + deterministic.run_until_parked(); + cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - // wait for the query editor change event to fire. - search_bar.next_notification(&cx).await; + // works with counts + cx.simulate_keystrokes(["4", "/", "c"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal); - cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + // check that searching resumes from cursor, not previous match + cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); + cx.simulate_keystrokes(["/", "d"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal); + cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx)); + cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); + cx.simulate_keystrokes(["/", "b"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index e1a06fce59d6ff5f9529552e422fbd0643418de1..6434b710b2a37f75bc0687069bec9b69431b6405 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,6 +1,7 @@ use gpui::keymap_matcher::KeymapContext; use language::CursorShape; use serde::{Deserialize, Serialize}; +use workspace::searchable::Direction; #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum Mode { @@ -38,6 +39,23 @@ pub enum Operator { pub struct VimState { pub mode: Mode, pub operator_stack: Vec, + pub search: SearchState, +} + +pub struct SearchState { + pub direction: Direction, + pub count: usize, + pub initial_query: String, +} + +impl Default for SearchState { + fn default() -> Self { + Self { + direction: Direction::Next, + count: 1, + initial_query: "".to_string(), + } + } } impl VimState { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7e3f7227b022f72bec64af3c765ab2b79cdafb49..bdbed072b0062dcadec26462de460a8ff258bf06 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -50,26 +50,21 @@ pub trait SearchableItem: Item { fn match_index_for_direction( &mut self, matches: &Vec, - mut current_index: usize, + current_index: usize, direction: Direction, + count: Option, _: &mut ViewContext, ) -> usize { match direction { Direction::Prev => { - if current_index == 0 { - matches.len() - 1 - } else { - current_index - 1 - } - } - Direction::Next => { - current_index += 1; - if current_index == matches.len() { - 0 + let count = count.unwrap_or(1) % matches.len(); + if current_index >= count { + current_index - count } else { - current_index + matches.len() - (count - current_index) } } + Direction::Next => (current_index + count.unwrap_or(1)) % matches.len(), } } fn find_matches( @@ -107,6 +102,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &Vec>, current_index: usize, direction: Direction, + count: Option, cx: &mut WindowContext, ) -> usize; fn find_matches( @@ -170,11 +166,12 @@ impl SearchableItemHandle for ViewHandle { matches: &Vec>, current_index: usize, direction: Direction, + count: Option, cx: &mut WindowContext, ) -> usize { let matches = downcast_matches(matches); self.update(cx, |this, cx| { - this.match_index_for_direction(&matches, current_index, direction, cx) + this.match_index_for_direction(&matches, current_index, direction, count, cx) }) } fn find_matches( From c9bf4074311dcb2cf23da842bb19c354efca026d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 17 Jul 2023 12:02:10 -0600 Subject: [PATCH 11/11] Avoid optional on select_match --- crates/ai/src/assistant.rs | 4 +- crates/editor/src/items.rs | 70 ++++++++++--------- crates/search/src/buffer_search.rs | 100 +++++++++------------------- crates/search/src/project_search.rs | 3 +- crates/vim/src/normal/search.rs | 7 +- crates/workspace/src/searchable.rs | 10 +-- 6 files changed, 80 insertions(+), 114 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 5d587401e5c05e24447b0600630a4239919c0add..8a4c04d3387784e0e2b5e4e7d745c690f72c02aa 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -330,13 +330,13 @@ impl AssistantPanel { fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, None, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); } } fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, None, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c7a93e754acd94695279974d11abf3831158079f..0ce41a97c96416af9bd4d45b1255c8f4b7ae0301 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -954,15 +954,19 @@ impl SearchableItem for Editor { fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { self.unfold_ranges(matches.clone(), false, false, cx); - self.change_selections(None, cx, |s| s.select_ranges(matches)); + let mut ranges = Vec::new(); + for m in &matches { + ranges.push(self.range_for_match(&m)) + } + self.change_selections(None, cx, |s| s.select_ranges(ranges)); } fn match_index_for_direction( &mut self, matches: &Vec>, - mut current_index: usize, + current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut ViewContext, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); @@ -971,39 +975,39 @@ impl SearchableItem for Editor { } else { matches[current_index].start }; - if count.is_none() - && matches[current_index] - .start - .cmp(¤t_index_position, &buffer) - .is_gt() - { - if direction == Direction::Prev { - if current_index == 0 { - current_index = matches.len() - 1; - } else { - current_index -= 1; + + let mut count = count % matches.len(); + if count == 0 { + return current_index; + } + match direction { + Direction::Next => { + if matches[current_index] + .start + .cmp(¤t_index_position, &buffer) + .is_gt() + { + count = count - 1 } + + (current_index + count) % matches.len() } - } else if count.is_none() - && matches[current_index] - .end - .cmp(¤t_index_position, &buffer) - .is_lt() - { - if direction == Direction::Next { - current_index = 0; - } - } else if direction == Direction::Prev { - let count = count.unwrap_or(1) % matches.len(); - if current_index >= count { - current_index = current_index - count; - } else { - current_index = matches.len() - (count - current_index); + Direction::Prev => { + if matches[current_index] + .end + .cmp(¤t_index_position, &buffer) + .is_lt() + { + count = count - 1; + } + + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } } - } else if direction == Direction::Next { - current_index = (current_index + count.unwrap_or(1)) % matches.len() - }; - current_index + } } fn find_matches( diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7d50794108cf55b10c62c2e43e46f8da99b7bf63..7fade13a509dabd633b7da26601f90b770df3b6f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -560,11 +560,11 @@ impl BufferSearchBar { } fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { - self.select_match(Direction::Next, None, cx); + self.select_match(Direction::Next, 1, cx); } fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { - self.select_match(Direction::Prev, None, cx); + self.select_match(Direction::Prev, 1, cx); } fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { @@ -581,12 +581,7 @@ impl BufferSearchBar { } } - pub fn select_match( - &mut self, - direction: Direction, - count: Option, - cx: &mut ViewContext, - ) { + pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self @@ -1086,15 +1081,17 @@ mod tests { } #[gpui::test] - async fn test_search_with_options(cx: &mut TestAppContext) { + async fn test_search_option_handling(cx: &mut TestAppContext) { let (editor, search_bar) = init_test(cx); // show with options should make current search case sensitive - search_bar.update(cx, |search_bar, cx| { - search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); - search_bar.search("us", cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| { + search_bar.show(cx); + search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -1105,31 +1102,20 @@ mod tests { ); }); - // show should return to the default options (case insensitive) + // search_suggested should restore default options search_bar.update(cx, |search_bar, cx| { - search_bar.show(true, true, cx); - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { - assert_eq!( - editor.all_background_highlights(cx), - &[ - ( - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), - ), - ( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - ) - ] - ); + search_bar.search_suggested(cx); + assert_eq!(search_bar.search_options, SearchOptions::NONE) }); - // toggling a search option (even in show_with_options mode) should update the defaults + // toggling a search option should update the defaults + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); search_bar.update(cx, |search_bar, cx| { - search_bar.search("regex", cx); - search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); editor.next_notification(cx).await; @@ -1145,38 +1131,11 @@ mod tests { // defaults should still include whole word search_bar.update(cx, |search_bar, cx| { - search_bar.show(true, true, cx); - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { - assert_eq!( - editor.all_background_highlights(cx), - &[( - DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), - ),] - ); - }); - - // removing whole word changes the search again - search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { + search_bar.search_suggested(cx); assert_eq!( - editor.all_background_highlights(cx), - &[ - ( - DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), - ), - ( - DisplayPoint::new(0, 44)..DisplayPoint::new(0, 49), - Color::red() - ) - ] - ); + search_bar.search_options, + SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD + ) }); } @@ -1207,15 +1166,18 @@ mod tests { let search_bar = cx.add_view(window_id, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(false, true, cx); + search_bar.show(cx); search_bar }); + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("a", cx); + search_bar.activate_current_match(cx); }); - editor.next_notification(cx).await; let initial_selections = editor.update(cx, |editor, cx| { let initial_selections = editor.selections.display_ranges(cx); assert_eq!( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5bdcf72a97391aa928056a0f0a5c751d942e1a0f..abebb9a48f1a853097248d796550f00bb3e6cd70 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -627,7 +627,7 @@ impl ProjectSearchView { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, None, cx) + editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) }); let range_to_select = match_ranges[new_index].clone(); @@ -668,7 +668,6 @@ impl ProjectSearchView { self.active_match_index = None; } else { self.active_match_index = Some(0); - self.select_match(Direction::Next, cx); self.update_match_index(cx); let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); let is_new_search = self.search_id != prev_search_id; diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 70e397bcb0b88081fdbb1c86f2b881302af27f90..cae64a40a6b0cdb290014539b7c10ead6a7a0b82 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -81,6 +81,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext find. fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { Vim::update(cx, |vim, _| vim.state.search = Default::default()); cx.propagate_action(); @@ -100,9 +101,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte if (search_bar.query(cx) != state.initial_query) && state.direction == Direction::Next { - count = count.saturating_sub(1); + count = count.saturating_sub(1) } - search_bar.select_match(state.direction, Some(count), cx); + search_bar.select_match(state.direction, count, cx); state.count = 1; search_bar.focus_editor(&Default::default(), cx); }); @@ -139,7 +140,7 @@ pub fn move_to_internal( cx.spawn(|_, mut cx| async move { search.await?; search_bar.update(&mut cx, |search_bar, cx| { - search_bar.select_match(direction, Some(count), cx) + search_bar.select_match(direction, count, cx) })?; anyhow::Ok(()) }) diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 4f5e7099fae05fb691c78c505ee4b27adc76f345..ae95838a742bb623a9376947e905d6054a82fff4 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -57,19 +57,19 @@ pub trait SearchableItem: Item { matches: &Vec, current_index: usize, direction: Direction, - count: Option, + count: usize, _: &mut ViewContext, ) -> usize { match direction { Direction::Prev => { - let count = count.unwrap_or(1) % matches.len(); + let count = count % matches.len(); if current_index >= count { current_index - count } else { matches.len() - (count - current_index) } } - Direction::Next => (current_index + count.unwrap_or(1)) % matches.len(), + Direction::Next => (current_index + count) % matches.len(), } } fn find_matches( @@ -108,7 +108,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &Vec>, current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut WindowContext, ) -> usize; fn find_matches( @@ -179,7 +179,7 @@ impl SearchableItemHandle for ViewHandle { matches: &Vec>, current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut WindowContext, ) -> usize { let matches = downcast_matches(matches);