diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4c099ad107..83e332a3f4 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -304,7 +304,8 @@ "ctrl-q": ["vim::PushOperator", { "Literal": {} }], "ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }], "ctrl-r": ["vim::PushOperator", "Register"], - "insert": "vim::ToggleReplace" + "insert": "vim::ToggleReplace", + "ctrl-o": "vim::TemporaryNormal" } }, { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index b015324a1b..ba83e2125b 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -3,10 +3,11 @@ use editor::{scroll::Autoscroll, Bias, Editor}; use gpui::{actions, Action, ViewContext}; use language::SelectionGoal; -actions!(vim, [NormalBefore]); +actions!(vim, [NormalBefore, TemporaryNormal]); pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, Vim::normal_before); + Vim::action(editor, cx, Vim::temporary_normal); } impl Vim { @@ -35,6 +36,11 @@ impl Vim { self.repeat(true, cx) } + + fn temporary_normal(&mut self, _: &TemporaryNormal, cx: &mut ViewContext) { + self.switch_mode(Mode::Normal, true, cx); + self.temp_mode = true; + } } #[cfg(test)] diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index e5bb31944b..619bb6e1f4 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -88,12 +88,19 @@ impl Render for ModeIndicator { return div().into_any(); }; + let vim_readable = vim.read(cx); + let mode = if vim_readable.temp_mode { + format!("(insert) {}", vim_readable.mode) + } else { + vim_readable.mode.to_string() + }; + 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)) + Label::new(format!("{} -- {} --", pending, mode)) .size(LabelSize::Small) .line_height_style(LineHeightStyle::UiLabel) .into_any_element() diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 52f922cd06..37a8115e33 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -185,6 +185,8 @@ impl Vim { error!("Unexpected normal mode motion operator: {:?}", operator) } } + // Exit temporary normal mode (if active). + self.exit_temporary_normal(cx); } pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext) { @@ -483,6 +485,12 @@ impl Vim { }); }); } + + fn exit_temporary_normal(&mut self, cx: &mut ViewContext) { + if self.temp_mode { + self.switch_mode(Mode::Insert, true, cx); + } + } } #[cfg(test)] mod test { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 5322f913c1..feb060d594 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -176,7 +176,7 @@ impl Vim { .0; } cursor = movement::indented_line_beginning(map, cursor, true); - } else if !is_multiline { + } else if !is_multiline && !vim.temp_mode { cursor = movement::saturating_left(map, cursor) } cursors.push(cursor); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index e0e34ceda7..c89b63ecc6 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc}; use crate::{ insert::NormalBefore, motion::Motion, + normal::InsertBefore, state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals}, Vim, }; @@ -308,6 +309,11 @@ impl Vim { actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); + if self.temp_mode { + self.temp_mode = false; + actions.push(ReplayableAction::Action(InsertBefore.boxed_clone())); + } + let globals = Vim::globals(cx); globals.dot_replaying = true; let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone(); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 6a9651e788..5d78c8937e 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -139,6 +139,11 @@ impl Vim { options |= SearchOptions::REGEX; } search_bar.set_search_options(options, cx); + let prior_mode = if self.temp_mode { + Mode::Insert + } else { + self.mode + }; self.search = SearchState { direction, @@ -146,7 +151,7 @@ impl Vim { initial_query: query, prior_selections, prior_operator: self.operator_stack.last().cloned(), - prior_mode: self.mode, + prior_mode, } }); } diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index c176cd6ca9..763f1a3d16 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -42,6 +42,7 @@ impl Vim { }); }); }); + self.exit_temporary_normal(cx); } pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext) { @@ -65,6 +66,7 @@ impl Vim { }); }); }); + self.exit_temporary_normal(cx); } pub fn yank_selections_content( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index b229f53142..510ed6557d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -153,6 +153,7 @@ pub struct VimGlobals { pub stop_recording_after_next_action: bool, pub ignore_current_insertion: bool, pub recorded_count: Option, + pub recording_actions: Vec, pub recorded_actions: Vec, pub recorded_selection: RecordedSelection, @@ -339,11 +340,12 @@ impl VimGlobals { pub fn observe_action(&mut self, action: Box) { if self.dot_recording { - self.recorded_actions + self.recording_actions .push(ReplayableAction::Action(action.boxed_clone())); if self.stop_recording_after_next_action { self.dot_recording = false; + self.recorded_actions = std::mem::take(&mut self.recording_actions); self.stop_recording_after_next_action = false; } } @@ -363,12 +365,13 @@ impl VimGlobals { return; } if self.dot_recording { - self.recorded_actions.push(ReplayableAction::Insertion { + self.recording_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.recorded_actions = std::mem::take(&mut self.recording_actions); self.stop_recording_after_next_action = false; } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index d907a2921f..947353e2d3 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1570,3 +1570,36 @@ async fn test_sentence_forwards(cx: &mut gpui::TestAppContext) { cx.set_shared_state("helˇlo.\n\n\nworld.").await; } + +#[gpui::test] +async fn test_ctrl_o_visual(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("helloˇ world.").await; + cx.simulate_shared_keystrokes("i ctrl-o v b r l").await; + cx.shared_state().await.assert_eq("ˇllllllworld."); + cx.simulate_shared_keystrokes("ctrl-o v f w d").await; + cx.shared_state().await.assert_eq("ˇorld."); +} + +#[gpui::test] +async fn test_ctrl_o_position(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("helˇlo world.").await; + cx.simulate_shared_keystrokes("i ctrl-o d i w").await; + cx.shared_state().await.assert_eq("ˇ world."); + cx.simulate_shared_keystrokes("ctrl-o p").await; + cx.shared_state().await.assert_eq(" helloˇworld."); +} + +#[gpui::test] +async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("heˇllo world.").await; + cx.simulate_shared_keystrokes("x i ctrl-o .").await; + cx.shared_state().await.assert_eq("heˇo world."); + cx.simulate_shared_keystrokes("l l escape .").await; + cx.shared_state().await.assert_eq("hellˇllo world."); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a5c2249734..77fc7db9d6 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -28,7 +28,7 @@ use gpui::{ actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent, Render, Subscription, View, ViewContext, WeakView, }; -use insert::NormalBefore; +use insert::{NormalBefore, TemporaryNormal}; use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; @@ -38,7 +38,7 @@ use serde::Deserialize; use serde_derive::Serialize; use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; -use std::{ops::Range, sync::Arc}; +use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use ui::{IntoElement, VisualContext}; use workspace::{self, Pane, Workspace}; @@ -147,6 +147,8 @@ impl editor::Addon for VimAddon { pub(crate) struct Vim { pub(crate) mode: Mode, pub last_mode: Mode, + pub temp_mode: bool, + pub exit_temporary_mode: bool, /// pre_count is the number before an operator is specified (3 in 3d2d) pre_count: Option, @@ -197,6 +199,8 @@ impl Vim { cx.new_view(|cx| Vim { mode: Mode::Normal, last_mode: Mode::Normal, + temp_mode: false, + exit_temporary_mode: false, pre_count: None, post_count: None, operator_stack: Vec::new(), @@ -353,6 +357,16 @@ impl Vim { /// 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 self.exit_temporary_mode { + self.exit_temporary_mode = false; + // Don't switch to insert mode if the action is temporary_normal. + if let Some(action) = keystroke_event.action.as_ref() { + if action.as_any().downcast_ref::().is_some() { + return; + } + } + self.switch_mode(Mode::Insert, false, cx) + } 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::") { @@ -438,6 +452,17 @@ impl Vim { } pub fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut ViewContext) { + if self.temp_mode && mode == Mode::Normal { + self.temp_mode = false; + self.switch_mode(Mode::Normal, leave_selections, cx); + self.switch_mode(Mode::Insert, false, cx); + return; + } else if self.temp_mode + && !matches!(mode, Mode::Visual | Mode::VisualLine | Mode::VisualBlock) + { + self.temp_mode = false; + } + let last_mode = self.mode; let prior_mode = self.last_mode; let prior_tx = self.current_tx; @@ -729,7 +754,7 @@ impl Vim { Vim::update_globals(cx, |globals, cx| { if !globals.dot_replaying { globals.dot_recording = true; - globals.recorded_actions = Default::default(); + globals.recording_actions = Default::default(); globals.recorded_count = None; let selections = self.editor().map(|editor| { @@ -784,6 +809,7 @@ impl Vim { if globals.dot_recording { globals.stop_recording_after_next_action = true; } + self.exit_temporary_mode = self.temp_mode; } /// Stops recording actions immediately rather than waiting until after the @@ -798,11 +824,13 @@ impl Vim { let globals = Vim::globals(cx); if globals.dot_recording { globals - .recorded_actions + .recording_actions .push(ReplayableAction::Action(action.boxed_clone())); + globals.recorded_actions = mem::take(&mut globals.recording_actions); globals.dot_recording = false; globals.stop_recording_after_next_action = false; } + self.exit_temporary_mode = self.temp_mode; } /// Explicitly record one action (equivalents to start_recording and stop_recording) diff --git a/crates/vim/test_data/test_ctrl_o_dot.json b/crates/vim/test_data/test_ctrl_o_dot.json new file mode 100644 index 0000000000..e414d785bf --- /dev/null +++ b/crates/vim/test_data/test_ctrl_o_dot.json @@ -0,0 +1,11 @@ +{"Put":{"state":"heˇllo world."}} +{"Key":"x"} +{"Key":"i"} +{"Key":"ctrl-o"} +{"Key":"."} +{"Get":{"state":"heˇo world.","mode":"Insert"}} +{"Key":"l"} +{"Key":"l"} +{"Key":"escape"} +{"Key":"."} +{"Get":{"state":"hellˇllo world.","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_o_position.json b/crates/vim/test_data/test_ctrl_o_position.json new file mode 100644 index 0000000000..d8d76ac188 --- /dev/null +++ b/crates/vim/test_data/test_ctrl_o_position.json @@ -0,0 +1,10 @@ +{"Put":{"state":"helˇlo world."}} +{"Key":"i"} +{"Key":"ctrl-o"} +{"Key":"d"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"ˇ world.","mode":"Insert"}} +{"Key":"ctrl-o"} +{"Key":"p"} +{"Get":{"state":" helloˇworld.","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_o_visual.json b/crates/vim/test_data/test_ctrl_o_visual.json new file mode 100644 index 0000000000..23ec11d766 --- /dev/null +++ b/crates/vim/test_data/test_ctrl_o_visual.json @@ -0,0 +1,14 @@ +{"Put":{"state":"helloˇ world."}} +{"Key":"i"} +{"Key":"ctrl-o"} +{"Key":"v"} +{"Key":"b"} +{"Key":"r"} +{"Key":"l"} +{"Get":{"state":"ˇllllllworld.","mode":"Insert"}} +{"Key":"ctrl-o"} +{"Key":"v"} +{"Key":"f"} +{"Key":"w"} +{"Key":"d"} +{"Get":{"state":"ˇorld.","mode":"Insert"}}