Detailed changes
@@ -1196,7 +1196,10 @@ pub struct Editor {
/// selected (needed for vim visual mode)
cursor_offset_on_selection: bool,
current_line_highlight: Option<CurrentLineHighlight>,
- 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<AutoindentMode>,
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
input_enabled: bool,
@@ -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"] }
@@ -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::<ProjectSearchView>())
+ .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::<VimAddon>()
+ .map(|addon| addon.entity.read(cx).mode);
+
+ assert_eq!(vim_mode, Some(Mode::HelixNormal));
+ });
+ }
}
@@ -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::<ProjectSearchView>())
+ .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::<VimAddon>()
+ .map(|addon| addon.entity.read(cx).mode);
+
+ assert_eq!(vim_mode, Some(Mode::Normal));
+ });
+}
@@ -633,12 +633,15 @@ impl Vim {
fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>) {
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>) {
- 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>,
+ ) {
+ 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)]