Detailed changes
@@ -75,37 +75,13 @@
{
"context": "Editor && vim_operator == c",
"bindings": {
- "w": [
- "vim::NextWordEnd",
- {
- "ignorePunctuation": false
- }
- ],
+ "w": "vim::ChangeWord",
"shift-W": [
- "vim::NextWordEnd",
+ "vim::ChangeWord",
{
"ignorePunctuation": true
}
]
}
- },
- {
- "context": "Editor && vim_operator == d",
- "bindings": {
- "w": [
- "vim::NextWordStart",
- {
- "ignorePunctuation": false,
- "stopAtNewline": true
- }
- ],
- "shift-W": [
- "vim::NextWordStart",
- {
- "ignorePunctuation": true,
- "stopAtNewline": true
- }
- ]
- }
}
]
@@ -814,14 +814,20 @@ pub mod tests {
DisplayPoint::new(0, 7)
);
assert_eq!(
- movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
+ movement::up(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::None,
+ false
+ ),
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
);
assert_eq!(
movement::down(
&snapshot,
DisplayPoint::new(0, 7),
- SelectionGoal::Column(10)
+ SelectionGoal::Column(10),
+ false
),
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
);
@@ -829,7 +835,8 @@ pub mod tests {
movement::down(
&snapshot,
DisplayPoint::new(1, 10),
- SelectionGoal::Column(10)
+ SelectionGoal::Column(10),
+ false
),
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
@@ -1134,8 +1134,10 @@ impl Editor {
}
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
- self.display_map
- .update(cx, |map, _| map.clip_at_line_ends = clip);
+ if self.display_map.read(cx).clip_at_line_ends != clip {
+ self.display_map
+ .update(cx, |map, _| map.clip_at_line_ends = clip);
+ }
}
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
@@ -3579,13 +3581,13 @@ impl Editor {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) = movement::up(&map, selection.start, selection.goal);
+ let (cursor, goal) = movement::up(&map, selection.start, selection.goal, false);
selection.collapse_to(cursor, goal);
});
}
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
- self.move_selection_heads(cx, movement::up)
+ self.move_selection_heads(cx, |map, head, goal| movement::up(map, head, goal, false))
}
pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
@@ -3606,13 +3608,13 @@ impl Editor {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) = movement::down(&map, selection.end, selection.goal);
+ let (cursor, goal) = movement::down(&map, selection.end, selection.goal, false);
selection.collapse_to(cursor, goal);
});
}
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
- self.move_selection_heads(cx, movement::down)
+ self.move_selection_heads(cx, |map, head, goal| movement::down(map, head, goal, false))
}
pub fn move_to_previous_word_start(
@@ -28,6 +28,7 @@ pub fn up(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
+ preserve_column_at_start: bool,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
@@ -42,6 +43,8 @@ pub fn up(
);
if point.row() < start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
+ } else if preserve_column_at_start {
+ return (start, goal);
} else {
point = DisplayPoint::new(0, 0);
goal_column = 0;
@@ -63,6 +66,7 @@ pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
+ preserve_column_at_end: bool,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
@@ -74,6 +78,8 @@ pub fn down(
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
+ } else if preserve_column_at_end {
+ return (start, goal);
} else {
point = map.max_point();
goal_column = map.column_to_chars(point.row(), point.column())
@@ -503,41 +509,81 @@ mod tests {
// Can't move up into the first excerpt's header
assert_eq!(
- up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 2),
+ SelectionGoal::Column(2),
+ false
+ ),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
assert_eq!(
- up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 0),
+ SelectionGoal::None,
+ false
+ ),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
// Move up and down within first excerpt
assert_eq!(
- up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
+ up(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::Column(4),
+ false
+ ),
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
);
assert_eq!(
- down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
+ down(
+ &snapshot,
+ DisplayPoint::new(2, 3),
+ SelectionGoal::Column(4),
+ false
+ ),
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
);
// Move up and down across second excerpt's header
assert_eq!(
- up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
+ up(
+ &snapshot,
+ DisplayPoint::new(6, 5),
+ SelectionGoal::Column(5),
+ false
+ ),
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
);
assert_eq!(
- down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
+ down(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::Column(5),
+ false
+ ),
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
);
// Can't move down off the end
assert_eq!(
- down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 0),
+ SelectionGoal::Column(0),
+ false
+ ),
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
);
assert_eq!(
- down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 2),
+ SelectionGoal::Column(2),
+ false
+ ),
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
);
}
@@ -28,7 +28,7 @@ mod test {
#[gpui::test]
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
+ let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
cx.simulate_keystrokes(["T", "e", "s", "t"]);
@@ -4,7 +4,7 @@ use editor::{
movement, Bias, DisplayPoint,
};
use gpui::{actions, impl_actions, MutableAppContext};
-use language::SelectionGoal;
+use language::{Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@@ -14,22 +14,15 @@ use crate::{
Vim,
};
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
pub enum Motion {
Left,
Down,
Up,
Right,
- NextWordStart {
- ignore_punctuation: bool,
- stop_at_newline: bool,
- },
- NextWordEnd {
- ignore_punctuation: bool,
- },
- PreviousWordStart {
- ignore_punctuation: bool,
- },
+ NextWordStart { ignore_punctuation: bool },
+ NextWordEnd { ignore_punctuation: bool },
+ PreviousWordStart { ignore_punctuation: bool },
StartOfLine,
EndOfLine,
StartOfDocument,
@@ -41,8 +34,6 @@ pub enum Motion {
struct NextWordStart {
#[serde(default)]
ignore_punctuation: bool,
- #[serde(default)]
- stop_at_newline: bool,
}
#[derive(Clone, Deserialize)]
@@ -87,19 +78,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
cx.add_action(
- |_: &mut Workspace,
- &NextWordStart {
- ignore_punctuation,
- stop_at_newline,
- }: &NextWordStart,
- cx: _| {
- motion(
- Motion::NextWordStart {
- ignore_punctuation,
- stop_at_newline,
- },
- cx,
- )
+ |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
+ motion(Motion::NextWordStart { ignore_punctuation }, cx)
},
);
cx.add_action(
@@ -128,29 +108,48 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
}
}
+// Motion handling is specified here:
+// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
impl Motion {
+ pub fn linewise(self) -> bool {
+ use Motion::*;
+ match self {
+ Down | Up | StartOfDocument | EndOfDocument => true,
+ _ => false,
+ }
+ }
+
+ pub fn inclusive(self) -> bool {
+ use Motion::*;
+ if self.linewise() {
+ return true;
+ }
+
+ match self {
+ EndOfLine | NextWordEnd { .. } => true,
+ Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
+ _ => panic!("Exclusivity not defined for {self:?}"),
+ }
+ }
+
pub fn move_point(
self,
map: &DisplaySnapshot,
point: DisplayPoint,
goal: SelectionGoal,
- block_cursor_positioning: bool,
) -> (DisplayPoint, SelectionGoal) {
use Motion::*;
match self {
Left => (left(map, point), SelectionGoal::None),
- Down => movement::down(map, point, goal),
- Up => movement::up(map, point, goal),
+ Down => movement::down(map, point, goal, true),
+ Up => movement::up(map, point, goal, true),
Right => (right(map, point), SelectionGoal::None),
- NextWordStart {
- ignore_punctuation,
- stop_at_newline,
- } => (
- next_word_start(map, point, ignore_punctuation, stop_at_newline),
+ NextWordStart { ignore_punctuation } => (
+ next_word_start(map, point, ignore_punctuation),
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
- next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
+ next_word_end(map, point, ignore_punctuation),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
@@ -164,11 +163,55 @@ impl Motion {
}
}
- pub fn line_wise(self) -> bool {
- use Motion::*;
- match self {
- Down | Up | StartOfDocument | EndOfDocument => true,
- _ => false,
+ // Expands a selection using self motion for an operator
+ pub fn expand_selection(
+ self,
+ map: &DisplaySnapshot,
+ selection: &mut Selection<DisplayPoint>,
+ expand_to_surrounding_newline: bool,
+ ) {
+ let (head, goal) = self.move_point(map, selection.head(), selection.goal);
+ selection.set_head(head, goal);
+
+ if self.linewise() {
+ selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+
+ if expand_to_surrounding_newline {
+ if selection.end.row() < map.max_point().row() {
+ *selection.end.row_mut() += 1;
+ *selection.end.column_mut() = 0;
+ // Don't reset the end here
+ return;
+ } else if selection.start.row() > 0 {
+ *selection.start.row_mut() -= 1;
+ *selection.start.column_mut() = map.line_len(selection.start.row());
+ }
+ }
+
+ selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+ } else {
+ // If the motion is exclusive and the end of the motion is in column 1, the
+ // end of the motion is moved to the end of the previous line and the motion
+ // becomes inclusive. Example: "}" moves to the first line after a paragraph,
+ // but "d}" will not include that line.
+ let mut inclusive = self.inclusive();
+ if !inclusive
+ && selection.end.row() > selection.start.row()
+ && selection.end.column() == 0
+ && selection.end.row() > 0
+ {
+ inclusive = true;
+ *selection.end.row_mut() -= 1;
+ *selection.end.column_mut() = 0;
+ selection.end = map.clip_point(
+ map.next_line_boundary(selection.end.to_point(map)).1,
+ Bias::Left,
+ );
+ }
+
+ if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
+ *selection.end.column_mut() += 1;
+ }
}
}
}
@@ -187,7 +230,6 @@ fn next_word_start(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
- stop_at_newline: bool,
) -> DisplayPoint {
let mut crossed_newline = false;
movement::find_boundary(map, point, |left, right| {
@@ -196,8 +238,8 @@ fn next_word_start(
let at_newline = right == '\n';
let found = (left_kind != right_kind && !right.is_whitespace())
- || (at_newline && (crossed_newline || stop_at_newline))
- || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
+ || at_newline && crossed_newline
+ || at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
@@ -210,7 +252,6 @@ fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
- before_end_character: bool,
) -> DisplayPoint {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
@@ -221,13 +262,12 @@ fn next_word_end(
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
- if before_end_character
- && !map
- .chars_at(point)
- .skip(1)
- .next()
- .map(|c| c == '\n')
- .unwrap_or(true)
+ if !map
+ .chars_at(point)
+ .skip(1)
+ .next()
+ .map(|c| c == '\n')
+ .unwrap_or(true)
{
*point.column_mut() = point.column().saturating_sub(1);
}
@@ -1,11 +1,17 @@
-use crate::{
- motion::Motion,
- state::{Mode, Operator},
- Vim,
-};
-use editor::Bias;
-use gpui::MutableAppContext;
-use language::SelectionGoal;
+mod change;
+mod delete;
+
+use crate::{motion::Motion, state::Operator, Vim};
+use change::init as change_init;
+use gpui::{actions, MutableAppContext};
+
+use self::{change::change_over, delete::delete_over};
+
+actions!(vim, [InsertLineAbove, InsertLineBelow, InsertAfter]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ change_init(cx);
+}
pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
@@ -23,82 +29,7 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, cursor, goal| {
- motion.move_point(map, cursor, goal, true)
- })
- });
-}
-
-fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
- vim.update_active_editor(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- // Don't clip at line ends during change operation
- editor.set_clip_at_line_ends(false, cx);
- editor.move_selections(cx, |map, selection| {
- let (head, goal) = motion.move_point(map, selection.head(), selection.goal, false);
- selection.set_head(head, goal);
-
- if motion.line_wise() {
- selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
- selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
- }
- });
- editor.set_clip_at_line_ends(true, cx);
- editor.insert(&"", cx);
- });
- });
- vim.switch_mode(Mode::Insert, cx)
-}
-
-fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
- vim.update_active_editor(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- // Use goal column to preserve previous position
- editor.set_clip_at_line_ends(false, cx);
- editor.move_selections(cx, |map, selection| {
- let original_head = selection.head();
- let (head, _) = motion.move_point(map, selection.head(), selection.goal, false);
- // Set the goal column to the original position in order to fix it up
- // after the deletion
- selection.set_head(head, SelectionGoal::Column(original_head.column()));
-
- if motion.line_wise() {
- if selection.end.row() == map.max_point().row() {
- // Delete previous line break since we are at the end of the document
- if selection.start.row() > 0 {
- *selection.start.row_mut() = selection.start.row().saturating_sub(1);
- selection.start = map.clip_point(selection.start, Bias::Left);
- selection.start =
- map.next_line_boundary(selection.start.to_point(map)).1;
- } else {
- // Selection covers the whole document. Just delete to the start of the
- // line.
- selection.start =
- map.prev_line_boundary(selection.start.to_point(map)).1;
- }
- selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
- } else {
- // Delete next line break so that we leave the previous line alone
- selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
- *selection.end.column_mut() = 0;
- *selection.end.row_mut() += 1;
- selection.end = map.clip_point(selection.end, Bias::Left);
- }
- }
- });
- editor.insert(&"", cx);
-
- // Fixup cursor position after the deletion
- editor.set_clip_at_line_ends(true, cx);
- editor.move_cursors(cx, |map, mut cursor, goal| {
- if motion.line_wise() {
- if let SelectionGoal::Column(column) = goal {
- *cursor.column_mut() = column
- }
- }
- (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
- });
- });
+ editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
});
}
@@ -116,144 +47,218 @@ mod test {
};
#[gpui::test]
- async fn test_hjkl(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
- cx.simulate_keystroke("l");
- cx.assert_editor_state(indoc! {"
- T|est
- TestTest
- Test"});
- cx.simulate_keystroke("h");
- cx.assert_editor_state(indoc! {"
- |Test
- TestTest
- Test"});
- cx.simulate_keystroke("j");
- cx.assert_editor_state(indoc! {"
- Test
- |TestTest
- Test"});
- cx.simulate_keystroke("k");
- cx.assert_editor_state(indoc! {"
- |Test
- TestTest
- Test"});
- cx.simulate_keystroke("j");
- cx.assert_editor_state(indoc! {"
- Test
- |TestTest
- Test"});
+ async fn test_h(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["h"]);
+ cx.assert("The q|uick", "The |quick");
+ cx.assert("|The quick", "|The quick");
+ cx.assert(
+ indoc! {"
+ The quick
+ |brown"},
+ indoc! {"
+ The quick
+ |brown"},
+ );
+ }
- // When moving left, cursor does not wrap to the previous line
- cx.simulate_keystroke("h");
- cx.assert_editor_state(indoc! {"
- Test
- |TestTest
- Test"});
+ #[gpui::test]
+ async fn test_l(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["l"]);
+ cx.assert("The q|uick", "The qu|ick");
+ cx.assert("The quic|k", "The quic|k");
+ cx.assert(
+ indoc! {"
+ The quic|k
+ brown"},
+ indoc! {"
+ The quic|k
+ brown"},
+ );
+ }
- // When moving right, cursor does not reach the line end or wrap to the next line
- for _ in 0..9 {
- cx.simulate_keystroke("l");
- }
- cx.assert_editor_state(indoc! {"
- Test
- TestTes|t
- Test"});
+ #[gpui::test]
+ async fn test_j(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["j"]);
+ cx.assert(
+ indoc! {"
+ The |quick
+ brown fox"},
+ indoc! {"
+ The quick
+ brow|n fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brow|n fox"},
+ indoc! {"
+ The quick
+ brow|n fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quic|k
+ brown"},
+ indoc! {"
+ The quick
+ brow|n"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |brown"},
+ indoc! {"
+ The quick
+ |brown"},
+ );
+ }
- // Goal column respects the inability to reach the end of the line
- cx.simulate_keystroke("k");
- cx.assert_editor_state(indoc! {"
- Tes|t
- TestTest
- Test"});
- cx.simulate_keystroke("j");
- cx.assert_editor_state(indoc! {"
- Test
- TestTes|t
- Test"});
+ #[gpui::test]
+ async fn test_k(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["k"]);
+ cx.assert(
+ indoc! {"
+ The |quick
+ brown fox"},
+ indoc! {"
+ The |quick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brow|n fox"},
+ indoc! {"
+ The |quick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The
+ quic|k"},
+ indoc! {"
+ Th|e
+ quick"},
+ );
}
#[gpui::test]
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
- let initial_content = indoc! {"
- Test Test
-
- T"};
- let mut cx = VimTestContext::new(cx, true, initial_content).await;
-
- cx.simulate_keystroke("shift-$");
- cx.assert_editor_state(indoc! {"
- Test Tes|t
-
- T"});
- cx.simulate_keystroke("0");
- cx.assert_editor_state(indoc! {"
- |Test Test
-
- T"});
-
- cx.simulate_keystroke("j");
- cx.simulate_keystroke("shift-$");
- cx.assert_editor_state(indoc! {"
- Test Test
- |
- T"});
- cx.simulate_keystroke("0");
- cx.assert_editor_state(indoc! {"
- Test Test
- |
- T"});
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-$"]);
+ cx.assert("T|est test", "Test tes|t");
+ cx.assert("Test tes|t", "Test tes|t");
+ cx.assert(
+ indoc! {"
+ The |quick
+ brown"},
+ indoc! {"
+ The quic|k
+ brown"},
+ );
+ cx.assert(
+ indoc! {"
+ The quic|k
+ brown"},
+ indoc! {"
+ The quic|k
+ brown"},
+ );
- cx.simulate_keystroke("j");
- cx.simulate_keystroke("shift-$");
- cx.assert_editor_state(indoc! {"
- Test Test
-
- |T"});
- cx.simulate_keystroke("0");
- cx.assert_editor_state(indoc! {"
- Test Test
-
- |T"});
+ let mut cx = cx.binding(["0"]);
+ cx.assert("Test |test", "|Test test");
+ cx.assert("|Test test", "|Test test");
+ cx.assert(
+ indoc! {"
+ The |quick
+ brown"},
+ indoc! {"
+ |The quick
+ brown"},
+ );
+ cx.assert(
+ indoc! {"
+ |The quick
+ brown"},
+ indoc! {"
+ |The quick
+ brown"},
+ );
}
#[gpui::test]
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["shift-G"]);
- cx.set_state(
+ cx.assert(
indoc! {"
- The |quick
-
- brown fox jumps
- over the lazy dog"},
- Mode::Normal,
+ The |quick
+
+ brown fox jumps
+ over the lazy dog"},
+ indoc! {"
+ The quick
+
+ brown fox jumps
+ over| the lazy dog"},
);
- cx.simulate_keystroke("shift-G");
- cx.assert_editor_state(indoc! {"
+ cx.assert(
+ indoc! {"
+ The quick
+
+ brown fox jumps
+ over| the lazy dog"},
+ indoc! {"
+ The quick
+
+ brown fox jumps
+ over| the lazy dog"},
+ );
+ cx.assert(
+ indoc! {"
+ The qui|ck
+
+ brown"},
+ indoc! {"
The quick
- brown fox jumps
- over| the lazy dog"});
-
- // Repeat the action doesn't move
- cx.simulate_keystroke("shift-G");
- cx.assert_editor_state(indoc! {"
+ brow|n"},
+ );
+ cx.assert(
+ indoc! {"
+ The qui|ck
+
+ "},
+ indoc! {"
The quick
- brown fox jumps
- over| the lazy dog"});
+ |"},
+ );
}
#[gpui::test]
async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
- let (initial_content, cursor_offsets) = marked_text(indoc! {"
+ let mut cx = VimTestContext::new(cx, true).await;
+ let (_, cursor_offsets) = marked_text(indoc! {"
The |quick|-|brown
|
|
|fox_jumps |over
|th||e"});
- let mut cx = VimTestContext::new(cx, true, &initial_content).await;
+ cx.set_state(
+ indoc! {"
+ |The quick-brown
+
+
+ fox_jumps over
+ the"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("w");
@@ -261,13 +266,21 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystrokes(["g", "g", "0"]);
let (_, cursor_offsets) = marked_text(indoc! {"
The |quick-brown
|
|
|fox_jumps |over
|th||e"});
+ cx.set_state(
+ indoc! {"
+ |The quick-brown
+
+
+ fox_jumps over
+ the"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("shift-W");
@@ -277,13 +290,22 @@ mod test {
#[gpui::test]
async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
- let (initial_content, cursor_offsets) = marked_text(indoc! {"
+ let mut cx = VimTestContext::new(cx, true).await;
+ let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quic|k|-brow|n
fox_jump|s ove|r
th|e"});
- let mut cx = VimTestContext::new(cx, true, &initial_content).await;
+ cx.set_state(
+ indoc! {"
+ |The quick-brown
+
+
+ fox_jumps over
+ the"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("e");
@@ -291,13 +313,21 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystrokes(["g", "g", "0"]);
let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quick-brow|n
fox_jump|s ove|r
th||e"});
+ cx.set_state(
+ indoc! {"
+ |The quick-brown
+
+
+ fox_jumps over
+ the"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("shift-E");
cx.assert_newest_selection_head_offset(cursor_offset);
@@ -306,14 +336,22 @@ mod test {
#[gpui::test]
async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
- let (initial_content, cursor_offsets) = marked_text(indoc! {"
+ let mut cx = VimTestContext::new(cx, true).await;
+ let (_, cursor_offsets) = marked_text(indoc! {"
||The |quick|-|brown
|
|
|fox_jumps |over
|the"});
- let mut cx = VimTestContext::new(cx, true, &initial_content).await;
- cx.simulate_keystrokes(["shift-G", "shift-$"]);
+ cx.set_state(
+ indoc! {"
+ The quick-brown
+
+
+ fox_jumps over
+ th|e"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets.into_iter().rev() {
cx.simulate_keystroke("b");
@@ -321,13 +359,21 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystrokes(["shift-G", "shift-$"]);
let (_, cursor_offsets) = marked_text(indoc! {"
||The |quick-brown
|
|
|fox_jumps |over
|the"});
+ cx.set_state(
+ indoc! {"
+ The quick-brown
+
+
+ fox_jumps over
+ th|e"},
+ Mode::Normal,
+ );
for cursor_offset in cursor_offsets.into_iter().rev() {
cx.simulate_keystroke("shift-B");
cx.assert_newest_selection_head_offset(cursor_offset);
@@ -336,7 +382,7 @@ mod test {
#[gpui::test]
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
+ let mut cx = VimTestContext::new(cx, true).await;
// Can abort with escape to get back to normal mode
cx.simulate_keystroke("g");
@@ -352,455 +398,55 @@ mod test {
#[gpui::test]
async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
-
- cx.set_state(
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["g", "g"]);
+ cx.assert(
indoc! {"
- The q|uick
+ The quick
- brown fox jumps
- over the lazy dog"},
- Mode::Normal,
- );
-
- // Jump to the end to
- cx.simulate_keystroke("shift-G");
- cx.assert_editor_state(indoc! {"
- The quick
-
- brown fox jumps
- over |the lazy dog"});
-
- // Jump to the start
- cx.simulate_keystrokes(["g", "g"]);
- cx.assert_editor_state(indoc! {"
- The q|uick
-
- brown fox jumps
- over the lazy dog"});
- assert_eq!(cx.mode(), Normal);
- assert_eq!(cx.active_operator(), None);
-
- // Repeat action doesn't change
- cx.simulate_keystrokes(["g", "g"]);
- cx.assert_editor_state(indoc! {"
- The q|uick
-
- brown fox jumps
- over the lazy dog"});
- assert_eq!(cx.mode(), Normal);
- assert_eq!(cx.active_operator(), None);
- }
-
- #[gpui::test]
- async fn test_change(cx: &mut gpui::TestAppContext) {
- fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
- cx.assert_binding(
- ["c", motion],
- initial_state,
- Mode::Normal,
- state_after,
- Mode::Insert,
- );
- }
- let cx = &mut VimTestContext::new(cx, true, "").await;
- assert("h", "Te|st", "T|st", cx);
- assert("l", "Te|st", "Te|t", cx);
- assert("w", "|Test", "|", cx);
- assert("w", "Te|st", "Te|", cx);
- assert("w", "Te|st Test", "Te| Test", cx);
- assert("e", "Te|st Test", "Te| Test", cx);
- assert("b", "Te|st", "|st", cx);
- assert("b", "Test Te|st", "Test |st", cx);
- assert(
- "w",
- indoc! {"
- The quick
- brown |fox
- jumps over"},
- indoc! {"
- The quick
- brown |
- jumps over"},
- cx,
- );
- assert(
- "shift-W",
- indoc! {"
- The quick
- brown |fox-fox
- jumps over"},
- indoc! {"
- The quick
- brown |
- jumps over"},
- cx,
- );
- assert(
- "k",
- indoc! {"
- The quick
- brown |fox"},
- indoc! {"
- |"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The q|uick
- brown fox"},
- indoc! {"
- |"},
- cx,
- );
- assert(
- "shift-$",
- indoc! {"
- The q|uick
- brown fox"},
- indoc! {"
- The q|
- brown fox"},
- cx,
- );
- assert(
- "0",
- indoc! {"
- The q|uick
- brown fox"},
- indoc! {"
- |uick
- brown fox"},
- cx,
- );
- }
-
- #[gpui::test]
- async fn test_delete(cx: &mut gpui::TestAppContext) {
- fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
- cx.assert_binding(
- ["d", motion],
- initial_state,
- Mode::Normal,
- state_after,
- Mode::Normal,
- );
- }
- let cx = &mut VimTestContext::new(cx, true, "").await;
- assert("h", "Te|st", "T|st", cx);
- assert("l", "Te|st", "Te|t", cx);
- assert("w", "|Test", "|", cx);
- assert("w", "Te|st", "T|e", cx);
- assert("w", "Te|st Test", "Te|Test", cx);
- assert("e", "Te|st Test", "Te| Test", cx);
- assert("b", "Te|st", "|st", cx);
- assert("b", "Test Te|st", "Test |st", cx);
- assert(
- "w",
- indoc! {"
- The quick
- brown |fox
- jumps over"},
- // Trailing space after cursor
- indoc! {"
- The quick
- brown|
- jumps over"},
- cx,
- );
- assert(
- "shift-W",
- indoc! {"
- The quick
- brown |fox-fox
- jumps over"},
- // Trailing space after cursor
- indoc! {"
- The quick
- brown|
- jumps over"},
- cx,
- );
- assert(
- "shift-$",
- indoc! {"
- The q|uick
- brown fox"},
- indoc! {"
- The |q
- brown fox"},
- cx,
- );
- assert(
- "0",
- indoc! {"
- The q|uick
- brown fox"},
- indoc! {"
- |uick
- brown fox"},
- cx,
- );
- }
-
- #[gpui::test]
- async fn test_linewise_delete(cx: &mut gpui::TestAppContext) {
- fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
- cx.assert_binding(
- ["d", motion],
- initial_state,
- Mode::Normal,
- state_after,
- Mode::Normal,
- );
- }
- let cx = &mut VimTestContext::new(cx, true, "").await;
- assert(
- "k",
- indoc! {"
- The quick
- brown |fox
- jumps over"},
- indoc! {"
- jumps |over"},
- cx,
- );
- assert(
- "k",
- indoc! {"
- The quick
- brown fox
- jumps |over"},
- indoc! {"
- The qu|ick"},
- cx,
- );
- assert(
- "j",
+ brown fox jumps
+ over |the lazy dog"},
indoc! {"
- The q|uick
- brown fox
- jumps over"},
- indoc! {"
- jumps| over"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The quick
- brown| fox
- jumps over"},
- indoc! {"
- The q|uick"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The quick
- brown| fox
- jumps over"},
- indoc! {"
- The q|uick"},
- cx,
- );
- cx.assert_binding(
- ["d", "g", "g"],
- indoc! {"
- The quick
- brown| fox
- jumps over
- the lazy"},
- Mode::Normal,
- indoc! {"
- jumps| over
- the lazy"},
- Mode::Normal,
- );
- cx.assert_binding(
- ["d", "g", "g"],
- indoc! {"
- The quick
- brown fox
- jumps over
- the l|azy"},
- Mode::Normal,
- "|",
- Mode::Normal,
- );
- assert(
- "shift-G",
- indoc! {"
- The quick
- brown| fox
- jumps over
- the lazy"},
- indoc! {"
- The q|uick"},
- cx,
- );
- cx.assert_binding(
- ["d", "g", "g"],
- indoc! {"
- The q|uick
- brown fox
- jumps over
- the lazy"},
- Mode::Normal,
- indoc! {"
- brown| fox
- jumps over
- the lazy"},
- Mode::Normal,
- );
- }
-
- #[gpui::test]
- async fn test_linewise_change(cx: &mut gpui::TestAppContext) {
- fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
- cx.assert_binding(
- ["c", motion],
- initial_state,
- Mode::Normal,
- state_after,
- Mode::Insert,
- );
- }
- let cx = &mut VimTestContext::new(cx, true, "").await;
- assert(
- "k",
- indoc! {"
- The quick
- brown |fox
- jumps over"},
- indoc! {"
- |
- jumps over"},
- cx,
- );
- assert(
- "k",
- indoc! {"
- The quick
- brown fox
- jumps |over"},
- indoc! {"
- The quick
- |"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The q|uick
- brown fox
- jumps over"},
- indoc! {"
- |
- jumps over"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The quick
- brown| fox
- jumps over"},
- indoc! {"
- The quick
- |"},
- cx,
- );
- assert(
- "j",
- indoc! {"
- The quick
- brown| fox
- jumps over"},
- indoc! {"
- The quick
- |"},
- cx,
- );
- assert(
- "shift-G",
- indoc! {"
- The quick
- brown| fox
- jumps over
- the lazy"},
- indoc! {"
- The quick
- |"},
- cx,
- );
- assert(
- "shift-G",
- indoc! {"
- The quick
- brown| fox
- jumps over
- the lazy"},
- indoc! {"
- The quick
- |"},
- cx,
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"},
);
- assert(
- "shift-G",
+ cx.assert(
indoc! {"
- The quick
- brown fox
- jumps over
- the l|azy"},
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"},
indoc! {"
- The quick
- brown fox
- jumps over
- |"},
- cx,
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"},
);
- cx.assert_binding(
- ["c", "g", "g"],
+ cx.assert(
indoc! {"
- The quick
- brown| fox
- jumps over
- the lazy"},
- Mode::Normal,
- indoc! {"
- |
- jumps over
- the lazy"},
- Mode::Insert,
- );
- cx.assert_binding(
- ["c", "g", "g"],
+ The quick
+
+ brown fox jumps
+ over the la|zy dog"},
indoc! {"
- The quick
- brown fox
- jumps over
- the l|azy"},
- Mode::Normal,
- "|",
- Mode::Insert,
+ The quic|k
+
+ brown fox jumps
+ over the lazy dog"},
);
- cx.assert_binding(
- ["c", "g", "g"],
+ cx.assert(
indoc! {"
- The q|uick
- brown fox
- jumps over
- the lazy"},
- Mode::Normal,
+
+
+ brown fox jumps
+ over the la|zy dog"},
indoc! {"
- |
- brown fox
- jumps over
- the lazy"},
- Mode::Insert,
+ |
+
+ brown fox jumps
+ over the lazy dog"},
);
}
}
@@ -0,0 +1,436 @@
+use crate::{motion::Motion, state::Mode, Vim};
+use editor::{char_kind, movement};
+use gpui::{impl_actions, MutableAppContext, ViewContext};
+use serde::Deserialize;
+use workspace::Workspace;
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ChangeWord {
+ #[serde(default)]
+ ignore_punctuation: bool,
+}
+
+impl_actions!(vim, [ChangeWord]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(change_word);
+}
+
+pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ // We are swapping to insert mode anyway. Just set the line end clipping behavior now
+ editor.set_clip_at_line_ends(false, cx);
+ editor.move_selections(cx, |map, selection| {
+ motion.expand_selection(map, selection, false);
+ });
+ editor.insert(&"", cx);
+ });
+ });
+ vim.switch_mode(Mode::Insert, cx)
+}
+
+// From the docs https://vimhelp.org/change.txt.html#cw
+// Special case: When the cursor is in a word, "cw" and "cW" do not include the
+// white space after a word, they only change up to the end of the word. This is
+// because Vim interprets "cw" as change-word, and a word does not include the
+// following white space.
+fn change_word(
+ _: &mut Workspace,
+ &ChangeWord { ignore_punctuation }: &ChangeWord,
+ cx: &mut ViewContext<Workspace>,
+) {
+ Vim::update(cx, |vim, cx| {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ // We are swapping to insert mode anyway. Just set the line end clipping behavior now
+ editor.set_clip_at_line_ends(false, cx);
+ editor.move_selections(cx, |map, selection| {
+ if selection.end.column() == map.line_len(selection.end.row()) {
+ return;
+ }
+
+ selection.end = movement::find_boundary(map, selection.end, |left, right| {
+ let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+ left_kind != right_kind || left == '\n' || right == '\n'
+ });
+ });
+ editor.insert(&"", cx);
+ });
+ });
+ vim.switch_mode(Mode::Insert, cx);
+ });
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+
+ use crate::{state::Mode, vim_test_context::VimTestContext};
+
+ #[gpui::test]
+ async fn test_change_h(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
+ cx.assert("Te|st", "T|st");
+ cx.assert("T|est", "|est");
+ cx.assert("|Test", "|Test");
+ cx.assert(
+ indoc! {"
+ Test
+ |test"},
+ indoc! {"
+ Test
+ |test"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_l(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
+ cx.assert("Te|st", "Te|t");
+ cx.assert("Tes|t", "Tes|");
+ }
+
+ #[gpui::test]
+ async fn test_change_w(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
+ cx.assert("Te|st", "Te|");
+ cx.assert("T|est test", "T| test");
+ cx.assert("Test| test", "Test|test");
+ cx.assert(
+ indoc! {"
+ Test te|st
+ test"},
+ indoc! {"
+ Test te|
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test tes|t
+ test"},
+ indoc! {"
+ Test tes|
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ indoc! {"
+ Test test
+ |
+ test"},
+ );
+
+ let mut cx = cx.binding(["c", "shift-W"]);
+ cx.assert("Test te|st-test test", "Test te| test");
+ }
+
+ #[gpui::test]
+ async fn test_change_e(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
+ cx.assert("Te|st Test", "Te| Test");
+ cx.assert("T|est test", "T| test");
+ cx.assert(
+ indoc! {"
+ Test te|st
+ test"},
+ indoc! {"
+ Test te|
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test tes|t
+ test"},
+ "Test tes|",
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ indoc! {"
+ Test test
+ |
+ test"},
+ );
+
+ let mut cx = cx.binding(["c", "shift-E"]);
+ cx.assert("Test te|st-test test", "Test te| test");
+ }
+
+ #[gpui::test]
+ async fn test_change_b(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
+ cx.assert("Te|st Test", "|st Test");
+ cx.assert("Test |test", "|test");
+ cx.assert("Test1 test2 |test3", "Test1 |test3");
+ cx.assert(
+ indoc! {"
+ Test test
+ |test"},
+ indoc! {"
+ Test |
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ indoc! {"
+ Test |
+
+ test"},
+ );
+
+ let mut cx = cx.binding(["c", "shift-B"]);
+ cx.assert("Test test-test |test", "Test |test");
+ }
+
+ #[gpui::test]
+ async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The q|
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_0(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |uick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_k(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ |
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ brown fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ brown fox
+ jumps over"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_j(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown fox
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ |"},
+ indoc! {"
+ The quick
+ brown fox
+ |"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ The quick
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ The quick
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_gg(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ |
+ jumps over
+ the lazy"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ "|",
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ |
+ brown fox
+ jumps over
+ the lazy"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ brown fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ |
+ brown fox
+ jumps over
+ the lazy"},
+ );
+ }
+}
@@ -0,0 +1,386 @@
+use crate::{motion::Motion, Vim};
+use editor::Bias;
+use gpui::MutableAppContext;
+use language::SelectionGoal;
+
+pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.set_clip_at_line_ends(false, cx);
+ editor.move_selections(cx, |map, selection| {
+ let original_head = selection.head();
+ motion.expand_selection(map, selection, true);
+ selection.goal = SelectionGoal::Column(original_head.column());
+ });
+ editor.insert(&"", cx);
+
+ // Fixup cursor position after the deletion
+ editor.set_clip_at_line_ends(true, cx);
+ editor.move_cursors(cx, |map, mut cursor, goal| {
+ if motion.linewise() {
+ if let SelectionGoal::Column(column) = goal {
+ *cursor.column_mut() = column
+ }
+ }
+
+ (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+ });
+ });
+ });
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+
+ use crate::vim_test_context::VimTestContext;
+
+ #[gpui::test]
+ async fn test_delete_h(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "h"]);
+ cx.assert("Te|st", "T|st");
+ cx.assert("T|est", "|est");
+ cx.assert("|Test", "|Test");
+ cx.assert(
+ indoc! {"
+ Test
+ |test"},
+ indoc! {"
+ Test
+ |test"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_l(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "l"]);
+ cx.assert("|Test", "|est");
+ cx.assert("Te|st", "Te|t");
+ cx.assert("Tes|t", "Te|s");
+ cx.assert(
+ indoc! {"
+ Tes|t
+ test"},
+ indoc! {"
+ Te|s
+ test"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_w(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "w"]);
+ cx.assert("Te|st", "T|e");
+ cx.assert("T|est test", "T|test");
+ cx.assert(
+ indoc! {"
+ Test te|st
+ test"},
+ indoc! {"
+ Test t|e
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test tes|t
+ test"},
+ indoc! {"
+ Test te|s
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ indoc! {"
+ Test test
+ |
+ test"},
+ );
+
+ let mut cx = cx.binding(["d", "shift-W"]);
+ cx.assert("Test te|st-test test", "Test te|test");
+ }
+
+ #[gpui::test]
+ async fn test_delete_e(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "e"]);
+ cx.assert("Te|st Test", "Te| Test");
+ cx.assert("T|est test", "T| test");
+ cx.assert(
+ indoc! {"
+ Test te|st
+ test"},
+ indoc! {"
+ Test t|e
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test tes|t
+ test"},
+ "Test te|s",
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ indoc! {"
+ Test test
+ |
+ test"},
+ );
+
+ let mut cx = cx.binding(["d", "shift-E"]);
+ cx.assert("Test te|st-test test", "Test te| test");
+ }
+
+ #[gpui::test]
+ async fn test_delete_b(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "b"]);
+ cx.assert("Te|st Test", "|st Test");
+ cx.assert("Test |test", "|test");
+ cx.assert("Test1 test2 |test3", "Test1 |test3");
+ cx.assert(
+ indoc! {"
+ Test test
+ |test"},
+ // Trailing whitespace after cursor
+ indoc! {"
+ Test|
+ test"},
+ );
+ cx.assert(
+ indoc! {"
+ Test test
+ |
+ test"},
+ // Trailing whitespace after cursor
+ indoc! {"
+ Test|
+
+ test"},
+ );
+
+ let mut cx = cx.binding(["d", "shift-B"]);
+ cx.assert("Test test-test |test", "Test |test");
+ }
+
+ #[gpui::test]
+ async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "shift-$"]);
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The |q
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_0(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "0"]);
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |uick
+ brown fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ indoc! {"
+ The quick
+ |
+ brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_k(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "k"]);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ "jumps |over",
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ "The qu|ick",
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ brown| fox
+ jumps over"},
+ );
+ cx.assert(
+ indoc! {"
+ |brown fox
+ jumps over"},
+ "|jumps over",
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_j(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "j"]);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ "The qu|ick",
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ brown |fox"},
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ "jumps| over",
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ |"},
+ indoc! {"
+ The quick
+ |brown fox"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "shift-G"]);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ "The q|uick",
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ "The q|uick",
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ indoc! {"
+ The quick
+ brown fox
+ jumps| over"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ indoc! {"
+ The quick
+ brown fox
+ |jumps over"},
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
+ let cx = VimTestContext::new(cx, true).await;
+ let mut cx = cx.binding(["d", "g", "g"]);
+ cx.assert(
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ jumps| over
+ the lazy"},
+ );
+ cx.assert(
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ "|",
+ );
+ cx.assert(
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ brown| fox
+ jumps over
+ the lazy"},
+ );
+ cx.assert(
+ indoc! {"
+ |
+ brown fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ |brown fox
+ jumps over
+ the lazy"},
+ );
+ }
+}
@@ -1,10 +1,11 @@
+#[cfg(test)]
+mod vim_test_context;
+
mod editor_events;
mod insert;
mod motion;
mod normal;
mod state;
-#[cfg(test)]
-mod vim_test_context;
use collections::HashMap;
use editor::{CursorShape, Editor};
@@ -25,6 +26,7 @@ impl_actions!(vim, [SwitchMode, PushOperator]);
pub fn init(cx: &mut MutableAppContext) {
editor_events::init(cx);
+ normal::init(cx);
insert::init(cx);
motion::init(cx);
@@ -142,14 +144,14 @@ mod test {
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, false, "").await;
+ let mut cx = VimTestContext::new(cx, false).await;
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjkl|");
}
#[gpui::test]
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
+ let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
@@ -15,11 +15,7 @@ pub struct VimTestContext<'a> {
}
impl<'a> VimTestContext<'a> {
- pub async fn new(
- cx: &'a mut gpui::TestAppContext,
- enabled: bool,
- initial_editor_text: &str,
- ) -> VimTestContext<'a> {
+ pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
cx.update(|cx| {
editor::init(cx);
crate::init(cx);
@@ -38,10 +34,7 @@ impl<'a> VimTestContext<'a> {
params
.fs
.as_fake()
- .insert_tree(
- "/root",
- json!({ "dir": { "test.txt": initial_editor_text } }),
- )
+ .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
@@ -202,6 +195,14 @@ impl<'a> VimTestContext<'a> {
assert_eq!(self.mode(), mode_after);
assert_eq!(self.active_operator(), None);
}
+
+ pub fn binding<const COUNT: usize>(
+ mut self,
+ keystrokes: [&'static str; COUNT],
+ ) -> VimBindingTestContext<'a, COUNT> {
+ let mode = self.mode();
+ VimBindingTestContext::new(keystrokes, mode, mode, self)
+ }
}
impl<'a> Deref for VimTestContext<'a> {
@@ -211,3 +212,61 @@ impl<'a> Deref for VimTestContext<'a> {
self.cx
}
}
+
+pub struct VimBindingTestContext<'a, const COUNT: usize> {
+ cx: VimTestContext<'a>,
+ keystrokes_under_test: [&'static str; COUNT],
+ initial_mode: Mode,
+ mode_after: Mode,
+}
+
+impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
+ pub fn new(
+ keystrokes_under_test: [&'static str; COUNT],
+ initial_mode: Mode,
+ mode_after: Mode,
+ cx: VimTestContext<'a>,
+ ) -> Self {
+ Self {
+ cx,
+ keystrokes_under_test,
+ initial_mode,
+ mode_after,
+ }
+ }
+
+ pub fn binding<const NEW_COUNT: usize>(
+ self,
+ keystrokes_under_test: [&'static str; NEW_COUNT],
+ ) -> VimBindingTestContext<'a, NEW_COUNT> {
+ VimBindingTestContext {
+ keystrokes_under_test,
+ cx: self.cx,
+ initial_mode: self.initial_mode,
+ mode_after: self.mode_after,
+ }
+ }
+
+ pub fn mode_after(mut self, mode_after: Mode) -> Self {
+ self.mode_after = mode_after;
+ self
+ }
+
+ pub fn assert(&mut self, initial_state: &str, state_after: &str) {
+ self.cx.assert_binding(
+ self.keystrokes_under_test,
+ initial_state,
+ self.initial_mode,
+ state_after,
+ self.mode_after,
+ )
+ }
+}
+
+impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
+ type Target = VimTestContext<'a>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.cx
+ }
+}