diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ddc33c2de8607291d5b69b278d737412937d8a53..0b1bd2f8bc998f156e3eb3f40d18ba02ab9678be 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1196,7 +1196,10 @@ pub struct Editor { /// selected (needed for vim visual mode) cursor_offset_on_selection: bool, current_line_highlight: Option, - pub collapse_matches: bool, + /// Whether to collapse search match ranges to just their start position. + /// When true, navigating to a match positions the cursor at the match + /// without selecting the matched text. + collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakEntity, Option)>, input_enabled: bool, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 3905eac969dbff1caab6fc91b7f82a250d0b5f96..38bf9fed621aa3aa378cbcaa3479f7ecd7b60e11 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -75,3 +75,4 @@ settings.workspace = true perf.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } +search = { workspace = true, features = ["test-support"] } diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index ce9261d0feafc9c3b470db40ed7044c36925a6d9..4a1ab02f1a86e660399b58ccf0323a2b36a64970 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -862,9 +862,16 @@ impl Vim { #[cfg(test)] mod test { + use gpui::{UpdateGlobal, VisualTestContext}; use indoc::indoc; + use project::FakeFs; + use search::{ProjectSearchView, project_search}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + use workspace::DeploySearch; - use crate::{state::Mode, test::VimTestContext}; + use crate::{VimAddon, state::Mode, test::VimTestContext}; #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { @@ -1694,4 +1701,61 @@ mod test { Mode::HelixNormal, ); } + + #[gpui::test] + async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) { + VimTestContext::init(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/dir"), + json!({ + "file_a.rs": "// File A.", + "file_b.rs": "// File B.", + }), + ) + .await; + + let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let workspace = + cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); + + cx.update(|cx| { + VimTestContext::init_keybindings(true, cx); + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |store| store.helix_mode = Some(true)); + }) + }); + + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + workspace + .update(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) + }) + .unwrap(); + + let search_view = workspace + .update(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view should be active") + }) + .unwrap(); + + project_search::perform_project_search(&search_view, "File A", cx); + + search_view.update(cx, |search_view, cx| { + let vim_mode = search_view + .results_editor() + .read(cx) + .addon::() + .map(|addon| addon.entity.read(cx).mode); + + assert_eq!(vim_mode, Some(Mode::HelixNormal)); + }); + } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3b39bd468c57d171b29d84c6dcc2a92fd9e82af6..98548ff387e678ca59263e5b3f4f859c39e5f779 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -20,13 +20,18 @@ use language::{CursorShape, Language, LanguageConfig, Point}; pub use neovim_backed_test_context::*; use settings::SettingsStore; use ui::Pixels; -use util::test::marked_text_ranges; +use util::{path, test::marked_text_ranges}; pub use vim_test_context::*; +use gpui::VisualTestContext; use indoc::indoc; +use project::FakeFs; use search::BufferSearchBar; +use search::{ProjectSearchView, project_search}; +use serde_json::json; +use workspace::DeploySearch; -use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode}; +use crate::{PushSneak, PushSneakBackward, VimAddon, insert::NormalBefore, motion, state::Mode}; use util_macros::perf; @@ -2534,3 +2539,57 @@ async fn test_deactivate(cx: &mut gpui::TestAppContext) { assert_eq!(editor.cursor_shape(), CursorShape::Underline); }); } + +#[gpui::test] +async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) { + VimTestContext::init(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/dir"), + json!({ + "file_a.rs": "// File A.", + "file_b.rs": "// File B.", + }), + ) + .await; + + let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let workspace = + cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); + + cx.update(|cx| { + VimTestContext::init_keybindings(true, cx); + }); + + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + workspace + .update(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx) + }) + .unwrap(); + + let search_view = workspace + .update(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view should be active") + }) + .unwrap(); + + project_search::perform_project_search(&search_view, "File A", cx); + + search_view.update(cx, |search_view, cx| { + let vim_mode = search_view + .results_editor() + .read(cx) + .addon::() + .map(|addon| addon.entity.read(cx).mode); + + assert_eq!(vim_mode, Some(Mode::Normal)); + }); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 744dcb63f539c618c65c3e1e09adaea606cd45f3..205b55f01d93951e0452a67760cf781e754f4188 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -633,12 +633,15 @@ impl Vim { fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context) { let vim = Vim::new(window, cx); - - if !editor.mode().is_full() { - vim.update(cx, |vim, _| { + let state = vim.update(cx, |vim, cx| { + if !editor.mode().is_full() { vim.mode = Mode::Insert; - }); - } + } + + vim.state_for_editor_settings(cx) + }); + + Vim::sync_vim_settings_to_editor(&state, editor, window, cx); editor.register_addon(VimAddon { entity: vim.clone(), @@ -1305,7 +1308,7 @@ impl Vim { forced_motion } - pub fn cursor_shape(&self, cx: &mut App) -> CursorShape { + pub fn cursor_shape(&self, cx: &App) -> CursorShape { let cursor_shape = VimSettings::get_global(cx).cursor_shape; match self.mode { Mode::Normal => { @@ -2022,23 +2025,52 @@ impl Vim { } fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_cursor_shape(vim.cursor_shape(cx), cx); - editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); - let collapse_matches = !HelixModeSetting::get_global(cx).0; - editor.set_collapse_matches(collapse_matches); - editor.set_input_enabled(vim.editor_input_enabled()); - editor.set_autoindent(vim.should_autoindent()); - editor.set_cursor_offset_on_selection(vim.mode.is_visual()); - editor - .selections - .set_line_mode(matches!(vim.mode, Mode::VisualLine)); - - let hide_edit_predictions = !matches!(vim.mode, Mode::Insert | Mode::Replace); - editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); + let state = self.state_for_editor_settings(cx); + self.update_editor(cx, |_, editor, cx| { + Vim::sync_vim_settings_to_editor(&state, editor, window, cx); }); cx.notify() } + + fn state_for_editor_settings(&self, cx: &App) -> VimEditorSettingsState { + VimEditorSettingsState { + cursor_shape: self.cursor_shape(cx), + clip_at_line_ends: self.clip_at_line_ends(), + collapse_matches: !HelixModeSetting::get_global(cx).0, + input_enabled: self.editor_input_enabled(), + autoindent: self.should_autoindent(), + cursor_offset_on_selection: self.mode.is_visual(), + line_mode: matches!(self.mode, Mode::VisualLine), + hide_edit_predictions: !matches!(self.mode, Mode::Insert | Mode::Replace), + } + } + + fn sync_vim_settings_to_editor( + state: &VimEditorSettingsState, + editor: &mut Editor, + window: &mut Window, + cx: &mut Context, + ) { + editor.set_cursor_shape(state.cursor_shape, cx); + editor.set_clip_at_line_ends(state.clip_at_line_ends, cx); + editor.set_collapse_matches(state.collapse_matches); + editor.set_input_enabled(state.input_enabled); + editor.set_autoindent(state.autoindent); + editor.set_cursor_offset_on_selection(state.cursor_offset_on_selection); + editor.selections.set_line_mode(state.line_mode); + editor.set_edit_predictions_hidden_for_vim_mode(state.hide_edit_predictions, window, cx); + } +} + +struct VimEditorSettingsState { + cursor_shape: CursorShape, + clip_at_line_ends: bool, + collapse_matches: bool, + input_enabled: bool, + autoindent: bool, + cursor_offset_on_selection: bool, + line_mode: bool, + hide_edit_predictions: bool, } #[derive(RegisterSetting)]