Detailed changes
@@ -533,7 +533,7 @@
// TODO: Move this to a dock open action
"cmd-shift-c": "collab_panel::ToggleFocus",
"cmd-alt-i": "zed::DebugElements",
- "ctrl-:": "editor::ToggleInlayHints",
+ "ctrl-:": "editor::ToggleInlayHints"
}
},
{
@@ -499,7 +499,7 @@
"around": true
}
}
- ],
+ ]
}
},
{
@@ -1,6 +1,6 @@
-use crate::{state::Mode, Vim};
+use crate::{normal::repeat, state::Mode, Vim};
use editor::{scroll::autoscroll::Autoscroll, Bias};
-use gpui::{actions, AppContext, ViewContext};
+use gpui::{actions, Action, AppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
@@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(normal_before);
}
-fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
- Vim::update(cx, |vim, cx| {
- vim.stop_recording();
- vim.update_active_editor(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.move_cursors_with(|map, mut cursor, _| {
- *cursor.column_mut() = cursor.column().saturating_sub(1);
- (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
+ let should_repeat = Vim::update(cx, |vim, cx| {
+ let count = vim.take_count().unwrap_or(1);
+ vim.stop_recording_immediately(action.boxed_clone());
+ if count <= 1 || vim.workspace_state.replaying {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, mut cursor, _| {
+ *cursor.column_mut() = cursor.column().saturating_sub(1);
+ (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+ });
});
});
- });
- vim.switch_mode(Mode::Normal, false, cx);
- })
+ vim.switch_mode(Mode::Normal, false, cx);
+ false
+ } else {
+ true
+ }
+ });
+
+ if should_repeat {
+ repeat::repeat(cx, true)
+ }
}
#[cfg(test)]
mod test {
- use crate::{state::Mode, test::VimTestContext};
+ use std::sync::Arc;
+
+ use gpui::executor::Deterministic;
+
+ use crate::{
+ state::Mode,
+ test::{NeovimBackedTestContext, VimTestContext},
+ };
#[gpui::test]
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
@@ -40,4 +57,78 @@ mod test {
assert_eq!(cx.mode(), Mode::Normal);
cx.assert_editor_state("Tesˇt");
}
+
+ #[gpui::test]
+ async fn test_insert_with_counts(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("----ˇ-hello\n").await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("h----ˇ-ello\n").await;
+
+ cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("---ˇ-h-----ello\n").await;
+
+ cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("----h-----ello--ˇ-\n").await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
+ }
+
+ #[gpui::test]
+ async fn test_insert_with_repeat(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("--ˇ-hello\n").await;
+ cx.simulate_shared_keystrokes(["."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("----ˇ--hello\n").await;
+ cx.simulate_shared_keystrokes(["2", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("-----ˇ---hello\n").await;
+
+ cx.set_shared_state("ˇhello\n").await;
+ cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
+ .await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("hello\nkk\nkˇk\n").await;
+ cx.simulate_shared_keystrokes(["."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
+ cx.simulate_shared_keystrokes(["1", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
+ }
}
@@ -2,7 +2,7 @@ mod case;
mod change;
mod delete;
mod paste;
-mod repeat;
+pub(crate) mod repeat;
mod scroll;
mod search;
pub mod substitute;
@@ -1,10 +1,11 @@
use crate::{
+ insert::NormalBefore,
motion::Motion,
state::{Mode, RecordedSelection, ReplayableAction},
visual::visual_motion,
Vim,
};
-use gpui::{actions, Action, AppContext};
+use gpui::{actions, Action, AppContext, WindowContext};
use workspace::Workspace;
actions!(vim, [Repeat, EndRepeat,]);
@@ -17,6 +18,27 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
true
}
+fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
+ match action {
+ ReplayableAction::Action(action) => {
+ if super::InsertBefore.id() == action.id()
+ || super::InsertAfter.id() == action.id()
+ || super::InsertFirstNonWhitespace.id() == action.id()
+ || super::InsertEndOfLine.id() == action.id()
+ {
+ Some(super::InsertBefore.boxed_clone())
+ } else if super::InsertLineAbove.id() == action.id()
+ || super::InsertLineBelow.id() == action.id()
+ {
+ Some(super::InsertLineBelow.boxed_clone())
+ } else {
+ None
+ }
+ }
+ ReplayableAction::Insertion { .. } => None,
+ }
+}
+
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
Vim::update(cx, |vim, cx| {
@@ -28,127 +50,156 @@ pub(crate) fn init(cx: &mut AppContext) {
});
});
- cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
- let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
- let actions = vim.workspace_state.recorded_actions.clone();
- let Some(editor) = vim.active_editor.clone() else {
- return None;
- };
- let count = vim.take_count();
-
- vim.workspace_state.replaying = true;
-
- let selection = vim.workspace_state.recorded_selection.clone();
- match selection {
- RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
- vim.workspace_state.recorded_count = None;
- vim.switch_mode(Mode::Visual, false, cx)
- }
- RecordedSelection::VisualLine { .. } => {
- vim.workspace_state.recorded_count = None;
- vim.switch_mode(Mode::VisualLine, false, cx)
- }
- 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 let Some(editor) = editor.upgrade(cx) {
- editor.update(cx, |editor, _| {
- editor.show_local_selections = false;
- })
- } else {
- return None;
- }
+ cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
+}
- Some((actions, editor, selection))
- }) else {
- return;
+pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
+ let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
+ let actions = vim.workspace_state.recorded_actions.clone();
+ let Some(editor) = vim.active_editor.clone() else {
+ return None;
};
+ let count = vim.take_count();
+ let selection = vim.workspace_state.recorded_selection.clone();
match selection {
- RecordedSelection::SingleLine { cols } => {
- if cols > 1 {
- visual_motion(Motion::Right, Some(cols as usize - 1), cx)
- }
+ RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
+ vim.workspace_state.recorded_count = None;
+ vim.switch_mode(Mode::Visual, false, 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::VisualLine { .. } => {
+ vim.workspace_state.recorded_count = None;
+ vim.switch_mode(Mode::VisualLine, false, 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 { .. } => {
+ 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);
}
}
- RecordedSelection::VisualLine { rows } => {
- visual_motion(
- Motion::Down {
- display_lines: false,
- },
- Some(rows as usize),
- cx,
- );
+ }
+
+ if let Some(editor) = editor.upgrade(cx) {
+ editor.update(cx, |editor, _| {
+ editor.show_local_selections = false;
+ })
+ } else {
+ return None;
+ }
+
+ Some((actions, editor, selection))
+ }) else {
+ return;
+ };
+
+ match selection {
+ RecordedSelection::SingleLine { cols } => {
+ if cols > 1 {
+ 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::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::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 action.id() == NormalBefore.id() {
+ actions.pop();
}
- RecordedSelection::None => {}
}
- let window = cx.window();
- cx.app_context()
- .spawn(move |mut cx| async move {
- for action in actions {
- match action {
- ReplayableAction::Action(action) => {
- if should_replay(&action) {
- window
- .dispatch_action(editor.id(), action.as_ref(), &mut cx)
- .ok_or_else(|| anyhow::anyhow!("window was closed"))
- } else {
- Ok(())
- }
+ let mut new_actions = actions.clone();
+ actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
+
+ let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
+
+ // if we came from insert mode we're just doing repititions 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;
+ }
+
+ Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
+ let window = cx.window();
+ cx.app_context()
+ .spawn(move |mut cx| async move {
+ for action in actions {
+ match action {
+ ReplayableAction::Action(action) => {
+ if should_replay(&action) {
+ window
+ .dispatch_action(editor.id(), action.as_ref(), &mut cx)
+ .ok_or_else(|| anyhow::anyhow!("window was closed"))
+ } else {
+ Ok(())
}
- ReplayableAction::Insertion {
- text,
- utf16_range_to_replace,
- } => editor.update(&mut cx, |editor, cx| {
- editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
- }),
- }?
- }
- window
- .dispatch_action(editor.id(), &EndRepeat, &mut cx)
- .ok_or_else(|| anyhow::anyhow!("window was closed"))
- })
- .detach_and_log_err(cx);
- });
+ }
+ ReplayableAction::Insertion {
+ text,
+ utf16_range_to_replace,
+ } => editor.update(&mut cx, |editor, cx| {
+ editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
+ }),
+ }?
+ }
+ window
+ .dispatch_action(editor.id(), &EndRepeat, &mut cx)
+ .ok_or_else(|| anyhow::anyhow!("window was closed"))
+ })
+ .detach_and_log_err(cx);
}
#[cfg(test)]
@@ -15,8 +15,8 @@ use anyhow::Result;
use collections::{CommandPaletteFilter, HashMap};
use editor::{movement, Editor, EditorMode, Event};
use gpui::{
- actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
- Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
+ AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::{CursorShape, Point, Selection, SelectionGoal};
pub use mode_indicator::ModeIndicator;
@@ -284,6 +284,16 @@ impl Vim {
}
}
+ pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
+ if self.workspace_state.recording {
+ self.workspace_state
+ .recorded_actions
+ .push(ReplayableAction::Action(action.boxed_clone()));
+ self.workspace_state.recording = false;
+ self.workspace_state.stop_recording_after_next_action = false;
+ }
+ }
+
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
self.start_recording(cx);
self.stop_recording();
@@ -0,0 +1,36 @@
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"5"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}}
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"5"}
+{"Key":"a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}}
+{"Key":"4"}
+{"Key":"shift-i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}}
+{"Key":"3"}
+{"Key":"shift-a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}}
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"3"}
+{"Key":"o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}}
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"3"}
+{"Key":"shift-o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}}
@@ -0,0 +1,23 @@
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"3"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}}
+{"Put":{"state":"ˇhello\n"}}
+{"Key":"2"}
+{"Key":"o"}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"escape"}
+{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}