From 36d51fe4a565a104d32e5db4fa5b57808c533dc7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 20 Aug 2024 20:48:50 -0600 Subject: [PATCH] vim: Improve lifecycle (#16477) Closes #13579 A major painpoint in the Vim crate has been life-cycle management. We used to have one global Vim instance that tried to track per-editor state; this led to a number of subtle issues (e.g. #13579, the mode indicator being global, and quick toggling between windows letting vim mode's notion of the active editor get out of sync). This PR changes the internal structure of the code so that there is now one `Vim` instance per `Editor` (stored as an `Addon`); and the global stuff is separated out. This fixes the above problems, and tidies up a bunch of the mess in the codebase. Release Notes: * vim: Fixed accidental visual mode in project search and go to references ([#13579](https://github.com/zed-industries/zed/issues/13579)). --- crates/editor/src/editor.rs | 59 +- crates/editor/src/element.rs | 2 +- crates/vim/src/change_list.rs | 110 +- crates/vim/src/command.rs | 136 +- crates/vim/src/digraph.rs | 27 +- crates/vim/src/editor_events.rs | 153 -- crates/vim/src/insert.rs | 45 +- crates/vim/src/mode_indicator.rs | 117 +- crates/vim/src/motion.rs | 281 ++-- crates/vim/src/normal.rs | 551 ++++--- crates/vim/src/normal/case.rs | 186 ++- crates/vim/src/normal/change.rs | 165 ++- crates/vim/src/normal/delete.rs | 246 ++-- crates/vim/src/normal/increment.rs | 153 +- crates/vim/src/normal/indent.rs | 118 +- crates/vim/src/normal/mark.rs | 138 +- crates/vim/src/normal/paste.rs | 47 +- crates/vim/src/normal/repeat.rs | 380 +++-- crates/vim/src/normal/scroll.rs | 46 +- crates/vim/src/normal/search.rs | 564 ++++---- crates/vim/src/normal/substitute.rs | 138 +- crates/vim/src/normal/toggle_comments.rs | 91 +- crates/vim/src/normal/yank.rs | 298 ++-- crates/vim/src/object.rs | 81 +- crates/vim/src/replace.rs | 131 +- crates/vim/src/state.rs | 354 +++-- crates/vim/src/surrounds.rs | 139 +- crates/vim/src/test.rs | 57 +- .../src/test/neovim_backed_test_context.rs | 5 +- crates/vim/src/test/vim_test_context.rs | 30 +- crates/vim/src/vim.rs | 1271 ++++++++--------- crates/vim/src/visual.rs | 688 +++++---- 32 files changed, 3292 insertions(+), 3515 deletions(-) delete mode 100644 crates/vim/src/editor_events.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2314c70e8f50948e1f41e47c672bd021c561ce3..a83553565e1b97b90acb5322ef3a62d03f5f877c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -462,6 +462,14 @@ struct ResolvedTasks { struct MultiBufferOffset(usize); #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] struct BufferOffset(usize); + +// Addons allow storing per-editor state in other crates (e.g. Vim) +pub trait Addon: 'static { + fn extend_key_context(&self, _: &mut KeyContext, _: &AppContext) {} + + fn to_any(&self) -> &dyn std::any::Any; +} + /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// /// See the [module level documentation](self) for more information. @@ -533,7 +541,6 @@ pub struct Editor { collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, Option)>, - keymap_context_layers: BTreeMap, input_enabled: bool, use_modal_editing: bool, read_only: bool, @@ -551,7 +558,6 @@ pub struct Editor { _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, gutter_dimensions: GutterDimensions, - pub vim_replace_map: HashMap, String>, style: Option, next_editor_action_id: EditorActionId, editor_actions: Rc)>>>>, @@ -581,6 +587,7 @@ pub struct Editor { breadcrumb_header: Option, focused_block: Option, next_scroll_position: NextScrollCursorCenterTopBottom, + addons: HashMap>, _scroll_cursor_center_top_bottom_task: Task<()>, } @@ -1875,7 +1882,6 @@ impl Editor { autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, - keymap_context_layers: Default::default(), input_enabled: true, use_modal_editing: mode == EditorMode::Full, read_only: false, @@ -1900,7 +1906,6 @@ impl Editor { hovered_cursors: Default::default(), next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), - vim_replace_map: Default::default(), show_inline_completions: mode == EditorMode::Full, custom_context_menu: None, show_git_blame_gutter: false, @@ -1939,6 +1944,7 @@ impl Editor { breadcrumb_header: None, focused_block: None, next_scroll_position: NextScrollCursorCenterTopBottom::default(), + addons: HashMap::default(), _scroll_cursor_center_top_bottom_task: Task::ready(()), }; this.tasks_update_task = Some(this.refresh_runnables(cx)); @@ -1961,13 +1967,13 @@ impl Editor { this } - pub fn mouse_menu_is_focused(&self, cx: &mut WindowContext) -> bool { + pub fn mouse_menu_is_focused(&self, cx: &WindowContext) -> bool { self.mouse_context_menu .as_ref() .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(cx)) } - fn key_context(&self, cx: &AppContext) -> KeyContext { + fn key_context(&self, cx: &ViewContext) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { @@ -1998,8 +2004,13 @@ impl Editor { } } - for layer in self.keymap_context_layers.values() { - key_context.extend(layer); + // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. + if !self.focus_handle(cx).contains_focused(cx) + || (self.is_focused(cx) || self.mouse_menu_is_focused(cx)) + { + for addon in self.addons.values() { + addon.extend_key_context(&mut key_context, cx) + } } if let Some(extension) = self @@ -2241,21 +2252,6 @@ impl Editor { } } - pub fn set_keymap_context_layer( - &mut self, - context: KeyContext, - cx: &mut ViewContext, - ) { - self.keymap_context_layers - .insert(TypeId::of::(), context); - cx.notify(); - } - - pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { - self.keymap_context_layers.remove(&TypeId::of::()); - cx.notify(); - } - pub fn set_input_enabled(&mut self, input_enabled: bool) { self.input_enabled = input_enabled; } @@ -11864,7 +11860,6 @@ impl Editor { self.editor_actions.borrow_mut().insert( id, Box::new(move |cx| { - let _view = cx.view().clone(); let cx = cx.window_context(); let listener = listener.clone(); cx.on_action(TypeId::of::(), move |action, phase, cx| { @@ -11950,6 +11945,22 @@ impl Editor { menu.visible() && matches!(menu, ContextMenu::Completions(_)) }) } + + pub fn register_addon(&mut self, instance: T) { + self.addons + .insert(std::any::TypeId::of::(), Box::new(instance)); + } + + pub fn unregister_addon(&mut self) { + self.addons.remove(&std::any::TypeId::of::()); + } + + pub fn addon(&self) -> Option<&T> { + let type_id = std::any::TypeId::of::(); + self.addons + .get(&type_id) + .and_then(|item| item.to_any().downcast_ref::()) + } } fn hunks_for_selections( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8ec6e806af3b2e09dfe8cb8bbe8659cfd2078e8a..4867210cbe959b6b91acf7d408f07e4a94129693 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5613,7 +5613,7 @@ impl Element for EditorElement { cx: &mut WindowContext, ) { let focus_handle = self.editor.focus_handle(cx); - let key_context = self.editor.read(cx).key_context(cx); + let key_context = self.editor.update(cx, |editor, cx| editor.key_context(cx)); cx.set_key_context(key_context); cx.handle_input( &focus_handle, diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index 78001458f191ffcf4eb8a37a810db4134aa06929..69fcdd83192f529cd67420bc0a01c36991ea688e 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -1,64 +1,55 @@ use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor}; -use gpui::{actions, View}; -use ui::{ViewContext, WindowContext}; -use workspace::Workspace; +use gpui::{actions, ViewContext}; use crate::{state::Mode, Vim}; actions!(vim, [ChangeListOlder, ChangeListNewer]); -pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_, _: &ChangeListOlder, cx| { - Vim::update(cx, |vim, cx| { - move_to_change(vim, Direction::Prev, cx); - }) +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &ChangeListOlder, cx| { + vim.move_to_change(Direction::Prev, cx); }); - workspace.register_action(|_, _: &ChangeListNewer, cx| { - Vim::update(cx, |vim, cx| { - move_to_change(vim, Direction::Next, cx); - }) + Vim::action(editor, cx, |vim, _: &ChangeListNewer, cx| { + vim.move_to_change(Direction::Next, cx); }); } -fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) { - let count = vim.take_count(cx).unwrap_or(1); - let selections = vim.update_state(|state| { - if state.change_list.is_empty() { - return None; +impl Vim { + fn move_to_change(&mut self, direction: Direction, cx: &mut ViewContext) { + let count = self.take_count(cx).unwrap_or(1); + if self.change_list.is_empty() { + return; } - let prev = state - .change_list_position - .unwrap_or(state.change_list.len()); + let prev = self.change_list_position.unwrap_or(self.change_list.len()); let next = if direction == Direction::Prev { prev.saturating_sub(count) } else { - (prev + count).min(state.change_list.len() - 1) + (prev + count).min(self.change_list.len() - 1) }; - state.change_list_position = Some(next); - state.change_list.get(next).cloned() - }); - - let Some(selections) = selections else { - return; - }; - vim.update_active_editor(cx, |_, editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - let map = s.display_map(); - s.select_display_ranges(selections.into_iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - }); -} + self.change_list_position = Some(next); + let Some(selections) = self.change_list.get(next).cloned() else { + return; + }; + self.update_editor(cx, |_, editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let map = s.display_map(); + s.select_display_ranges(selections.into_iter().map(|a| { + let point = a.to_display_point(&map); + point..point + })) + }) + }); + } -pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View, cx: &mut WindowContext) { - let (map, selections) = - editor.update(cx, |editor, cx| editor.selections.all_adjusted_display(cx)); + pub(crate) fn push_to_change_list(&mut self, cx: &mut ViewContext) { + let Some((map, selections)) = self.update_editor(cx, |_, editor, cx| { + editor.selections.all_adjusted_display(cx) + }) else { + return; + }; - let pop_state = - vim.state() + let pop_state = self .change_list .last() .map(|previous| { @@ -69,25 +60,24 @@ pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View, cx: &mut }) .unwrap_or(false); - let new_positions = selections - .into_iter() - .map(|s| { - let point = if vim.state().mode == Mode::Insert { - movement::saturating_left(&map, s.head()) - } else { - s.head() - }; - map.display_point_to_anchor(point, Bias::Left) - }) - .collect(); - - vim.update_state(|state| { - state.change_list_position.take(); + let new_positions = selections + .into_iter() + .map(|s| { + let point = if self.mode == Mode::Insert { + movement::saturating_left(&map, s.head()) + } else { + s.head() + }; + map.display_point_to_anchor(point, Bias::Left) + }) + .collect(); + + self.change_list_position.take(); if pop_state { - state.change_list.pop(); + self.change_list.pop(); } - state.change_list.push(new_positions); - }) + self.change_list.push(new_positions); + } } #[cfg(test)] diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 45c21de2ee9f8fa35ed900fe94062940880813a0..2345b76d91f4326ac0405a38f29e4e0cb6b50585 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -12,12 +12,11 @@ use multi_buffer::MultiBufferRow; use serde::Deserialize; use ui::WindowContext; use util::ResultExt; -use workspace::{notifications::NotifyResultExt, SaveIntent, Workspace}; +use workspace::{notifications::NotifyResultExt, SaveIntent}; use crate::{ motion::{EndOfDocument, Motion, StartOfDocument}, normal::{ - move_cursor, search::{FindCommand, ReplaceCommand, Replacement}, JoinLines, }, @@ -66,77 +65,89 @@ impl Clone for WithRange { } } -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &VisualCommand, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>", cx); - }); - - workspace.register_action(|workspace, _: &CountCommand, cx| { - let count = Vim::update(cx, |vim, cx| vim.take_count(cx)).unwrap_or(1); - command_palette::CommandPalette::toggle( - workspace, - &format!(".,.+{}", count.saturating_sub(1)), - cx, - ); +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &VisualCommand, cx| { + let Some(workspace) = vim.workspace(cx) else { + return; + }; + workspace.update(cx, |workspace, cx| { + command_palette::CommandPalette::toggle(workspace, "'<,'>", cx); + }) }); - workspace.register_action(|workspace: &mut Workspace, action: &GoToLine, cx| { - Vim::update(cx, |vim, cx| { - vim.switch_mode(Mode::Normal, false, cx); - let result = vim.update_active_editor(cx, |vim, editor, cx| { - action.range.head().buffer_row(vim, editor, cx) - }); - let Some(buffer_row) = result else { - return anyhow::Ok(()); - }; - move_cursor( - vim, - Motion::StartOfDocument, - Some(buffer_row?.0 as usize + 1), + Vim::action(editor, cx, |vim, _: &CountCommand, cx| { + let Some(workspace) = vim.workspace(cx) else { + return; + }; + let count = vim.take_count(cx).unwrap_or(1); + workspace.update(cx, |workspace, cx| { + command_palette::CommandPalette::toggle( + workspace, + &format!(".,.+{}", count.saturating_sub(1)), cx, ); - Ok(()) }) - .notify_err(workspace, cx); }); - workspace.register_action(|workspace: &mut Workspace, action: &WithRange, cx| { + Vim::action(editor, cx, |vim, action: &GoToLine, cx| { + vim.switch_mode(Mode::Normal, false, cx); + let result = vim.update_editor(cx, |vim, editor, cx| { + action.range.head().buffer_row(vim, editor, cx) + }); + let buffer_row = match result { + None => return, + Some(e @ Err(_)) => { + let Some(workspace) = vim.workspace(cx) else { + return; + }; + workspace.update(cx, |workspace, cx| { + e.notify_err(workspace, cx); + }); + return; + } + Some(Ok(result)) => result, + }; + vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx); + }); + + Vim::action(editor, cx, |vim, action: &WithRange, cx| { if action.is_count { for _ in 0..action.range.as_count() { cx.dispatch_action(action.action.boxed_clone()) } - } else { - Vim::update(cx, |vim, cx| { - let result = vim.update_active_editor(cx, |vim, editor, cx| { - action.range.buffer_range(vim, editor, cx) - }); - let Some(range) = result else { - return anyhow::Ok(()); + return; + } + let result = vim.update_editor(cx, |vim, editor, cx| { + action.range.buffer_range(vim, editor, cx) + }); + + let range = match result { + None => return, + Some(e @ Err(_)) => { + let Some(workspace) = vim.workspace(cx) else { + return; }; - let range = range?; - vim.update_active_editor(cx, |_, editor, cx| { - editor.change_selections(None, cx, |s| { - let end = Point::new(range.end.0, s.buffer().line_len(range.end)); - s.select_ranges([end..Point::new(range.start.0, 0)]); - }) - }); - cx.dispatch_action(action.action.boxed_clone()); - cx.defer(move |cx| { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |_, editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(range.start.0, 0)..Point::new(range.start.0, 0) - ]); - }) - }); - }) + workspace.update(cx, |workspace, cx| { + e.notify_err(workspace, cx); }); - - Ok(()) + return; + } + Some(Ok(result)) => result, + }; + vim.update_editor(cx, |_, editor, cx| { + editor.change_selections(None, cx, |s| { + let end = Point::new(range.end.0, s.buffer().line_len(range.end)); + s.select_ranges([end..Point::new(range.start.0, 0)]); }) - .notify_err(workspace, cx); - } + }); + cx.dispatch_action(action.action.boxed_clone()); + cx.defer(move |vim, cx| { + vim.update_editor(cx, |_, editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(range.start.0, 0)..Point::new(range.start.0, 0)]); + }) + }); + }); }); } @@ -343,12 +354,7 @@ impl Position { let target = match self { Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)), Position::Mark { name, offset } => { - let Some(mark) = vim - .state() - .marks - .get(&name.to_string()) - .and_then(|vec| vec.last()) - else { + let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else { return Err(anyhow!("mark {} not set", name)); }; mark.to_point(&snapshot.buffer_snapshot) diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index b9b61524341221ab703c35af64c5b2ca6152c282..74abce2a31a79842063f25474eabfbb4519f2aec 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::AppContext; use settings::Settings; use std::sync::LazyLock; -use ui::WindowContext; +use ui::ViewContext; use crate::{Vim, VimSettings}; @@ -34,16 +34,21 @@ fn lookup_digraph(a: char, b: char, cx: &AppContext) -> Arc { .unwrap_or_else(|| b.to_string().into()) } -pub fn insert_digraph(first_char: char, second_char: char, cx: &mut WindowContext) { - let text = lookup_digraph(first_char, second_char, &cx); - - Vim::update(cx, |vim, cx| vim.pop_operator(cx)); - if Vim::read(cx).state().editor_input_enabled() { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |_, editor, cx| editor.insert(&text, cx)); - }); - } else { - Vim::active_editor_input_ignored(text, cx); +impl Vim { + pub fn insert_digraph( + &mut self, + first_char: char, + second_char: char, + cx: &mut ViewContext, + ) { + let text = lookup_digraph(first_char, second_char, &cx); + + self.pop_operator(cx); + if self.editor_input_enabled() { + self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx)); + } else { + self.input_ignored(text, cx); + } } } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs deleted file mode 100644 index 48a59c3e4e662b5ae1c7511f0abd7f400b9e8669..0000000000000000000000000000000000000000 --- a/crates/vim/src/editor_events.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::{insert::NormalBefore, Vim, VimModeSetting}; -use editor::{Editor, EditorEvent}; -use gpui::{Action, AppContext, Entity, EntityId, UpdateGlobal, View, ViewContext, WindowContext}; -use settings::{Settings, SettingsStore}; - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|_, cx: &mut ViewContext| { - let editor = cx.view().clone(); - cx.subscribe(&editor, |_, editor, event: &EditorEvent, cx| match event { - EditorEvent::Focused => cx.window_context().defer(|cx| focused(editor, cx)), - EditorEvent::Blurred => cx.window_context().defer(|cx| blurred(editor, cx)), - _ => {} - }) - .detach(); - - let mut enabled = VimModeSetting::get_global(cx).0; - cx.observe_global::(move |editor, cx| { - if VimModeSetting::get_global(cx).0 != enabled { - enabled = VimModeSetting::get_global(cx).0; - if !enabled { - Vim::unhook_vim_settings(editor, cx); - } - } - }) - .detach(); - - let id = cx.view().entity_id(); - cx.on_release(move |_, _, cx| released(id, cx)).detach(); - }) - .detach(); -} -fn focused(editor: View, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - if !vim.enabled { - return; - } - vim.activate_editor(editor.clone(), cx); - }); -} - -fn blurred(editor: View, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - if !vim.enabled { - return; - } - if let Some(previous_editor) = vim.active_editor.clone() { - vim.stop_recording_immediately(NormalBefore.boxed_clone()); - if previous_editor - .upgrade() - .is_some_and(|previous| previous == editor.clone()) - { - vim.store_visual_marks(cx); - vim.clear_operator(cx); - } - } - editor.update(cx, |editor, cx| { - if editor.use_modal_editing() { - editor.set_cursor_shape(language::CursorShape::Hollow, cx); - } - }); - }); -} - -fn released(entity_id: EntityId, cx: &mut AppContext) { - Vim::update_global(cx, |vim, _cx| { - if vim - .active_editor - .as_ref() - .is_some_and(|previous| previous.entity_id() == entity_id) - { - vim.active_editor = None; - vim.editor_subscription = None; - } - vim.editor_states.remove(&entity_id) - }); -} - -#[cfg(test)] -mod test { - use crate::{test::VimTestContext, Vim}; - use editor::Editor; - use gpui::{Context, Entity, VisualTestContext}; - use language::Buffer; - - // regression test for blur called with a different active editor - #[gpui::test] - async fn test_blur_focus(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let buffer = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx)); - let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx)); - let editor2 = cx - .update(|cx| { - window2.update(cx, |_, cx| { - cx.activate_window(); - cx.focus_self(); - cx.view().clone() - }) - }) - .unwrap(); - cx.run_until_parked(); - - cx.update(|cx| { - let vim = Vim::read(cx); - assert_eq!( - vim.active_editor.as_ref().unwrap().entity_id(), - editor2.entity_id(), - ) - }); - - // no panic when blurring an editor in a different window. - cx.update_editor(|editor1, cx| { - editor1.handle_blur(cx); - }); - } - - // regression test for focus_in/focus_out being called on window activation - #[gpui::test] - async fn test_focus_across_windows(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let mut cx1 = VisualTestContext::from_window(cx.window, &cx); - let editor1 = cx.editor.clone(); - - let buffer = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx)); - let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx)); - - editor2.update(cx2, |_, cx| { - cx.focus_self(); - cx.activate_window(); - }); - cx.run_until_parked(); - - cx1.update(|cx| { - assert_eq!( - Vim::read(cx).active_editor.as_ref().unwrap().entity_id(), - editor2.entity_id(), - ) - }); - - cx1.update(|cx| { - cx.activate_window(); - }); - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - Vim::read(cx).active_editor.as_ref().unwrap().entity_id(), - editor1.entity_id(), - ) - }); - } -} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index fbf2d5fc7761eb44f817e31cfe13937d8fd86977..b015324a1b4f8005199df61b91ba49dc6bb7bded 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,31 +1,26 @@ -use crate::{ - normal::{mark::create_mark, repeat}, - state::Mode, - Vim, -}; -use editor::{scroll::Autoscroll, Bias}; +use crate::{state::Mode, Vim}; +use editor::{scroll::Autoscroll, Bias, Editor}; use gpui::{actions, Action, ViewContext}; use language::SelectionGoal; -use workspace::Workspace; actions!(vim, [NormalBefore]); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(normal_before); +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, Vim::normal_before); } -fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext) { - let should_repeat = Vim::update(cx, |vim, cx| { - if vim.state().active_operator().is_some() { - vim.update_state(|state| state.operator_stack.clear()); - vim.sync_vim_settings(cx); - return false; +impl Vim { + fn normal_before(&mut self, action: &NormalBefore, cx: &mut ViewContext) { + if self.active_operator().is_some() { + self.operator_stack.clear(); + self.sync_vim_settings(cx); + return; } - let count = vim.take_count(cx).unwrap_or(1); - vim.stop_recording_immediately(action.boxed_clone()); - if count <= 1 || vim.workspace_state.dot_replaying { - create_mark(vim, "^".into(), false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + let count = self.take_count(cx).unwrap_or(1); + self.stop_recording_immediately(action.boxed_clone(), cx); + if count <= 1 || Vim::globals(cx).dot_replaying { + self.create_mark("^".into(), false, cx); + self.update_editor(cx, |_, editor, cx| { editor.dismiss_menus_and_popups(false, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, mut cursor, _| { @@ -34,15 +29,11 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext< }); }); }); - vim.switch_mode(Mode::Normal, false, cx); - false - } else { - true + self.switch_mode(Mode::Normal, false, cx); + return; } - }); - if should_repeat { - repeat::repeat(cx, true) + self.repeat(true, cx) } } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 1cfb598e7f4ff00fddc01837799be9e2a84caf0a..214462bc8df92a108d40f40ef2439cf29dadfeff 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,93 +1,95 @@ -use gpui::{div, Element, Render, Subscription, ViewContext}; +use gpui::{div, Element, Render, Subscription, View, ViewContext, WeakView}; use itertools::Itertools; use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; -use crate::{state::Mode, Vim}; +use crate::{Vim, VimEvent}; /// The ModeIndicator displays the current mode in the status bar. pub struct ModeIndicator { - pub(crate) mode: Option, - pub(crate) operators: String, + vim: Option>, pending_keys: Option, - _subscriptions: Vec, + vim_subscription: Option, } impl ModeIndicator { /// Construct a new mode indicator in this window. pub fn new(cx: &mut ViewContext) -> Self { - let _subscriptions = vec![ - cx.observe_global::(|this, cx| this.update_mode(cx)), - cx.observe_pending_input(|this, cx| { - this.update_pending_keys(cx); - cx.notify(); - }), - ]; + cx.observe_pending_input(|this, cx| { + this.update_pending_keys(cx); + cx.notify(); + }) + .detach(); - let mut this = Self { - mode: None, - operators: "".to_string(), - pending_keys: None, - _subscriptions, - }; - this.update_mode(cx); - this - } + let handle = cx.view().clone(); + let window = cx.window_handle(); + cx.observe_new_views::(move |_, cx| { + if cx.window_handle() != window { + return; + } + let vim = cx.view().clone(); + handle.update(cx, |_, cx| { + cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event { + VimEvent::Focused => { + mode_indicator.vim_subscription = + Some(cx.observe(&vim, |_, _, cx| cx.notify())); + mode_indicator.vim = Some(vim.downgrade()); + } + }) + .detach() + }) + }) + .detach(); - fn update_mode(&mut self, cx: &mut ViewContext) { - if let Some(vim) = self.vim(cx) { - self.mode = Some(vim.state().mode); - self.operators = self.current_operators_description(&vim); - } else { - self.mode = None; + Self { + vim: None, + pending_keys: None, + vim_subscription: None, } } fn update_pending_keys(&mut self, cx: &mut ViewContext) { - if self.vim(cx).is_some() { - self.pending_keys = cx.pending_input_keystrokes().map(|keystrokes| { - keystrokes - .iter() - .map(|keystroke| format!("{}", keystroke)) - .join(" ") - }); - } else { - self.pending_keys = None; - } + self.pending_keys = cx.pending_input_keystrokes().map(|keystrokes| { + keystrokes + .iter() + .map(|keystroke| format!("{}", keystroke)) + .join(" ") + }); } - fn vim<'a>(&self, cx: &'a mut ViewContext) -> Option<&'a Vim> { - // In some tests Vim isn't enabled, so we use try_global. - cx.try_global::().filter(|vim| vim.enabled) + fn vim(&self) -> Option> { + self.vim.as_ref().and_then(|vim| vim.upgrade()) } - fn current_operators_description(&self, vim: &Vim) -> String { - vim.workspace_state + fn current_operators_description(&self, vim: View, cx: &mut ViewContext) -> String { + let recording = Vim::globals(cx) .recording_register .map(|reg| format!("recording @{reg} ")) - .into_iter() - .chain(vim.state().pre_count.map(|count| format!("{}", count))) - .chain(vim.state().selected_register.map(|reg| format!("\"{reg}"))) - .chain( - vim.state() - .operator_stack - .iter() - .map(|item| item.id().to_string()), - ) - .chain(vim.state().post_count.map(|count| format!("{}", count))) + .into_iter(); + + let vim = vim.read(cx); + recording + .chain(vim.pre_count.map(|count| format!("{}", count))) + .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) + .chain(vim.operator_stack.iter().map(|item| item.id().to_string())) + .chain(vim.post_count.map(|count| format!("{}", count))) .collect::>() .join("") } } impl Render for ModeIndicator { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - let Some(mode) = self.mode.as_ref() else { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let vim = self.vim(); + let Some(vim) = vim else { return div().into_any(); }; - let pending = self.pending_keys.as_ref().unwrap_or(&self.operators); - - Label::new(format!("{} -- {} --", pending, mode)) + let current_operators_description = self.current_operators_description(vim.clone(), cx); + let pending = self + .pending_keys + .as_ref() + .unwrap_or(¤t_operators_description); + Label::new(format!("{} -- {} --", pending, vim.read(cx).mode)) .size(LabelSize::Small) .line_height_style(LineHeightStyle::UiLabel) .into_any_element() @@ -100,6 +102,5 @@ impl StatusItemView for ModeIndicator { _active_pane_item: Option<&dyn ItemHandle>, _cx: &mut ViewContext, ) { - // nothing to do. } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 29857ae989b6b6a2b22befc0076e1ec3cd9fc27e..5da9d44493581520d68df80fb54fbda94d873b27 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -4,20 +4,18 @@ use editor::{ self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails, }, scroll::Autoscroll, - Anchor, Bias, DisplayPoint, RowExt, ToOffset, + Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, }; -use gpui::{actions, impl_actions, px, ViewContext, WindowContext}; +use gpui::{actions, impl_actions, px, ViewContext}; use language::{char_kind, CharKind, Point, Selection, SelectionGoal}; use multi_buffer::MultiBufferRow; use serde::Deserialize; use std::ops::Range; -use workspace::Workspace; use crate::{ - normal::{mark, normal_motion}, + normal::mark, state::{Mode, Operator}, surrounds::SurroundsType, - visual::visual_motion, Vim, }; @@ -248,214 +246,227 @@ actions!( ] ); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); - workspace - .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); - workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| { - motion( +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx)); + Vim::action(editor, cx, |vim, _: &Backspace, cx| { + vim.motion(Motion::Backspace, cx) + }); + Vim::action(editor, cx, |vim, action: &Down, cx| { + vim.motion( Motion::Down { display_lines: action.display_lines, }, cx, ) }); - workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| { - motion( + Vim::action(editor, cx, |vim, action: &Up, cx| { + vim.motion( Motion::Up { display_lines: action.display_lines, }, cx, ) }); - workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); - workspace.register_action(|_: &mut Workspace, _: &Space, cx: _| motion(Motion::Space, cx)); - workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { - motion( + Vim::action(editor, cx, |vim, _: &Right, cx| { + vim.motion(Motion::Right, cx) + }); + Vim::action(editor, cx, |vim, _: &Space, cx| { + vim.motion(Motion::Space, cx) + }); + Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| { + vim.motion( Motion::FirstNonWhitespace { display_lines: action.display_lines, }, cx, ) }); - workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { - motion( + Vim::action(editor, cx, |vim, action: &StartOfLine, cx| { + vim.motion( Motion::StartOfLine { display_lines: action.display_lines, }, cx, ) }); - workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { - motion( + Vim::action(editor, cx, |vim, action: &EndOfLine, cx| { + vim.motion( Motion::EndOfLine { display_lines: action.display_lines, }, cx, ) }); - workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| { - motion(Motion::CurrentLine, cx) + Vim::action(editor, cx, |vim, _: &CurrentLine, cx| { + vim.motion(Motion::CurrentLine, cx) }); - workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { - motion(Motion::StartOfParagraph, cx) + Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| { + vim.motion(Motion::StartOfParagraph, cx) }); - workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| { - motion(Motion::EndOfParagraph, cx) + Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| { + vim.motion(Motion::EndOfParagraph, cx) }); - workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { - motion(Motion::StartOfDocument, cx) + Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| { + vim.motion(Motion::StartOfDocument, cx) }); - workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| { - motion(Motion::EndOfDocument, cx) + Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| { + vim.motion(Motion::EndOfDocument, cx) + }); + Vim::action(editor, cx, |vim, _: &Matching, cx| { + vim.motion(Motion::Matching, cx) }); - workspace - .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx)); - workspace.register_action( - |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| { - motion(Motion::NextWordStart { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| { + vim.motion(Motion::NextWordStart { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| { - motion(Motion::NextWordEnd { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| { + vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, - &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, - cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) }, + Vim::action( + editor, + cx, + |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| { + vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx) + }, ); - workspace.register_action( - |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| { - motion(Motion::PreviousWordEnd { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &PreviousWordEnd { ignore_punctuation }, cx| { + vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| { - motion(Motion::NextSubwordStart { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| { + vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| { - motion(Motion::NextSubwordEnd { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| { + vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, - &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, - cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) }, + Vim::action( + editor, + cx, + |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| { + vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) + }, ); - workspace.register_action( - |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| { - motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &PreviousSubwordEnd { ignore_punctuation }, cx| { + vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| { - motion(Motion::NextLineStart, cx) + Vim::action(editor, cx, |vim, &NextLineStart, cx| { + vim.motion(Motion::NextLineStart, cx) }); - workspace.register_action(|_: &mut Workspace, &PreviousLineStart, cx: _| { - motion(Motion::PreviousLineStart, cx) + Vim::action(editor, cx, |vim, &PreviousLineStart, cx| { + vim.motion(Motion::PreviousLineStart, cx) }); - workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| { - motion(Motion::StartOfLineDownward, cx) + Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| { + vim.motion(Motion::StartOfLineDownward, cx) }); - workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { - motion(Motion::EndOfLineDownward, cx) + Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| { + vim.motion(Motion::EndOfLineDownward, cx) }); - workspace - .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx)); - - workspace.register_action(|_: &mut Workspace, _: &RepeatFind, cx: _| { - if let Some(last_find) = Vim::read(cx) - .workspace_state - .last_find - .clone() - .map(Box::new) - { - motion(Motion::RepeatFind { last_find }, cx); + Vim::action(editor, cx, |vim, &GoToColumn, cx| { + vim.motion(Motion::GoToColumn, cx) + }); + + Vim::action(editor, cx, |vim, _: &RepeatFind, cx| { + if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { + vim.motion(Motion::RepeatFind { last_find }, cx); } }); - workspace.register_action(|_: &mut Workspace, _: &RepeatFindReversed, cx: _| { - if let Some(last_find) = Vim::read(cx) - .workspace_state - .last_find - .clone() - .map(Box::new) - { - motion(Motion::RepeatFindReversed { last_find }, cx); + Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| { + if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { + vim.motion(Motion::RepeatFindReversed { last_find }, cx); } }); - workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx)); - workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| { - motion(Motion::WindowMiddle, cx) + Vim::action(editor, cx, |vim, &WindowTop, cx| { + vim.motion(Motion::WindowTop, cx) }); - workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| { - motion(Motion::WindowBottom, cx) + Vim::action(editor, cx, |vim, &WindowMiddle, cx| { + vim.motion(Motion::WindowMiddle, cx) + }); + Vim::action(editor, cx, |vim, &WindowBottom, cx| { + vim.motion(Motion::WindowBottom, cx) }); } -pub(crate) fn search_motion(m: Motion, cx: &mut WindowContext) { - if let Motion::ZedSearchResult { - prior_selections, .. - } = &m - { - match Vim::read(cx).state().mode { - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - if !prior_selections.is_empty() { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |_, editor, cx| { +impl Vim { + pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext) { + if let Motion::ZedSearchResult { + prior_selections, .. + } = &m + { + match self.mode { + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + if !prior_selections.is_empty() { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(prior_selections.iter().cloned()) }) }); - }); + } } - } - Mode::Normal | Mode::Replace | Mode::Insert => { - if Vim::read(cx).active_operator().is_none() { - return; + Mode::Normal | Mode::Replace | Mode::Insert => { + if self.active_operator().is_none() { + return; + } } } } - } - motion(m, cx) -} - -pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { - if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) = - Vim::read(cx).active_operator() - { - Vim::update(cx, |vim, cx| vim.pop_operator(cx)); + self.motion(m, cx) } - let count = Vim::update(cx, |vim, cx| vim.take_count(cx)); - let active_operator = Vim::read(cx).active_operator(); - let mut waiting_operator: Option = None; - match Vim::read(cx).state().mode { - Mode::Normal | Mode::Replace | Mode::Insert => { - if active_operator == Some(Operator::AddSurrounds { target: None }) { - waiting_operator = Some(Operator::AddSurrounds { - target: Some(SurroundsType::Motion(motion)), - }); - } else { - normal_motion(motion.clone(), active_operator.clone(), count, cx) - } + pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext) { + if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) = + self.active_operator() + { + self.pop_operator(cx); } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - visual_motion(motion.clone(), count, cx) + + let count = self.take_count(cx); + let active_operator = self.active_operator(); + let mut waiting_operator: Option = None; + match self.mode { + Mode::Normal | Mode::Replace | Mode::Insert => { + if active_operator == Some(Operator::AddSurrounds { target: None }) { + waiting_operator = Some(Operator::AddSurrounds { + target: Some(SurroundsType::Motion(motion)), + }); + } else { + self.normal_motion(motion.clone(), active_operator.clone(), count, cx) + } + } + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + self.visual_motion(motion.clone(), count, cx) + } } - } - Vim::update(cx, |vim, cx| { - vim.clear_operator(cx); + self.clear_operator(cx); if let Some(operator) = waiting_operator { - vim.push_operator(operator, cx); - vim.update_state(|state| state.pre_count = count) + self.push_operator(operator, cx); + self.pre_count = count } - }); + } } // Motion handling is specified here: diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index bc2fefe7f52d51435d5a271663a70a947454ae41..c9d3a7a4725ff5e243d25a25c96c5df31f2198bc 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -19,30 +19,22 @@ use crate::{ motion::{self, first_non_whitespace, next_line_end, right, Motion}, object::Object, state::{Mode, Operator}, - surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType}, + surrounds::SurroundsType, Vim, }; -use case::{change_case_motion, change_case_object, CaseTarget}; +use case::CaseTarget; use collections::BTreeSet; use editor::scroll::Autoscroll; use editor::Anchor; use editor::Bias; use editor::Editor; use editor::{display_map::ToDisplayPoint, movement}; -use gpui::{actions, ViewContext, WindowContext}; +use gpui::{actions, ViewContext}; use language::{Point, SelectionGoal}; use log::error; use multi_buffer::MultiBufferRow; -use workspace::Workspace; - -use self::{ - case::{change_case, convert_to_lower_case, convert_to_upper_case}, - change::{change_motion, change_object}, - delete::{delete_motion, delete_object}, - indent::{indent_motion, indent_object, IndentDirection}, - toggle_comments::{toggle_comments_motion, toggle_comments_object}, - yank::{yank_motion, yank_object}, -}; + +use self::indent::IndentDirection; actions!( vim, @@ -73,216 +65,195 @@ actions!( ] ); -pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext) { - workspace.register_action(insert_after); - workspace.register_action(insert_before); - workspace.register_action(insert_first_non_whitespace); - workspace.register_action(insert_end_of_line); - workspace.register_action(insert_line_above); - workspace.register_action(insert_line_below); - workspace.register_action(insert_at_previous); - workspace.register_action(change_case); - workspace.register_action(convert_to_upper_case); - workspace.register_action(convert_to_lower_case); - workspace.register_action(yank_line); - workspace.register_action(yank_to_end_of_line); - workspace.register_action(toggle_comments); - - workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let times = vim.take_count(cx); - delete_motion(vim, Motion::Left, times, cx); - }) +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, Vim::insert_after); + Vim::action(editor, cx, Vim::insert_before); + Vim::action(editor, cx, Vim::insert_first_non_whitespace); + Vim::action(editor, cx, Vim::insert_end_of_line); + Vim::action(editor, cx, Vim::insert_line_above); + Vim::action(editor, cx, Vim::insert_line_below); + Vim::action(editor, cx, Vim::insert_at_previous); + Vim::action(editor, cx, Vim::change_case); + Vim::action(editor, cx, Vim::convert_to_upper_case); + Vim::action(editor, cx, Vim::convert_to_lower_case); + Vim::action(editor, cx, Vim::yank_line); + Vim::action(editor, cx, Vim::yank_to_end_of_line); + Vim::action(editor, cx, Vim::toggle_comments); + Vim::action(editor, cx, Vim::paste); + + Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| { + vim.record_current_action(cx); + let times = vim.take_count(cx); + vim.delete_motion(Motion::Left, times, cx); }); - workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let times = vim.take_count(cx); - delete_motion(vim, Motion::Right, times, cx); - }) + Vim::action(editor, cx, |vim, _: &DeleteRight, cx| { + vim.record_current_action(cx); + let times = vim.take_count(cx); + vim.delete_motion(Motion::Right, times, cx); }); - workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - let times = vim.take_count(cx); - change_motion( - vim, - Motion::EndOfLine { - display_lines: false, - }, - times, - cx, - ); - }) + Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| { + vim.start_recording(cx); + let times = vim.take_count(cx); + vim.change_motion( + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }); - workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let times = vim.take_count(cx); - delete_motion( - vim, - Motion::EndOfLine { - display_lines: false, - }, - times, - cx, - ); - }) + Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| { + vim.record_current_action(cx); + let times = vim.take_count(cx); + vim.delete_motion( + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }); - workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let mut times = vim.take_count(cx).unwrap_or(1); - if vim.state().mode.is_visual() { - times = 1; - } else if times > 1 { - // 2J joins two lines together (same as J or 1J) - times -= 1; - } + Vim::action(editor, cx, |vim, _: &JoinLines, cx| { + vim.record_current_action(cx); + let mut times = vim.take_count(cx).unwrap_or(1); + if vim.mode.is_visual() { + times = 1; + } else if times > 1 { + // 2J joins two lines together (same as J or 1J) + times -= 1; + } - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - for _ in 0..times { - editor.join_lines(&Default::default(), cx) - } - }) - }); - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) - } + vim.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + for _ in 0..times { + editor.join_lines(&Default::default(), cx) + } + }) }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, cx) + } }); - workspace.register_action(|_: &mut Workspace, _: &Indent, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - let mut original_positions = save_selection_starts(editor, cx); - for _ in 0..count { - editor.indent(&Default::default(), cx); - } - restore_selection_cursors(editor, cx, &mut original_positions); - }); + Vim::action(editor, cx, |vim, _: &Indent, cx| { + vim.record_current_action(cx); + let count = vim.take_count(cx).unwrap_or(1); + vim.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions = save_selection_starts(editor, cx); + for _ in 0..count { + editor.indent(&Default::default(), cx); + } + restore_selection_cursors(editor, cx, &mut original_positions); }); - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) - } }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, cx) + } }); - workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - let mut original_positions = save_selection_starts(editor, cx); - for _ in 0..count { - editor.outdent(&Default::default(), cx); - } - restore_selection_cursors(editor, cx, &mut original_positions); - }); + Vim::action(editor, cx, |vim, _: &Outdent, cx| { + vim.record_current_action(cx); + let count = vim.take_count(cx).unwrap_or(1); + vim.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions = save_selection_starts(editor, cx); + for _ in 0..count { + editor.outdent(&Default::default(), cx); + } + restore_selection_cursors(editor, cx, &mut original_positions); }); - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) - } }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, cx) + } }); - workspace.register_action(|_: &mut Workspace, _: &Undo, cx| { - Vim::update(cx, |vim, cx| { - let times = vim.take_count(cx); - vim.update_active_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.undo(&editor::actions::Undo, cx); - } - }); - }) + Vim::action(editor, cx, |vim, _: &Undo, cx| { + let times = vim.take_count(cx); + vim.update_editor(cx, |_, editor, cx| { + for _ in 0..times.unwrap_or(1) { + editor.undo(&editor::actions::Undo, cx); + } + }); }); - workspace.register_action(|_: &mut Workspace, _: &Redo, cx| { - Vim::update(cx, |vim, cx| { - let times = vim.take_count(cx); - vim.update_active_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.redo(&editor::actions::Redo, cx); - } - }); - }) + Vim::action(editor, cx, |vim, _: &Redo, cx| { + let times = vim.take_count(cx); + vim.update_editor(cx, |_, editor, cx| { + for _ in 0..times.unwrap_or(1) { + editor.redo(&editor::actions::Redo, cx); + } + }); }); - paste::register(workspace, cx); - repeat::register(workspace, cx); - scroll::register(workspace, cx); - search::register(workspace, cx); - substitute::register(workspace, cx); - increment::register(workspace, cx); + repeat::register(editor, cx); + scroll::register(editor, cx); + search::register(editor, cx); + substitute::register(editor, cx); + increment::register(editor, cx); } -pub fn normal_motion( - motion: Motion, - operator: Option, - times: Option, - cx: &mut WindowContext, -) { - Vim::update(cx, |vim, cx| { +impl Vim { + pub fn normal_motion( + &mut self, + motion: Motion, + operator: Option, + times: Option, + cx: &mut ViewContext, + ) { match operator { - None => move_cursor(vim, motion, times, cx), - Some(Operator::Change) => change_motion(vim, motion, times, cx), - Some(Operator::Delete) => delete_motion(vim, motion, times, cx), - Some(Operator::Yank) => yank_motion(vim, motion, times, cx), + None => self.move_cursor(motion, times, cx), + Some(Operator::Change) => self.change_motion(motion, times, cx), + Some(Operator::Delete) => self.delete_motion(motion, times, cx), + Some(Operator::Yank) => self.yank_motion(motion, times, cx), Some(Operator::AddSurrounds { target: None }) => {} - Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx), - Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx), + Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx), + Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx), Some(Operator::Lowercase) => { - change_case_motion(vim, motion, times, CaseTarget::Lowercase, cx) + self.change_case_motion(motion, times, CaseTarget::Lowercase, cx) } Some(Operator::Uppercase) => { - change_case_motion(vim, motion, times, CaseTarget::Uppercase, cx) + self.change_case_motion(motion, times, CaseTarget::Uppercase, cx) } Some(Operator::OppositeCase) => { - change_case_motion(vim, motion, times, CaseTarget::OppositeCase, cx) + self.change_case_motion(motion, times, CaseTarget::OppositeCase, cx) } - Some(Operator::ToggleComments) => toggle_comments_motion(vim, motion, times, cx), + Some(Operator::ToggleComments) => self.toggle_comments_motion(motion, times, cx), Some(operator) => { // Can't do anything for text objects, Ignoring error!("Unexpected normal mode motion operator: {:?}", operator) } } - }); -} + } -pub fn normal_object(object: Object, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { + pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext) { let mut waiting_operator: Option = None; - match vim.maybe_pop_operator() { - Some(Operator::Object { around }) => match vim.maybe_pop_operator() { - Some(Operator::Change) => change_object(vim, object, around, cx), - Some(Operator::Delete) => delete_object(vim, object, around, cx), - Some(Operator::Yank) => yank_object(vim, object, around, cx), + match self.maybe_pop_operator() { + Some(Operator::Object { around }) => match self.maybe_pop_operator() { + Some(Operator::Change) => self.change_object(object, around, cx), + Some(Operator::Delete) => self.delete_object(object, around, cx), + Some(Operator::Yank) => self.yank_object(object, around, cx), Some(Operator::Indent) => { - indent_object(vim, object, around, IndentDirection::In, cx) + self.indent_object(object, around, IndentDirection::In, cx) } Some(Operator::Outdent) => { - indent_object(vim, object, around, IndentDirection::Out, cx) + self.indent_object(object, around, IndentDirection::Out, cx) } Some(Operator::Lowercase) => { - change_case_object(vim, object, around, CaseTarget::Lowercase, cx) + self.change_case_object(object, around, CaseTarget::Lowercase, cx) } Some(Operator::Uppercase) => { - change_case_object(vim, object, around, CaseTarget::Uppercase, cx) + self.change_case_object(object, around, CaseTarget::Uppercase, cx) } Some(Operator::OppositeCase) => { - change_case_object(vim, object, around, CaseTarget::OppositeCase, cx) + self.change_case_object(object, around, CaseTarget::OppositeCase, cx) } Some(Operator::AddSurrounds { target: None }) => { waiting_operator = Some(Operator::AddSurrounds { target: Some(SurroundsType::Object(object)), }); } - Some(Operator::ToggleComments) => toggle_comments_object(vim, object, around, cx), + Some(Operator::ToggleComments) => self.toggle_comments_object(object, around, cx), _ => { // Can't do anything for namespace operators. Ignoring } @@ -291,7 +262,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) { waiting_operator = Some(Operator::DeleteSurrounds); } Some(Operator::ChangeSurrounds { target: None }) => { - if check_and_move_to_valid_bracket_pair(vim, object, cx) { + if self.check_and_move_to_valid_bracket_pair(object, cx) { waiting_operator = Some(Operator::ChangeSurrounds { target: Some(object), }); @@ -301,59 +272,53 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) { // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring } } - vim.clear_operator(cx); + self.clear_operator(cx); if let Some(operator) = waiting_operator { - vim.push_operator(operator, cx); + self.push_operator(operator, cx); } - }); -} + } -pub(crate) fn move_cursor( - vim: &mut Vim, - motion: Motion, - times: Option, - cx: &mut WindowContext, -) { - vim.update_active_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, cursor, goal| { - motion - .move_point(map, cursor, goal, times, &text_layout_details) - .unwrap_or((cursor, goal)) + pub(crate) fn move_cursor( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, cursor, goal| { + motion + .move_point(map, cursor, goal, times, &text_layout_details) + .unwrap_or((cursor, goal)) + }) }) - }) - }); -} + }); + } -fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn insert_after(&mut self, _: &InsertAfter, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None)); }); }); - }); -} + } -fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - }); -} + fn insert_before(&mut self, _: &InsertBefore, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + } -fn insert_first_non_whitespace( - _: &mut Workspace, - _: &InsertFirstNonWhitespace, - cx: &mut ViewContext, -) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn insert_first_non_whitespace( + &mut self, + _: &InsertFirstNonWhitespace, + cx: &mut ViewContext, + ) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, _| { ( @@ -363,42 +328,36 @@ fn insert_first_non_whitespace( }); }); }); - }); -} + } -fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn insert_end_of_line(&mut self, _: &InsertEndOfLine, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, _| { (next_line_end(map, cursor, 1), SelectionGoal::None) }); }); }); - }); -} + } -fn insert_at_previous(_: &mut Workspace, _: &InsertAtPrevious, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |vim, editor, cx| { - if let Some(marks) = vim.state().marks.get("^") { + fn insert_at_previous(&mut self, _: &InsertAtPrevious, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |vim, editor, cx| { + if let Some(marks) = vim.marks.get("^") { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark)) }); } }); - }); -} + } -fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn insert_line_above(&mut self, _: &InsertLineAbove, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { let selections = editor.selections.all::(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); @@ -425,14 +384,12 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex }); }); }); - }); -} + } -fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - vim.switch_mode(Mode::Insert, false, cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn insert_line_below(&mut self, _: &InsertLineBelow, cx: &mut ViewContext) { + self.start_recording(cx); + self.switch_mode(Mode::Insert, false, cx); + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { let selections = editor.selections.all::(cx); @@ -464,79 +421,43 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex editor.edit_with_autoindent(edits, cx); }); }); - }); -} + } -fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - let count = vim.take_count(cx); - yank_motion(vim, motion::Motion::CurrentLine, count, cx) - }) -} + fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext) { + let count = self.take_count(cx); + self.yank_motion(motion::Motion::CurrentLine, count, cx) + } -fn yank_to_end_of_line(_: &mut Workspace, _: &YankToEndOfLine, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let count = vim.take_count(cx); - yank_motion( - vim, + fn yank_to_end_of_line(&mut self, _: &YankToEndOfLine, cx: &mut ViewContext) { + self.record_current_action(cx); + let count = self.take_count(cx); + self.yank_motion( motion::Motion::EndOfLine { display_lines: false, }, count, cx, ) - }) -} + } -fn toggle_comments(_: &mut Workspace, _: &ToggleComments, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - vim.update_active_editor(cx, |_, editor, cx| { + fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { + self.record_current_action(cx); + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { let mut original_positions = save_selection_starts(editor, cx); editor.toggle_comments(&Default::default(), cx); restore_selection_cursors(editor, cx, &mut original_positions); }); }); - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) + if self.mode.is_visual() { + self.switch_mode(Mode::Normal, false, cx) } - }); -} - -fn save_selection_starts(editor: &Editor, cx: &mut ViewContext) -> HashMap { - let (map, selections) = editor.selections.all_display(cx); - selections - .iter() - .map(|selection| { - ( - selection.id, - map.display_point_to_anchor(selection.start, Bias::Right), - ) - }) - .collect::>() -} - -fn restore_selection_cursors( - editor: &mut Editor, - cx: &mut ViewContext, - positions: &mut HashMap, -) { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - if let Some(anchor) = positions.remove(&selection.id) { - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - } - }); - }); -} + } -pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - let count = vim.take_count(cx).unwrap_or(1); - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { + pub(crate) fn normal_replace(&mut self, text: Arc, cx: &mut ViewContext) { + let count = self.take_count(cx).unwrap_or(1); + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let (map, display_selections) = editor.selections.all_display(cx); @@ -571,10 +492,36 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { }); }); }); - vim.pop_operator(cx) - }); + self.pop_operator(cx); + } } +fn save_selection_starts(editor: &Editor, cx: &mut ViewContext) -> HashMap { + let (map, selections) = editor.selections.all_display(cx); + selections + .iter() + .map(|selection| { + ( + selection.id, + map.display_point_to_anchor(selection.start, Bias::Right), + ) + }) + .collect::>() +} + +fn restore_selection_cursors( + editor: &mut Editor, + cx: &mut ViewContext, + positions: &mut HashMap, +) { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if let Some(anchor) = positions.remove(&selection.id) { + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + } + }); + }); +} #[cfg(test)] mod test { use gpui::{KeyBinding, TestAppContext}; diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index f9254ce5f08b64aa0bdf1928987d19b8026befd3..fa9044ef9c4dab2d1ae2fad6b13a35ef518053c0 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -3,8 +3,6 @@ use editor::{display_map::ToDisplayPoint, scroll::Autoscroll}; use gpui::ViewContext; use language::{Bias, Point, SelectionGoal}; use multi_buffer::MultiBufferRow; -use ui::WindowContext; -use workspace::Workspace; use crate::{ motion::Motion, @@ -20,120 +18,112 @@ pub enum CaseTarget { OppositeCase, } -pub fn change_case_motion( - vim: &mut Vim, - motion: Motion, - times: Option, - mode: CaseTarget, - cx: &mut WindowContext, -) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); - selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, false, &text_layout_details); +impl Vim { + pub fn change_case_motion( + &mut self, + motion: Motion, + times: Option, + mode: CaseTarget, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + let mut selection_starts: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); + selection_starts.insert(selection.id, anchor); + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); }); - }); - match mode { - CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), - CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), - CaseTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), cx) + match mode { + CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), + CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), + CaseTarget::OppositeCase => { + editor.convert_to_opposite_case(&Default::default(), cx) + } } - } - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = selection_starts.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); -} + } -pub fn change_case_object( - vim: &mut Vim, - object: Object, - around: bool, - mode: CaseTarget, - cx: &mut WindowContext, -) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around); - original_positions.insert( - selection.id, - map.display_point_to_anchor(selection.start, Bias::Left), - ); + pub fn change_case_object( + &mut self, + object: Object, + around: bool, + mode: CaseTarget, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + object.expand_selection(map, selection, around); + original_positions.insert( + selection.id, + map.display_point_to_anchor(selection.start, Bias::Left), + ); + }); }); - }); - match mode { - CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), - CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), - CaseTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), cx) + match mode { + CaseTarget::Lowercase => editor.convert_to_lower_case(&Default::default(), cx), + CaseTarget::Uppercase => editor.convert_to_upper_case(&Default::default(), cx), + CaseTarget::OppositeCase => { + editor.convert_to_opposite_case(&Default::default(), cx) + } } - } - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); -} + } -pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { - manipulate_text(cx, |c| { - if c.is_lowercase() { - c.to_uppercase().collect::>() - } else { - c.to_lowercase().collect::>() - } - }) -} + pub fn change_case(&mut self, _: &ChangeCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |c| { + if c.is_lowercase() { + c.to_uppercase().collect::>() + } else { + c.to_lowercase().collect::>() + } + }) + } -pub fn convert_to_upper_case( - _: &mut Workspace, - _: &ConvertToUpperCase, - cx: &mut ViewContext, -) { - manipulate_text(cx, |c| c.to_uppercase().collect::>()) -} + pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |c| c.to_uppercase().collect::>()) + } -pub fn convert_to_lower_case( - _: &mut Workspace, - _: &ConvertToLowerCase, - cx: &mut ViewContext, -) { - manipulate_text(cx, |c| c.to_lowercase().collect::>()) -} + pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |c| c.to_lowercase().collect::>()) + } -fn manipulate_text(cx: &mut ViewContext, transform: F) -where - F: Fn(char) -> Vec + Copy, -{ - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - vim.store_visual_marks(cx); - let count = vim.take_count(cx).unwrap_or(1) as u32; + fn manipulate_text(&mut self, cx: &mut ViewContext, transform: F) + where + F: Fn(char) -> Vec + Copy, + { + self.record_current_action(cx); + self.store_visual_marks(cx); + let count = self.take_count(cx).unwrap_or(1) as u32; - vim.update_active_editor(cx, |vim, editor, cx| { + self.update_editor(cx, |vim, editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); for selection in editor.selections.all::(cx) { - match vim.state().mode { + match vim.mode { Mode::VisualLine => { let start = Point::new(selection.start.row, 0); let end = Point::new( @@ -186,8 +176,8 @@ where }) }); }); - vim.switch_mode(Mode::Normal, true, cx) - }) + self.switch_mode(Mode::Normal, true, cx) + } } #[cfg(test)] diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 07486c6d933e65bd3b47853c450a8ea09370c15d..dd9fbab0d8f258e33ea6b41ad604be999c6b82e8 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,6 +1,5 @@ use crate::{ motion::{self, Motion}, - normal::yank::copy_selections_content, object::Object, state::Mode, Vim, @@ -11,98 +10,108 @@ use editor::{ scroll::Autoscroll, Bias, DisplayPoint, }; -use gpui::WindowContext; use language::{char_kind, CharKind, Selection}; - -pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { - // Some motions ignore failure when switching to normal mode - let mut motion_succeeded = matches!( - motion, - Motion::Left - | Motion::Right - | Motion::EndOfLine { .. } - | Motion::Backspace - | Motion::StartOfLine { .. } - ); - vim.update_active_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - motion_succeeded |= match motion { - Motion::NextWordStart { ignore_punctuation } - | Motion::NextSubwordStart { ignore_punctuation } => { - expand_changed_word_selection( - map, - selection, - times, - ignore_punctuation, - &text_layout_details, - motion == Motion::NextSubwordStart { ignore_punctuation }, - ) - } - _ => { - let result = motion.expand_selection( - map, - selection, - times, - false, - &text_layout_details, - ); - if let Motion::CurrentLine = motion { - let mut start_offset = selection.start.to_offset(map, Bias::Left); - let scope = map - .buffer_snapshot - .language_scope_at(selection.start.to_point(&map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace { - break; +use ui::ViewContext; + +impl Vim { + pub fn change_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + // Some motions ignore failure when switching to normal mode + let mut motion_succeeded = matches!( + motion, + Motion::Left + | Motion::Right + | Motion::EndOfLine { .. } + | Motion::Backspace + | Motion::StartOfLine { .. } + ); + self.update_editor(cx, |vim, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + // We are swapping to insert mode anyway. Just set the line end clipping behavior now + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + motion_succeeded |= match motion { + Motion::NextWordStart { ignore_punctuation } + | Motion::NextSubwordStart { ignore_punctuation } => { + expand_changed_word_selection( + map, + selection, + times, + ignore_punctuation, + &text_layout_details, + motion == Motion::NextSubwordStart { ignore_punctuation }, + ) + } + _ => { + let result = motion.expand_selection( + map, + selection, + times, + false, + &text_layout_details, + ); + if let Motion::CurrentLine = motion { + let mut start_offset = + selection.start.to_offset(map, Bias::Left); + let scope = map + .buffer_snapshot + .language_scope_at(selection.start.to_point(&map)); + for (ch, offset) in map.buffer_chars_at(start_offset) { + if ch == '\n' + || char_kind(&scope, ch) != CharKind::Whitespace + { + break; + } + start_offset = offset + ch.len_utf8(); } - start_offset = offset + ch.len_utf8(); + selection.start = start_offset.to_display_point(map); } - selection.start = start_offset.to_display_point(map); + result } - result } - } + }); }); + vim.copy_selections_content(editor, motion.linewise(), cx); + editor.insert("", cx); }); - copy_selections_content(vim, editor, motion.linewise(), cx); - editor.insert("", cx); }); - }); - if motion_succeeded { - vim.switch_mode(Mode::Insert, false, cx) - } else { - vim.switch_mode(Mode::Normal, false, cx) + if motion_succeeded { + self.switch_mode(Mode::Insert, false, cx) + } else { + self.switch_mode(Mode::Normal, false, cx) + } } -} -pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - let mut objects_found = false; - vim.update_active_editor(cx, |vim, editor, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.transact(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - objects_found |= object.expand_selection(map, selection, around); + pub fn change_object(&mut self, object: Object, around: bool, cx: &mut ViewContext) { + let mut objects_found = false; + self.update_editor(cx, |vim, editor, cx| { + // We are swapping to insert mode anyway. Just set the line end clipping behavior now + editor.set_clip_at_line_ends(false, cx); + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + objects_found |= object.expand_selection(map, selection, around); + }); }); + if objects_found { + vim.copy_selections_content(editor, false, cx); + editor.insert("", cx); + } }); - if objects_found { - copy_selections_content(vim, editor, false, cx); - editor.insert("", cx); - } }); - }); - if objects_found { - vim.switch_mode(Mode::Insert, false, cx); - } else { - vim.switch_mode(Mode::Normal, false, cx); + if objects_found { + self.switch_mode(Mode::Insert, false, cx); + } else { + self.switch_mode(Mode::Normal, false, cx); + } } } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index e43998188863e07cb4505956a93bfdda10fa6f5f..466b2ac250e63e0fbcf54a612a361374b4edec23 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,146 +1,154 @@ -use crate::{motion::Motion, normal::yank::copy_selections_content, object::Object, Vim}; +use crate::{motion::Motion, object::Object, Vim}; use collections::{HashMap, HashSet}; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, scroll::Autoscroll, Bias, DisplayPoint, }; -use gpui::WindowContext; use language::{Point, Selection}; use multi_buffer::MultiBufferRow; - -pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { - vim.stop_recording(); - vim.update_active_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_columns: HashMap<_, _> = Default::default(); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let original_head = selection.head(); - original_columns.insert(selection.id, original_head.column()); - motion.expand_selection(map, selection, times, true, &text_layout_details); - - // Motion::NextWordStart on an empty line should delete it. - if let Motion::NextWordStart { - ignore_punctuation: _, - } = motion - { - if selection.is_empty() - && map - .buffer_snapshot - .line_len(MultiBufferRow(selection.start.to_point(&map).row)) - == 0 +use ui::ViewContext; + +impl Vim { + pub fn delete_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |vim, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let mut original_columns: HashMap<_, _> = Default::default(); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let original_head = selection.head(); + original_columns.insert(selection.id, original_head.column()); + motion.expand_selection(map, selection, times, true, &text_layout_details); + + // Motion::NextWordStart on an empty line should delete it. + if let Motion::NextWordStart { + ignore_punctuation: _, + } = motion { - selection.end = map - .buffer_snapshot - .clip_point( - Point::new(selection.start.to_point(&map).row + 1, 0), - Bias::Left, - ) - .to_display_point(map) + if selection.is_empty() + && map + .buffer_snapshot + .line_len(MultiBufferRow(selection.start.to_point(&map).row)) + == 0 + { + selection.end = map + .buffer_snapshot + .clip_point( + Point::new(selection.start.to_point(&map).row + 1, 0), + Bias::Left, + ) + .to_display_point(map) + } } - } + }); }); - }); - copy_selections_content(vim, editor, motion.linewise(), cx); - editor.insert("", cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if motion.linewise() { - if let Some(column) = original_columns.get(&selection.id) { - *cursor.column_mut() = *column + vim.copy_selections_content(editor, motion.linewise(), cx); + editor.insert("", cx); + + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head(); + if motion.linewise() { + if let Some(column) = original_columns.get(&selection.id) { + *cursor.column_mut() = *column + } } - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) + cursor = map.clip_point(cursor, Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); }); }); }); - }); -} + } -pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - vim.stop_recording(); - vim.update_active_editor(cx, |vim, editor, cx| { - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - // Emulates behavior in vim where if we expanded backwards to include a newline - // the cursor gets set back to the start of the line - let mut should_move_to_start: HashSet<_> = Default::default(); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around); - let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); - let mut move_selection_start_to_previous_line = - |map: &DisplaySnapshot, selection: &mut Selection| { - let start = selection.start.to_offset(map, Bias::Left); - if selection.start.row().0 > 0 { - should_move_to_start.insert(selection.id); - selection.start = (start - '\n'.len_utf8()).to_display_point(map); + pub fn delete_object(&mut self, object: Object, around: bool, cx: &mut ViewContext) { + self.stop_recording(cx); + self.update_editor(cx, |vim, editor, cx| { + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + // Emulates behavior in vim where if we expanded backwards to include a newline + // the cursor gets set back to the start of the line + let mut should_move_to_start: HashSet<_> = Default::default(); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + object.expand_selection(map, selection, around); + let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); + let mut move_selection_start_to_previous_line = + |map: &DisplaySnapshot, selection: &mut Selection| { + let start = selection.start.to_offset(map, Bias::Left); + if selection.start.row().0 > 0 { + should_move_to_start.insert(selection.id); + selection.start = + (start - '\n'.len_utf8()).to_display_point(map); + } + }; + let range = selection.start.to_offset(map, Bias::Left) + ..selection.end.to_offset(map, Bias::Right); + let contains_only_newlines = map + .buffer_chars_at(range.start) + .take_while(|(_, p)| p < &range.end) + .all(|(char, _)| char == '\n') + && !offset_range.is_empty(); + let end_at_newline = map + .buffer_chars_at(range.end) + .next() + .map(|(c, _)| c == '\n') + .unwrap_or(false); + + // If expanded range contains only newlines and + // the object is around or sentence, expand to include a newline + // at the end or start + if (around || object == Object::Sentence) && contains_only_newlines { + if end_at_newline { + move_selection_end_to_next_line(map, selection); + } else { + move_selection_start_to_previous_line(map, selection); } - }; - let range = selection.start.to_offset(map, Bias::Left) - ..selection.end.to_offset(map, Bias::Right); - let contains_only_newlines = map - .buffer_chars_at(range.start) - .take_while(|(_, p)| p < &range.end) - .all(|(char, _)| char == '\n') - && !offset_range.is_empty(); - let end_at_newline = map - .buffer_chars_at(range.end) - .next() - .map(|(c, _)| c == '\n') - .unwrap_or(false); - - // If expanded range contains only newlines and - // the object is around or sentence, expand to include a newline - // at the end or start - if (around || object == Object::Sentence) && contains_only_newlines { - if end_at_newline { - move_selection_end_to_next_line(map, selection); - } else { - move_selection_start_to_previous_line(map, selection); - } - } - - // Does post-processing for the trailing newline and EOF - // when not cancelled. - let cancelled = around && selection.start == selection.end; - if object == Object::Paragraph && !cancelled { - // EOF check should be done before including a trailing newline. - if ends_at_eof(map, selection) { - move_selection_start_to_previous_line(map, selection); } - if end_at_newline { - move_selection_end_to_next_line(map, selection); + // Does post-processing for the trailing newline and EOF + // when not cancelled. + let cancelled = around && selection.start == selection.end; + if object == Object::Paragraph && !cancelled { + // EOF check should be done before including a trailing newline. + if ends_at_eof(map, selection) { + move_selection_start_to_previous_line(map, selection); + } + + if end_at_newline { + move_selection_end_to_next_line(map, selection); + } } - } + }); }); - }); - copy_selections_content(vim, editor, false, cx); - editor.insert("", cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if should_move_to_start.contains(&selection.id) { - *cursor.column_mut() = 0; - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) + vim.copy_selections_content(editor, false, cx); + editor.insert("", cx); + + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head(); + if should_move_to_start.contains(&selection.id) { + *cursor.column_mut() = 0; + } + cursor = map.clip_point(cursor, Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); }); }); }); - }); + } } fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection) { diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index e0638f50de21864b159a40cffd6231e0ea7f5a4a..5002bb5a04a141cbf2982c47df6dc6a48fcb9a54 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -1,10 +1,9 @@ use std::ops::Range; -use editor::{scroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint}; -use gpui::{impl_actions, ViewContext, WindowContext}; +use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint}; +use gpui::{impl_actions, ViewContext}; use language::{Bias, Point}; use serde::Deserialize; -use workspace::Workspace; use crate::{state::Mode, Vim}; @@ -24,92 +23,90 @@ struct Decrement { impl_actions!(vim, [Increment, Decrement]); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, action: &Increment, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); - let step = if action.step { 1 } else { 0 }; - increment(vim, count as i32, step, cx) - }) +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, action: &Increment, cx| { + vim.record_current_action(cx); + let count = vim.take_count(cx).unwrap_or(1); + let step = if action.step { 1 } else { 0 }; + vim.increment(count as i32, step, cx) }); - workspace.register_action(|_: &mut Workspace, action: &Decrement, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); - let step = if action.step { -1 } else { 0 }; - increment(vim, count as i32 * -1, step, cx) - }) + Vim::action(editor, cx, |vim, action: &Decrement, cx| { + vim.record_current_action(cx); + let count = vim.take_count(cx).unwrap_or(1); + let step = if action.step { -1 } else { 0 }; + vim.increment(count as i32 * -1, step, cx) }); } -fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { - vim.store_visual_marks(cx); - vim.update_active_editor(cx, |vim, editor, cx| { - let mut edits = Vec::new(); - let mut new_anchors = Vec::new(); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(cx) { - if !selection.is_empty() { - if vim.state().mode != Mode::VisualBlock || new_anchors.is_empty() { - new_anchors.push((true, snapshot.anchor_before(selection.start))) +impl Vim { + fn increment(&mut self, mut delta: i32, step: i32, cx: &mut ViewContext) { + self.store_visual_marks(cx); + self.update_editor(cx, |vim, editor, cx| { + let mut edits = Vec::new(); + let mut new_anchors = Vec::new(); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + for selection in editor.selections.all_adjusted(cx) { + if !selection.is_empty() { + if vim.mode != Mode::VisualBlock || new_anchors.is_empty() { + new_anchors.push((true, snapshot.anchor_before(selection.start))) + } } - } - for row in selection.start.row..=selection.end.row { - let start = if row == selection.start.row { - selection.start - } else { - Point::new(row, 0) - }; - - if let Some((range, num, radix)) = find_number(&snapshot, start) { - if let Ok(val) = i32::from_str_radix(&num, radix) { - let result = val + delta; - delta += step; - let replace = match radix { - 10 => format!("{}", result), - 16 => { - if num.to_ascii_lowercase() == num { - format!("{:x}", result) - } else { - format!("{:X}", result) + for row in selection.start.row..=selection.end.row { + let start = if row == selection.start.row { + selection.start + } else { + Point::new(row, 0) + }; + + if let Some((range, num, radix)) = find_number(&snapshot, start) { + if let Ok(val) = i32::from_str_radix(&num, radix) { + let result = val + delta; + delta += step; + let replace = match radix { + 10 => format!("{}", result), + 16 => { + if num.to_ascii_lowercase() == num { + format!("{:x}", result) + } else { + format!("{:X}", result) + } } - } - 2 => format!("{:b}", result), - _ => unreachable!(), - }; - edits.push((range.clone(), replace)); - } - if selection.is_empty() { - new_anchors.push((false, snapshot.anchor_after(range.end))) - } - } else { - if selection.is_empty() { - new_anchors.push((true, snapshot.anchor_after(start))) + 2 => format!("{:b}", result), + _ => unreachable!(), + }; + edits.push((range.clone(), replace)); + } + if selection.is_empty() { + new_anchors.push((false, snapshot.anchor_after(range.end))) + } + } else { + if selection.is_empty() { + new_anchors.push((true, snapshot.anchor_after(start))) + } } } } - } - editor.transact(cx, |editor, cx| { - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - let mut new_ranges = Vec::new(); - for (visual, anchor) in new_anchors.iter() { - let mut point = anchor.to_point(&snapshot); - if !*visual && point.column > 0 { - point.column -= 1; - point = snapshot.clip_point(point, Bias::Left) + editor.transact(cx, |editor, cx| { + editor.edit(edits, cx); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut new_ranges = Vec::new(); + for (visual, anchor) in new_anchors.iter() { + let mut point = anchor.to_point(&snapshot); + if !*visual && point.column > 0 { + point.column -= 1; + point = snapshot.clip_point(point, Bias::Left) + } + new_ranges.push(point..point); } - new_ranges.push(point..point); - } - s.select_ranges(new_ranges) - }) + s.select_ranges(new_ranges) + }) + }); }); - }); - vim.switch_mode(Mode::Normal, true, cx) + self.switch_mode(Mode::Normal, true, cx) + } } fn find_number( diff --git a/crates/vim/src/normal/indent.rs b/crates/vim/src/normal/indent.rs index c69b0712b3e16f1f009359941d73d25e36303dce..4b4d5e7e80bbf169dec9ee777d5b587dd0e68a5d 100644 --- a/crates/vim/src/normal/indent.rs +++ b/crates/vim/src/normal/indent.rs @@ -1,78 +1,80 @@ use crate::{motion::Motion, object::Object, Vim}; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Bias}; -use gpui::WindowContext; use language::SelectionGoal; +use ui::ViewContext; #[derive(PartialEq, Eq)] -pub(super) enum IndentDirection { +pub(crate) enum IndentDirection { In, Out, } -pub fn indent_motion( - vim: &mut Vim, - motion: Motion, - times: Option, - dir: IndentDirection, - cx: &mut WindowContext, -) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, false, &text_layout_details); +impl Vim { + pub(crate) fn indent_motion( + &mut self, + motion: Motion, + times: Option, + dir: IndentDirection, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + let mut selection_starts: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); + selection_starts.insert(selection.id, anchor); + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); }); - }); - if dir == IndentDirection::In { - editor.indent(&Default::default(), cx); - } else { - editor.outdent(&Default::default(), cx); - } - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + if dir == IndentDirection::In { + editor.indent(&Default::default(), cx); + } else { + editor.outdent(&Default::default(), cx); + } + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = selection_starts.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); -} + } -pub fn indent_object( - vim: &mut Vim, - object: Object, - around: bool, - dir: IndentDirection, - cx: &mut WindowContext, -) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around); + pub(crate) fn indent_object( + &mut self, + object: Object, + around: bool, + dir: IndentDirection, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); + original_positions.insert(selection.id, anchor); + object.expand_selection(map, selection, around); + }); }); - }); - if dir == IndentDirection::In { - editor.indent(&Default::default(), cx); - } else { - editor.outdent(&Default::default(), cx); - } - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + if dir == IndentDirection::In { + editor.indent(&Default::default(), cx); + } else { + editor.outdent(&Default::default(), cx); + } + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); + } } diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index eb9dcb2a8c1fd3705735e2ebc213975ced9eeb5d..7f9af3e06c841f7a1d63d581ad91c0d741e21e20 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -6,7 +6,7 @@ use editor::{ scroll::Autoscroll, Anchor, Bias, DisplayPoint, }; -use gpui::WindowContext; +use gpui::ViewContext; use language::SelectionGoal; use crate::{ @@ -15,56 +15,62 @@ use crate::{ Vim, }; -pub fn create_mark(vim: &mut Vim, text: Arc, tail: bool, cx: &mut WindowContext) { - let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| { - editor - .selections - .disjoint_anchors() - .iter() - .map(|s| if tail { s.tail() } else { s.head() }) - .collect::>() - }) else { - return; - }; - vim.update_state(|state| state.marks.insert(text.to_string(), anchors)); - vim.clear_operator(cx); -} - -pub fn create_visual_marks(vim: &mut Vim, mode: Mode, cx: &mut WindowContext) { - let mut starts = vec![]; - let mut ends = vec![]; - let mut reversed = vec![]; +impl Vim { + pub fn create_mark(&mut self, text: Arc, tail: bool, cx: &mut ViewContext) { + let Some(anchors) = self.update_editor(cx, |_, editor, _| { + editor + .selections + .disjoint_anchors() + .iter() + .map(|s| if tail { s.tail() } else { s.head() }) + .collect::>() + }) else { + return; + }; + self.marks.insert(text.to_string(), anchors); + self.clear_operator(cx); + } - vim.update_active_editor(cx, |_, editor, cx| { - let (map, selections) = editor.selections.all_display(cx); - for selection in selections { - let end = movement::saturating_left(&map, selection.end); - ends.push( - map.buffer_snapshot - .anchor_before(end.to_offset(&map, Bias::Left)), - ); - starts.push( - map.buffer_snapshot - .anchor_after(selection.start.to_offset(&map, Bias::Right)), - ); - reversed.push(selection.reversed) + // When handling an action, you must create visual marks if you will switch to normal + // mode without the default selection behavior. + pub(crate) fn store_visual_marks(&mut self, cx: &mut ViewContext) { + if self.mode.is_visual() { + self.create_visual_marks(self.mode, cx); } - }); + } - vim.update_state(|state| { - state.marks.insert("<".to_string(), starts); - state.marks.insert(">".to_string(), ends); - state.stored_visual_mode.replace((mode, reversed)); - }); - vim.clear_operator(cx); -} + pub(crate) fn create_visual_marks(&mut self, mode: Mode, cx: &mut ViewContext) { + let mut starts = vec![]; + let mut ends = vec![]; + let mut reversed = vec![]; -pub fn jump(text: Arc, line: bool, cx: &mut WindowContext) { - let anchors = Vim::update(cx, |vim, cx| { - vim.pop_operator(cx); + self.update_editor(cx, |_, editor, cx| { + let (map, selections) = editor.selections.all_display(cx); + for selection in selections { + let end = movement::saturating_left(&map, selection.end); + ends.push( + map.buffer_snapshot + .anchor_before(end.to_offset(&map, Bias::Left)), + ); + starts.push( + map.buffer_snapshot + .anchor_after(selection.start.to_offset(&map, Bias::Right)), + ); + reversed.push(selection.reversed) + } + }); - match &*text { - "{" | "}" => vim.update_active_editor(cx, |_, editor, cx| { + self.marks.insert("<".to_string(), starts); + self.marks.insert(">".to_string(), ends); + self.stored_visual_mode.replace((mode, reversed)); + self.clear_operator(cx); + } + + pub fn jump(&mut self, text: Arc, line: bool, cx: &mut ViewContext) { + self.pop_operator(cx); + + let anchors = match &*text { + "{" | "}" => self.update_editor(cx, |_, editor, cx| { let (map, selections) = editor.selections.all_display(cx); selections .into_iter() @@ -79,28 +85,26 @@ pub fn jump(text: Arc, line: bool, cx: &mut WindowContext) { }) .collect::>() }), - "." => vim.state().change_list.last().cloned(), - _ => vim.state().marks.get(&*text).cloned(), - } - }); + "." => self.change_list.last().cloned(), + _ => self.marks.get(&*text).cloned(), + }; - let Some(anchors) = anchors else { return }; + let Some(anchors) = anchors else { return }; - let is_active_operator = Vim::read(cx).state().active_operator().is_some(); - if is_active_operator { - if let Some(anchor) = anchors.last() { - motion::motion( - Motion::Jump { - anchor: *anchor, - line, - }, - cx, - ) - } - return; - } else { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |_, editor, cx| { + let is_active_operator = self.active_operator().is_some(); + if is_active_operator { + if let Some(anchor) = anchors.last() { + self.motion( + Motion::Jump { + anchor: *anchor, + line, + }, + cx, + ) + } + return; + } else { + self.update_editor(cx, |_, editor, cx| { let map = editor.snapshot(cx); let mut ranges: Vec> = Vec::new(); for mut anchor in anchors { @@ -120,7 +124,7 @@ pub fn jump(text: Arc, line: bool, cx: &mut WindowContext) { s.select_anchor_ranges(ranges) }) }); - }) + } } } diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 84f144a16a7f743fb683c03b1edd48c53b27dc94..d1cc5d53b1f99a970c389b0f20b103e35fa170be 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -4,17 +4,15 @@ use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayP use gpui::{impl_actions, ViewContext}; use language::{Bias, SelectionGoal}; use serde::Deserialize; -use workspace::Workspace; use crate::{ - normal::yank::copy_selections_content, state::{Mode, Register}, Vim, }; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -struct Paste { +pub struct Paste { #[serde(default)] before: bool, #[serde(default)] @@ -23,37 +21,34 @@ struct Paste { impl_actions!(vim, [Paste]); -pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(paste); -} - -fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - vim.store_visual_marks(cx); - let count = vim.take_count(cx).unwrap_or(1); +impl Vim { + pub fn paste(&mut self, action: &Paste, cx: &mut ViewContext) { + self.record_current_action(cx); + self.store_visual_marks(cx); + let count = self.take_count(cx).unwrap_or(1); - vim.update_active_editor(cx, |vim, editor, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); - let selected_register = vim.update_state(|state| state.selected_register.take()); + let selected_register = vim.selected_register.take(); let Some(Register { text, clipboard_selections, - }) = vim - .read_register(selected_register, Some(editor), cx) - .filter(|reg| !reg.text.is_empty()) + }) = Vim::update_globals(cx, |globals, cx| { + globals.read_register(selected_register, Some(editor), cx) + }) + .filter(|reg| !reg.text.is_empty()) else { return; }; let clipboard_selections = clipboard_selections - .filter(|sel| sel.len() > 1 && vim.state().mode != Mode::VisualLine); + .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine); - if !action.preserve_clipboard && vim.state().mode.is_visual() { - copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx); + if !action.preserve_clipboard && vim.mode.is_visual() { + vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, cx); } let (display_map, current_selections) = editor.selections.all_adjusted_display(cx); @@ -90,7 +85,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { .first() .map(|selection| selection.first_line_indent) }); - let before = action.before || vim.state().mode == Mode::VisualLine; + let before = action.before || vim.mode == Mode::VisualLine; let mut edits = Vec::new(); let mut new_selections = Vec::new(); @@ -121,7 +116,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { } else { to_insert = "\n".to_owned() + &to_insert; } - } else if !line_mode && vim.state().mode == Mode::VisualLine { + } else if !line_mode && vim.mode == Mode::VisualLine { to_insert = to_insert + "\n"; } @@ -145,7 +140,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { let point_range = display_range.start.to_point(&display_map) ..display_range.end.to_point(&display_map); - let anchor = if is_multiline || vim.state().mode == Mode::VisualLine { + let anchor = if is_multiline || vim.mode == Mode::VisualLine { display_map.buffer_snapshot.anchor_before(point_range.start) } else { display_map.buffer_snapshot.anchor_after(point_range.end) @@ -185,7 +180,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { cursor = movement::saturating_left(map, cursor) } cursors.push(cursor); - if vim.state().mode == Mode::VisualBlock { + if vim.mode == Mode::VisualBlock { break; } } @@ -195,8 +190,8 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { }) }); }); - vim.switch_mode(Mode::Normal, true, cx); - }); + self.switch_mode(Mode::Normal, true, cx); + } } #[cfg(test)] diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 78ae5b9ec4448fe58c8dda2ee400de2f4fb1eeb0..6e327fee032f4f09f087e1ee57e176c7c9b16b39 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -1,12 +1,12 @@ -use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc}; use crate::{ insert::NormalBefore, motion::Motion, - state::{Mode, Operator, RecordedSelection, ReplayableAction}, - visual::visual_motion, + state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals}, Vim, }; +use editor::Editor; use gpui::{actions, Action, ViewContext, WindowContext}; use util::ResultExt; use workspace::Workspace; @@ -44,30 +44,28 @@ fn repeatable_insert(action: &ReplayableAction) -> Option> { } } -pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| { - Vim::update(cx, |vim, cx| { - vim.workspace_state.dot_replaying = false; - vim.switch_mode(Mode::Normal, false, cx) - }); +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &EndRepeat, cx| { + Vim::globals(cx).dot_replaying = false; + vim.switch_mode(Mode::Normal, false, cx) }); - workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false)); - workspace.register_action(|_: &mut Workspace, _: &ToggleRecord, cx| { - Vim::update(cx, |vim, cx| { - if let Some(char) = vim.workspace_state.recording_register.take() { - vim.workspace_state.last_recorded_register = Some(char) - } else { - vim.push_operator(Operator::RecordRegister, cx); - } - }) + Vim::action(editor, cx, |vim, _: &Repeat, cx| vim.repeat(false, cx)); + + Vim::action(editor, cx, |vim, _: &ToggleRecord, cx| { + let globals = Vim::globals(cx); + if let Some(char) = globals.recording_register.take() { + globals.last_recorded_register = Some(char) + } else { + vim.push_operator(Operator::RecordRegister, cx); + } }); - workspace.register_action(|_: &mut Workspace, _: &ReplayLastRecording, cx| { - let Some(register) = Vim::read(cx).workspace_state.last_recorded_register else { + Vim::action(editor, cx, |vim, _: &ReplayLastRecording, cx| { + let Some(register) = Vim::globals(cx).last_recorded_register else { return; }; - replay_register(register, cx) + vim.replay_register(register, cx) }); } @@ -116,54 +114,60 @@ impl Replayer { lock.ix += 1; drop(lock); let Some(action) = action else { - Vim::update(cx, |vim, _| vim.workspace_state.replayer.take()); + Vim::globals(cx).replayer.take(); return; }; match action { ReplayableAction::Action(action) => { if should_replay(&*action) { cx.dispatch_action(action.boxed_clone()); - cx.defer(move |cx| observe_action(action.boxed_clone(), cx)); + cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone())); } } ReplayableAction::Insertion { text, utf16_range_to_replace, } => { - if let Some(editor) = Vim::read(cx).active_editor.clone() { - editor - .update(cx, |editor, cx| { + cx.window_handle() + .update(cx, |handle, cx| { + let Ok(workspace) = handle.downcast::() else { + return; + }; + let Some(editor) = workspace.read(cx).active_item_as::(cx) else { + return; + }; + editor.update(cx, |editor, cx| { editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) }) - .log_err(); - } + }) + .log_err(); } } cx.defer(move |cx| self.next(cx)); } } -pub(crate) fn record_register(register: char, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.workspace_state.recording_register = Some(register); - vim.workspace_state.recordings.remove(®ister); - vim.workspace_state.ignore_current_insertion = true; - vim.clear_operator(cx) - }) -} +impl Vim { + pub(crate) fn record_register(&mut self, register: char, cx: &mut ViewContext) { + let globals = Vim::globals(cx); + globals.recording_register = Some(register); + globals.recordings.remove(®ister); + globals.ignore_current_insertion = true; + self.clear_operator(cx) + } -pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - let mut count = vim.take_count(cx).unwrap_or(1); - vim.clear_operator(cx); + pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext) { + let mut count = self.take_count(cx).unwrap_or(1); + self.clear_operator(cx); + let globals = Vim::globals(cx); if register == '@' { - let Some(last) = vim.workspace_state.last_replayed_register else { + let Some(last) = globals.last_replayed_register else { return; }; register = last; } - let Some(actions) = vim.workspace_state.recordings.get(®ister) else { + let Some(actions) = globals.recordings.get(®ister) else { return; }; @@ -173,206 +177,148 @@ pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) { count -= 1 } - vim.workspace_state.last_replayed_register = Some(register); - - vim.workspace_state + globals.last_replayed_register = Some(register); + let mut replayer = globals .replayer .get_or_insert_with(|| Replayer::new()) - .replay(repeated_actions, cx); - }); -} - -pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { - let Some((mut actions, selection)) = Vim::update(cx, |vim, cx| { - let actions = vim.workspace_state.recorded_actions.clone(); - if actions.is_empty() { - return None; - } - - let count = vim.take_count(cx); + .clone(); + replayer.replay(repeated_actions, cx); + } - let selection = vim.workspace_state.recorded_selection.clone(); - match selection { - RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::Visual, false, cx) - } - RecordedSelection::VisualLine { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualLine, false, cx) + pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext) { + let count = self.take_count(cx); + let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| { + let actions = globals.recorded_actions.clone(); + if actions.is_empty() { + return None; } - RecordedSelection::VisualBlock { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualBlock, false, cx) - } - RecordedSelection::None => { - if let Some(count) = count { - vim.workspace_state.recorded_count = Some(count); + if globals.replayer.is_none() { + if let Some(recording_register) = globals.recording_register { + globals + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(Repeat.boxed_clone())); } } - } - if vim.workspace_state.replayer.is_none() { - if let Some(recording_register) = vim.workspace_state.recording_register { - vim.workspace_state - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(Repeat.boxed_clone())); + let mut mode = None; + let selection = globals.recorded_selection.clone(); + match selection { + RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { + globals.recorded_count = None; + mode = Some(Mode::Visual); + } + RecordedSelection::VisualLine { .. } => { + globals.recorded_count = None; + mode = Some(Mode::VisualLine) + } + RecordedSelection::VisualBlock { .. } => { + globals.recorded_count = None; + mode = Some(Mode::VisualBlock) + } + RecordedSelection::None => { + if let Some(count) = count { + globals.recorded_count = Some(count); + } + } } - } - Some((actions, selection)) - }) else { - return; - }; + Some((actions, selection, mode)) + }) else { + return; + }; + if let Some(mode) = mode { + self.switch_mode(mode, false, cx) + } - match selection { - RecordedSelection::SingleLine { cols } => { - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) + match selection { + RecordedSelection::SingleLine { cols } => { + if cols > 1 { + self.visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } } - } - RecordedSelection::Visual { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - visual_motion( - Motion::StartOfLine { - display_lines: false, - }, - None, - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) + RecordedSelection::Visual { rows, cols } => { + self.visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + self.visual_motion( + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ); + if cols > 1 { + self.visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } } - } - RecordedSelection::VisualBlock { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx); + RecordedSelection::VisualBlock { rows, cols } => { + self.visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + if cols > 1 { + self.visual_motion(Motion::Right, Some(cols as usize - 1), cx); + } } + RecordedSelection::VisualLine { rows } => { + self.visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + } + RecordedSelection::None => {} } - RecordedSelection::VisualLine { rows } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - } - RecordedSelection::None => {} - } - // insert internally uses repeat to handle counts - // vim doesn't treat 3a1 as though you literally repeated a1 - // 3 times, instead it inserts the content thrice at the insert position. - if let Some(to_repeat) = repeatable_insert(&actions[0]) { - if let Some(ReplayableAction::Action(action)) = actions.last() { - if NormalBefore.partial_eq(&**action) { - actions.pop(); + // insert internally uses repeat to handle counts + // vim doesn't treat 3a1 as though you literally repeated a1 + // 3 times, instead it inserts the content thrice at the insert position. + if let Some(to_repeat) = repeatable_insert(&actions[0]) { + if let Some(ReplayableAction::Action(action)) = actions.last() { + if NormalBefore.partial_eq(&**action) { + actions.pop(); + } } - } - let mut new_actions = actions.clone(); - actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); + let mut new_actions = actions.clone(); + actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); - let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); + let mut count = cx.global::().recorded_count.unwrap_or(1); - // if we came from insert mode we're just doing repetitions 2 onwards. - if from_insert_mode { - count -= 1; - new_actions[0] = actions[0].clone(); - } + // if we came from insert mode we're just doing repetitions 2 onwards. + if from_insert_mode { + count -= 1; + new_actions[0] = actions[0].clone(); + } - for _ in 1..count { - new_actions.append(actions.clone().as_mut()); + for _ in 1..count { + new_actions.append(actions.clone().as_mut()); + } + new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); + actions = new_actions; } - new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); - actions = new_actions; - } - - actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - Vim::update(cx, |vim, cx| { - vim.workspace_state.dot_replaying = true; + actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - vim.workspace_state + let globals = Vim::globals(cx); + globals.dot_replaying = true; + let mut replayer = globals .replayer .get_or_insert_with(|| Replayer::new()) - .replay(actions, cx); - }) -} - -pub(crate) fn observe_action(action: Box, cx: &mut WindowContext) { - Vim::update(cx, |vim, _| { - if vim.workspace_state.dot_recording { - vim.workspace_state - .recorded_actions - .push(ReplayableAction::Action(action.boxed_clone())); - - if vim.workspace_state.stop_recording_after_next_action { - vim.workspace_state.dot_recording = false; - vim.workspace_state.stop_recording_after_next_action = false; - } - } - if vim.workspace_state.replayer.is_none() { - if let Some(recording_register) = vim.workspace_state.recording_register { - vim.workspace_state - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(action)); - } - } - }) -} - -pub(crate) fn observe_insertion( - text: &Arc, - range_to_replace: Option>, - cx: &mut WindowContext, -) { - Vim::update(cx, |vim, _| { - if vim.workspace_state.ignore_current_insertion { - vim.workspace_state.ignore_current_insertion = false; - return; - } - if vim.workspace_state.dot_recording { - vim.workspace_state - .recorded_actions - .push(ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace.clone(), - }); - if vim.workspace_state.stop_recording_after_next_action { - vim.workspace_state.dot_recording = false; - vim.workspace_state.stop_recording_after_next_action = false; - } - } - if let Some(recording_register) = vim.workspace_state.recording_register { - vim.workspace_state - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace, - }); - } - }); + .clone(); + replayer.replay(actions, cx); + } } #[cfg(test)] diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index d754cc7bb17ce1c1e0632f7beb23596ab6b00191..6aceb06425ab2a69f79da25a09a6d9695f8a0a3d 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -7,28 +7,27 @@ use editor::{ use gpui::{actions, ViewContext}; use language::Bias; use settings::Settings; -use workspace::Workspace; actions!( vim, [LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown] ); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, _: &LineDown, cx| { - scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.))) +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &LineDown, cx| { + vim.scroll(false, cx, |c| ScrollAmount::Line(c.unwrap_or(1.))) }); - workspace.register_action(|_: &mut Workspace, _: &LineUp, cx| { - scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.))) + Vim::action(editor, cx, |vim, _: &LineUp, cx| { + vim.scroll(false, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.))) }); - workspace.register_action(|_: &mut Workspace, _: &PageDown, cx| { - scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.))) + Vim::action(editor, cx, |vim, _: &PageDown, cx| { + vim.scroll(false, cx, |c| ScrollAmount::Page(c.unwrap_or(1.))) }); - workspace.register_action(|_: &mut Workspace, _: &PageUp, cx| { - scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.))) + Vim::action(editor, cx, |vim, _: &PageUp, cx| { + vim.scroll(false, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.))) }); - workspace.register_action(|_: &mut Workspace, _: &ScrollDown, cx| { - scroll(cx, true, |c| { + Vim::action(editor, cx, |vim, _: &ScrollDown, cx| { + vim.scroll(true, cx, |c| { if let Some(c) = c { ScrollAmount::Line(c) } else { @@ -36,8 +35,8 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { } }) }); - workspace.register_action(|_: &mut Workspace, _: &ScrollUp, cx| { - scroll(cx, true, |c| { + Vim::action(editor, cx, |vim, _: &ScrollUp, cx| { + vim.scroll(true, cx, |c| { if let Some(c) = c { ScrollAmount::Line(-c) } else { @@ -47,17 +46,18 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { }); } -fn scroll( - cx: &mut ViewContext, - move_cursor: bool, - by: fn(c: Option) -> ScrollAmount, -) { - Vim::update(cx, |vim, cx| { - let amount = by(vim.take_count(cx).map(|c| c as f32)); - vim.update_active_editor(cx, |_, editor, cx| { +impl Vim { + fn scroll( + &mut self, + move_cursor: bool, + cx: &mut ViewContext, + by: fn(c: Option) -> ScrollAmount, + ) { + let amount = by(self.take_count(cx).map(|c| c as f32)); + self.update_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, &amount, cx) }); - }) + } } fn scroll_editor( diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 72872f21332eb659e13d450045c15d6e2915f842..c417372f873bebb6fd2293873b40d3ff5ff3a413 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,15 +1,15 @@ use std::{iter::Peekable, str::Chars, time::Duration}; +use editor::Editor; use gpui::{actions, impl_actions, ViewContext}; use language::Point; use search::{buffer_search, BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; -use workspace::{notifications::NotifyResultExt, searchable::Direction, Workspace}; +use workspace::{notifications::NotifyResultExt, searchable::Direction}; use crate::{ command::CommandRange, - motion::{search_motion, Motion}, - normal::move_cursor, + motion::Motion, state::{Mode, SearchState}, Vim, }; @@ -60,53 +60,43 @@ impl_actions!( [FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext] ); -pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(move_to_next); - workspace.register_action(move_to_prev); - workspace.register_action(move_to_next_match); - workspace.register_action(move_to_prev_match); - workspace.register_action(search); - workspace.register_action(search_submit); - workspace.register_action(search_deploy); - - workspace.register_action(find_command); - workspace.register_action(replace_command); +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, Vim::move_to_next); + Vim::action(editor, cx, Vim::move_to_prev); + Vim::action(editor, cx, Vim::move_to_next_match); + Vim::action(editor, cx, Vim::move_to_prev_match); + Vim::action(editor, cx, Vim::search); + Vim::action(editor, cx, Vim::search_deploy); + Vim::action(editor, cx, Vim::find_command); + Vim::action(editor, cx, Vim::replace_command); } -fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { - move_to_internal(workspace, Direction::Next, !action.partial_word, cx) -} +impl Vim { + fn move_to_next(&mut self, action: &MoveToNext, cx: &mut ViewContext) { + self.move_to_internal(Direction::Next, !action.partial_word, cx) + } -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_prev(&mut self, action: &MoveToPrev, cx: &mut ViewContext) { + self.move_to_internal(Direction::Prev, !action.partial_word, cx) + } -fn move_to_next_match( - workspace: &mut Workspace, - _: &MoveToNextMatch, - cx: &mut ViewContext, -) { - move_to_match_internal(workspace, Direction::Next, cx) -} + fn move_to_next_match(&mut self, _: &MoveToNextMatch, cx: &mut ViewContext) { + self.move_to_match_internal(Direction::Next, cx) + } -fn move_to_prev_match( - workspace: &mut Workspace, - _: &MoveToPrevMatch, - cx: &mut ViewContext, -) { - move_to_match_internal(workspace, Direction::Prev, cx) -} + fn move_to_prev_match(&mut self, _: &MoveToPrevMatch, cx: &mut ViewContext) { + self.move_to_match_internal(Direction::Prev, cx) + } -fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { - let pane = workspace.active_pane().clone(); - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - Vim::update(cx, |vim, cx| { - let count = vim.take_count(cx).unwrap_or(1); - let prior_selections = vim.editor_selections(cx); + fn search(&mut self, action: &Search, cx: &mut ViewContext) { + let Some(pane) = self.pane(cx) else { return }; + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + let count = self.take_count(cx).unwrap_or(1); + let prior_selections = self.editor_selections(cx); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { @@ -122,241 +112,229 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext find. -fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewContext) { - Vim::update(cx, |vim, _| { - vim.update_state(|state| state.search = Default::default()) - }); - cx.propagate(); -} - -fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext) { - let mut motion = None; - Vim::update(cx, |vim, cx| { - vim.store_visual_marks(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 prior_selections, prior_mode, prior_operator) = - vim.update_state(|state| { - let mut count = state.search.count; - let direction = state.search.direction; - // in the case that the query has changed, the search bar - // will have selected the next match already. - if (search_bar.query(cx) != state.search.initial_query) - && state.search.direction == Direction::Next - { - count = count.saturating_sub(1) - } - state.search.count = 1; - search_bar.select_match(direction, count, cx); - search_bar.focus_editor(&Default::default(), cx); - - let prior_selections: Vec<_> = - state.search.prior_selections.drain(..).collect(); - let prior_mode = state.search.prior_mode; - let prior_operator = state.search.prior_operator.take(); - (prior_selections, prior_mode, prior_operator) - }); + } - vim.workspace_state - .registers - .insert('/', search_bar.query(cx).into()); + // hook into the existing to clear out any vim search state on cmd+f or edit -> find. + fn search_deploy(&mut self, _: &buffer_search::Deploy, cx: &mut ViewContext) { + self.search = Default::default(); + cx.propagate(); + } - let new_selections = vim.editor_selections(cx); + pub fn search_submit(&mut self, cx: &mut ViewContext) { + self.store_visual_marks(cx); + let Some(pane) = self.pane(cx) else { return }; + let result = pane.update(cx, |pane, cx| { + let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { + return None; + }; + search_bar.update(cx, |search_bar, cx| { + let mut count = self.search.count; + let direction = self.search.direction; + // in the case that the query has changed, the search bar + // will have selected the next match already. + if (search_bar.query(cx) != self.search.initial_query) + && self.search.direction == Direction::Next + { + count = count.saturating_sub(1) + } + self.search.count = 1; + search_bar.select_match(direction, count, cx); + search_bar.focus_editor(&Default::default(), cx); - // If the active editor has changed during a search, don't panic. - if prior_selections.iter().any(|s| { - vim.update_active_editor(cx, |_vim, editor, cx| { - !s.start.is_valid(&editor.snapshot(cx).buffer_snapshot) - }) - .unwrap_or(true) - }) { - prior_selections.clear(); - } + let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect(); + let prior_mode = self.search.prior_mode; + let prior_operator = self.search.prior_operator.take(); - if prior_mode != vim.state().mode { - vim.switch_mode(prior_mode, true, cx); - } - if let Some(operator) = prior_operator { - vim.push_operator(operator, cx); - }; - motion = Some(Motion::ZedSearchResult { - prior_selections, - new_selections, - }); - }); - } + let query = search_bar.query(cx).into(); + Vim::globals(cx).registers.insert('/', query); + Some((prior_selections, prior_mode, prior_operator)) + }) }); - }); - if let Some(motion) = motion { - search_motion(motion, cx) - } -} + let Some((mut prior_selections, prior_mode, prior_operator)) = result else { + return; + }; -pub fn move_to_match_internal( - workspace: &mut Workspace, - direction: Direction, - cx: &mut ViewContext, -) { - let mut motion = None; - Vim::update(cx, |vim, cx| { - let pane = workspace.active_pane().clone(); - let count = vim.take_count(cx).unwrap_or(1); - let prior_selections = vim.editor_selections(cx); + let new_selections = self.editor_selections(cx); - 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.has_active_match() || !search_bar.show(cx) { - return; - } - search_bar.select_match(direction, count, cx); + // If the active editor has changed during a search, don't panic. + if prior_selections.iter().any(|s| { + self.update_editor(cx, |_, editor, cx| { + !s.start.is_valid(&editor.snapshot(cx).buffer_snapshot) + }) + .unwrap_or(true) + }) { + prior_selections.clear(); + } - let new_selections = vim.editor_selections(cx); - motion = Some(Motion::ZedSearchResult { - prior_selections, - new_selections, - }); - }) - } - }) - }); - if let Some(motion) = motion { - search_motion(motion, cx); + if prior_mode != self.mode { + self.switch_mode(prior_mode, true, cx); + } + if let Some(operator) = prior_operator { + self.push_operator(operator, cx); + }; + self.search_motion( + Motion::ZedSearchResult { + prior_selections, + new_selections, + }, + cx, + ); } -} -pub 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(); - let count = vim.take_count(cx).unwrap_or(1); - let prior_selections = vim.editor_selections(cx); + pub fn move_to_match_internal(&mut self, direction: Direction, cx: &mut ViewContext) { + let Some(pane) = self.pane(cx) else { return }; + let count = self.take_count(cx).unwrap_or(1); + let prior_selections = self.editor_selections(cx); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - let search = search_bar.update(cx, |search_bar, cx| { - let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; - if !search_bar.show(cx) { - return None; - } - let Some(query) = search_bar.query_suggestion(cx) else { - vim.clear_operator(cx); - drop(search_bar.search("", None, cx)); - return None; - }; - let mut query = regex::escape(&query); - if whole_word { - query = format!(r"\<{}\>", query); - } - Some(search_bar.search(&query, Some(options), cx)) - }); - - 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, count, cx); - - let new_selections = - Vim::update(cx, |vim, cx| vim.editor_selections(cx)); - search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - cx, - ) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let success = pane.update(cx, |pane, cx| { + let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { + return false; + }; + search_bar.update(cx, |search_bar, cx| { + if !search_bar.has_active_match() || !search_bar.show(cx) { + return false; } - } + search_bar.select_match(direction, count, cx); + true + }) }); - - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) + if !success { + return; } - }); -} -fn find_command(workspace: &mut Workspace, action: &FindCommand, 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::() { + let new_selections = self.editor_selections(cx); + self.search_motion( + Motion::ZedSearchResult { + prior_selections, + new_selections, + }, + cx, + ); + } + + pub fn move_to_internal( + &mut self, + direction: Direction, + whole_word: bool, + cx: &mut ViewContext, + ) { + let Some(pane) = self.pane(cx) else { return }; + let count = self.take_count(cx).unwrap_or(1); + let prior_selections = self.editor_selections(cx); + let vim = cx.view().clone(); + + let searched = pane.update(cx, |pane, cx| { + let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { + return false; + }; let search = search_bar.update(cx, |search_bar, cx| { + let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; if !search_bar.show(cx) { return None; } - let mut query = action.query.clone(); - if query == "" { - query = search_bar.query(cx); + let Some(query) = search_bar.query_suggestion(cx) else { + drop(search_bar.search("", None, cx)); + return None; }; - - Some(search_bar.search( - &query, - Some(SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX), - cx, - )) + let mut query = regex::escape(&query); + if whole_word { + query = format!(r"\<{}\>", query); + } + Some(search_bar.search(&query, Some(options), cx)) }); - let Some(search) = search else { return }; + + let Some(search) = search else { return false }; + let search_bar = search_bar.downgrade(); - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; cx.spawn(|_, mut cx| async move { search.await?; search_bar.update(&mut cx, |search_bar, cx| { - search_bar.select_match(direction, 1, cx) + search_bar.select_match(direction, count, cx); + + vim.update(cx, |vim, cx| { + let new_selections = vim.editor_selections(cx); + vim.search_motion( + Motion::ZedSearchResult { + prior_selections, + new_selections, + }, + cx, + ) + }); })?; anyhow::Ok(()) }) .detach_and_log_err(cx); + true + }); + if !searched { + self.clear_operator(cx) } - }) -} -fn replace_command( - workspace: &mut Workspace, - action: &ReplaceCommand, - cx: &mut ViewContext, -) { - let replacement = action.replacement.clone(); - let pane = workspace.active_pane().clone(); - let editor = Vim::read(cx) - .active_editor - .as_ref() - .and_then(|editor| editor.upgrade()); - if let Some(range) = &action.range { - if let Some(result) = Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |vim, editor, cx| { + if self.mode.is_visual() { + self.switch_mode(Mode::Normal, false, cx) + } + } + + fn find_command(&mut self, action: &FindCommand, cx: &mut ViewContext) { + let Some(pane) = self.pane(cx) else { return }; + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return None; + } + let mut query = action.query.clone(); + if query == "" { + query = search_bar.query(cx); + }; + + Some(search_bar.search( + &query, + Some(SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX), + cx, + )) + }); + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(direction, 1, cx) + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }) + } + + fn replace_command(&mut self, action: &ReplaceCommand, cx: &mut ViewContext) { + let replacement = action.replacement.clone(); + let Some(((pane, workspace), editor)) = + self.pane(cx).zip(self.workspace(cx)).zip(self.editor()) + else { + return; + }; + if let Some(range) = &action.range { + if let Some(result) = self.update_editor(cx, |vim, editor, cx| { let range = range.buffer_range(vim, editor, cx)?; let snapshot = &editor.snapshot(cx).buffer_snapshot; let end_point = Point::new(range.end.0, snapshot.line_len(range.end)); @@ -364,42 +342,43 @@ fn replace_command( ..snapshot.anchor_after(end_point); editor.set_search_within_ranges(&[range], cx); anyhow::Ok(()) - }) - }) { - result.notify_err(workspace, cx); - } - } - pane.update(cx, |pane, cx| { - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return; - }; - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(cx) { - return None; - } - - let mut options = SearchOptions::REGEX; - if replacement.is_case_sensitive { - options.set(SearchOptions::CASE_SENSITIVE, true) + }) { + workspace.update(cx, |workspace, cx| { + result.notify_err(workspace, cx); + }) } - let search = if replacement.search == "" { - search_bar.query(cx) - } else { - replacement.search + } + let vim = cx.view().clone(); + pane.update(cx, |pane, cx| { + let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { + return; }; + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return None; + } - search_bar.set_replacement(Some(&replacement.replacement), cx); - Some(search_bar.search(&search, Some(options), cx)) - }); - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - cx.spawn(|_, mut cx| async move { - search.await?; - search_bar.update(&mut cx, |search_bar, cx| { - if replacement.should_replace_all { - search_bar.select_last_match(cx); - search_bar.replace_all(&Default::default(), cx); - if let Some(editor) = editor { + let mut options = SearchOptions::REGEX; + if replacement.is_case_sensitive { + options.set(SearchOptions::CASE_SENSITIVE, true) + } + let search = if replacement.search == "" { + search_bar.query(cx) + } else { + replacement.search + }; + + search_bar.set_replacement(Some(&replacement.replacement), cx); + Some(search_bar.search(&search, Some(options), cx)) + }); + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + if replacement.should_replace_all { + search_bar.select_last_match(cx); + search_bar.replace_all(&Default::default(), cx); cx.spawn(|_, mut cx| async move { cx.background_executor() .timer(Duration::from_millis(200)) @@ -409,23 +388,22 @@ fn replace_command( .ok(); }) .detach(); + vim.update(cx, |vim, cx| { + vim.move_cursor( + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ) + }); } - Vim::update(cx, |vim, cx| { - move_cursor( - vim, - Motion::StartOfLine { - display_lines: false, - }, - None, - cx, - ) - }) - } - })?; - anyhow::Ok(()) + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); }) - .detach_and_log_err(cx); - }) + } } impl Replacement { @@ -697,7 +675,7 @@ mod test { #[gpui::test] async fn test_non_vim_search(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, false).await; - cx.set_state("ˇone one one one", Mode::Normal); + cx.cx.set_state("ˇone one one one"); cx.simulate_keystrokes("cmd-f"); cx.run_until_parked(); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 39576819d6c5c6bc3c82f884c093f97b72076f4f..dc27e2b2190d0ac50ecbf06dbe8ac1e0940c3d59 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,85 +1,87 @@ -use editor::movement; -use gpui::{actions, ViewContext, WindowContext}; +use editor::{movement, Editor}; +use gpui::{actions, ViewContext}; use language::Point; -use workspace::Workspace; -use crate::{motion::Motion, normal::yank::copy_selections_content, Mode, Vim}; +use crate::{motion::Motion, Mode, Vim}; actions!(vim, [Substitute, SubstituteLine]); -pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, _: &Substitute, cx| { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - let count = vim.take_count(cx); - substitute(vim, count, vim.state().mode == Mode::VisualLine, cx); - }) +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &Substitute, cx| { + vim.start_recording(cx); + let count = vim.take_count(cx); + vim.substitute(count, vim.mode == Mode::VisualLine, cx); }); - workspace.register_action(|_: &mut Workspace, _: &SubstituteLine, cx| { - Vim::update(cx, |vim, cx| { - vim.start_recording(cx); - if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { - vim.switch_mode(Mode::VisualLine, false, cx) - } - let count = vim.take_count(cx); - substitute(vim, count, true, cx) - }) + Vim::action(editor, cx, |vim, _: &SubstituteLine, cx| { + vim.start_recording(cx); + if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) { + vim.switch_mode(Mode::VisualLine, false, cx) + } + let count = vim.take_count(cx); + vim.substitute(count, true, cx) }); } -pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut WindowContext) { - vim.store_visual_marks(cx); - vim.update_active_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(cx, |editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - Motion::Right.expand_selection( - map, - selection, - count, - true, - &text_layout_details, - ); - } - if line_mode { - // in Visual mode when the selection contains the newline at the end - // of the line, we should exclude it. - if !selection.is_empty() && selection.end.column() == 0 { - selection.end = movement::left(map, selection.end); +impl Vim { + pub fn substitute( + &mut self, + count: Option, + line_mode: bool, + cx: &mut ViewContext, + ) { + self.store_visual_marks(cx); + self.update_editor(cx, |vim, editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.transact(cx, |editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if selection.start == selection.end { + Motion::Right.expand_selection( + map, + selection, + count, + true, + &text_layout_details, + ); } - Motion::CurrentLine.expand_selection( - map, - selection, - None, - false, - &text_layout_details, - ); - if let Some((point, _)) = (Motion::FirstNonWhitespace { - display_lines: false, - }) - .move_point( - map, - selection.start, - selection.goal, - None, - &text_layout_details, - ) { - selection.start = point; + if line_mode { + // in Visual mode when the selection contains the newline at the end + // of the line, we should exclude it. + if !selection.is_empty() && selection.end.column() == 0 { + selection.end = movement::left(map, selection.end); + } + Motion::CurrentLine.expand_selection( + map, + selection, + None, + false, + &text_layout_details, + ); + if let Some((point, _)) = (Motion::FirstNonWhitespace { + display_lines: false, + }) + .move_point( + map, + selection.start, + selection.goal, + None, + &text_layout_details, + ) { + selection.start = point; + } } - } - }) + }) + }); + vim.copy_selections_content(editor, line_mode, cx); + let selections = editor.selections.all::(cx).into_iter(); + let edits = selections.map(|selection| (selection.start..selection.end, "")); + editor.edit(edits, cx); }); - copy_selections_content(vim, editor, line_mode, cx); - let selections = editor.selections.all::(cx).into_iter(); - let edits = selections.map(|selection| (selection.start..selection.end, "")); - editor.edit(edits, cx); }); - }); - vim.switch_mode(Mode::Insert, true, cx); + self.switch_mode(Mode::Insert, true, cx); + } } #[cfg(test)] diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index cbd00e757e2ec9564c0fd57f433fb4c49fd2f9c0..a8a675a7e7b25b276691d3a5c8d92ae1ff8ba3ac 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -1,57 +1,64 @@ use crate::{motion::Motion, object::Object, Vim}; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Bias}; -use gpui::WindowContext; use language::SelectionGoal; +use ui::ViewContext; -pub fn toggle_comments_motion( - vim: &mut Vim, - motion: Motion, - times: Option, - cx: &mut WindowContext, -) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, false, &text_layout_details); +impl Vim { + pub fn toggle_comments_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + let mut selection_starts: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); + selection_starts.insert(selection.id, anchor); + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); }); - }); - editor.toggle_comments(&Default::default(), cx); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + editor.toggle_comments(&Default::default(), cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = selection_starts.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); -} + } -pub fn toggle_comments_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around); + pub fn toggle_comments_object( + &mut self, + object: Object, + around: bool, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); + original_positions.insert(selection.id, anchor); + object.expand_selection(map, selection, around); + }); }); - }); - editor.toggle_comments(&Default::default(), cx); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + editor.toggle_comments(&Default::default(), cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let anchor = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); + }); }); }); }); - }); + } } diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 922bb7c7f2631bf1148d81e7394434700f5e7313..9694fb51357c8723d9fd17857dad4dff38d58070 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -8,181 +8,187 @@ use crate::{ }; use collections::HashMap; use editor::{ClipboardSelection, Editor}; -use gpui::WindowContext; +use gpui::ViewContext; use language::Point; use multi_buffer::MultiBufferRow; -use ui::ViewContext; - -pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { - vim.update_active_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(cx); - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let original_position = (selection.head(), selection.goal); - original_positions.insert(selection.id, original_position); - motion.expand_selection(map, selection, times, true, &text_layout_details); + +struct HighlightOnYank; + +impl Vim { + pub fn yank_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.update_editor(cx, |vim, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let original_position = (selection.head(), selection.goal); + original_positions.insert(selection.id, original_position); + motion.expand_selection(map, selection, times, true, &text_layout_details); + }); }); - }); - yank_selections_content(vim, editor, motion.linewise(), cx); - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); + vim.yank_selections_content(editor, motion.linewise(), cx); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + let (head, goal) = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(head, goal); + }); }); }); }); - }); -} + } -pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { - vim.update_active_editor(cx, |vim, editor, cx| { - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - let original_position = (selection.head(), selection.goal); - object.expand_selection(map, selection, around); - original_positions.insert(selection.id, original_position); + pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext) { + self.update_editor(cx, |vim, editor, cx| { + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let mut original_positions: HashMap<_, _> = Default::default(); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + let original_position = (selection.head(), selection.goal); + object.expand_selection(map, selection, around); + original_positions.insert(selection.id, original_position); + }); }); - }); - yank_selections_content(vim, editor, false, cx); - editor.change_selections(None, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); + vim.yank_selections_content(editor, false, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + let (head, goal) = original_positions.remove(&selection.id).unwrap(); + selection.collapse_to(head, goal); + }); }); }); }); - }); -} - -pub fn yank_selections_content( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - cx: &mut ViewContext, -) { - copy_selections_content_internal(vim, editor, linewise, true, cx); -} + } -pub fn copy_selections_content( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - cx: &mut ViewContext, -) { - copy_selections_content_internal(vim, editor, linewise, false, cx); -} + pub fn yank_selections_content( + &mut self, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, + ) { + self.copy_selections_content_internal(editor, linewise, true, cx); + } -struct HighlightOnYank; + pub fn copy_selections_content( + &mut self, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, + ) { + self.copy_selections_content_internal(editor, linewise, false, cx); + } -fn copy_selections_content_internal( - vim: &mut Vim, - editor: &mut Editor, - linewise: bool, - is_yank: bool, - cx: &mut ViewContext, -) { - let selections = editor.selections.all_adjusted(cx); - let buffer = editor.buffer().read(cx).snapshot(cx); - let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - let mut ranges_to_highlight = Vec::new(); - - vim.update_state(|state| { - state.marks.insert( + fn copy_selections_content_internal( + &mut self, + editor: &mut Editor, + linewise: bool, + is_yank: bool, + cx: &mut ViewContext, + ) { + let selections = editor.selections.all_adjusted(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + let mut ranges_to_highlight = Vec::new(); + + self.marks.insert( "[".to_string(), selections .iter() .map(|s| buffer.anchor_before(s.start)) .collect(), ); - state.marks.insert( + self.marks.insert( "]".to_string(), selections .iter() .map(|s| buffer.anchor_after(s.end)) .collect(), - ) - }); - - { - let mut is_first = true; - for selection in selections.iter() { - let mut start = selection.start; - let end = selection.end; - if is_first { - is_first = false; - } else { - text.push_str("\n"); - } - let initial_len = text.len(); - - // if the file does not end with \n, and our line-mode selection ends on - // that line, we will have expanded the start of the selection to ensure it - // contains a newline (so that delete works as expected). We undo that change - // here. - let is_last_line = linewise - && end.row == buffer.max_buffer_row().0 - && buffer.max_point().column > 0 - && start.row < buffer.max_buffer_row().0 - && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row))); - - if is_last_line { - start = Point::new(start.row + 1, 0); - } - - let start_anchor = buffer.anchor_after(start); - let end_anchor = buffer.anchor_before(end); - ranges_to_highlight.push(start_anchor..end_anchor); + ); - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - } - if is_last_line { - text.push_str("\n"); + { + let mut is_first = true; + for selection in selections.iter() { + let mut start = selection.start; + let end = selection.end; + if is_first { + is_first = false; + } else { + text.push_str("\n"); + } + let initial_len = text.len(); + + // if the file does not end with \n, and our line-mode selection ends on + // that line, we will have expanded the start of the selection to ensure it + // contains a newline (so that delete works as expected). We undo that change + // here. + let is_last_line = linewise + && end.row == buffer.max_buffer_row().0 + && buffer.max_point().column > 0 + && start.row < buffer.max_buffer_row().0 + && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row))); + + if is_last_line { + start = Point::new(start.row + 1, 0); + } + + let start_anchor = buffer.anchor_after(start); + let end_anchor = buffer.anchor_before(end); + ranges_to_highlight.push(start_anchor..end_anchor); + + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + } + if is_last_line { + text.push_str("\n"); + } + clipboard_selections.push(ClipboardSelection { + len: text.len() - initial_len, + is_entire_line: linewise, + first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, + }); } - clipboard_selections.push(ClipboardSelection { - len: text.len() - initial_len, - is_entire_line: linewise, - first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, - }); } - } - let selected_register = vim.update_state(|state| state.selected_register.take()); - vim.write_registers( - Register { - text: text.into(), - clipboard_selections: Some(clipboard_selections), - }, - selected_register, - is_yank, - linewise, - cx, - ); - - if !is_yank || vim.state().mode == Mode::Visual { - return; - } + let selected_register = self.selected_register.take(); + Vim::update_globals(cx, |globals, cx| { + globals.write_registers( + Register { + text: text.into(), + clipboard_selections: Some(clipboard_selections), + }, + selected_register, + is_yank, + linewise, + cx, + ) + }); - editor.highlight_background::( - &ranges_to_highlight, - |colors| colors.editor_document_highlight_read_background, - cx, - ); - cx.spawn(|this, mut cx| async move { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - this.update(&mut cx, |editor, cx| { - editor.clear_background_highlights::(cx) + if !is_yank || self.mode == Mode::Visual { + return; + } + + editor.highlight_background::( + &ranges_to_highlight, + |colors| colors.editor_document_highlight_read_background, + cx, + ); + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + this.update(&mut cx, |editor, cx| { + editor.clear_background_highlights::(cx) + }) + .ok(); }) - .ok(); - }) - .detach(); + .detach(); + } } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index fc011b0b913b1ab27d07a6331d4049a821a7dfe8..8d7ee051a3dbed99efc4bb4aa19abd6fab37965b 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -2,24 +2,21 @@ use std::ops::Range; use crate::{ motion::{coerce_punctuation, right}, - normal::normal_object, state::Mode, - visual::visual_object, Vim, }; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, - Bias, DisplayPoint, + Bias, DisplayPoint, Editor, }; use itertools::Itertools; -use gpui::{actions, impl_actions, ViewContext, WindowContext}; +use gpui::{actions, impl_actions, ViewContext}; use language::{char_kind, BufferSnapshot, CharKind, Point, Selection}; use multi_buffer::MultiBufferRow; use serde::Deserialize; -use workspace::Workspace; #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] pub enum Object { @@ -65,48 +62,58 @@ actions!( ] ); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action( - |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| { - object(Object::Word { ignore_punctuation }, cx) +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action( + editor, + cx, + |vim, &Word { ignore_punctuation }: &Word, cx| { + vim.object(Object::Word { ignore_punctuation }, cx) }, ); - workspace.register_action(|_: &mut Workspace, _: &Tag, cx: _| object(Object::Tag, cx)); - workspace - .register_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx)); - workspace - .register_action(|_: &mut Workspace, _: &Paragraph, cx: _| object(Object::Paragraph, cx)); - workspace.register_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx)); - workspace - .register_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx)); - workspace.register_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| { - object(Object::DoubleQuotes, cx) + Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx)); + Vim::action(editor, cx, |vim, _: &Sentence, cx| { + vim.object(Object::Sentence, cx) }); - workspace.register_action(|_: &mut Workspace, _: &Parentheses, cx: _| { - object(Object::Parentheses, cx) + Vim::action(editor, cx, |vim, _: &Paragraph, cx| { + vim.object(Object::Paragraph, cx) }); - workspace.register_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| { - object(Object::SquareBrackets, cx) + Vim::action(editor, cx, |vim, _: &Quotes, cx| { + vim.object(Object::Quotes, cx) }); - workspace.register_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| { - object(Object::CurlyBrackets, cx) + Vim::action(editor, cx, |vim, _: &BackQuotes, cx| { + vim.object(Object::BackQuotes, cx) }); - workspace.register_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| { - object(Object::AngleBrackets, cx) + Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| { + vim.object(Object::DoubleQuotes, cx) }); - workspace.register_action(|_: &mut Workspace, _: &VerticalBars, cx: _| { - object(Object::VerticalBars, cx) + Vim::action(editor, cx, |vim, _: &Parentheses, cx| { + vim.object(Object::Parentheses, cx) + }); + Vim::action(editor, cx, |vim, _: &SquareBrackets, cx| { + vim.object(Object::SquareBrackets, cx) + }); + Vim::action(editor, cx, |vim, _: &CurlyBrackets, cx| { + vim.object(Object::CurlyBrackets, cx) + }); + Vim::action(editor, cx, |vim, _: &AngleBrackets, cx| { + vim.object(Object::AngleBrackets, cx) + }); + Vim::action(editor, cx, |vim, _: &VerticalBars, cx| { + vim.object(Object::VerticalBars, cx) + }); + Vim::action(editor, cx, |vim, _: &Argument, cx| { + vim.object(Object::Argument, cx) }); - workspace - .register_action(|_: &mut Workspace, _: &Argument, cx: _| object(Object::Argument, cx)); } -fn object(object: Object, cx: &mut WindowContext) { - match Vim::read(cx).state().mode { - Mode::Normal => normal_object(object, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), - Mode::Insert | Mode::Replace => { - // Shouldn't execute a text object in insert mode. Ignoring +impl Vim { + fn object(&mut self, object: Object, cx: &mut ViewContext) { + match self.mode { + Mode::Normal => self.normal_object(object, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx), + Mode::Insert | Mode::Replace => { + // Shouldn't execute a text object in insert mode. Ignoring + } } } } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 3b00a3fafe441a19e627f9a07610194dbb4600ef..6256c7e434f53a4164c3f71d662086cbd092d7f4 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -3,38 +3,33 @@ use crate::{ state::Mode, Vim, }; -use editor::{display_map::ToDisplayPoint, Bias, ToPoint}; -use gpui::{actions, ViewContext, WindowContext}; +use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint}; +use gpui::{actions, ViewContext}; use language::{AutoindentMode, Point}; use std::ops::Range; use std::sync::Arc; -use workspace::Workspace; actions!(vim, [ToggleReplace, UndoReplace]); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext| { - Vim::update(cx, |vim, cx| { - vim.update_state(|state| state.replacements = vec![]); - vim.start_recording(cx); - vim.switch_mode(Mode::Replace, false, cx); - }); +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &ToggleReplace, cx| { + vim.replacements = vec![]; + vim.start_recording(cx); + vim.switch_mode(Mode::Replace, false, cx); }); - workspace.register_action(|_, _: &UndoReplace, cx: &mut ViewContext| { - Vim::update(cx, |vim, cx| { - if vim.state().mode != Mode::Replace { - return; - } - let count = vim.take_count(cx); - undo_replace(vim, count, cx) - }); + Vim::action(editor, cx, |vim, _: &UndoReplace, cx| { + if vim.mode != Mode::Replace { + return; + } + let count = vim.take_count(cx); + vim.undo_replace(count, cx) }); } -pub(crate) fn multi_replace(text: Arc, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |vim, editor, cx| { +impl Vim { + pub(crate) fn multi_replace(&mut self, text: Arc, cx: &mut ViewContext) { + self.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let map = editor.snapshot(cx); @@ -58,11 +53,7 @@ pub(crate) fn multi_replace(text: Arc, cx: &mut WindowContext) { .buffer_snapshot .text_for_range(replace_range.clone()) .collect(); - vim.update_state(|state| { - state - .replacements - .push((replace_range.clone(), current_text)) - }); + vim.replacements.push((replace_range.clone(), current_text)); (replace_range, text.clone()) }) .collect::>(); @@ -83,58 +74,56 @@ pub(crate) fn multi_replace(text: Arc, cx: &mut WindowContext) { editor.set_clip_at_line_ends(true, cx); }); }); - }); -} + } -fn undo_replace(vim: &mut Vim, maybe_times: Option, cx: &mut WindowContext) { - vim.update_active_editor(cx, |vim, editor, cx| { - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let map = editor.snapshot(cx); - let selections = editor.selections.all::(cx); - let mut new_selections = vec![]; - let edits: Vec<(Range, String)> = selections - .into_iter() - .filter_map(|selection| { - let end = selection.head(); - let start = motion::backspace( - &map, - end.to_display_point(&map), - maybe_times.unwrap_or(1), - ) - .to_point(&map); - new_selections.push( - map.buffer_snapshot.anchor_before(start) - ..map.buffer_snapshot.anchor_before(start), - ); + fn undo_replace(&mut self, maybe_times: Option, cx: &mut ViewContext) { + self.update_editor(cx, |vim, editor, cx| { + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let map = editor.snapshot(cx); + let selections = editor.selections.all::(cx); + let mut new_selections = vec![]; + let edits: Vec<(Range, String)> = selections + .into_iter() + .filter_map(|selection| { + let end = selection.head(); + let start = motion::backspace( + &map, + end.to_display_point(&map), + maybe_times.unwrap_or(1), + ) + .to_point(&map); + new_selections.push( + map.buffer_snapshot.anchor_before(start) + ..map.buffer_snapshot.anchor_before(start), + ); - let mut undo = None; - let edit_range = start..end; - for (i, (range, inverse)) in vim.state().replacements.iter().rev().enumerate() { - if range.start.to_point(&map.buffer_snapshot) <= edit_range.start - && range.end.to_point(&map.buffer_snapshot) >= edit_range.end - { - undo = Some(inverse.clone()); - vim.update_state(|state| { - state.replacements.remove(state.replacements.len() - i - 1); - }); - break; + let mut undo = None; + let edit_range = start..end; + for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() { + if range.start.to_point(&map.buffer_snapshot) <= edit_range.start + && range.end.to_point(&map.buffer_snapshot) >= edit_range.end + { + undo = Some(inverse.clone()); + vim.replacements.remove(vim.replacements.len() - i - 1); + break; + } } - } - Some((edit_range, undo?)) - }) - .collect::>(); + Some((edit_range, undo?)) + }) + .collect::>(); - editor.buffer().update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); + editor.buffer().update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); - editor.change_selections(None, cx, |s| { - s.select_ranges(new_selections); + editor.change_selections(None, cx, |s| { + s.select_ranges(new_selections); + }); + editor.set_clip_at_line_ends(true, cx); }); - editor.set_clip_at_line_ends(true, cx); }); - }); + } } #[cfg(test)] diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6223aae942aa217c0d22c904ee695a41e6a5c378..db8cffa2bbc274313d47d3b63b710faf5e43e045 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,14 +1,19 @@ +use std::borrow::BorrowMut; use std::{fmt::Display, ops::Range, sync::Arc}; +use crate::command::command_interceptor; use crate::normal::repeat::Replayer; use crate::surrounds::SurroundsType; use crate::{motion::Motion, object::Object}; +use crate::{UseSystemClipboard, Vim, VimSettings}; use collections::HashMap; -use editor::{Anchor, ClipboardSelection}; -use gpui::{Action, ClipboardEntry, ClipboardItem, KeyContext}; -use language::{CursorShape, Selection, TransactionId}; +use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; +use editor::{Anchor, ClipboardSelection, Editor}; +use gpui::{Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global}; +use language::Point; use serde::{Deserialize, Serialize}; -use ui::SharedString; +use settings::{Settings, SettingsStore}; +use ui::{SharedString, ViewContext}; use workspace::searchable::Direction; #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -75,32 +80,6 @@ pub enum Operator { ToggleComments, } -#[derive(Default, Clone)] -pub struct EditorState { - pub mode: Mode, - pub last_mode: Mode, - - /// pre_count is the number before an operator is specified (3 in 3d2d) - pub pre_count: Option, - /// post_count is the number after an operator is specified (2 in 3d2d) - pub post_count: Option, - - pub operator_stack: Vec, - pub replacements: Vec<(Range, String)>, - - pub marks: HashMap>, - pub stored_visual_mode: Option<(Mode, Vec)>, - pub change_list: Vec>, - pub change_list_position: Option, - - pub current_tx: Option, - pub current_anchor: Option>, - pub undo_modes: HashMap, - - pub selected_register: Option, - pub search: SearchState, -} - #[derive(Default, Clone, Debug)] pub enum RecordedSelection { #[default] @@ -161,7 +140,7 @@ impl From for Register { } #[derive(Default, Clone)] -pub struct WorkspaceState { +pub struct VimGlobals { pub last_find: Option, pub dot_recording: bool, @@ -182,6 +161,232 @@ pub struct WorkspaceState { pub registers: HashMap, pub recordings: HashMap>, } +impl Global for VimGlobals {} + +impl VimGlobals { + pub(crate) fn register(cx: &mut AppContext) { + cx.set_global(VimGlobals::default()); + + cx.observe_keystrokes(|event, cx| { + let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else { + return; + }; + Vim::globals(cx).observe_action(action.boxed_clone()) + }) + .detach(); + + cx.observe_global::(move |cx| { + if Vim::enabled(cx) { + CommandPaletteFilter::update_global(cx, |filter, _| { + filter.show_namespace(Vim::NAMESPACE); + }); + CommandPaletteInterceptor::update_global(cx, |interceptor, _| { + interceptor.set(Box::new(command_interceptor)); + }); + } else { + *Vim::globals(cx) = VimGlobals::default(); + CommandPaletteInterceptor::update_global(cx, |interceptor, _| { + interceptor.clear(); + }); + CommandPaletteFilter::update_global(cx, |filter, _| { + filter.hide_namespace(Vim::NAMESPACE); + }); + } + }) + .detach(); + } + + pub(crate) fn write_registers( + &mut self, + content: Register, + register: Option, + is_yank: bool, + linewise: bool, + cx: &mut ViewContext, + ) { + if let Some(register) = register { + let lower = register.to_lowercase().next().unwrap_or(register); + if lower != register { + let current = self.registers.entry(lower).or_default(); + current.text = (current.text.to_string() + &content.text).into(); + // not clear how to support appending to registers with multiple cursors + current.clipboard_selections.take(); + let yanked = current.clone(); + self.registers.insert('"', yanked); + } else { + self.registers.insert('"', content.clone()); + match lower { + '_' | ':' | '.' | '%' | '#' | '=' | '/' => {} + '+' => { + cx.write_to_clipboard(content.into()); + } + '*' => { + #[cfg(target_os = "linux")] + cx.write_to_primary(content.into()); + #[cfg(not(target_os = "linux"))] + cx.write_to_clipboard(content.into()); + } + '"' => { + self.registers.insert('0', content.clone()); + self.registers.insert('"', content); + } + _ => { + self.registers.insert(lower, content); + } + } + } + } else { + let setting = VimSettings::get_global(cx).use_system_clipboard; + if setting == UseSystemClipboard::Always + || setting == UseSystemClipboard::OnYank && is_yank + { + self.last_yank.replace(content.text.clone()); + cx.write_to_clipboard(content.clone().into()); + } else { + self.last_yank = cx + .read_from_clipboard() + .and_then(|item| item.text().map(|string| string.into())); + } + + self.registers.insert('"', content.clone()); + if is_yank { + self.registers.insert('0', content); + } else { + let contains_newline = content.text.contains('\n'); + if !contains_newline { + self.registers.insert('-', content.clone()); + } + if linewise || contains_newline { + let mut content = content; + for i in '1'..'8' { + if let Some(moved) = self.registers.insert(i, content) { + content = moved; + } else { + break; + } + } + } + } + } + } + + pub(crate) fn read_register( + &mut self, + register: Option, + editor: Option<&mut Editor>, + cx: &ViewContext, + ) -> Option { + let Some(register) = register.filter(|reg| *reg != '"') else { + let setting = VimSettings::get_global(cx).use_system_clipboard; + return match setting { + UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()), + UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => { + cx.read_from_clipboard().map(|item| item.into()) + } + _ => self.registers.get(&'"').cloned(), + }; + }; + let lower = register.to_lowercase().next().unwrap_or(register); + match lower { + '_' | ':' | '.' | '#' | '=' => None, + '+' => cx.read_from_clipboard().map(|item| item.into()), + '*' => { + #[cfg(target_os = "linux")] + { + cx.read_from_primary().map(|item| item.into()) + } + #[cfg(not(target_os = "linux"))] + { + cx.read_from_clipboard().map(|item| item.into()) + } + } + '%' => editor.and_then(|editor| { + let selection = editor.selections.newest::(cx); + if let Some((_, buffer, _)) = editor + .buffer() + .read(cx) + .excerpt_containing(selection.head(), cx) + { + buffer + .read(cx) + .file() + .map(|file| file.path().to_string_lossy().to_string().into()) + } else { + None + } + }), + _ => self.registers.get(&lower).cloned(), + } + } + + fn system_clipboard_is_newer(&self, cx: &ViewContext) -> bool { + cx.read_from_clipboard().is_some_and(|item| { + if let Some(last_state) = &self.last_yank { + Some(last_state.as_ref()) != item.text().as_deref() + } else { + true + } + }) + } + + pub fn observe_action(&mut self, action: Box) { + if self.dot_recording { + self.recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); + + if self.stop_recording_after_next_action { + self.dot_recording = false; + self.stop_recording_after_next_action = false; + } + } + if self.replayer.is_none() { + if let Some(recording_register) = self.recording_register { + self.recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(action)); + } + } + } + + pub fn observe_insertion(&mut self, text: &Arc, range_to_replace: Option>) { + if self.ignore_current_insertion { + self.ignore_current_insertion = false; + return; + } + if self.dot_recording { + self.recorded_actions.push(ReplayableAction::Insertion { + text: text.clone(), + utf16_range_to_replace: range_to_replace.clone(), + }); + if self.stop_recording_after_next_action { + self.dot_recording = false; + self.stop_recording_after_next_action = false; + } + } + if let Some(recording_register) = self.recording_register { + self.recordings.entry(recording_register).or_default().push( + ReplayableAction::Insertion { + text: text.clone(), + utf16_range_to_replace: range_to_replace, + }, + ); + } + } +} + +impl Vim { + pub fn globals(cx: &mut AppContext) -> &mut VimGlobals { + cx.global_mut::() + } + + pub fn update_globals(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R + where + C: BorrowMut, + { + cx.update_global(f) + } +} #[derive(Debug)] pub enum ReplayableAction { @@ -218,93 +423,6 @@ pub struct SearchState { pub prior_mode: Mode, } -impl EditorState { - pub fn cursor_shape(&self) -> CursorShape { - match self.mode { - Mode::Normal => { - if self.operator_stack.is_empty() { - CursorShape::Block - } else { - CursorShape::Underscore - } - } - Mode::Replace => CursorShape::Underscore, - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, - Mode::Insert => CursorShape::Bar, - } - } - - pub fn editor_input_enabled(&self) -> bool { - match self.mode { - Mode::Insert => { - if let Some(operator) = self.operator_stack.last() { - !operator.is_waiting(self.mode) - } else { - true - } - } - Mode::Normal | Mode::Replace | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - false - } - } - } - - pub fn should_autoindent(&self) -> bool { - !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) - } - - pub fn clip_at_line_ends(&self) -> bool { - match self.mode { - Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::Replace => { - false - } - Mode::Normal => true, - } - } - - pub fn active_operator(&self) -> Option { - self.operator_stack.last().cloned() - } - - pub fn keymap_context_layer(&self) -> KeyContext { - let mut context = KeyContext::new_with_defaults(); - - let mut mode = match self.mode { - Mode::Normal => "normal", - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", - Mode::Insert => "insert", - Mode::Replace => "replace", - } - .to_string(); - - let mut operator_id = "none"; - - let active_operator = self.active_operator(); - if active_operator.is_none() && self.pre_count.is_some() - || active_operator.is_some() && self.post_count.is_some() - { - context.add("VimCount"); - } - - if let Some(active_operator) = active_operator { - if active_operator.is_waiting(self.mode) { - mode = "waiting".to_string(); - } else { - mode = "operator".to_string(); - operator_id = active_operator.id(); - } - } - - if mode != "waiting" && mode != "insert" && mode != "replace" { - context.add("VimControl"); - } - context.set("vim_mode", mode); - context.set("vim_operator", operator_id); - - context - } -} - impl Operator { pub fn id(&self) -> &'static str { match self { diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 7c0b2449de4113e6aff4e524277c423f2f4e3622..12bf490db1a01e5a8d842b665ec86f9ca3689e73 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -5,10 +5,10 @@ use crate::{ Vim, }; use editor::{movement, scroll::Autoscroll, Bias}; -use gpui::WindowContext; use language::BracketPair; use serde::Deserialize; use std::sync::Arc; +use ui::ViewContext; #[derive(Clone, Debug, PartialEq, Eq)] pub enum SurroundsType { @@ -27,12 +27,17 @@ impl<'de> Deserialize<'de> for SurroundsType { } } -pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); - let count = vim.take_count(cx); - let mode = vim.state().mode; - vim.update_active_editor(cx, |_, editor, cx| { +impl Vim { + pub fn add_surrounds( + &mut self, + text: Arc, + target: SurroundsType, + cx: &mut ViewContext, + ) { + self.stop_recording(cx); + let count = self.take_count(cx); + let mode = self.mode; + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -128,13 +133,11 @@ pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowConte }); }); }); - vim.switch_mode(Mode::Normal, false, cx); - }); -} + self.switch_mode(Mode::Normal, false, cx); + } -pub fn delete_surrounds(text: Arc, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); + pub fn delete_surrounds(&mut self, text: Arc, cx: &mut ViewContext) { + self.stop_recording(cx); // only legitimate surrounds can be removed let pair = match find_surround_pair(&all_support_surround_pair(), &text) { @@ -147,7 +150,7 @@ pub fn delete_surrounds(text: Arc, cx: &mut WindowContext) { }; let surround = pair.end != *text; - vim.update_active_editor(cx, |_, editor, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -224,14 +227,12 @@ pub fn delete_surrounds(text: Arc, cx: &mut WindowContext) { editor.set_clip_at_line_ends(true, cx); }); }); - }); -} + } -pub fn change_surrounds(text: Arc, target: Object, cx: &mut WindowContext) { - if let Some(will_replace_pair) = object_to_bracket_pair(target) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { + pub fn change_surrounds(&mut self, text: Arc, target: Object, cx: &mut ViewContext) { + if let Some(will_replace_pair) = object_to_bracket_pair(target) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -332,65 +333,67 @@ pub fn change_surrounds(text: Arc, target: Object, cx: &mut WindowContext) }); }); }); - }); + } } -} -/// Checks if any of the current cursors are surrounded by a valid pair of brackets. -/// -/// This method supports multiple cursors and checks each cursor for a valid pair of brackets. -/// A pair of brackets is considered valid if it is well-formed and properly closed. -/// -/// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair. -/// If no valid pair of brackets is found for any cursor, the method returns `false`. -pub fn check_and_move_to_valid_bracket_pair( - vim: &mut Vim, - object: Object, - cx: &mut WindowContext, -) -> bool { - let mut valid = false; - if let Some(pair) = object_to_bracket_pair(object) { - vim.update_active_editor(cx, |_, editor, cx| { - editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let (display_map, selections) = editor.selections.all_adjusted_display(cx); - let mut anchors = Vec::new(); + /// Checks if any of the current cursors are surrounded by a valid pair of brackets. + /// + /// This method supports multiple cursors and checks each cursor for a valid pair of brackets. + /// A pair of brackets is considered valid if it is well-formed and properly closed. + /// + /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair. + /// If no valid pair of brackets is found for any cursor, the method returns `false`. + pub fn check_and_move_to_valid_bracket_pair( + &mut self, + object: Object, + cx: &mut ViewContext, + ) -> bool { + let mut valid = false; + if let Some(pair) = object_to_bracket_pair(object) { + self.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let mut anchors = Vec::new(); - for selection in &selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = object.range(&display_map, selection.clone(), true) { - // If the current parenthesis object is single-line, - // then we need to filter whether it is the current line or not - if object.is_multiline() - || (!object.is_multiline() - && selection.start.row() == range.start.row() - && selection.end.row() == range.end.row()) - { - valid = true; - let mut chars_and_offset = display_map - .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) - .peekable(); - while let Some((ch, offset)) = chars_and_offset.next() { - if ch.to_string() == pair.start { - anchors.push(offset..offset); - break; + for selection in &selections { + let start = selection.start.to_offset(&display_map, Bias::Left); + if let Some(range) = object.range(&display_map, selection.clone(), true) { + // If the current parenthesis object is single-line, + // then we need to filter whether it is the current line or not + if object.is_multiline() + || (!object.is_multiline() + && selection.start.row() == range.start.row() + && selection.end.row() == range.end.row()) + { + valid = true; + let mut chars_and_offset = display_map + .buffer_chars_at( + range.start.to_offset(&display_map, Bias::Left), + ) + .peekable(); + while let Some((ch, offset)) = chars_and_offset.next() { + if ch.to_string() == pair.start { + anchors.push(offset..offset); + break; + } } + } else { + anchors.push(start..start) } } else { anchors.push(start..start) } - } else { - anchors.push(start..start) } - } - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges(anchors); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(anchors); + }); + editor.set_clip_at_line_ends(true, cx); }); - editor.set_clip_at_line_ends(true, cx); }); - }); + } + return valid; } - return valid; } fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index dc447708c6d81a0f0f1fbee483d6bcd847916642..ce5559528ea5372e558901496635e6e1184e9ba8 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -17,7 +17,7 @@ use indoc::indoc; use search::BufferSearchBar; use workspace::WorkspaceSettings; -use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator}; +use crate::{insert::NormalBefore, motion, state::Mode}; #[gpui::test] async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { @@ -51,7 +51,7 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("hjklˇ"); // Selections aren't changed if editor is blurred but vim-mode is still disabled. - cx.set_state("«hjklˇ»", Mode::Normal); + cx.cx.set_state("«hjklˇ»"); cx.assert_editor_state("«hjklˇ»"); cx.update_editor(|_, cx| cx.blur()); cx.assert_editor_state("«hjklˇ»"); @@ -279,59 +279,6 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); } -#[gpui::test] -async fn test_status_indicator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let mode_indicator = cx.workspace(|workspace, cx| { - let status_bar = workspace.status_bar().read(cx); - let mode_indicator = status_bar.item_of_type::(); - assert!(mode_indicator.is_some()); - mode_indicator.unwrap() - }); - - assert_eq!( - cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Normal) - ); - - // shows the correct mode - cx.simulate_keystrokes("i"); - assert_eq!( - cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Insert) - ); - cx.simulate_keystrokes("escape shift-r"); - assert_eq!( - cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Replace) - ); - - // shows even in search - cx.simulate_keystrokes("escape v /"); - assert_eq!( - cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Visual) - ); - - // hides if vim mode is disabled - cx.disable_vim(); - cx.run_until_parked(); - cx.workspace(|workspace, cx| { - let status_bar = workspace.status_bar().read(cx); - let mode_indicator = status_bar.item_of_type::().unwrap(); - assert!(mode_indicator.read(cx).mode.is_none()); - }); - - cx.enable_vim(); - cx.run_until_parked(); - cx.workspace(|workspace, cx| { - let status_bar = workspace.status_bar().read(cx); - let mode_indicator = status_bar.item_of_type::().unwrap(); - assert!(mode_indicator.read(cx).mode.is_some()); - }); -} - #[gpui::test] async fn test_word_characters(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_typescript(cx).await; diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 075f5621d7bf6d68346ce6f95d3fd706acd057be..16ab7771bdd76e325cf4968347c956ae113f2247 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -10,7 +10,7 @@ use language::language_settings::{AllLanguageSettings, SoftWrap}; use util::test::marked_text_offsets; use super::{neovim_connection::NeovimConnection, VimTestContext}; -use crate::{state::Mode, Vim}; +use crate::state::{Mode, VimGlobals}; pub struct NeovimBackedTestContext { cx: VimTestContext, @@ -263,8 +263,7 @@ impl NeovimBackedTestContext { state: self.shared_state().await, neovim: self.neovim.read_register(register).await, editor: self.update(|cx| { - Vim::read(cx) - .workspace_state + cx.global::() .registers .get(®ister) .cloned() diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index f1137d902981359c9bf435ab859f74c4e3c779d9..5ae4d517b0fb09a7f77a4eb224969df2943dcffa 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use editor::test::editor_lsp_test_context::EditorLspTestContext; -use gpui::{Context, SemanticVersion, View, VisualContext}; +use gpui::{Context, SemanticVersion, UpdateGlobal, View, VisualContext}; use search::{project_search::ProjectSearchBar, BufferSearchBar}; use crate::{state::Operator, *}; @@ -12,7 +12,7 @@ pub struct VimTestContext { impl VimTestContext { pub fn init(cx: &mut gpui::TestAppContext) { - if cx.has_global::() { + if cx.has_global::() { return; } cx.update(|cx| { @@ -119,23 +119,31 @@ impl VimTestContext { } pub fn mode(&mut self) -> Mode { - self.cx.read(|cx| cx.global::().state().mode) + self.update_editor(|editor, cx| editor.addon::().unwrap().view.read(cx).mode) } pub fn active_operator(&mut self) -> Option { - self.cx - .read(|cx| cx.global::().state().operator_stack.last().cloned()) + self.update_editor(|editor, cx| { + editor + .addon::() + .unwrap() + .view + .read(cx) + .operator_stack + .last() + .cloned() + }) } pub fn set_state(&mut self, text: &str, mode: Mode) { - let window = self.window; self.cx.set_state(text); - self.update_window(window, |_, cx| { - Vim::update(cx, |vim, cx| { + let vim = self.update_editor(|editor, _cx| editor.addon::().cloned().unwrap()); + + self.update(|cx| { + vim.view.update(cx, |vim, cx| { vim.switch_mode(mode, true, cx); - }) - }) - .unwrap(); + }); + }); self.cx.cx.cx.run_until_parked(); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 26ddca9d855e19e5f9344ef6d2d8f25c8ee1b8fe..fe3236a52cd2cc9c987d642baffc5819d1fe4831 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -6,7 +6,6 @@ mod test; mod change_list; mod command; mod digraph; -mod editor_events; mod insert; mod mode_indicator; mod motion; @@ -18,40 +17,33 @@ mod surrounds; mod visual; use anyhow::Result; -use change_list::push_to_change_list; use collections::HashMap; -use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; use editor::{ movement::{self, FindRange}, Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint, }; use gpui::{ - actions, impl_actions, Action, AppContext, EntityId, FocusableView, Global, KeystrokeEvent, - Subscription, UpdateGlobal, View, ViewContext, WeakView, WindowContext, + actions, impl_actions, Action, AppContext, EventEmitter, KeyContext, KeystrokeEvent, Render, + View, ViewContext, WeakView, }; -use language::{CursorShape, Point, SelectionGoal, TransactionId}; +use insert::NormalBefore; +use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; -use normal::{ - mark::create_visual_marks, - normal_replace, - repeat::{observe_action, observe_insertion, record_register, replay_register}, -}; -use replace::multi_replace; +use normal::search::SearchSubmit; use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; -use state::{EditorState, Mode, Operator, RecordedSelection, Register, WorkspaceState}; +use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{ops::Range, sync::Arc}; -use surrounds::{add_surrounds, change_surrounds, delete_surrounds, SurroundsType}; -use ui::BorrowAppContext; -use visual::{visual_block_motion, visual_replace}; -use workspace::{self, Workspace}; +use surrounds::SurroundsType; +use ui::{IntoElement, VisualContext}; +use workspace::{self, Pane, Workspace}; use crate::state::ReplayableAction; -/// Whether or not to enable Vim mode (work in progress). +/// Whether or not to enable Vim mode. /// /// Default: false pub struct VimModeSetting(pub bool); @@ -95,316 +87,336 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]); /// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { - cx.set_global(Vim::default()); VimModeSetting::register(cx); VimSettings::register(cx); + VimGlobals::register(cx); - cx.observe_keystrokes(observe_keystrokes).detach(); - editor_events::init(cx); - - cx.observe_new_views(|workspace: &mut Workspace, cx| register(workspace, cx)) + cx.observe_new_views(|editor: &mut Editor, cx| Vim::register(editor, cx)) .detach(); - // Any time settings change, update vim mode to match. The Vim struct - // will be initialized as disabled by default, so we filter its commands - // out when starting up. - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.hide_namespace(Vim::NAMESPACE); - }); - Vim::update_global(cx, |vim, cx| { - vim.set_enabled(VimModeSetting::get_global(cx).0, cx) - }); - cx.observe_global::(|cx| { - Vim::update_global(cx, |vim, cx| { - vim.set_enabled(VimModeSetting::get_global(cx).0, cx) + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleVimMode, cx| { + let fs = workspace.app_state().fs.clone(); + let currently_enabled = Vim::enabled(cx); + update_settings_file::(fs, cx, move |setting, _| { + *setting = Some(!currently_enabled) + }) + }); + + workspace.register_action(|_, _: &OpenDefaultKeymap, cx| { + cx.emit(workspace::Event::OpenBundledFile { + text: settings::vim_keymap(), + title: "Default Vim Bindings", + language: "JSON", + }); + }); + + workspace.register_action(|workspace, _: &SearchSubmit, cx| { + let Some(vim) = workspace + .active_item_as::(cx) + .and_then(|editor| editor.read(cx).addon::().cloned()) + else { + return; + }; + vim.view + .update(cx, |_, cx| cx.defer(|vim, cx| vim.search_submit(cx))) }); }) .detach(); } -fn register(workspace: &mut Workspace, cx: &mut ViewContext) { - workspace.register_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { - Vim::update(cx, |vim, cx| vim.switch_mode(mode, false, cx)) - }); - workspace.register_action( - |_: &mut Workspace, PushOperator(operator): &PushOperator, cx| { - Vim::update(cx, |vim, cx| vim.push_operator(operator.clone(), cx)) - }, - ); - workspace.register_action(|_: &mut Workspace, _: &ClearOperators, cx| { - Vim::update(cx, |vim, cx| vim.clear_operator(cx)) - }); - workspace.register_action(|_: &mut Workspace, n: &Number, cx: _| { - Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx)); - }); - workspace.register_action(|_: &mut Workspace, _: &Tab, cx| { - Vim::active_editor_input_ignored(" ".into(), cx) - }); - - workspace.register_action(|_: &mut Workspace, _: &Enter, cx| { - Vim::active_editor_input_ignored("\n".into(), cx) - }); - - workspace.register_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| { - let fs = workspace.app_state().fs.clone(); - let currently_enabled = VimModeSetting::get_global(cx).0; - update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(!currently_enabled) - }) - }); - - workspace.register_action(|_: &mut Workspace, _: &OpenDefaultKeymap, cx| { - cx.emit(workspace::Event::OpenBundledFile { - text: settings::vim_keymap(), - title: "Default Vim Bindings", - language: "JSON", - }); - }); - - normal::register(workspace, cx); - insert::register(workspace, cx); - motion::register(workspace, cx); - command::register(workspace, cx); - replace::register(workspace, cx); - object::register(workspace, cx); - visual::register(workspace, cx); - change_list::register(workspace, cx); +#[derive(Clone)] +pub(crate) struct VimAddon { + pub(crate) view: View, } -/// Called whenever an keystroke is typed so vim can observe all actions -/// and keystrokes accordingly. -fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) { - if let Some(action) = keystroke_event - .action - .as_ref() - .map(|action| action.boxed_clone()) - { - observe_action(action.boxed_clone(), cx); - - // Keystroke is handled by the vim system, so continue forward - if action.name().starts_with("vim::") { - return; - } - } else if cx.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() { - return; +impl editor::Addon for VimAddon { + fn extend_key_context(&self, key_context: &mut KeyContext, cx: &AppContext) { + self.view.read(cx).extend_key_context(key_context) } - Vim::update(cx, |vim, cx| { - if let Some(operator) = vim.active_operator() { - if !operator.is_waiting(vim.state().mode) { - vim.clear_operator(cx); - vim.stop_recording_immediately(Box::new(ClearOperators)) - } - } - }); + fn to_any(&self) -> &dyn std::any::Any { + self + } } /// The state pertaining to Vim mode. -#[derive(Default)] -struct Vim { - active_editor: Option>, - editor_subscription: Option, - enabled: bool, - editor_states: HashMap, - workspace_state: WorkspaceState, - default_state: EditorState, +pub(crate) struct Vim { + pub(crate) mode: Mode, + pub last_mode: Mode, + + /// pre_count is the number before an operator is specified (3 in 3d2d) + pre_count: Option, + /// post_count is the number after an operator is specified (2 in 3d2d) + post_count: Option, + + operator_stack: Vec, + pub(crate) replacements: Vec<(Range, String)>, + + pub(crate) marks: HashMap>, + pub(crate) stored_visual_mode: Option<(Mode, Vec)>, + pub(crate) change_list: Vec>, + pub(crate) change_list_position: Option, + + pub(crate) current_tx: Option, + pub(crate) current_anchor: Option>, + pub(crate) undo_modes: HashMap, + + selected_register: Option, + pub search: SearchState, + + editor: WeakView, } -impl Global for Vim {} +// Hack: Vim intercepts events dispatched to a window and updates the view in response. +// This means it needs a VisualContext. The easiest way to satisfy that constraint is +// to make Vim a "View" that is just never actually rendered. +impl Render for Vim { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + gpui::Empty + } +} + +enum VimEvent { + Focused, +} +impl EventEmitter for Vim {} impl Vim { /// The namespace for Vim actions. const NAMESPACE: &'static str = "vim"; - fn read(cx: &mut AppContext) -> &Self { - cx.global::() - } + pub fn new(cx: &mut ViewContext) -> View { + let editor = cx.view().clone(); - fn update(cx: &mut WindowContext, update: F) -> S - where - F: FnOnce(&mut Self, &mut WindowContext) -> S, - { - cx.update_global(update) + cx.new_view(|cx: &mut ViewContext| { + cx.subscribe(&editor, |vim, _, event, cx| { + vim.handle_editor_event(event, cx) + }) + .detach(); + + let listener = cx.listener(Vim::observe_keystrokes); + cx.observe_keystrokes(listener).detach(); + + Vim { + mode: Mode::Normal, + last_mode: Mode::Normal, + pre_count: None, + post_count: None, + operator_stack: Vec::new(), + replacements: Vec::new(), + + marks: HashMap::default(), + stored_visual_mode: None, + change_list: Vec::new(), + change_list_position: None, + current_tx: None, + current_anchor: None, + undo_modes: HashMap::default(), + + selected_register: None, + search: SearchState::default(), + + editor: editor.downgrade(), + } + }) } - fn activate_editor(&mut self, editor: View, cx: &mut WindowContext) { - if !editor.read(cx).use_modal_editing() { + fn register(editor: &mut Editor, cx: &mut ViewContext) { + if !editor.use_modal_editing() { return; } - self.active_editor = Some(editor.clone().downgrade()); - self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { - EditorEvent::SelectionsChanged { local: true } => { - if editor.read(cx).leader_peer_id().is_none() { - Vim::update(cx, |vim, cx| { - vim.local_selections_changed(editor, cx); - }) - } - } - EditorEvent::InputIgnored { text } => { - Vim::active_editor_input_ignored(text.clone(), cx); - observe_insertion(text, None, cx) + let mut was_enabled = Vim::enabled(cx); + cx.observe_global::(move |editor, cx| { + let enabled = Vim::enabled(cx); + if was_enabled == enabled { + return; } - EditorEvent::InputHandled { - text, - utf16_range_to_replace: range_to_replace, - } => observe_insertion(text, range_to_replace.clone(), cx), - EditorEvent::TransactionBegun { transaction_id } => Vim::update(cx, |vim, cx| { - vim.transaction_begun(*transaction_id, cx); - }), - EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, cx| { - vim.transaction_undone(transaction_id, cx); - }), - EditorEvent::Edited { .. } => { - Vim::update(cx, |vim, cx| vim.transaction_ended(editor, cx)) + was_enabled = enabled; + if enabled { + Self::activate(editor, cx) + } else { + Self::deactivate(editor, cx) } - EditorEvent::FocusedIn => Vim::update(cx, |vim, cx| vim.sync_vim_settings(cx)), - _ => {} - })); + }) + .detach(); + if was_enabled { + Self::activate(editor, cx) + } + } - let editor = editor.read(cx); - let editor_mode = editor.mode(); - let newest_selection_empty = editor.selections.newest::(cx).is_empty(); + fn activate(editor: &mut Editor, cx: &mut ViewContext) { + let vim = Vim::new(cx); - if editor_mode == EditorMode::Full - && !newest_selection_empty - && self.state().mode == Mode::Normal - // When following someone, don't switch vim mode. - && editor.leader_peer_id().is_none() - { - self.switch_mode(Mode::Visual, true, cx); - } + editor.register_addon(VimAddon { view: vim.clone() }); - self.sync_vim_settings(cx); + vim.update(cx, |_, cx| { + Vim::action(editor, cx, |vim, action: &SwitchMode, cx| { + vim.switch_mode(action.0, false, cx) + }); + + Vim::action(editor, cx, |vim, action: &PushOperator, cx| { + vim.push_operator(action.0.clone(), cx) + }); + + Vim::action(editor, cx, |vim, _: &ClearOperators, cx| { + vim.clear_operator(cx) + }); + Vim::action(editor, cx, |vim, n: &Number, cx| { + vim.push_count_digit(n.0, cx); + }); + Vim::action(editor, cx, |vim, _: &Tab, cx| { + vim.input_ignored(" ".into(), cx) + }); + Vim::action(editor, cx, |vim, _: &Enter, cx| { + vim.input_ignored("\n".into(), cx) + }); + + normal::register(editor, cx); + insert::register(editor, cx); + motion::register(editor, cx); + command::register(editor, cx); + replace::register(editor, cx); + object::register(editor, cx); + visual::register(editor, cx); + change_list::register(editor, cx); + + cx.defer(|vim, cx| { + vim.focused(false, cx); + }) + }) } - fn update_active_editor( - &mut self, - cx: &mut WindowContext, - update: impl FnOnce(&mut Vim, &mut Editor, &mut ViewContext) -> S, - ) -> Option { - let editor = self.active_editor.clone()?.upgrade()?; - Some(editor.update(cx, |editor, cx| update(self, editor, cx))) + fn deactivate(editor: &mut Editor, cx: &mut ViewContext) { + editor.set_cursor_shape(CursorShape::Bar, cx); + editor.set_clip_at_line_ends(false, cx); + editor.set_collapse_matches(false); + editor.set_input_enabled(true); + editor.set_autoindent(true); + editor.selections.line_mode = false; + editor.unregister_addon::(); } - fn editor_selections(&mut self, cx: &mut WindowContext) -> Vec> { - self.update_active_editor(cx, |_, editor, _| { - editor - .selections - .disjoint_anchors() - .iter() - .map(|selection| selection.tail()..selection.head()) - .collect() - }) - .unwrap_or_default() + /// Register an action on the editor. + pub fn action( + editor: &mut Editor, + cx: &mut ViewContext, + f: impl Fn(&mut Vim, &A, &mut ViewContext) + 'static, + ) { + let subscription = editor.register_action(cx.listener(f)); + cx.on_release(|_, _, _| drop(subscription)).detach(); } - /// When doing an action that modifies the buffer, we start recording so that `.` - /// will replay the action. - pub fn start_recording(&mut self, cx: &mut WindowContext) { - if !self.workspace_state.dot_replaying { - self.workspace_state.dot_recording = true; - self.workspace_state.recorded_actions = Default::default(); - self.workspace_state.recorded_count = None; - - let selections = self - .active_editor - .as_ref() - .and_then(|editor| editor.upgrade()) - .map(|editor| { - let editor = editor.read(cx); - ( - editor.selections.oldest::(cx), - editor.selections.newest::(cx), - ) - }); + pub fn editor(&self) -> Option> { + self.editor.upgrade() + } - if let Some((oldest, newest)) = selections { - self.workspace_state.recorded_selection = match self.state().mode { - Mode::Visual if newest.end.row == newest.start.row => { - RecordedSelection::SingleLine { - cols: newest.end.column - newest.start.column, - } - } - Mode::Visual => RecordedSelection::Visual { - rows: newest.end.row - newest.start.row, - cols: newest.end.column, - }, - Mode::VisualLine => RecordedSelection::VisualLine { - rows: newest.end.row - newest.start.row, - }, - Mode::VisualBlock => RecordedSelection::VisualBlock { - rows: newest.end.row.abs_diff(oldest.start.row), - cols: newest.end.column.abs_diff(oldest.start.column), - }, - _ => RecordedSelection::None, - } - } else { - self.workspace_state.recorded_selection = RecordedSelection::None; - } - } + pub fn workspace(&self, cx: &ViewContext) -> Option> { + self.editor().and_then(|editor| editor.read(cx).workspace()) } - pub fn stop_replaying(&mut self, _: &mut WindowContext) { - self.workspace_state.dot_replaying = false; - if let Some(replayer) = self.workspace_state.replayer.take() { - replayer.stop(); - } + pub fn pane(&self, cx: &ViewContext) -> Option> { + self.workspace(cx) + .and_then(|workspace| workspace.read(cx).pane_for(&self.editor()?)) } - /// When finishing an action that modifies the buffer, stop recording. - /// as you usually call this within a keystroke handler we also ensure that - /// the current action is recorded. - pub fn stop_recording(&mut self) { - if self.workspace_state.dot_recording { - self.workspace_state.stop_recording_after_next_action = true; - } + pub fn enabled(cx: &mut AppContext) -> bool { + VimModeSetting::get_global(cx).0 } - /// Stops recording actions immediately rather than waiting until after the - /// next action to stop recording. - /// - /// This doesn't include the current action. - pub fn stop_recording_immediately(&mut self, action: Box) { - if self.workspace_state.dot_recording { - self.workspace_state - .recorded_actions - .push(ReplayableAction::Action(action.boxed_clone())); - self.workspace_state.dot_recording = false; - self.workspace_state.stop_recording_after_next_action = false; + /// Called whenever an keystroke is typed so vim can observe all actions + /// and keystrokes accordingly. + fn observe_keystrokes(&mut self, keystroke_event: &KeystrokeEvent, cx: &mut ViewContext) { + if let Some(action) = keystroke_event.action.as_ref() { + // Keystroke is handled by the vim system, so continue forward + if action.name().starts_with("vim::") { + return; + } + } else if cx.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() { + return; } - } - /// Explicitly record one action (equivalents to start_recording and stop_recording) - pub fn record_current_action(&mut self, cx: &mut WindowContext) { - self.start_recording(cx); - self.stop_recording(); + if let Some(operator) = self.active_operator() { + if !operator.is_waiting(self.mode) { + self.clear_operator(cx); + self.stop_recording_immediately(Box::new(ClearOperators), cx) + } + } } - // When handling an action, you must create visual marks if you will switch to normal - // mode without the default selection behavior. - fn store_visual_marks(&mut self, cx: &mut WindowContext) { - let mode = self.state().mode; - if mode.is_visual() { - create_visual_marks(self, mode, cx); + fn handle_editor_event(&mut self, event: &EditorEvent, cx: &mut ViewContext) { + match event { + EditorEvent::Focused => self.focused(true, cx), + EditorEvent::Blurred => self.blurred(cx), + EditorEvent::SelectionsChanged { local: true } => { + self.local_selections_changed(cx); + } + EditorEvent::InputIgnored { text } => { + self.input_ignored(text.clone(), cx); + Vim::globals(cx).observe_insertion(text, None) + } + EditorEvent::InputHandled { + text, + utf16_range_to_replace: range_to_replace, + } => Vim::globals(cx).observe_insertion(text, range_to_replace.clone()), + EditorEvent::TransactionBegun { transaction_id } => { + self.transaction_begun(*transaction_id, cx) + } + EditorEvent::TransactionUndone { transaction_id } => { + self.transaction_undone(transaction_id, cx) + } + EditorEvent::Edited { .. } => self.push_to_change_list(cx), + EditorEvent::FocusedIn => self.sync_vim_settings(cx), + _ => {} } } - fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { - let state = self.state(); - let last_mode = state.mode; - let prior_mode = state.last_mode; - let prior_tx = state.current_tx; - self.update_state(|state| { - state.last_mode = last_mode; - state.mode = mode; - state.operator_stack.clear(); - state.selected_register.take(); - if mode == Mode::Normal || mode != last_mode { - state.current_tx.take(); - state.current_anchor.take(); + fn push_operator(&mut self, operator: Operator, cx: &mut ViewContext) { + if matches!( + operator, + Operator::Change + | Operator::Delete + | Operator::Replace + | Operator::Indent + | Operator::Outdent + | Operator::Lowercase + | Operator::Uppercase + | Operator::OppositeCase + | Operator::ToggleComments + ) { + self.start_recording(cx) + }; + // Since these operations can only be entered with pre-operators, + // we need to clear the previous operators when pushing, + // so that the current stack is the most correct + if matches!( + operator, + Operator::AddSurrounds { .. } + | Operator::ChangeSurrounds { .. } + | Operator::DeleteSurrounds + ) { + self.operator_stack.clear(); + if let Operator::AddSurrounds { target: None } = operator { + self.start_recording(cx); } - }); + }; + self.operator_stack.push(operator); + self.sync_vim_settings(cx); + } + + pub fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut ViewContext) { + let last_mode = self.mode; + let prior_mode = self.last_mode; + let prior_tx = self.current_tx; + self.last_mode = last_mode; + self.mode = mode; + self.operator_stack.clear(); + self.selected_register.take(); + if mode == Mode::Normal || mode != last_mode { + self.current_tx.take(); + self.current_anchor.take(); + } if mode != Mode::Insert && mode != Mode::Replace { self.take_count(cx); } @@ -417,14 +429,14 @@ impl Vim { } if !mode.is_visual() && last_mode.is_visual() { - create_visual_marks(self, last_mode, cx); + self.create_visual_marks(last_mode, cx); } // Adjust selections - self.update_active_editor(cx, |_, editor, cx| { + self.update_editor(cx, |vim, editor, cx| { if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock { - visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) + vim.visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) } if last_mode == Mode::Insert || last_mode == Mode::Replace { if let Some(prior_tx) = prior_tx { @@ -478,278 +490,327 @@ impl Vim { }); } - fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) { - if self.active_operator().is_some() { - self.update_state(|state| { - let post_count = state.post_count.unwrap_or(0); - - state.post_count = Some( - post_count - .checked_mul(10) - .and_then(|post_count| post_count.checked_add(number)) - .unwrap_or(post_count), - ) - }) - } else { - self.update_state(|state| { - let pre_count = state.pre_count.unwrap_or(0); - - state.pre_count = Some( - pre_count - .checked_mul(10) - .and_then(|pre_count| pre_count.checked_add(number)) - .unwrap_or(pre_count), - ) - }) - } - // update the keymap so that 0 works - self.sync_vim_settings(cx) - } - - fn take_count(&mut self, cx: &mut WindowContext) -> Option { - if self.workspace_state.dot_replaying { - return self.workspace_state.recorded_count; + fn take_count(&mut self, cx: &mut ViewContext) -> Option { + let global_state = cx.global_mut::(); + if global_state.dot_replaying { + return global_state.recorded_count; } - let count = if self.state().post_count == None && self.state().pre_count == None { + let count = if self.post_count == None && self.pre_count == None { return None; } else { - Some(self.update_state(|state| { - state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1) - })) + Some(self.post_count.take().unwrap_or(1) * self.pre_count.take().unwrap_or(1)) }; - if self.workspace_state.dot_recording { - self.workspace_state.recorded_count = count; + + if global_state.dot_recording { + global_state.recorded_count = count; } self.sync_vim_settings(cx); count } - fn select_register(&mut self, register: Arc, cx: &mut WindowContext) { - self.update_state(|state| { - if register.chars().count() == 1 { - state - .selected_register - .replace(register.chars().next().unwrap()); + pub fn cursor_shape(&self) -> CursorShape { + match self.mode { + Mode::Normal => { + if self.operator_stack.is_empty() { + CursorShape::Block + } else { + CursorShape::Underscore + } } - state.operator_stack.clear(); - }); - self.sync_vim_settings(cx); + Mode::Replace => CursorShape::Underscore, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, + Mode::Insert => CursorShape::Bar, + } } - fn write_registers( - &mut self, - content: Register, - register: Option, - is_yank: bool, - linewise: bool, - cx: &mut ViewContext, - ) { - if let Some(register) = register { - let lower = register.to_lowercase().next().unwrap_or(register); - if lower != register { - let current = self.workspace_state.registers.entry(lower).or_default(); - current.text = (current.text.to_string() + &content.text).into(); - // not clear how to support appending to registers with multiple cursors - current.clipboard_selections.take(); - let yanked = current.clone(); - self.workspace_state.registers.insert('"', yanked); - } else { - self.workspace_state.registers.insert('"', content.clone()); - match lower { - '_' | ':' | '.' | '%' | '#' | '=' | '/' => {} - '+' => { - cx.write_to_clipboard(content.into()); - } - '*' => { - #[cfg(target_os = "linux")] - cx.write_to_primary(content.into()); - #[cfg(not(target_os = "linux"))] - cx.write_to_clipboard(content.into()); - } - '"' => { - self.workspace_state.registers.insert('0', content.clone()); - self.workspace_state.registers.insert('"', content); - } - _ => { - self.workspace_state.registers.insert(lower, content); - } + pub fn editor_input_enabled(&self) -> bool { + match self.mode { + Mode::Insert => { + if let Some(operator) = self.operator_stack.last() { + !operator.is_waiting(self.mode) + } else { + true } } - } else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - if setting == UseSystemClipboard::Always - || setting == UseSystemClipboard::OnYank && is_yank - { - self.workspace_state.last_yank.replace(content.text.clone()); - cx.write_to_clipboard(content.clone().into()); + Mode::Normal | Mode::Replace | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + false + } + } + } + + pub fn should_autoindent(&self) -> bool { + !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) + } + + pub fn clip_at_line_ends(&self) -> bool { + match self.mode { + Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::Replace => { + false + } + Mode::Normal => true, + } + } + + pub fn extend_key_context(&self, context: &mut KeyContext) { + let mut mode = match self.mode { + Mode::Normal => "normal", + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", + Mode::Insert => "insert", + Mode::Replace => "replace", + } + .to_string(); + + let mut operator_id = "none"; + + let active_operator = self.active_operator(); + if active_operator.is_none() && self.pre_count.is_some() + || active_operator.is_some() && self.post_count.is_some() + { + context.add("VimCount"); + } + + if let Some(active_operator) = active_operator { + if active_operator.is_waiting(self.mode) { + mode = "waiting".to_string(); } else { - self.workspace_state.last_yank = cx - .read_from_clipboard() - .and_then(|item| item.text().map(|string| string.into())) + mode = "operator".to_string(); + operator_id = active_operator.id(); } + } + + if mode != "waiting" && mode != "insert" && mode != "replace" { + context.add("VimControl"); + } + context.set("vim_mode", mode); + context.set("vim_operator", operator_id); + } + + fn focused(&mut self, preserve_selection: bool, cx: &mut ViewContext) { + let Some(editor) = self.editor() else { + return; + }; + let editor = editor.read(cx); + let editor_mode = editor.mode(); + let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - self.workspace_state.registers.insert('"', content.clone()); - if is_yank { - self.workspace_state.registers.insert('0', content); + if editor_mode == EditorMode::Full + && !newest_selection_empty + && self.mode == Mode::Normal + // When following someone, don't switch vim mode. + && editor.leader_peer_id().is_none() + { + if preserve_selection { + self.switch_mode(Mode::Visual, true, cx); } else { - let contains_newline = content.text.contains('\n'); - if !contains_newline { - self.workspace_state.registers.insert('-', content.clone()); - } - if linewise || contains_newline { - let mut content = content; - for i in '1'..'8' { - if let Some(moved) = self.workspace_state.registers.insert(i, content) { - content = moved; - } else { - break; + self.update_editor(cx, |_, editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.collapse_to(selection.start, selection.goal) + }) + }); + }); + } + } + + cx.emit(VimEvent::Focused); + self.sync_vim_settings(cx); + } + + fn blurred(&mut self, cx: &mut ViewContext) { + self.stop_recording_immediately(NormalBefore.boxed_clone(), cx); + self.store_visual_marks(cx); + self.clear_operator(cx); + self.update_editor(cx, |_, editor, cx| { + editor.set_cursor_shape(language::CursorShape::Hollow, cx); + }); + } + + fn update_editor( + &mut self, + cx: &mut ViewContext, + update: impl FnOnce(&mut Self, &mut Editor, &mut ViewContext) -> S, + ) -> Option { + let editor = self.editor.upgrade()?; + Some(editor.update(cx, |editor, cx| update(self, editor, cx))) + } + + fn editor_selections(&mut self, cx: &mut ViewContext) -> Vec> { + self.update_editor(cx, |_, editor, _| { + editor + .selections + .disjoint_anchors() + .iter() + .map(|selection| selection.tail()..selection.head()) + .collect() + }) + .unwrap_or_default() + } + + /// When doing an action that modifies the buffer, we start recording so that `.` + /// will replay the action. + pub fn start_recording(&mut self, cx: &mut ViewContext) { + Vim::update_globals(cx, |globals, cx| { + if !globals.dot_replaying { + globals.dot_recording = true; + globals.recorded_actions = Default::default(); + globals.recorded_count = None; + + let selections = self.editor().map(|editor| { + let editor = editor.read(cx); + ( + editor.selections.oldest::(cx), + editor.selections.newest::(cx), + ) + }); + + if let Some((oldest, newest)) = selections { + globals.recorded_selection = match self.mode { + Mode::Visual if newest.end.row == newest.start.row => { + RecordedSelection::SingleLine { + cols: newest.end.column - newest.start.column, + } } + Mode::Visual => RecordedSelection::Visual { + rows: newest.end.row - newest.start.row, + cols: newest.end.column, + }, + Mode::VisualLine => RecordedSelection::VisualLine { + rows: newest.end.row - newest.start.row, + }, + Mode::VisualBlock => RecordedSelection::VisualBlock { + rows: newest.end.row.abs_diff(oldest.start.row), + cols: newest.end.column.abs_diff(oldest.start.column), + }, + _ => RecordedSelection::None, } + } else { + globals.recorded_selection = RecordedSelection::None; } } + }) + } + + pub fn stop_replaying(&mut self, cx: &mut ViewContext) { + let globals = Vim::globals(cx); + globals.dot_replaying = false; + if let Some(replayer) = globals.replayer.take() { + replayer.stop(); } } - fn read_register( + /// When finishing an action that modifies the buffer, stop recording. + /// as you usually call this within a keystroke handler we also ensure that + /// the current action is recorded. + pub fn stop_recording(&mut self, cx: &mut ViewContext) { + let globals = Vim::globals(cx); + if globals.dot_recording { + globals.stop_recording_after_next_action = true; + } + } + + /// Stops recording actions immediately rather than waiting until after the + /// next action to stop recording. + /// + /// This doesn't include the current action. + pub fn stop_recording_immediately( &mut self, - register: Option, - editor: Option<&mut Editor>, - cx: &mut WindowContext, - ) -> Option { - let Some(register) = register.filter(|reg| *reg != '"') else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - return match setting { - UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()), - UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => { - cx.read_from_clipboard().map(|item| item.into()) - } - _ => self.workspace_state.registers.get(&'"').cloned(), - }; - }; - let lower = register.to_lowercase().next().unwrap_or(register); - match lower { - '_' | ':' | '.' | '#' | '=' => None, - '+' => cx.read_from_clipboard().map(|item| item.into()), - '*' => { - #[cfg(target_os = "linux")] - { - cx.read_from_primary().map(|item| item.into()) - } - #[cfg(not(target_os = "linux"))] - { - cx.read_from_clipboard().map(|item| item.into()) - } - } - '%' => editor.and_then(|editor| { - let selection = editor.selections.newest::(cx); - if let Some((_, buffer, _)) = editor - .buffer() - .read(cx) - .excerpt_containing(selection.head(), cx) - { - buffer - .read(cx) - .file() - .map(|file| file.path().to_string_lossy().to_string().into()) - } else { - None - } - }), - _ => self.workspace_state.registers.get(&lower).cloned(), + action: Box, + cx: &mut ViewContext, + ) { + let globals = Vim::globals(cx); + if globals.dot_recording { + globals + .recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); + globals.dot_recording = false; + globals.stop_recording_after_next_action = false; } } - fn system_clipboard_is_newer(&self, cx: &mut AppContext) -> bool { - cx.read_from_clipboard().is_some_and(|item| { - if let Some(last_state) = &self.workspace_state.last_yank { - Some(last_state.as_ref()) != item.text().as_deref() - } else { - true - } - }) + /// Explicitly record one action (equivalents to start_recording and stop_recording) + pub fn record_current_action(&mut self, cx: &mut ViewContext) { + self.start_recording(cx); + self.stop_recording(cx); } - fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { - if matches!( - operator, - Operator::Change - | Operator::Delete - | Operator::Replace - | Operator::Indent - | Operator::Outdent - | Operator::Lowercase - | Operator::Uppercase - | Operator::OppositeCase - | Operator::ToggleComments - ) { - self.start_recording(cx) - }; - // Since these operations can only be entered with pre-operators, - // we need to clear the previous operators when pushing, - // so that the current stack is the most correct - if matches!( - operator, - Operator::AddSurrounds { .. } - | Operator::ChangeSurrounds { .. } - | Operator::DeleteSurrounds - ) { - self.update_state(|state| state.operator_stack.clear()); - if let Operator::AddSurrounds { target: None } = operator { - self.start_recording(cx); - } - }; - self.update_state(|state| state.operator_stack.push(operator)); + fn push_count_digit(&mut self, number: usize, cx: &mut ViewContext) { + if self.active_operator().is_some() { + let post_count = self.post_count.unwrap_or(0); + + self.post_count = Some( + post_count + .checked_mul(10) + .and_then(|post_count| post_count.checked_add(number)) + .unwrap_or(post_count), + ) + } else { + let pre_count = self.pre_count.unwrap_or(0); + + self.pre_count = Some( + pre_count + .checked_mul(10) + .and_then(|pre_count| pre_count.checked_add(number)) + .unwrap_or(pre_count), + ) + } + // update the keymap so that 0 works + self.sync_vim_settings(cx) + } + + fn select_register(&mut self, register: Arc, cx: &mut ViewContext) { + if register.chars().count() == 1 { + self.selected_register + .replace(register.chars().next().unwrap()); + } + self.operator_stack.clear(); self.sync_vim_settings(cx); } fn maybe_pop_operator(&mut self) -> Option { - self.update_state(|state| state.operator_stack.pop()) + self.operator_stack.pop() } - fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator { - let popped_operator = self.update_state(|state| state.operator_stack.pop()) + fn pop_operator(&mut self, cx: &mut ViewContext) -> Operator { + let popped_operator = self.operator_stack.pop() .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); self.sync_vim_settings(cx); popped_operator } - fn clear_operator(&mut self, cx: &mut WindowContext) { + fn clear_operator(&mut self, cx: &mut ViewContext) { self.take_count(cx); - self.update_state(|state| { - state.selected_register.take(); - state.operator_stack.clear() - }); + self.selected_register.take(); + self.operator_stack.clear(); self.sync_vim_settings(cx); } fn active_operator(&self) -> Option { - self.state().operator_stack.last().cloned() + self.operator_stack.last().cloned() } - fn transaction_begun(&mut self, transaction_id: TransactionId, _: &mut WindowContext) { - self.update_state(|state| { - let mode = if (state.mode == Mode::Insert - || state.mode == Mode::Replace - || state.mode == Mode::Normal) - && state.current_tx.is_none() - { - state.current_tx = Some(transaction_id); - state.last_mode - } else { - state.mode - }; - if mode == Mode::VisualLine || mode == Mode::VisualBlock { - state.undo_modes.insert(transaction_id, mode); - } - }); + fn transaction_begun(&mut self, transaction_id: TransactionId, _: &mut ViewContext) { + let mode = if (self.mode == Mode::Insert + || self.mode == Mode::Replace + || self.mode == Mode::Normal) + && self.current_tx.is_none() + { + self.current_tx = Some(transaction_id); + self.last_mode + } else { + self.mode + }; + if mode == Mode::VisualLine || mode == Mode::VisualBlock { + self.undo_modes.insert(transaction_id, mode); + } } - fn transaction_undone(&mut self, transaction_id: &TransactionId, cx: &mut WindowContext) { - match self.state().mode { + fn transaction_undone(&mut self, transaction_id: &TransactionId, cx: &mut ViewContext) { + match self.mode { Mode::VisualLine | Mode::VisualBlock | Mode::Visual => { - self.update_active_editor(cx, |vim, editor, cx| { - let original_mode = vim.state().undo_modes.get(transaction_id); + self.update_editor(cx, |vim, editor, cx| { + let original_mode = vim.undo_modes.get(transaction_id); editor.change_selections(None, cx, |s| match original_mode { Some(Mode::VisualLine) => { s.move_with(|map, selection| { @@ -777,7 +838,7 @@ impl Vim { self.switch_mode(Mode::Normal, true, cx) } Mode::Normal => { - self.update_active_editor(cx, |_, editor, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { selection @@ -790,26 +851,26 @@ impl Vim { } } - fn transaction_ended(&mut self, editor: View, cx: &mut WindowContext) { - push_to_change_list(self, editor, cx) - } + fn local_selections_changed(&mut self, cx: &mut ViewContext) { + let Some(editor) = self.editor() else { return }; + + if editor.read(cx).leader_peer_id().is_some() { + return; + } - fn local_selections_changed(&mut self, editor: View, cx: &mut WindowContext) { let newest = editor.read(cx).selections.newest_anchor().clone(); let is_multicursor = editor.read(cx).selections.count() > 1; - - let state = self.state(); - if state.mode == Mode::Insert && state.current_tx.is_some() { - if state.current_anchor.is_none() { - self.update_state(|state| state.current_anchor = Some(newest)); - } else if state.current_anchor.as_ref().unwrap() != &newest { - if let Some(tx_id) = self.update_state(|state| state.current_tx.take()) { - self.update_active_editor(cx, |_, editor, cx| { + if self.mode == Mode::Insert && self.current_tx.is_some() { + if self.current_anchor.is_none() { + self.current_anchor = Some(newest); + } else if self.current_anchor.as_ref().unwrap() != &newest { + if let Some(tx_id) = self.current_tx.take() { + self.update_editor(cx, |_, editor, cx| { editor.group_until_transaction(tx_id, cx) }); } } - } else if state.mode == Mode::Normal && newest.start != newest.end { + } else if self.mode == Mode::Normal && newest.start != newest.end { if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { self.switch_mode(Mode::VisualBlock, false, cx); } else { @@ -817,18 +878,18 @@ impl Vim { } } else if newest.start == newest.end && !is_multicursor - && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode) + && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&self.mode) { self.switch_mode(Mode::Normal, true, cx); } } - fn active_editor_input_ignored(text: Arc, cx: &mut WindowContext) { + fn input_ignored(&mut self, text: Arc, cx: &mut ViewContext) { if text.is_empty() { return; } - match Vim::read(cx).active_operator() { + match self.active_operator() { Some(Operator::FindForward { before }) => { let find = Motion::FindForward { before, @@ -840,10 +901,8 @@ impl Vim { }, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::update(cx, |vim, _| { - vim.workspace_state.last_find = Some(find.clone()) - }); - motion::motion(find, cx) + Vim::globals(cx).last_find = Some(find.clone()); + self.motion(find, cx) } Some(Operator::FindBackward { after }) => { let find = Motion::FindBackward { @@ -856,69 +915,69 @@ impl Vim { }, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::update(cx, |vim, _| { - vim.workspace_state.last_find = Some(find.clone()) - }); - motion::motion(find, cx) + Vim::globals(cx).last_find = Some(find.clone()); + self.motion(find, cx) } - Some(Operator::Replace) => match Vim::read(cx).state().mode { - Mode::Normal => normal_replace(text, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), - _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), + Some(Operator::Replace) => match self.mode { + Mode::Normal => self.normal_replace(text, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + self.visual_replace(text, cx) + } + _ => self.clear_operator(cx), }, Some(Operator::Digraph { first_char }) => { if let Some(first_char) = first_char { if let Some(second_char) = text.chars().next() { - digraph::insert_digraph(first_char, second_char, cx); + self.insert_digraph(first_char, second_char, cx); } } else { let first_char = text.chars().next(); - Vim::update(cx, |vim, cx| { - vim.pop_operator(cx); - vim.push_operator(Operator::Digraph { first_char }, cx); - }); + self.pop_operator(cx); + self.push_operator(Operator::Digraph { first_char }, cx); } } - Some(Operator::AddSurrounds { target }) => match Vim::read(cx).state().mode { + Some(Operator::AddSurrounds { target }) => match self.mode { Mode::Normal => { if let Some(target) = target { - add_surrounds(text, target, cx); - Vim::update(cx, |vim, cx| vim.clear_operator(cx)); + self.add_surrounds(text, target, cx); + self.clear_operator(cx); } } Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - add_surrounds(text, SurroundsType::Selection, cx); - Vim::update(cx, |vim, cx| vim.clear_operator(cx)); + self.add_surrounds(text, SurroundsType::Selection, cx); + self.clear_operator(cx); } - _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), + _ => self.clear_operator(cx), }, - Some(Operator::ChangeSurrounds { target }) => match Vim::read(cx).state().mode { + Some(Operator::ChangeSurrounds { target }) => match self.mode { Mode::Normal => { if let Some(target) = target { - change_surrounds(text, target, cx); - Vim::update(cx, |vim, cx| vim.clear_operator(cx)); + self.change_surrounds(text, target, cx); + self.clear_operator(cx); } } - _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), + _ => self.clear_operator(cx), }, - Some(Operator::DeleteSurrounds) => match Vim::read(cx).state().mode { + Some(Operator::DeleteSurrounds) => match self.mode { Mode::Normal => { - delete_surrounds(text, cx); - Vim::update(cx, |vim, cx| vim.clear_operator(cx)); + self.delete_surrounds(text, cx); + self.clear_operator(cx); } - _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), + _ => self.clear_operator(cx), }, - Some(Operator::Mark) => Vim::update(cx, |vim, cx| { - normal::mark::create_mark(vim, text, false, cx) - }), - Some(Operator::RecordRegister) => record_register(text.chars().next().unwrap(), cx), - Some(Operator::ReplayRegister) => replay_register(text.chars().next().unwrap(), cx), - Some(Operator::Register) => Vim::update(cx, |vim, cx| match vim.state().mode { + Some(Operator::Mark) => self.create_mark(text, false, cx), + Some(Operator::RecordRegister) => { + self.record_register(text.chars().next().unwrap(), cx) + } + Some(Operator::ReplayRegister) => { + self.replay_register(text.chars().next().unwrap(), cx) + } + Some(Operator::Register) => match self.mode { Mode::Insert => { - vim.update_active_editor(cx, |vim, editor, cx| { - if let Some(register) = - vim.read_register(text.chars().next(), Some(editor), cx) - { + self.update_editor(cx, |_, editor, cx| { + if let Some(register) = Vim::update_globals(cx, |globals, cx| { + globals.read_register(text.chars().next(), Some(editor), cx) + }) { editor.do_paste( ®ister.text.to_string(), register.clipboard_selections.clone(), @@ -927,110 +986,30 @@ impl Vim { ) } }); - vim.clear_operator(cx); + self.clear_operator(cx); } _ => { - vim.select_register(text, cx); + self.select_register(text, cx); } - }), - Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx), - _ => match Vim::read(cx).state().mode { - Mode::Replace => multi_replace(text, cx), + }, + Some(Operator::Jump { line }) => self.jump(text, line, cx), + _ => match self.mode { + Mode::Replace => self.multi_replace(text, cx), _ => {} }, } } - fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { - if self.enabled == enabled { - return; - } - if !enabled { - CommandPaletteInterceptor::update_global(cx, |interceptor, _| { - interceptor.clear(); - }); - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.hide_namespace(Self::NAMESPACE); - }); - *self = Default::default(); - return; - } - - self.enabled = true; - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.show_namespace(Self::NAMESPACE); - }); - CommandPaletteInterceptor::update_global(cx, |interceptor, _| { - interceptor.set(Box::new(command::command_interceptor)); - }); - - if let Some(active_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - active_window - .update(cx, |workspace, cx| { - let active_editor = workspace.active_item_as::(cx); - if let Some(active_editor) = active_editor { - self.activate_editor(active_editor, cx); - self.switch_mode(Mode::Normal, false, cx); - } - }) - .ok(); - } - } - - /// Returns the state of the active editor. - pub fn state(&self) -> &EditorState { - if let Some(active_editor) = self.active_editor.as_ref() { - if let Some(state) = self.editor_states.get(&active_editor.entity_id()) { - return state; - } - } - - &self.default_state - } - - /// Updates the state of the active editor. - pub fn update_state(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T { - let mut state = self.state().clone(); - let ret = func(&mut state); - - if let Some(active_editor) = self.active_editor.as_ref() { - self.editor_states.insert(active_editor.entity_id(), state); - } - - ret - } - - fn sync_vim_settings(&mut self, cx: &mut WindowContext) { - self.update_active_editor(cx, |vim, editor, cx| { - let state = vim.state(); - editor.set_cursor_shape(state.cursor_shape(), cx); - editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); + fn sync_vim_settings(&mut self, cx: &mut ViewContext) { + self.update_editor(cx, |vim, editor, cx| { + editor.set_cursor_shape(vim.cursor_shape(), cx); + editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); editor.set_collapse_matches(true); - editor.set_input_enabled(state.editor_input_enabled()); - editor.set_autoindent(state.should_autoindent()); - editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); - if editor.is_focused(cx) || editor.mouse_menu_is_focused(cx) { - editor.set_keymap_context_layer::(state.keymap_context_layer(), cx); - // disable vim mode if a sub-editor (inline assist, rename, etc.) is focused - } else if editor.focus_handle(cx).contains_focused(cx) { - editor.remove_keymap_context_layer::(cx); - } + editor.set_input_enabled(vim.editor_input_enabled()); + editor.set_autoindent(vim.should_autoindent()); + editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); }); - } - - fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext) { - if editor.mode() == EditorMode::Full { - editor.set_cursor_shape(CursorShape::Bar, cx); - editor.set_clip_at_line_ends(false, cx); - editor.set_collapse_matches(false); - editor.set_input_enabled(true); - editor.set_autoindent(true); - editor.selections.line_mode = false; - } - editor.remove_keymap_context_layer::(cx) + cx.notify() } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ea0449b2493d7766e21075633f973d77781a43ca..a4d0441b80bcf6f89c44d74e97fa808a8a18b99c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -7,17 +7,15 @@ use editor::{ scroll::Autoscroll, Bias, DisplayPoint, Editor, ToOffset, }; -use gpui::{actions, ViewContext, WindowContext}; +use gpui::{actions, ViewContext}; use language::{Point, Selection, SelectionGoal}; use multi_buffer::MultiBufferRow; use search::BufferSearchBar; use util::ResultExt; -use workspace::{searchable::Direction, Workspace}; +use workspace::searchable::Direction; use crate::{ motion::{start_of_line, Motion}, - normal::yank::{copy_selections_content, yank_selections_content}, - normal::{mark::create_visual_marks, substitute::substitute}, object::Object, state::{Mode, Operator}, Vim, @@ -41,102 +39,87 @@ actions!( ] ); -pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext| { - toggle_mode(Mode::Visual, cx) +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| { + vim.toggle_mode(Mode::Visual, cx) }); - workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext| { - toggle_mode(Mode::VisualLine, cx) + Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| { + vim.toggle_mode(Mode::VisualLine, cx) }); - workspace.register_action( - |_, _: &ToggleVisualBlock, cx: &mut ViewContext| { - toggle_mode(Mode::VisualBlock, cx) - }, - ); - workspace.register_action(other_end); - workspace.register_action(|_, _: &VisualDelete, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - delete(vim, false, cx); - }); + Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| { + vim.toggle_mode(Mode::VisualBlock, cx) }); - workspace.register_action(|_, _: &VisualDeleteLine, cx| { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - delete(vim, true, cx); - }); + Vim::action(editor, cx, Vim::other_end); + Vim::action(editor, cx, |vim, _: &VisualDelete, cx| { + vim.record_current_action(cx); + vim.visual_delete(false, cx); }); - workspace.register_action(|_, _: &VisualYank, cx| { - Vim::update(cx, |vim, cx| { - yank(vim, cx); - }); + Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| { + vim.record_current_action(cx); + vim.visual_delete(true, cx); }); + Vim::action(editor, cx, |vim, _: &VisualYank, cx| vim.visual_yank(cx)); - workspace.register_action(select_next); - workspace.register_action(select_previous); - workspace.register_action(|workspace, _: &SelectNextMatch, cx| { - Vim::update(cx, |vim, cx| { - select_match(workspace, vim, Direction::Next, cx); - }); + Vim::action(editor, cx, Vim::select_next); + Vim::action(editor, cx, Vim::select_previous); + Vim::action(editor, cx, |vim, _: &SelectNextMatch, cx| { + vim.select_match(Direction::Next, cx); }); - workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| { - Vim::update(cx, |vim, cx| { - select_match(workspace, vim, Direction::Prev, cx); - }); + Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| { + vim.select_match(Direction::Prev, cx); }); - workspace.register_action(|_, _: &RestoreVisualSelection, cx| { - Vim::update(cx, |vim, cx| { - let Some((stored_mode, reversed)) = - vim.update_state(|state| state.stored_visual_mode.take()) - else { - return; - }; - let Some((start, end)) = vim.state().marks.get("<").zip(vim.state().marks.get(">")) - else { - return; - }; - let ranges = start - .into_iter() - .zip(end) - .zip(reversed) - .map(|((start, end), reversed)| (*start, *end, reversed)) - .collect::>(); - - if vim.state().mode.is_visual() { - create_visual_marks(vim, vim.state().mode, cx); - } + Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| { + let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else { + return; + }; + let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else { + return; + }; + let ranges = start + .into_iter() + .zip(end) + .zip(reversed) + .map(|((start, end), reversed)| (*start, *end, reversed)) + .collect::>(); + + if vim.mode.is_visual() { + vim.create_visual_marks(vim.mode, cx); + } - vim.update_active_editor(cx, |_, editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - let map = s.display_map(); - let ranges = ranges - .into_iter() - .map(|(start, end, reversed)| { - let new_end = - movement::saturating_right(&map, end.to_display_point(&map)); - Selection { - id: s.new_selection_id(), - start: start.to_offset(&map.buffer_snapshot), - end: new_end.to_offset(&map, Bias::Left), - reversed, - goal: SelectionGoal::None, - } - }) - .collect(); - s.select(ranges); - }) - }); - vim.switch_mode(stored_mode, true, cx) + vim.update_editor(cx, |_, editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let map = s.display_map(); + let ranges = ranges + .into_iter() + .map(|(start, end, reversed)| { + let new_end = movement::saturating_right(&map, end.to_display_point(&map)); + Selection { + id: s.new_selection_id(), + start: start.to_offset(&map.buffer_snapshot), + end: new_end.to_offset(&map, Bias::Left), + reversed, + goal: SelectionGoal::None, + } + }) + .collect(); + s.select(ranges); + }) }); + vim.switch_mode(stored_mode, true, cx) }); } -pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |vim, editor, cx| { +impl Vim { + pub fn visual_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); - if vim.state().mode == Mode::VisualBlock + if vim.mode == Mode::VisualBlock && !matches!( motion, Motion::EndOfLine { @@ -145,7 +128,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex ) { let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); - visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { + vim.visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times, &text_layout_details) }) } else { @@ -183,7 +166,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { - let next_point = if vim.state().mode == Mode::VisualBlock { + let next_point = if vim.mode == Mode::VisualBlock { movement::saturating_right(map, selection.end) } else { movement::right(map, selection.end) @@ -206,127 +189,126 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex }); } }); - }); -} - -pub fn visual_block_motion( - preserve_goal: bool, - editor: &mut Editor, - cx: &mut ViewContext, - mut move_selection: impl FnMut( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> Option<(DisplayPoint, SelectionGoal)>, -) { - let text_layout_details = editor.text_layout_details(cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - let map = &s.display_map(); - let mut head = s.newest_anchor().head().to_display_point(map); - let mut tail = s.oldest_anchor().tail().to_display_point(map); - - let mut head_x = map.x_for_display_point(head, &text_layout_details); - let mut tail_x = map.x_for_display_point(tail, &text_layout_details); - - let (start, end) = match s.newest_anchor().goal { - SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), - SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), - _ => (tail_x.0, head_x.0), - }; - let mut goal = SelectionGoal::HorizontalRange { start, end }; - - let was_reversed = tail_x > head_x; - if !was_reversed && !preserve_goal { - head = movement::saturating_left(map, head); - } + } - let Some((new_head, _)) = move_selection(&map, head, goal) else { - return; - }; - head = new_head; - head_x = map.x_for_display_point(head, &text_layout_details); - - let is_reversed = tail_x > head_x; - if was_reversed && !is_reversed { - tail = movement::saturating_left(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } else if !was_reversed && is_reversed { - tail = movement::saturating_right(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } - if !is_reversed && !preserve_goal { - head = movement::saturating_right(map, head); - head_x = map.x_for_display_point(head, &text_layout_details); - } + pub fn visual_block_motion( + &mut self, + preserve_goal: bool, + editor: &mut Editor, + cx: &mut ViewContext, + mut move_selection: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> Option<(DisplayPoint, SelectionGoal)>, + ) { + let text_layout_details = editor.text_layout_details(cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let map = &s.display_map(); + let mut head = s.newest_anchor().head().to_display_point(map); + let mut tail = s.oldest_anchor().tail().to_display_point(map); - let positions = if is_reversed { - head_x..tail_x - } else { - tail_x..head_x - }; + let mut head_x = map.x_for_display_point(head, &text_layout_details); + let mut tail_x = map.x_for_display_point(tail, &text_layout_details); - if !preserve_goal { - goal = SelectionGoal::HorizontalRange { - start: positions.start.0, - end: positions.end.0, + let (start, end) = match s.newest_anchor().goal { + SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), + SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), + _ => (tail_x.0, head_x.0), }; - } + let mut goal = SelectionGoal::HorizontalRange { start, end }; - let mut selections = Vec::new(); - let mut row = tail.row(); - - loop { - let laid_out_line = map.layout_row(row, &text_layout_details); - let start = DisplayPoint::new( - row, - laid_out_line.closest_index_for_x(positions.start) as u32, - ); - let mut end = - DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); - if end <= start { - if start.column() == map.line_len(start.row()) { - end = start; - } else { - end = movement::saturating_right(map, start); - } + let was_reversed = tail_x > head_x; + if !was_reversed && !preserve_goal { + head = movement::saturating_left(map, head); } - if positions.start <= laid_out_line.width { - let selection = Selection { - id: s.new_selection_id(), - start: start.to_point(map), - end: end.to_point(map), - reversed: is_reversed, - goal, - }; + let Some((new_head, _)) = move_selection(&map, head, goal) else { + return; + }; + head = new_head; + head_x = map.x_for_display_point(head, &text_layout_details); - selections.push(selection); + let is_reversed = tail_x > head_x; + if was_reversed && !is_reversed { + tail = movement::saturating_left(map, tail); + tail_x = map.x_for_display_point(tail, &text_layout_details); + } else if !was_reversed && is_reversed { + tail = movement::saturating_right(map, tail); + tail_x = map.x_for_display_point(tail, &text_layout_details); } - if row == head.row() { - break; + if !is_reversed && !preserve_goal { + head = movement::saturating_right(map, head); + head_x = map.x_for_display_point(head, &text_layout_details); } - if tail.row() > head.row() { - row.0 -= 1 + + let positions = if is_reversed { + head_x..tail_x } else { - row.0 += 1 + tail_x..head_x + }; + + if !preserve_goal { + goal = SelectionGoal::HorizontalRange { + start: positions.start.0, + end: positions.end.0, + }; } - } - s.select(selections); - }) -} + let mut selections = Vec::new(); + let mut row = tail.row(); + + loop { + let laid_out_line = map.layout_row(row, &text_layout_details); + let start = DisplayPoint::new( + row, + laid_out_line.closest_index_for_x(positions.start) as u32, + ); + let mut end = + DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); + if end <= start { + if start.column() == map.line_len(start.row()) { + end = start; + } else { + end = movement::saturating_right(map, start); + } + } + + if positions.start <= laid_out_line.width { + let selection = Selection { + id: s.new_selection_id(), + start: start.to_point(map), + end: end.to_point(map), + reversed: is_reversed, + goal, + }; + + selections.push(selection); + } + if row == head.row() { + break; + } + if tail.row() > head.row() { + row.0 -= 1 + } else { + row.0 += 1 + } + } + + s.select(selections); + }) + } -pub fn visual_object(object: Object, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - if let Some(Operator::Object { around }) = vim.active_operator() { - vim.pop_operator(cx); - let current_mode = vim.state().mode; + pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext) { + if let Some(Operator::Object { around }) = self.active_operator() { + self.pop_operator(cx); + let current_mode = self.mode; let target_mode = object.target_visual_mode(current_mode); if target_mode != current_mode { - vim.switch_mode(target_mode, true, cx); + self.switch_mode(target_mode, true, cx); } - vim.update_active_editor(cx, |_, editor, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut mut_selection = selection.clone(); @@ -384,109 +366,103 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { }); }); } - }); -} + } -fn toggle_mode(mode: Mode, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - if vim.state().mode == mode { - vim.switch_mode(Mode::Normal, false, cx); + fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext) { + if self.mode == mode { + self.switch_mode(Mode::Normal, false, cx); } else { - vim.switch_mode(mode, false, cx); + self.switch_mode(mode, false, cx); } - }) -} + } -pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |_, editor, cx| { + pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext) { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; }) }) - }) - }); -} + }); + } -pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) { - vim.store_visual_marks(cx); - vim.update_active_editor(cx, |vim, editor, cx| { - let mut original_columns: HashMap<_, _> = Default::default(); - let line_mode = line_mode || editor.selections.line_mode; + pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext) { + self.store_visual_marks(cx); + self.update_editor(cx, |vim, editor, cx| { + let mut original_columns: HashMap<_, _> = Default::default(); + let line_mode = line_mode || editor.selections.line_mode; - editor.transact(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); - } - original_columns.insert(selection.id, position.to_point(map).column); - if vim.state().mode == Mode::VisualBlock { - *selection.end.column_mut() = map.line_len(selection.end.row()) - } else if vim.state().mode != Mode::VisualLine { - selection.start = DisplayPoint::new(selection.start.row(), 0); - if selection.end.row() == map.max_point().row() { - selection.end = map.max_point() - } else { - *selection.end.row_mut() += 1; - *selection.end.column_mut() = 0; + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if line_mode { + let mut position = selection.head(); + if !selection.reversed { + position = movement::left(map, position); + } + original_columns.insert(selection.id, position.to_point(map).column); + if vim.mode == Mode::VisualBlock { + *selection.end.column_mut() = map.line_len(selection.end.row()) + } else if vim.mode != Mode::VisualLine { + selection.start = DisplayPoint::new(selection.start.row(), 0); + if selection.end.row() == map.max_point().row() { + selection.end = map.max_point() + } else { + *selection.end.row_mut() += 1; + *selection.end.column_mut() = 0; + } } } - } - selection.goal = SelectionGoal::None; + selection.goal = SelectionGoal::None; + }); }); - }); - copy_selections_content(vim, editor, line_mode, cx); - editor.insert("", cx); + vim.copy_selections_content(editor, line_mode, cx); + editor.insert("", cx); - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head().to_point(map); - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column + if let Some(column) = original_columns.get(&selection.id) { + cursor.column = *column + } + let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); + if vim.mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) }); - if vim.state().mode == Mode::VisualBlock { + }) + }); + self.switch_mode(Mode::Normal, true, cx); + } + + pub fn visual_yank(&mut self, cx: &mut ViewContext) { + self.store_visual_marks(cx); + self.update_editor(cx, |vim, editor, cx| { + let line_mode = editor.selections.line_mode; + vim.yank_selections_content(editor, line_mode, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if line_mode { + selection.start = start_of_line(map, false, selection.start); + }; + selection.collapse_to(selection.start, SelectionGoal::None) + }); + if vim.mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); - }) - }); - vim.switch_mode(Mode::Normal, true, cx); -} - -pub fn yank(vim: &mut Vim, cx: &mut WindowContext) { - vim.store_visual_marks(cx); - vim.update_active_editor(cx, |vim, editor, cx| { - let line_mode = editor.selections.line_mode; - yank_selections_content(vim, editor, line_mode, cx); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - selection.start = start_of_line(map, false, selection.start); - }; - selection.collapse_to(selection.start, SelectionGoal::None) - }); - if vim.state().mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } }); - }); - vim.switch_mode(Mode::Normal, true, cx); -} + self.switch_mode(Mode::Normal, true, cx); + } -pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); - vim.update_active_editor(cx, |_, editor, cx| { + pub(crate) fn visual_replace(&mut self, text: Arc, cx: &mut ViewContext) { + self.stop_recording(cx); + self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { let (display_map, selections) = editor.selections.all_adjusted_display(cx); @@ -522,16 +498,14 @@ pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors)); }); }); - vim.switch_mode(Mode::Normal, false, cx); - }); -} + self.switch_mode(Mode::Normal, false, cx); + } -pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - let count = - vim.take_count(cx) - .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |_, editor, cx| { + pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + let count = self + .take_count(cx) + .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); + self.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); for _ in 0..count { if editor @@ -542,16 +516,14 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - let count = - vim.take_count(cx) - .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); - vim.update_active_editor(cx, |_, editor, cx| { + pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext) { + let count = self + .take_count(cx) + .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); + self.update_editor(cx, |_, editor, cx| { for _ in 0..count { if editor .select_previous(&Default::default(), cx) @@ -561,89 +533,91 @@ pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewConte break; } } - }) - }); -} + }); + } -pub fn select_match( - workspace: &mut Workspace, - vim: &mut Vim, - direction: Direction, - cx: &mut WindowContext, -) { - let count = vim.take_count(cx).unwrap_or(1); - let pane = workspace.active_pane().clone(); - let vim_is_normal = vim.state().mode == Mode::Normal; - let mut start_selection = 0usize; - let mut end_selection = 0usize; - - vim.update_active_editor(cx, |_, editor, _| { - editor.set_collapse_matches(false); - }); - if vim_is_normal { + pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + let count = self.take_count(cx).unwrap_or(1); + let Some(workspace) = self + .editor + .upgrade() + .and_then(|editor| editor.read(cx).workspace()) + else { + return; + }; + let pane = workspace.read(cx).active_pane().clone(); + let vim_is_normal = self.mode == Mode::Normal; + let mut start_selection = 0usize; + let mut end_selection = 0usize; + + self.update_editor(cx, |_, editor, _| { + editor.set_collapse_matches(false); + }); + if vim_is_normal { + 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.has_active_match() || !search_bar.show(cx) { + return; + } + // without update_match_index there is a bug when the cursor is before the first match + search_bar.update_match_index(cx); + search_bar.select_match(direction.opposite(), 1, cx); + }); + } + }); + } + self.update_editor(cx, |_, editor, cx| { + let latest = editor.selections.newest::(cx); + start_selection = latest.start; + end_selection = latest.end; + }); + + let mut match_exists = false; 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.has_active_match() || !search_bar.show(cx) { - return; - } - // without update_match_index there is a bug when the cursor is before the first match search_bar.update_match_index(cx); - search_bar.select_match(direction.opposite(), 1, cx); + search_bar.select_match(direction, count, cx); + match_exists = search_bar.match_exists(cx); }); } }); - } - vim.update_active_editor(cx, |_, editor, cx| { - let latest = editor.selections.newest::(cx); - start_selection = latest.start; - end_selection = latest.end; - }); - - let mut match_exists = false; - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - search_bar.update_match_index(cx); - search_bar.select_match(direction, count, cx); - match_exists = search_bar.match_exists(cx); - }); - } - }); - if !match_exists { - vim.clear_operator(cx); - vim.stop_replaying(cx); - return; - } - vim.update_active_editor(cx, |_, editor, cx| { - let latest = editor.selections.newest::(cx); - if vim_is_normal { - start_selection = latest.start; - end_selection = latest.end; - } else { - start_selection = start_selection.min(latest.start); - end_selection = end_selection.max(latest.end); - } - if direction == Direction::Prev { - std::mem::swap(&mut start_selection, &mut end_selection); + if !match_exists { + self.clear_operator(cx); + self.stop_replaying(cx); + return; } - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([start_selection..end_selection]); + self.update_editor(cx, |_, editor, cx| { + let latest = editor.selections.newest::(cx); + if vim_is_normal { + start_selection = latest.start; + end_selection = latest.end; + } else { + start_selection = start_selection.min(latest.start); + end_selection = end_selection.max(latest.end); + } + if direction == Direction::Prev { + std::mem::swap(&mut start_selection, &mut end_selection); + } + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([start_selection..end_selection]); + }); + editor.set_collapse_matches(true); }); - editor.set_collapse_matches(true); - }); - match vim.maybe_pop_operator() { - Some(Operator::Change) => substitute(vim, None, false, cx), - Some(Operator::Delete) => { - vim.stop_recording(); - delete(vim, false, cx) + match self.maybe_pop_operator() { + Some(Operator::Change) => self.substitute(None, false, cx), + Some(Operator::Delete) => { + self.stop_recording(cx); + self.visual_delete(false, cx) + } + Some(Operator::Yank) => self.visual_yank(cx), + _ => {} // Ignoring other operators } - Some(Operator::Yank) => yank(vim, cx), - _ => {} // Ignoring other operators } } - #[cfg(test)] mod test { use indoc::indoc;