vim counts (#2958)
Release Notes: - vim: Fix counts with operators (`2yy`, `d3d`, etc.) ([#1496](https://github.com/zed-industries/community/issues/1496)) ([#970](https://github.com/zed-industries/community/issues/970)). - vim: Add support for counts with insert actions (`2i`, `2o`, `2a`, etc.) - vim: add `_` and `g_`
This commit is contained in:
commit
329a0724e0
24 changed files with 792 additions and 319 deletions
|
@ -540,7 +540,7 @@
|
||||||
// TODO: Move this to a dock open action
|
// TODO: Move this to a dock open action
|
||||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||||
"cmd-alt-i": "zed::DebugElements",
|
"cmd-alt-i": "zed::DebugElements",
|
||||||
"ctrl-:": "editor::ToggleInlayHints",
|
"ctrl-:": "editor::ToggleInlayHints"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
"right": "vim::Right",
|
"right": "vim::Right",
|
||||||
"$": "vim::EndOfLine",
|
"$": "vim::EndOfLine",
|
||||||
"^": "vim::FirstNonWhitespace",
|
"^": "vim::FirstNonWhitespace",
|
||||||
|
"_": "vim::StartOfLineDownward",
|
||||||
|
"g _": "vim::EndOfLineDownward",
|
||||||
"shift-g": "vim::EndOfDocument",
|
"shift-g": "vim::EndOfDocument",
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
"{": "vim::StartOfParagraph",
|
"{": "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": {
|
"bindings": {
|
||||||
".": "vim::Repeat",
|
".": "vim::Repeat",
|
||||||
"c": [
|
"c": [
|
||||||
|
@ -389,7 +391,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == n",
|
"context": "Editor && VimCount",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"0": [
|
"0": [
|
||||||
"vim::Number",
|
"vim::Number",
|
||||||
|
@ -497,7 +499,7 @@
|
||||||
"around": true
|
"around": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,7 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
||||||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||||
editor.window().update(cx, |cx| {
|
editor.window().update(cx, |cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.clear_operator(cx);
|
||||||
vim.workspace_state.recording = false;
|
vim.workspace_state.recording = false;
|
||||||
|
vim.workspace_state.recorded_actions.clear();
|
||||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||||
if previous_editor == editor.clone() {
|
if previous_editor == editor.clone() {
|
||||||
vim.active_editor = None;
|
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 editor::{scroll::autoscroll::Autoscroll, Bias};
|
||||||
use gpui::{actions, AppContext, ViewContext};
|
use gpui::{actions, Action, AppContext, ViewContext};
|
||||||
use language::SelectionGoal;
|
use language::SelectionGoal;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(normal_before);
|
cx.add_action(normal_before);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
let should_repeat = Vim::update(cx, |vim, cx| {
|
||||||
vim.stop_recording();
|
let count = vim.take_count(cx).unwrap_or(1);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.stop_recording_immediately(action.boxed_clone());
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
if count <= 1 || vim.workspace_state.replaying {
|
||||||
s.move_cursors_with(|map, mut cursor, _| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
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)]
|
#[cfg(test)]
|
||||||
mod 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]
|
#[gpui::test]
|
||||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -40,4 +57,78 @@ mod test {
|
||||||
assert_eq!(cx.mode(), Mode::Normal);
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
cx.assert_editor_state("Tesˇt");
|
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 },
|
FindForward { before: bool, char: char },
|
||||||
FindBackward { after: bool, char: char },
|
FindBackward { after: bool, char: char },
|
||||||
NextLineStart,
|
NextLineStart,
|
||||||
|
StartOfLineDownward,
|
||||||
|
EndOfLineDownward,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -117,6 +119,8 @@ actions!(
|
||||||
EndOfDocument,
|
EndOfDocument,
|
||||||
Matching,
|
Matching,
|
||||||
NextLineStart,
|
NextLineStart,
|
||||||
|
StartOfLineDownward,
|
||||||
|
EndOfLineDownward,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_actions!(
|
impl_actions!(
|
||||||
|
@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||||
);
|
);
|
||||||
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, 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: _| {
|
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||||
repeat_motion(action.backwards, 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));
|
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();
|
let operator = Vim::read(cx).active_operator();
|
||||||
match Vim::read(cx).state().mode {
|
match Vim::read(cx).state().mode {
|
||||||
Mode::Normal => normal_motion(motion, operator, times, cx),
|
Mode::Normal => normal_motion(motion, operator, count, cx),
|
||||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
|
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
|
||||||
Mode::Insert => {
|
Mode::Insert => {
|
||||||
// Shouldn't execute a motion in insert mode. Ignoring
|
// Shouldn't execute a motion in insert mode. Ignoring
|
||||||
}
|
}
|
||||||
|
@ -272,6 +282,7 @@ impl Motion {
|
||||||
| EndOfDocument
|
| EndOfDocument
|
||||||
| CurrentLine
|
| CurrentLine
|
||||||
| NextLineStart
|
| NextLineStart
|
||||||
|
| StartOfLineDownward
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
| EndOfParagraph => true,
|
| EndOfParagraph => true,
|
||||||
EndOfLine { .. }
|
EndOfLine { .. }
|
||||||
|
@ -282,6 +293,7 @@ impl Motion {
|
||||||
| Backspace
|
| Backspace
|
||||||
| Right
|
| Right
|
||||||
| StartOfLine { .. }
|
| StartOfLine { .. }
|
||||||
|
| EndOfLineDownward
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
| PreviousWordStart { .. }
|
| PreviousWordStart { .. }
|
||||||
| FirstNonWhitespace { .. }
|
| FirstNonWhitespace { .. }
|
||||||
|
@ -305,6 +317,8 @@ impl Motion {
|
||||||
| StartOfLine { .. }
|
| StartOfLine { .. }
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
| EndOfParagraph
|
| EndOfParagraph
|
||||||
|
| StartOfLineDownward
|
||||||
|
| EndOfLineDownward
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
| PreviousWordStart { .. }
|
| PreviousWordStart { .. }
|
||||||
| FirstNonWhitespace { .. }
|
| FirstNonWhitespace { .. }
|
||||||
|
@ -322,6 +336,7 @@ impl Motion {
|
||||||
| EndOfDocument
|
| EndOfDocument
|
||||||
| CurrentLine
|
| CurrentLine
|
||||||
| EndOfLine { .. }
|
| EndOfLine { .. }
|
||||||
|
| EndOfLineDownward
|
||||||
| NextWordEnd { .. }
|
| NextWordEnd { .. }
|
||||||
| Matching
|
| Matching
|
||||||
| FindForward { .. }
|
| FindForward { .. }
|
||||||
|
@ -330,6 +345,7 @@ impl Motion {
|
||||||
| Backspace
|
| Backspace
|
||||||
| Right
|
| Right
|
||||||
| StartOfLine { .. }
|
| StartOfLine { .. }
|
||||||
|
| StartOfLineDownward
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
| EndOfParagraph
|
| EndOfParagraph
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
|
@ -396,7 +412,7 @@ impl Motion {
|
||||||
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
||||||
SelectionGoal::None,
|
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),
|
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||||
EndOfDocument => (
|
EndOfDocument => (
|
||||||
end_of_document(map, point, maybe_times),
|
end_of_document(map, point, maybe_times),
|
||||||
|
@ -412,6 +428,8 @@ impl Motion {
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
NextLineStart => (next_line_start(map, point, times), 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))
|
(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)
|
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)]
|
#[cfg(test)]
|
||||||
|
|
||||||
mod test {
|
mod test {
|
||||||
|
|
|
@ -2,7 +2,7 @@ mod case;
|
||||||
mod change;
|
mod change;
|
||||||
mod delete;
|
mod delete;
|
||||||
mod paste;
|
mod paste;
|
||||||
mod repeat;
|
pub(crate) mod repeat;
|
||||||
mod scroll;
|
mod scroll;
|
||||||
mod search;
|
mod search;
|
||||||
pub mod substitute;
|
pub mod substitute;
|
||||||
|
@ -68,21 +68,21 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(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);
|
delete_motion(vim, Motion::Left, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(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);
|
delete_motion(vim, Motion::Right, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording(cx);
|
vim.start_recording(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.take_count(cx);
|
||||||
change_motion(
|
change_motion(
|
||||||
vim,
|
vim,
|
||||||
Motion::EndOfLine {
|
Motion::EndOfLine {
|
||||||
|
@ -96,7 +96,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(cx);
|
vim.record_current_action(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.take_count(cx);
|
||||||
delete_motion(
|
delete_motion(
|
||||||
vim,
|
vim,
|
||||||
Motion::EndOfLine {
|
Motion::EndOfLine {
|
||||||
|
@ -110,7 +110,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(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() {
|
if vim.state().mode.is_visual() {
|
||||||
times = 1;
|
times = 1;
|
||||||
} else if times > 1 {
|
} else if times > 1 {
|
||||||
|
@ -356,7 +356,7 @@ mod test {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
state::Mode::{self},
|
state::Mode::{self},
|
||||||
test::{ExemptionFeatures, NeovimBackedTestContext},
|
test::NeovimBackedTestContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -762,20 +762,22 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_dd(cx: &mut gpui::TestAppContext) {
|
async fn test_dd(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
cx.assert("ˇ").await;
|
cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
|
||||||
cx.assert("The ˇquick").await;
|
cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
|
||||||
cx.assert_all(indoc! {"
|
for marked_text in cx.each_marked_position(indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown ˇfox
|
brown ˇfox
|
||||||
jumps ˇover"})
|
jumps ˇover"})
|
||||||
.await;
|
{
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
|
||||||
|
}
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
ˇ
|
ˇ
|
||||||
brown fox"},
|
brown fox"},
|
||||||
ExemptionFeatures::DeletionOnEmptyLine,
|
["d", "d"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(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| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
let mut cursor_positions = Vec::new();
|
let mut cursor_positions = Vec::new();
|
||||||
|
|
|
@ -121,7 +121,7 @@ fn expand_changed_word_selection(
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
|
use crate::test::NeovimBackedTestContext;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -239,150 +239,178 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
cx.assert(indoc! {"
|
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown fox"})
|
brown fox"},
|
||||||
.await;
|
["c", "0"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
ˇ
|
ˇ
|
||||||
brown fox"})
|
brown fox"},
|
||||||
.await;
|
["c", "0"],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
cx.assert(indoc! {"
|
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown ˇfox
|
brown ˇfox
|
||||||
jumps over"})
|
jumps over"},
|
||||||
.await;
|
["c", "k"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps ˇover"})
|
jumps ˇover"},
|
||||||
.await;
|
["c", "k"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over"},
|
jumps over"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "k"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
ˇ
|
ˇ
|
||||||
brown fox
|
brown fox
|
||||||
jumps over"},
|
jumps over"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "k"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
cx.assert(indoc! {"
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown ˇfox
|
brown ˇfox
|
||||||
jumps over"})
|
jumps over"},
|
||||||
.await;
|
["c", "j"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps ˇover"},
|
jumps ˇover"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "j"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert(indoc! {"
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over"})
|
jumps over"},
|
||||||
.await;
|
["c", "j"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
ˇ"},
|
ˇ"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "j"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx)
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
.await
|
cx.assert_neovim_compatible(
|
||||||
.binding(["c", "shift-g"]);
|
indoc! {"
|
||||||
cx.assert(indoc! {"
|
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["c", "shift-g"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["c", "shift-g"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lˇazy"},
|
the lˇazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "shift-g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
ˇ"},
|
ˇ"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "shift-g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx)
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
.await
|
cx.assert_neovim_compatible(
|
||||||
.binding(["c", "g", "g"]);
|
indoc! {"
|
||||||
cx.assert(indoc! {"
|
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["c", "g", "g"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lˇazy"})
|
the lˇazy"},
|
||||||
.await;
|
["c", "g", "g"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"},
|
the lazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "g", "g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
ˇ
|
ˇ
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"},
|
the lazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["c", "g", "g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -427,27 +455,17 @@ mod test {
|
||||||
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
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 {
|
for count in 1..=5 {
|
||||||
cx.assert_binding_matches_all(
|
for marked_text in cx.each_marked_position(indoc! {"
|
||||||
["c", &count.to_string(), "b"],
|
ˇThe quˇickˇ browˇn
|
||||||
indoc! {"
|
ˇ
|
||||||
ˇThe quˇickˇ browˇn
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
ˇ
|
ˇthe lazy dog
|
||||||
ˇfox ˇjumpsˇ-ˇoˇver
|
"})
|
||||||
ˇthe lazy dog
|
{
|
||||||
"},
|
cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
|
||||||
)
|
.await;
|
||||||
.await;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -278,37 +278,41 @@ mod test {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx)
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
.await
|
cx.assert_neovim_compatible(
|
||||||
.binding(["d", "shift-g"]);
|
indoc! {"
|
||||||
cx.assert(indoc! {"
|
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["d", "shift-g"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["d", "shift-g"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lˇazy"},
|
the lˇazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["d", "shift-g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
ˇ"},
|
ˇ"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["d", "shift-g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -318,34 +322,40 @@ mod test {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx)
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
.await
|
.await
|
||||||
.binding(["d", "g", "g"]);
|
.binding(["d", "g", "g"]);
|
||||||
cx.assert(indoc! {"
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brownˇ fox
|
brownˇ fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"})
|
the lazy"},
|
||||||
.await;
|
["d", "g", "g"],
|
||||||
cx.assert(indoc! {"
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
|
indoc! {"
|
||||||
The quick
|
The quick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lˇazy"})
|
the lˇazy"},
|
||||||
.await;
|
["d", "g", "g"],
|
||||||
cx.assert_exempted(
|
)
|
||||||
|
.await;
|
||||||
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The qˇuick
|
The qˇuick
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"},
|
the lazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["d", "g", "g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
cx.assert_exempted(
|
cx.assert_neovim_compatible(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
ˇ
|
ˇ
|
||||||
brown fox
|
brown fox
|
||||||
jumps over
|
jumps over
|
||||||
the lazy"},
|
the lazy"},
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
["d", "g", "g"],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -387,4 +397,40 @@ mod test {
|
||||||
assert_eq!(cx.active_operator(), None);
|
assert_eq!(cx.active_operator(), None);
|
||||||
assert_eq!(cx.mode(), Mode::Normal);
|
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::{
|
use crate::{
|
||||||
|
insert::NormalBefore,
|
||||||
motion::Motion,
|
motion::Motion,
|
||||||
state::{Mode, RecordedSelection, ReplayableAction},
|
state::{Mode, RecordedSelection, ReplayableAction},
|
||||||
visual::visual_motion,
|
visual::visual_motion,
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
use gpui::{actions, Action, AppContext};
|
use gpui::{actions, Action, AppContext, WindowContext};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
actions!(vim, [Repeat, EndRepeat,]);
|
actions!(vim, [Repeat, EndRepeat,]);
|
||||||
|
@ -17,138 +18,187 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||||
true
|
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) {
|
pub(crate) fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.workspace_state.replaying = false;
|
vim.workspace_state.replaying = false;
|
||||||
vim.update_active_editor(cx, |editor, _| {
|
|
||||||
editor.show_local_selections = true;
|
|
||||||
});
|
|
||||||
vim.switch_mode(Mode::Normal, false, cx)
|
vim.switch_mode(Mode::Normal, false, cx)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
|
||||||
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;
|
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
|
||||||
|
let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||||
let selection = vim.workspace_state.recorded_selection.clone();
|
let actions = vim.workspace_state.recorded_actions.clone();
|
||||||
match selection {
|
if actions.is_empty() {
|
||||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
return None;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = cx.window();
|
let Some(editor) = vim.active_editor.clone() else {
|
||||||
cx.app_context()
|
return None;
|
||||||
.spawn(move |mut cx| async move {
|
};
|
||||||
for action in actions {
|
let count = vim.take_count(cx);
|
||||||
match action {
|
|
||||||
ReplayableAction::Action(action) => {
|
let selection = vim.workspace_state.recorded_selection.clone();
|
||||||
if should_replay(&action) {
|
match selection {
|
||||||
window
|
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
vim.workspace_state.recorded_count = None;
|
||||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
vim.switch_mode(Mode::Visual, false, cx)
|
||||||
} else {
|
}
|
||||||
Ok(())
|
RecordedSelection::VisualLine { .. } => {
|
||||||
}
|
vim.workspace_state.recorded_count = None;
|
||||||
}
|
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||||
ReplayableAction::Insertion {
|
}
|
||||||
text,
|
RecordedSelection::VisualBlock { .. } => {
|
||||||
utf16_range_to_replace,
|
vim.workspace_state.recorded_count = None;
|
||||||
} => editor.update(&mut cx, |editor, cx| {
|
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||||
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
}
|
||||||
}),
|
RecordedSelection::None => {
|
||||||
}?
|
if let Some(count) = count {
|
||||||
|
vim.workspace_state.recorded_count = Some(count);
|
||||||
}
|
}
|
||||||
window
|
}
|
||||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
}
|
||||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
|
||||||
})
|
Some((actions, editor, selection))
|
||||||
.detach_and_log_err(cx);
|
}) 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}),
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -203,7 +253,7 @@ mod test {
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
cx.simulate_shared_keystrokes(["."]).await;
|
cx.simulate_shared_keystrokes(["."]).await;
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
cx.set_shared_state("THE QUICK ˇbrown fox").await;
|
cx.assert_shared_state("THE QUICK ˇbrown fox").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -424,4 +474,55 @@ mod test {
|
||||||
})
|
})
|
||||||
.await;
|
.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) {
|
fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
|
||||||
Vim::update(cx, |vim, cx| {
|
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));
|
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
|
Direction::Next
|
||||||
};
|
};
|
||||||
Vim::update(cx, |vim, cx| {
|
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| {
|
pane.update(cx, |pane, cx| {
|
||||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
search_bar.update(cx, |search_bar, cx| {
|
search_bar.update(cx, |search_bar, cx| {
|
||||||
|
@ -119,7 +119,7 @@ pub fn move_to_internal(
|
||||||
) {
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
let pane = workspace.active_pane().clone();
|
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| {
|
pane.update(cx, |pane, cx| {
|
||||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
let search = search_bar.update(cx, |search_bar, cx| {
|
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| {
|
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording(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);
|
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) {
|
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
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)
|
substitute(vim, count, true, cx)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,6 @@ impl Default for Mode {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub enum Operator {
|
pub enum Operator {
|
||||||
Number(usize),
|
|
||||||
Change,
|
Change,
|
||||||
Delete,
|
Delete,
|
||||||
Yank,
|
Yank,
|
||||||
|
@ -47,6 +46,12 @@ pub enum Operator {
|
||||||
pub struct EditorState {
|
pub struct EditorState {
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub last_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>,
|
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 {
|
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||||
let mut context = KeymapContext::default();
|
let mut context = KeymapContext::default();
|
||||||
context.add_identifier("VimEnabled");
|
context.add_identifier("VimEnabled");
|
||||||
|
@ -174,7 +183,14 @@ impl EditorState {
|
||||||
context.add_identifier("VimControl");
|
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 {
|
if let Some(active_operator) = active_operator {
|
||||||
for context_flag in active_operator.context_flags().into_iter() {
|
for context_flag in active_operator.context_flags().into_iter() {
|
||||||
|
@ -194,7 +210,6 @@ impl EditorState {
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn id(&self) -> &'static str {
|
pub fn id(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Operator::Number(_) => "n",
|
|
||||||
Operator::Object { around: false } => "i",
|
Operator::Object { around: false } => "i",
|
||||||
Operator::Object { around: true } => "a",
|
Operator::Object { around: true } => "a",
|
||||||
Operator::Change => "c",
|
Operator::Change => "c",
|
||||||
|
|
|
@ -574,3 +574,47 @@ async fn test_folds(cx: &mut gpui::TestAppContext) {
|
||||||
"})
|
"})
|
||||||
.await;
|
.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 super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||||
use crate::state::Mode;
|
use crate::state::Mode;
|
||||||
|
|
||||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
|
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
|
||||||
ExemptionFeatures::DeletionOnEmptyLine,
|
|
||||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Enum representing features we have tests for but which don't work, yet. Used
|
/// Enum representing features we have tests for but which don't work, yet. Used
|
||||||
/// to add exemptions and automatically
|
/// to add exemptions and automatically
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum ExemptionFeatures {
|
pub enum ExemptionFeatures {
|
||||||
// MOTIONS
|
// 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
|
// When an operator completes at the end of the file, an extra newline is left
|
||||||
OperatorLastNewlineRemains,
|
OperatorLastNewlineRemains,
|
||||||
// Deleting a word on an empty line doesn't remove the newline
|
// 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>,
|
last_set_state: Option<String>,
|
||||||
recent_keystrokes: Vec<String>,
|
recent_keystrokes: Vec<String>,
|
||||||
|
|
||||||
|
is_dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NeovimBackedTestContext<'a> {
|
impl<'a> NeovimBackedTestContext<'a> {
|
||||||
|
@ -81,6 +76,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
|
|
||||||
last_set_state: None,
|
last_set_state: None,
|
||||||
recent_keystrokes: Default::default(),
|
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.last_set_state = Some(marked_text.to_string());
|
||||||
self.recent_keystrokes = Vec::new();
|
self.recent_keystrokes = Vec::new();
|
||||||
self.neovim.set_state(marked_text).await;
|
self.neovim.set_state(marked_text).await;
|
||||||
|
self.is_dirty = true;
|
||||||
context_handle
|
context_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +150,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||||
|
self.is_dirty = false;
|
||||||
let marked_text = marked_text.replace("•", " ");
|
let marked_text = marked_text.replace("•", " ");
|
||||||
let neovim = self.neovim_state().await;
|
let neovim = self.neovim_state().await;
|
||||||
let editor = self.editor_state();
|
let editor = self.editor_state();
|
||||||
|
@ -258,6 +256,7 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_state_matches(&mut self) {
|
pub async fn assert_state_matches(&mut self) {
|
||||||
|
self.is_dirty = false;
|
||||||
let neovim = self.neovim_state().await;
|
let neovim = self.neovim_state().await;
|
||||||
let editor = self.editor_state();
|
let editor = self.editor_state();
|
||||||
let initial_state = self
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
|
@ -15,8 +15,8 @@ use anyhow::Result;
|
||||||
use collections::{CommandPaletteFilter, HashMap};
|
use collections::{CommandPaletteFilter, HashMap};
|
||||||
use editor::{movement, Editor, EditorMode, Event};
|
use editor::{movement, Editor, EditorMode, Event};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
|
||||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||||
pub use mode_indicator::ModeIndicator;
|
pub use mode_indicator::ModeIndicator;
|
||||||
|
@ -40,9 +40,12 @@ pub struct SwitchMode(pub Mode);
|
||||||
pub struct PushOperator(pub Operator);
|
pub struct PushOperator(pub Operator);
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[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]);
|
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -70,7 +73,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
|
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| {
|
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||||
|
@ -225,23 +228,12 @@ impl Vim {
|
||||||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||||
Some(editor.update(cx, update))
|
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) {
|
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||||
if !self.workspace_state.replaying {
|
if !self.workspace_state.replaying {
|
||||||
self.workspace_state.recording = true;
|
self.workspace_state.recording = true;
|
||||||
self.workspace_state.recorded_actions = Default::default();
|
self.workspace_state.recorded_actions = Default::default();
|
||||||
self.workspace_state.recorded_count =
|
self.workspace_state.recorded_count = None;
|
||||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
|
||||||
Some(number)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let selections = self
|
let selections = self
|
||||||
.active_editor
|
.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) {
|
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||||
self.start_recording(cx);
|
self.start_recording(cx);
|
||||||
self.stop_recording();
|
self.stop_recording();
|
||||||
|
@ -300,6 +302,9 @@ impl Vim {
|
||||||
state.mode = mode;
|
state.mode = mode;
|
||||||
state.operator_stack.clear();
|
state.operator_stack.clear();
|
||||||
});
|
});
|
||||||
|
if mode != Mode::Insert {
|
||||||
|
self.take_count(cx);
|
||||||
|
}
|
||||||
|
|
||||||
cx.emit_global(VimEvent::ModeChanged { mode });
|
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) {
|
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||||
if matches!(
|
if matches!(
|
||||||
operator,
|
operator,
|
||||||
|
@ -363,15 +401,6 @@ impl Vim {
|
||||||
self.sync_vim_settings(cx);
|
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> {
|
fn maybe_pop_operator(&mut self) -> Option<Operator> {
|
||||||
self.update_state(|state| state.operator_stack.pop())
|
self.update_state(|state| state.operator_stack.pop())
|
||||||
}
|
}
|
||||||
|
@ -382,22 +411,8 @@ impl Vim {
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
popped_operator
|
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) {
|
fn clear_operator(&mut self, cx: &mut WindowContext) {
|
||||||
|
self.take_count(cx);
|
||||||
self.update_state(|state| state.operator_stack.clear());
|
self.update_state(|state| state.operator_stack.clear());
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
}
|
}
|
||||||
|
|
7
crates/vim/test_data/test_clear_counts.json
Normal file
7
crates/vim/test_data/test_clear_counts.json
Normal file
|
@ -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"}}
|
16
crates/vim/test_data/test_delete_with_counts.json
Normal file
16
crates/vim/test_data/test_delete_with_counts.json
Normal file
|
@ -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":"."}
|
{"Key":"."}
|
||||||
{"Put":{"state":"THE QUIˇck brown fox"}}
|
{"Put":{"state":"THE QUIˇck brown fox"}}
|
||||||
{"Key":"."}
|
{"Key":"."}
|
||||||
{"Put":{"state":"THE QUICK ˇbrown fox"}}
|
{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}}
|
||||||
|
|
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
36
crates/vim/test_data/test_insert_with_counts.json
Normal file
|
@ -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"}}
|
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
23
crates/vim/test_data/test_insert_with_repeat.json
Normal file
|
@ -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"}}
|
13
crates/vim/test_data/test_repeat_motion_counts.json
Normal file
13
crates/vim/test_data/test_repeat_motion_counts.json
Normal file
|
@ -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"}}
|
7
crates/vim/test_data/test_zero.json
Normal file
7
crates/vim/test_data/test_zero.json
Normal file
|
@ -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"}}
|
Loading…
Add table
Add a link
Reference in a new issue