Detailed changes
@@ -540,7 +540,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"
}
},
{
@@ -32,6 +32,8 @@
"right": "vim::Right",
"$": "vim::EndOfLine",
"^": "vim::FirstNonWhitespace",
+ "_": "vim::StartOfLineDownward",
+ "g _": "vim::EndOfLineDownward",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"{": "vim::StartOfParagraph",
@@ -326,7 +328,7 @@
}
},
{
- "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
+ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
".": "vim::Repeat",
"c": [
@@ -389,7 +391,7 @@
}
},
{
- "context": "Editor && vim_operator == n",
+ "context": "Editor && VimCount",
"bindings": {
"0": [
"vim::Number",
@@ -497,7 +499,7 @@
"around": true
}
}
- ],
+ ]
}
},
{
@@ -34,7 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
editor.window().update(cx, |cx| {
Vim::update(cx, |vim, cx| {
+ vim.clear_operator(cx);
vim.workspace_state.recording = false;
+ vim.workspace_state.recorded_actions.clear();
if let Some(previous_editor) = vim.active_editor.clone() {
if previous_editor == editor.clone() {
vim.active_editor = None;
@@ -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(cx).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;
+ }
}
@@ -40,6 +40,8 @@ pub enum Motion {
FindForward { before: bool, char: char },
FindBackward { after: bool, char: char },
NextLineStart,
+ StartOfLineDownward,
+ EndOfLineDownward,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -117,6 +119,8 @@ actions!(
EndOfDocument,
Matching,
NextLineStart,
+ StartOfLineDownward,
+ EndOfLineDownward,
]
);
impl_actions!(
@@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) {
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
+ cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
+ motion(Motion::StartOfLineDownward, cx)
+ });
+ cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
+ motion(Motion::EndOfLineDownward, cx)
+ });
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
repeat_motion(action.backwards, cx)
})
@@ -219,11 +229,11 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
- let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
+ let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
let operator = Vim::read(cx).active_operator();
match Vim::read(cx).state().mode {
- Mode::Normal => normal_motion(motion, operator, times, cx),
- Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
+ Mode::Normal => normal_motion(motion, operator, count, cx),
+ Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
Mode::Insert => {
// Shouldn't execute a motion in insert mode. Ignoring
}
@@ -272,6 +282,7 @@ impl Motion {
| EndOfDocument
| CurrentLine
| NextLineStart
+ | StartOfLineDownward
| StartOfParagraph
| EndOfParagraph => true,
EndOfLine { .. }
@@ -282,6 +293,7 @@ impl Motion {
| Backspace
| Right
| StartOfLine { .. }
+ | EndOfLineDownward
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace { .. }
@@ -305,6 +317,8 @@ impl Motion {
| StartOfLine { .. }
| StartOfParagraph
| EndOfParagraph
+ | StartOfLineDownward
+ | EndOfLineDownward
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace { .. }
@@ -322,6 +336,7 @@ impl Motion {
| EndOfDocument
| CurrentLine
| EndOfLine { .. }
+ | EndOfLineDownward
| NextWordEnd { .. }
| Matching
| FindForward { .. }
@@ -330,6 +345,7 @@ impl Motion {
| Backspace
| Right
| StartOfLine { .. }
+ | StartOfLineDownward
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. }
@@ -396,7 +412,7 @@ impl Motion {
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
SelectionGoal::None,
),
- CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
+ CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (
end_of_document(map, point, maybe_times),
@@ -412,6 +428,8 @@ impl Motion {
SelectionGoal::None,
),
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
+ StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
+ EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
};
(new_point != point || infallible).then_some((new_point, goal))
@@ -849,6 +867,13 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
first_non_whitespace(map, false, correct_line)
}
+fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+ if times > 1 {
+ point = down(map, point, SelectionGoal::None, times - 1).0;
+ }
+ end_of_line(map, false, point)
+}
+
#[cfg(test)]
mod test {
@@ -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;
@@ -68,21 +68,21 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
- let times = vim.pop_number_operator(cx);
+ let times = vim.take_count(cx);
delete_motion(vim, Motion::Left, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
- let times = vim.pop_number_operator(cx);
+ let times = vim.take_count(cx);
delete_motion(vim, Motion::Right, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
- let times = vim.pop_number_operator(cx);
+ let times = vim.take_count(cx);
change_motion(
vim,
Motion::EndOfLine {
@@ -96,7 +96,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
- let times = vim.pop_number_operator(cx);
+ let times = vim.take_count(cx);
delete_motion(
vim,
Motion::EndOfLine {
@@ -110,7 +110,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
- let mut times = vim.pop_number_operator(cx).unwrap_or(1);
+ let mut times = vim.take_count(cx).unwrap_or(1);
if vim.state().mode.is_visual() {
times = 1;
} else if times > 1 {
@@ -356,7 +356,7 @@ mod test {
use crate::{
state::Mode::{self},
- test::{ExemptionFeatures, NeovimBackedTestContext},
+ test::NeovimBackedTestContext,
};
#[gpui::test]
@@ -762,20 +762,22 @@ mod test {
#[gpui::test]
async fn test_dd(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
- cx.assert("ˇ").await;
- cx.assert("The ˇquick").await;
- cx.assert_all(indoc! {"
- The qˇuick
- brown ˇfox
- jumps ˇover"})
- .await;
- cx.assert_exempted(
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
+ cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
+ for marked_text in cx.each_marked_position(indoc! {"
+ The qˇuick
+ brown ˇfox
+ jumps ˇover"})
+ {
+ cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
+ }
+ cx.assert_neovim_compatible(
indoc! {"
The quick
ˇ
brown fox"},
- ExemptionFeatures::DeletionOnEmptyLine,
+ ["d", "d"],
)
.await;
}
@@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
- let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
+ let count = vim.take_count(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
@@ -121,7 +121,7 @@ fn expand_changed_word_selection(
mod test {
use indoc::indoc;
- use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
+ use crate::test::NeovimBackedTestContext;
#[gpui::test]
async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -239,150 +239,178 @@ mod test {
#[gpui::test]
async fn test_change_0(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.assert_neovim_compatible(
+ indoc! {"
The qˇuick
- brown fox"})
- .await;
- cx.assert(indoc! {"
+ brown fox"},
+ ["c", "0"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
ˇ
- brown fox"})
- .await;
+ brown fox"},
+ ["c", "0"],
+ )
+ .await;
}
#[gpui::test]
async fn test_change_k(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brown ˇfox
- jumps over"})
- .await;
- cx.assert(indoc! {"
+ jumps over"},
+ ["c", "k"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brown fox
- jumps ˇover"})
- .await;
- cx.assert_exempted(
+ jumps ˇover"},
+ ["c", "k"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The qˇuick
brown fox
jumps over"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "k"],
)
.await;
- cx.assert_exempted(
+ cx.assert_neovim_compatible(
indoc! {"
ˇ
brown fox
jumps over"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "k"],
)
.await;
}
#[gpui::test]
async fn test_change_j(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brown ˇfox
- jumps over"})
- .await;
- cx.assert_exempted(
+ jumps over"},
+ ["c", "j"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
jumps ˇover"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "j"],
)
.await;
- cx.assert(indoc! {"
+ cx.assert_neovim_compatible(
+ indoc! {"
The qˇuick
brown fox
- jumps over"})
- .await;
- cx.assert_exempted(
+ jumps over"},
+ ["c", "j"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
ˇ"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "j"],
)
.await;
}
#[gpui::test]
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx)
- .await
- .binding(["c", "shift-g"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert(indoc! {"
+ the lazy"},
+ ["c", "shift-g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert_exempted(
+ the lazy"},
+ ["c", "shift-g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
jumps over
the lˇazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "shift-g"],
)
.await;
- cx.assert_exempted(
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
jumps over
ˇ"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "shift-g"],
)
.await;
}
#[gpui::test]
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx)
- .await
- .binding(["c", "g", "g"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert(indoc! {"
+ the lazy"},
+ ["c", "g", "g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brown fox
jumps over
- the lˇazy"})
- .await;
- cx.assert_exempted(
+ the lˇazy"},
+ ["c", "g", "g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The qˇuick
brown fox
jumps over
the lazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "g", "g"],
)
.await;
- cx.assert_exempted(
+ cx.assert_neovim_compatible(
indoc! {"
ˇ
brown fox
jumps over
the lazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["c", "g", "g"],
)
.await;
}
@@ -427,27 +455,17 @@ mod test {
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
- cx.add_initial_state_exemptions(
- indoc! {"
- ˇThe quick brown
-
- fox jumps-over
- the lazy dog
- "},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
- );
-
for count in 1..=5 {
- cx.assert_binding_matches_all(
- ["c", &count.to_string(), "b"],
- indoc! {"
- ˇThe quˇickˇ browˇn
- ˇ
- ˇfox ˇjumpsˇ-ˇoˇver
- ˇthe lazy dog
- "},
- )
- .await;
+ for marked_text in cx.each_marked_position(indoc! {"
+ ˇThe quˇickˇ browˇn
+ ˇ
+ ˇfox ˇjumpsˇ-ˇoˇver
+ ˇthe lazy dog
+ "})
+ {
+ cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
+ .await;
+ }
}
}
@@ -278,37 +278,41 @@ mod test {
#[gpui::test]
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx)
- .await
- .binding(["d", "shift-g"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert(indoc! {"
+ the lazy"},
+ ["d", "shift-g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert_exempted(
+ the lazy"},
+ ["d", "shift-g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
jumps over
the lˇazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["d", "shift-g"],
)
.await;
- cx.assert_exempted(
+ cx.assert_neovim_compatible(
indoc! {"
The quick
brown fox
jumps over
ˇ"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["d", "shift-g"],
)
.await;
}
@@ -318,34 +322,40 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["d", "g", "g"]);
- cx.assert(indoc! {"
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brownˇ fox
jumps over
- the lazy"})
- .await;
- cx.assert(indoc! {"
+ the lazy"},
+ ["d", "g", "g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
+ indoc! {"
The quick
brown fox
jumps over
- the lˇazy"})
- .await;
- cx.assert_exempted(
+ the lˇazy"},
+ ["d", "g", "g"],
+ )
+ .await;
+ cx.assert_neovim_compatible(
indoc! {"
The qˇuick
brown fox
jumps over
the lazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["d", "g", "g"],
)
.await;
- cx.assert_exempted(
+ cx.assert_neovim_compatible(
indoc! {"
ˇ
brown fox
jumps over
the lazy"},
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
+ ["d", "g", "g"],
)
.await;
}
@@ -387,4 +397,40 @@ mod test {
assert_eq!(cx.active_operator(), None);
assert_eq!(cx.mode(), Mode::Normal);
}
+
+ #[gpui::test]
+ async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ the ˇlazy dog"})
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ the ˇlazy dog"})
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the moon,
+ a star, and
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ the ˇlazy dog"})
+ .await;
+ }
}
@@ -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,138 +18,187 @@ 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| {
vim.workspace_state.replaying = false;
- vim.update_active_editor(cx, |editor, _| {
- editor.show_local_selections = true;
- });
vim.switch_mode(Mode::Normal, false, cx)
});
});
- 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.pop_number_operator(cx);
-
- 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);
- }
- }
- }
+ cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
+}
- if let Some(editor) = editor.upgrade(cx) {
- editor.update(cx, |editor, _| {
- editor.show_local_selections = false;
- })
- } else {
- return None;
- }
+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();
+ if actions.is_empty() {
+ return None;
+ }
- Some((actions, editor, selection))
- }) else {
- return;
+ let Some(editor) = vim.active_editor.clone() else {
+ return None;
};
+ let count = vim.take_count(cx);
+ 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,
- );
+ }
+
+ 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 {
+ editor.update(&mut cx, |editor, _| {
+ editor.show_local_selections = false;
+ })?;
+ 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)
+ }),
+ }?
+ }
+ editor.update(&mut cx, |editor, _| {
+ editor.show_local_selections = true;
+ })?;
+ window
+ .dispatch_action(editor.id(), &EndRepeat, &mut cx)
+ .ok_or_else(|| anyhow::anyhow!("window was closed"))
+ })
+ .detach_and_log_err(cx);
}
#[cfg(test)]
@@ -203,7 +253,7 @@ mod test {
deterministic.run_until_parked();
cx.simulate_shared_keystrokes(["."]).await;
deterministic.run_until_parked();
- cx.set_shared_state("THE QUICK ˇbrown fox").await;
+ cx.assert_shared_state("THE QUICK ˇbrown fox").await;
}
#[gpui::test]
@@ -424,4 +474,55 @@ mod test {
})
.await;
}
+
+ #[gpui::test]
+ async fn test_repeat_motion_counts(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {
+ "ˇthe quick brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
+ cx.assert_shared_state(indoc! {
+ "ˇ brown
+ fox jumps over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ " brown
+ ˇ over
+ the lazy dog"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "2", "."]).await;
+ deterministic.run_until_parked();
+ cx.assert_shared_state(indoc! {
+ " brown
+ over
+ ˇe lazy dog"
+ })
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_record_interrupted(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state("ˇhello\n", Mode::Normal);
+ cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
+ deterministic.run_until_parked();
+ cx.assert_state("ˇjhello\n", Mode::Normal);
+ }
}
@@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
Vim::update(cx, |vim, cx| {
- let amount = by(vim.pop_number_operator(cx).map(|c| c as f32));
+ let amount = by(vim.take_count(cx).map(|c| c as f32));
vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
})
}
@@ -52,7 +52,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
Direction::Next
};
Vim::update(cx, |vim, cx| {
- let count = vim.pop_number_operator(cx).unwrap_or(1);
+ let count = vim.take_count(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
@@ -119,7 +119,7 @@ pub fn move_to_internal(
) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
- let count = vim.pop_number_operator(cx).unwrap_or(1);
+ let count = vim.take_count(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
@@ -11,7 +11,7 @@ pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
- let count = vim.pop_number_operator(cx);
+ let count = vim.take_count(cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
});
@@ -22,7 +22,7 @@ pub(crate) fn init(cx: &mut AppContext) {
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
vim.switch_mode(Mode::VisualLine, false, cx)
}
- let count = vim.pop_number_operator(cx);
+ let count = vim.take_count(cx);
substitute(vim, count, true, cx)
})
});
@@ -33,7 +33,6 @@ impl Default for Mode {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Operator {
- Number(usize),
Change,
Delete,
Yank,
@@ -47,6 +46,12 @@ pub enum Operator {
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<usize>,
+ /// post_count is the number after an operator is specified (2 in 3d2d)
+ pub post_count: Option<usize>,
+
pub operator_stack: Vec<Operator>,
}
@@ -158,6 +163,10 @@ impl EditorState {
}
}
+ pub fn active_operator(&self) -> Option<Operator> {
+ self.operator_stack.last().copied()
+ }
+
pub fn keymap_context_layer(&self) -> KeymapContext {
let mut context = KeymapContext::default();
context.add_identifier("VimEnabled");
@@ -174,7 +183,14 @@ impl EditorState {
context.add_identifier("VimControl");
}
- let active_operator = self.operator_stack.last();
+ if self.active_operator().is_none() && self.pre_count.is_some()
+ || self.active_operator().is_some() && self.post_count.is_some()
+ {
+ dbg!("VimCount");
+ context.add_identifier("VimCount");
+ }
+
+ let active_operator = self.active_operator();
if let Some(active_operator) = active_operator {
for context_flag in active_operator.context_flags().into_iter() {
@@ -194,7 +210,6 @@ impl EditorState {
impl Operator {
pub fn id(&self) -> &'static str {
match self {
- Operator::Number(_) => "n",
Operator::Object { around: false } => "i",
Operator::Object { around: true } => "a",
Operator::Change => "c",
@@ -574,3 +574,47 @@ async fn test_folds(cx: &mut gpui::TestAppContext) {
"})
.await;
}
+
+#[gpui::test]
+async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+
+ cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox juˇ over
+ the lazy dog"})
+ .await;
+}
+
+#[gpui::test]
+async fn test_zero(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {"
+ The quˇick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+
+ cx.simulate_shared_keystrokes(["0"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇThe quick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+
+ cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick ˇbrown
+ fox jumps over
+ the lazy dog"})
+ .await;
+}
@@ -13,20 +13,13 @@ use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
-pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
- ExemptionFeatures::DeletionOnEmptyLine,
- ExemptionFeatures::OperatorAbortsOnFailedMotion,
-];
+pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
/// Enum representing features we have tests for but which don't work, yet. Used
/// to add exemptions and automatically
#[derive(PartialEq, Eq)]
pub enum ExemptionFeatures {
// MOTIONS
- // Deletions on empty lines miss some newlines
- DeletionOnEmptyLine,
- // When a motion fails, it should should not apply linewise operations
- OperatorAbortsOnFailedMotion,
// When an operator completes at the end of the file, an extra newline is left
OperatorLastNewlineRemains,
// Deleting a word on an empty line doesn't remove the newline
@@ -68,6 +61,8 @@ pub struct NeovimBackedTestContext<'a> {
last_set_state: Option<String>,
recent_keystrokes: Vec<String>,
+
+ is_dirty: bool,
}
impl<'a> NeovimBackedTestContext<'a> {
@@ -81,6 +76,7 @@ impl<'a> NeovimBackedTestContext<'a> {
last_set_state: None,
recent_keystrokes: Default::default(),
+ is_dirty: false,
}
}
@@ -128,6 +124,7 @@ impl<'a> NeovimBackedTestContext<'a> {
self.last_set_state = Some(marked_text.to_string());
self.recent_keystrokes = Vec::new();
self.neovim.set_state(marked_text).await;
+ self.is_dirty = true;
context_handle
}
@@ -153,6 +150,7 @@ impl<'a> NeovimBackedTestContext<'a> {
}
pub async fn assert_shared_state(&mut self, marked_text: &str) {
+ self.is_dirty = false;
let marked_text = marked_text.replace("•", " ");
let neovim = self.neovim_state().await;
let editor = self.editor_state();
@@ -258,6 +256,7 @@ impl<'a> NeovimBackedTestContext<'a> {
}
pub async fn assert_state_matches(&mut self) {
+ self.is_dirty = false;
let neovim = self.neovim_state().await;
let editor = self.editor_state();
let initial_state = self
@@ -383,6 +382,17 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
}
}
+// a common mistake in tests is to call set_shared_state when
+// you mean asswert_shared_state. This notices that and lets
+// you know.
+impl<'a> Drop for NeovimBackedTestContext<'a> {
+ fn drop(&mut self) {
+ if self.is_dirty {
+ panic!("Test context was dropped after set_shared_state before assert_shared_state")
+ }
+ }
+}
+
#[cfg(test)]
mod test {
use gpui::TestAppContext;
@@ -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;
@@ -40,9 +40,12 @@ pub struct SwitchMode(pub Mode);
pub struct PushOperator(pub Operator);
#[derive(Clone, Deserialize, PartialEq)]
-struct Number(u8);
+struct Number(usize);
-actions!(vim, [Tab, Enter]);
+actions!(
+ vim,
+ [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
+);
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
#[derive(Copy, Clone, Debug)]
@@ -70,7 +73,7 @@ pub fn init(cx: &mut AppContext) {
},
);
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
- Vim::update(cx, |vim, cx| vim.push_number(n, cx));
+ Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
});
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
@@ -225,23 +228,12 @@ impl Vim {
let editor = self.active_editor.clone()?.upgrade(cx)?;
Some(editor.update(cx, update))
}
- // ~, shift-j, x, shift-x, p
- // shift-c, shift-d, shift-i, i, a, o, shift-o, s
- // c, d
- // r
- // TODO: shift-j?
- //
pub fn start_recording(&mut self, cx: &mut WindowContext) {
if !self.workspace_state.replaying {
self.workspace_state.recording = true;
self.workspace_state.recorded_actions = Default::default();
- self.workspace_state.recorded_count =
- if let Some(Operator::Number(number)) = self.active_operator() {
- Some(number)
- } else {
- None
- };
+ self.workspace_state.recorded_count = None;
let selections = self
.active_editor
@@ -286,6 +278,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();
@@ -300,6 +302,9 @@ impl Vim {
state.mode = mode;
state.operator_stack.clear();
});
+ if mode != Mode::Insert {
+ self.take_count(cx);
+ }
cx.emit_global(VimEvent::ModeChanged { mode });
@@ -352,6 +357,39 @@ impl Vim {
});
}
+ fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
+ if self.active_operator().is_some() {
+ self.update_state(|state| {
+ state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
+ })
+ } else {
+ self.update_state(|state| {
+ state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
+ })
+ }
+ // update the keymap so that 0 works
+ self.sync_vim_settings(cx)
+ }
+
+ fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
+ if self.workspace_state.replaying {
+ return self.workspace_state.recorded_count;
+ }
+
+ let count = if self.state().post_count == None && self.state().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)
+ }))
+ };
+ if self.workspace_state.recording {
+ self.workspace_state.recorded_count = count;
+ }
+ self.sync_vim_settings(cx);
+ count
+ }
+
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
if matches!(
operator,
@@ -363,15 +401,6 @@ impl Vim {
self.sync_vim_settings(cx);
}
- fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
- if let Some(Operator::Number(current_number)) = self.active_operator() {
- self.pop_operator(cx);
- self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
- } else {
- self.push_operator(Operator::Number(*number as usize), cx);
- }
- }
-
fn maybe_pop_operator(&mut self) -> Option<Operator> {
self.update_state(|state| state.operator_stack.pop())
}
@@ -382,22 +411,8 @@ impl Vim {
self.sync_vim_settings(cx);
popped_operator
}
-
- fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
- if self.workspace_state.replaying {
- if let Some(number) = self.workspace_state.recorded_count {
- return Some(number);
- }
- }
-
- if let Some(Operator::Number(number)) = self.active_operator() {
- self.pop_operator(cx);
- return Some(number);
- }
- None
- }
-
fn clear_operator(&mut self, cx: &mut WindowContext) {
+ self.take_count(cx);
self.update_state(|state| state.operator_stack.clear());
self.sync_vim_settings(cx);
}
@@ -0,0 +1,7 @@
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"4"}
+{"Key":"escape"}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,16 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the ˇlazy dog","mode":"Normal"}}
@@ -35,4 +35,4 @@
{"Key":"."}
{"Put":{"state":"THE QUIˇck brown fox"}}
{"Key":"."}
-{"Put":{"state":"THE QUICK ˇbrown fox"}}
+{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}}
@@ -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"}}
@@ -0,0 +1,13 @@
+{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"3"}
+{"Key":"l"}
+{"Get":{"state":"ˇ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}}
@@ -0,0 +1,7 @@
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"0"}
+{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"0"}
+{"Key":"l"}
+{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}}