diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2314c70e8..a83553565e 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 8ec6e806af..4867210cbe 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 78001458f1..69fcdd8319 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() - }); + 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 + })) + }) + }); + } - 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 - })) - }) - }); -} + 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; + }; -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)); - - 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(); + 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(); - vim.update_state(|state| { - state.change_list_position.take(); + 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 45c21de2ee..2345b76d91 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); +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, _: &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, - ); - }); - - 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(()); - }; - 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) - ]); - }) - }); - }) - }); - - Ok(()) - }) - .notify_err(workspace, cx); + 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; + }; + workspace.update(cx, |workspace, cx| { + e.notify_err(workspace, cx); + }); + 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)]); + }) + }); + 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 b9b6152434..74abce2a31 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); +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); - 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); + 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 48a59c3e4e..0000000000 --- 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 fbf2d5fc77..b015324a1b 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 1cfb598e7f..214462bc8d 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(), + 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(); + + Self { + vim: None, pending_keys: None, - _subscriptions, - }; - this.update_mode(cx); - this - } - - 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; + 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 29857ae989..5da9d44493 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) }, - ); - workspace.register_action( - |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| { - motion(Motion::PreviousWordEnd { 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, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| { - motion(Motion::NextSubwordStart { ignore_punctuation }, cx) + Vim::action( + editor, + cx, + |vim, &PreviousWordEnd { ignore_punctuation }, cx| { + vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action( - |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| { - motion(Motion::NextSubwordEnd { 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, - &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, - cx: _| { 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, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| { + vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx) }, ); - workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| { - motion(Motion::NextLineStart, cx) + Vim::action( + editor, + cx, + |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| { + vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) + }, + ); + Vim::action( + editor, + cx, + |vim, &PreviousSubwordEnd { ignore_punctuation }, cx| { + vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, 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) + }); + Vim::action(editor, cx, |vim, &GoToColumn, cx| { + vim.motion(Motion::GoToColumn, 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, _: &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 self.active_operator().is_none() { + return; + } } } + } + + self.motion(m, 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); + } + + 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 Vim::read(cx).active_operator().is_none() { - return; + 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) } } - } - } - - 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)); - } - - 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) + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + self.visual_motion(motion.clone(), count, cx) } } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - visual_motion(motion.clone(), count, cx) + self.clear_operator(cx); + if let Some(operator) = waiting_operator { + self.push_operator(operator, cx); + self.pre_count = count } } - Vim::update(cx, |vim, cx| { - vim.clear_operator(cx); - if let Some(operator) = waiting_operator { - vim.push_operator(operator, cx); - vim.update_state(|state| state.pre_count = count) - } - }); } // Motion handling is specified here: diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index bc2fefe7f5..c9d3a7a472 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); +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); - 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); - }) + 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) - } - }); - }); - - 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); - }); - }); - if vim.state().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); - }); - }); - if vim.state().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.update_editor(cx, |_, editor, cx| { + editor.transact(cx, |editor, cx| { + for _ in 0..times { + editor.join_lines(&Default::default(), 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); - } - }); - }) + }) + }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, 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); + 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.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, cx) + } + }); + + 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.mode.is_visual() { + vim.switch_mode(Mode::Normal, false, 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); + } + }); + }); + 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); + } + }); + }); + + 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 f9254ce5f0..fa9044ef9c 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 07486c6d93..dd9fbab0d8 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}; +use ui::ViewContext; -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| { +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(); + } + selection.start = start_offset.to_display_point(map); + } + result + } + } + }); + }); + vim.copy_selections_content(editor, motion.linewise(), cx); + editor.insert("", cx); + }); + }); + + if motion_succeeded { + self.switch_mode(Mode::Insert, false, cx) + } else { + self.switch_mode(Mode::Normal, false, cx) + } + } + + 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.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(); - } - selection.start = start_offset.to_display_point(map); - } - result - } - } + 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); + } }); - 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) - } -} - -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); - }); - }); - 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 e439981888..466b2ac250 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; +use ui::ViewContext; -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); +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 - { - if selection.is_empty() - && map - .buffer_snapshot - .line_len(MultiBufferRow(selection.start.to_point(&map).row)) - == 0 + // 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) - } - } - }); - }); - 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 - } - } - 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); + 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) } - }; - 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); - } - } + }); }); - }); - copy_selections_content(vim, editor, false, cx); - editor.insert("", cx); + 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 should_move_to_start.contains(&selection.id) { - *cursor.column_mut() = 0; - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) + // 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) + }); }); }); }); - }); + } + + 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); + } + } + + // 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); + } + } + }); + }); + 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 e0638f50de..5002bb5a04 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))) - } - } - 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))) - } - } - } - } - editor.transact(cx, |editor, cx| { - editor.edit(edits, cx); +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); - 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) + 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))) } - new_ranges.push(point..point); } - s.select_ranges(new_ranges) - }) + 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))) + } + } + } + } + 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); + } + 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 c69b0712b3..4b4d5e7e80 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 eb9dcb2a8c..7f9af3e06c 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); -} +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); + } -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![]; - - 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 84f144a16a..d1cc5d53b1 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); -} +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); -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); - - 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 78ae5b9ec4..6e327fee03 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); - }); -} + .clone(); + replayer.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); - - 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) + 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::VisualLine { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualLine, false, cx) - } - 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, mode)) + }) else { + return; + }; + if let Some(mode) = mode { + self.switch_mode(mode, false, cx) } - Some((actions, selection)) - }) else { - return; - }; - - 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 } => { - 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(); + RecordedSelection::VisualLine { rows } => { + self.visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); } + RecordedSelection::None => {} } - let mut new_actions = actions.clone(); - actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); + // 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 count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); + let mut new_actions = actions.clone(); + actions[0] = ReplayableAction::Action(to_repeat.boxed_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(); + 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(); + } + + for _ in 1..count { + new_actions.append(actions.clone().as_mut()); + } + new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); + actions = new_actions; } - for _ in 1..count { - new_actions.append(actions.clone().as_mut()); - } - new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); - actions = new_actions; - } + actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - - Vim::update(cx, |vim, cx| { - vim.workspace_state.dot_replaying = true; - - 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 d754cc7bb1..6aceb06425 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 72872f2133..c417372f87 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()); - - let new_selections = vim.editor_selections(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(); - } - - 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, - }); - }); - } - }); - }); - - if let Some(motion) = motion { - search_motion(motion, cx) } -} -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); - - 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); - - let new_selections = vim.editor_selections(cx); - motion = Some(Motion::ZedSearchResult { - prior_selections, - new_selections, - }); - }) - } - }) - }); - if let Some(motion) = motion { - search_motion(motion, cx); + // 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(); } -} -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); - - 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); + 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); + + 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(); + + let query = search_bar.query(cx).into(); + Vim::globals(cx).registers.insert('/', query); + Some((prior_selections, prior_mode, prior_operator)) + }) }); - if vim.state().mode.is_visual() { - vim.switch_mode(Mode::Normal, false, cx) - } - }); -} + let Some((mut prior_selections, prior_mode, prior_operator)) = result else { + 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); + + // 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(); + } + + 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_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); + + 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 !success { + return; + } + + 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); + }) { + workspace.update(cx, |workspace, cx| { + 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) - } - 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 39576819d6..dc27e2b219 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 cbd00e757e..a8a675a7e7 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 922bb7c7f2..9694fb5135 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); - }); - }); - 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); - }); - }); - }); - }); -} - -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); - }); - }); - 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); - }); - }); - }); - }); -} - -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); -} struct HighlightOnYank; -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(); +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); + }); + }); + 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); + }); + }); + }); + }); + } - vim.update_state(|state| { - state.marks.insert( + 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); + }); + }); + 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( + &mut self, + editor: &mut Editor, + linewise: bool, + cx: &mut ViewContext, + ) { + self.copy_selections_content_internal(editor, linewise, true, cx); + } + + 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( + &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 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, + }); } - 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, - }); } - } - 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, - ); + 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, + ) + }); - if !is_yank || vim.state().mode == Mode::Visual { - return; - } + 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) + 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 fc011b0b91..8d7ee051a3 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 3b00a3fafe..6256c7e434 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 6223aae942..db8cffa2bb 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 7c0b2449de..12bf490db1 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 dc447708c6..ce5559528e 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 075f5621d7..16ab7771bd 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 f1137d9029..5ae4d517b0 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 26ddca9d85..fe3236a52c 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(); + + 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 update(cx: &mut WindowContext, update: F) -> S - where - F: FnOnce(&mut Self, &mut WindowContext) -> S, - { - cx.update_global(update) - } - - 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 { + let mut was_enabled = Vim::enabled(cx); + cx.observe_global::(move |editor, cx| { + let enabled = Vim::enabled(cx); + if was_enabled == enabled { + return; + } + was_enabled = enabled; + if enabled { + Self::activate(editor, cx) + } else { + Self::deactivate(editor, cx) + } + }) + .detach(); + if was_enabled { + Self::activate(editor, cx) + } + } + + fn activate(editor: &mut Editor, cx: &mut ViewContext) { + let vim = Vim::new(cx); + + editor.register_addon(VimAddon { view: vim.clone() }); + + 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 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::(); + } + + /// 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(); + } + + pub fn editor(&self) -> Option> { + self.editor.upgrade() + } + + pub fn workspace(&self, cx: &ViewContext) -> Option> { + self.editor().and_then(|editor| editor.read(cx).workspace()) + } + + pub fn pane(&self, cx: &ViewContext) -> Option> { + self.workspace(cx) + .and_then(|workspace| workspace.read(cx).pane_for(&self.editor()?)) + } + + pub fn enabled(cx: &mut AppContext) -> bool { + VimModeSetting::get_global(cx).0 + } + + /// 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; + } + + 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) + } + } + } + + 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 } => { - if editor.read(cx).leader_peer_id().is_none() { - Vim::update(cx, |vim, cx| { - vim.local_selections_changed(editor, cx); - }) - } + self.local_selections_changed(cx); } EditorEvent::InputIgnored { text } => { - Vim::active_editor_input_ignored(text.clone(), cx); - observe_insertion(text, None, cx) + self.input_ignored(text.clone(), cx); + Vim::globals(cx).observe_insertion(text, None) } 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)) + } => Vim::globals(cx).observe_insertion(text, range_to_replace.clone()), + EditorEvent::TransactionBegun { transaction_id } => { + self.transaction_begun(*transaction_id, cx) } - EditorEvent::FocusedIn => Vim::update(cx, |vim, cx| vim.sync_vim_settings(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), _ => {} - })); - - let editor = editor.read(cx); - let editor_mode = editor.mode(); - let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - - 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); } + } + 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); } - 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 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() - } - - /// 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), - ) - }); - - 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 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(); } - } - - 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(); - } - } - - /// 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; - } - } - - /// 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; - } - } - - /// 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(); - } - - // 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 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(); - } - }); 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(); - }); + 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 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 { + 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(); + + 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 { + 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 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); - } - } - } - } 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()); - } else { - self.workspace_state.last_yank = cx - .read_from_clipboard() - .and_then(|item| item.text().map(|string| string.into())) - } + 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); + }); + } - self.workspace_state.registers.insert('"', content.clone()); - if is_yank { - self.workspace_state.registers.insert('0', content); - } 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; + 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, } - } - } - } - } - - fn read_register( - &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 + globals.recorded_selection = RecordedSelection::None; } - }), - _ => self.workspace_state.registers.get(&lower).cloned(), - } - } - - 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 } }) } - 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)); + 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(); + } + } + + /// 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, + 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; + } + } + + /// 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_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 ea0449b249..a4d0441b80 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)); + + 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); + }); + Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| { + vim.select_match(Direction::Prev, 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); - }); - }); - workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| { - Vim::update(cx, |vim, cx| { - select_match(workspace, vim, Direction::Prev, 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::>(); - 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.mode.is_visual() { + vim.create_visual_marks(vim.mode, cx); + } - if vim.state().mode.is_visual() { - create_visual_marks(vim, vim.state().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); + 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 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 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); - } - - let positions = if is_reversed { - head_x..tail_x - } else { - tail_x..head_x - }; - - 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(); + let was_reversed = tail_x > head_x; + if !was_reversed && !preserve_goal { + head = movement::saturating_left(map, head); + } - 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; + 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); + } + + let positions = if is_reversed { + head_x..tail_x + } else { + tail_x..head_x + }; + + if !preserve_goal { + goal = SelectionGoal::HorizontalRange { + start: positions.start.0, + end: positions.end.0, + }; + } + + 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 { - end = movement::saturating_right(map, start); + row.0 += 1 } } - 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, - }; + s.select(selections); + }) + } - 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| { + // 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 + } + 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()]) + } + }); + }) + }); + 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| { - let mut cursor = selection.head().to_point(map); - - 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 line_mode { + selection.start = start_of_line(map, false, selection.start); + }; + selection.collapse_to(selection.start, SelectionGoal::None) }); - if vim.state().mode == Mode::VisualBlock { + 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; + 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; - vim.update_active_editor(cx, |_, editor, _| { - editor.set_collapse_matches(false); - }); - if vim_is_normal { + 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 { + self.clear_operator(cx); + self.stop_replaying(cx); + return; + } + 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]); }); - } - }); - 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); - } - 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;