Merge pull request #947 from zed-industries/misc-normal-commands

Keith Simmons created

Misc vim normal commands

Change summary

assets/keymaps/vim.json         |  65 ++-
crates/editor/src/editor.rs     |  33 +
crates/vim/src/editor_events.rs |  10 
crates/vim/src/motion.rs        |  32 +
crates/vim/src/normal.rs        | 602 ++++++++++++++++++++++++++++++++++
5 files changed, 700 insertions(+), 42 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -2,10 +2,6 @@
     {
         "context": "Editor && VimControl",
         "bindings": {
-            "i": [
-                "vim::SwitchMode",
-                "Insert"
-            ],
             "g": [
                 "vim::PushOperator",
                 {
@@ -13,6 +9,7 @@
                 }
             ],
             "h": "vim::Left",
+            "backspace": "vim::Left",
             "j": "vim::Down",
             "k": "vim::Up",
             "l": "vim::Right",
@@ -46,29 +43,41 @@
             ]
         }
     },
-    {
-        "context": "Editor && vim_operator == g",
-        "bindings": {
-            "g": "vim::StartOfDocument"
-        }
-    },
-    {
-        "context": "Editor && vim_mode == insert",
-        "bindings": {
-            "escape": "vim::NormalBefore",
-            "ctrl-c": "vim::NormalBefore"
-        }
-    },
     {
         "context": "Editor && vim_mode == normal",
         "bindings": {
+            "escape": "editor::Cancel",
             "c": [
                 "vim::PushOperator",
                 "Change"
             ],
+            "shift-C": "vim::ChangeToEndOfLine",
             "d": [
                 "vim::PushOperator",
                 "Delete"
+            ],
+            "shift-D": "vim::DeleteToEndOfLine",
+            "i": [
+                "vim::SwitchMode",
+                "Insert"
+            ],
+            "shift-I": "vim::InsertFirstNonWhitespace",
+            "a": "vim::InsertAfter",
+            "shift-A": "vim::InsertEndOfLine",
+            "x": "vim::DeleteRight",
+            "shift-X": "vim::DeleteLeft",
+            "shift-^": "vim::FirstNonWhitespace",
+            "o": "vim::InsertLineBelow",
+            "shift-O": "vim::InsertLineAbove"
+        }
+    },
+    {
+        "context": "Editor && vim_operator == g",
+        "bindings": {
+            "g": "vim::StartOfDocument",
+            "escape": [
+                "vim::SwitchMode",
+                "Normal"
             ]
         }
     },
@@ -81,7 +90,27 @@
                 {
                     "ignorePunctuation": true
                 }
-            ]
+            ],
+            "c": "vim::CurrentLine"
+        }
+    },
+    {
+        "context": "Editor && vim_operator == d",
+        "bindings": {
+            "d": "vim::CurrentLine"
+        }
+    },
+    {
+        "context": "Editor && vim_mode == insert",
+        "bindings": {
+            "escape": "vim::NormalBefore",
+            "ctrl-c": "vim::NormalBefore"
+        }
+    },
+    {
+        "context": "Editor && mode == singleline",
+        "bindings": {
+            "escape": "editor::Cancel"
         }
     }
 ]

crates/editor/src/editor.rs 🔗

@@ -1334,6 +1334,19 @@ impl Editor {
         self.update_selections(vec![selection], None, cx);
     }
 
+    pub fn display_selections(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self
+            .local_selections::<Point>(cx)
+            .into_iter()
+            .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
+            .collect();
+        (display_map, selections)
+    }
+
     pub fn move_selections(
         &mut self,
         cx: &mut ViewContext<Self>,
@@ -1382,6 +1395,25 @@ impl Editor {
         });
     }
 
+    pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx));
+    }
+
+    pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        self.buffer
+            .update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx));
+    }
+
     fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
         self.hide_context_menu(cx);
 
@@ -1456,6 +1488,7 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
         let newest_selection = self.newest_anchor_selection().clone();
+        let position = display_map.clip_point(position, Bias::Left);
 
         let start;
         let end;

crates/vim/src/editor_events.rs 🔗

@@ -18,15 +18,11 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont
 }
 
 fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
-    let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
-        Mode::Insert
-    } else {
-        Mode::Normal
-    };
-
     Vim::update(cx, |state, cx| {
         state.active_editor = Some(editor.downgrade());
-        state.switch_mode(mode, cx);
+        if editor.read(cx).mode() != EditorMode::Full {
+            state.switch_mode(Mode::Insert, cx);
+        }
     });
 }
 

crates/vim/src/motion.rs 🔗

@@ -1,7 +1,7 @@
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, ToDisplayPoint},
-    movement, Bias, DisplayPoint,
+    movement, Bias, CharKind, DisplayPoint,
 };
 use gpui::{actions, impl_actions, MutableAppContext};
 use language::{Selection, SelectionGoal};
@@ -23,6 +23,8 @@ pub enum Motion {
     NextWordStart { ignore_punctuation: bool },
     NextWordEnd { ignore_punctuation: bool },
     PreviousWordStart { ignore_punctuation: bool },
+    FirstNonWhitespace,
+    CurrentLine,
     StartOfLine,
     EndOfLine,
     StartOfDocument,
@@ -57,8 +59,10 @@ actions!(
         Down,
         Up,
         Right,
+        FirstNonWhitespace,
         StartOfLine,
         EndOfLine,
+        CurrentLine,
         StartOfDocument,
         EndOfDocument
     ]
@@ -70,8 +74,12 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
     cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
     cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
+    cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
+        motion(Motion::FirstNonWhitespace, cx)
+    });
     cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
     cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
         motion(Motion::StartOfDocument, cx)
     });
@@ -114,7 +122,7 @@ impl Motion {
     pub fn linewise(self) -> bool {
         use Motion::*;
         match self {
-            Down | Up | StartOfDocument | EndOfDocument => true,
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine => true,
             _ => false,
         }
     }
@@ -156,8 +164,10 @@ impl Motion {
                 previous_word_start(map, point, ignore_punctuation),
                 SelectionGoal::None,
             ),
+            FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
             StartOfLine => (start_of_line(map, point), SelectionGoal::None),
             EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            CurrentLine => (end_of_line(map, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
             EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
         }
@@ -290,6 +300,24 @@ fn previous_word_start(
     point
 }
 
+fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    let mut column = 0;
+    for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
+        if ch == '\n' {
+            return point;
+        }
+
+        if char_kind(ch) != CharKind::Whitespace {
+            break;
+        }
+
+        column += ch.len_utf8() as u32;
+    }
+
+    *point.column_mut() = column;
+    map.clip_point(point, Bias::Left)
+}
+
 fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     map.prev_line_boundary(point.to_point(map)).1
 }

crates/vim/src/normal.rs 🔗

@@ -1,15 +1,62 @@
 mod change;
 mod delete;
 
-use crate::{motion::Motion, state::Operator, Vim};
+use crate::{
+    motion::Motion,
+    state::{Mode, Operator},
+    Vim,
+};
 use change::init as change_init;
-use gpui::{actions, MutableAppContext};
+use collections::HashSet;
+use editor::{Bias, DisplayPoint};
+use gpui::{actions, MutableAppContext, ViewContext};
+use language::SelectionGoal;
+use workspace::Workspace;
 
 use self::{change::change_over, delete::delete_over};
 
-actions!(vim, [InsertLineAbove, InsertLineBelow, InsertAfter]);
+actions!(
+    vim,
+    [
+        InsertAfter,
+        InsertFirstNonWhitespace,
+        InsertEndOfLine,
+        InsertLineAbove,
+        InsertLineBelow,
+        DeleteLeft,
+        DeleteRight,
+        ChangeToEndOfLine,
+        DeleteToEndOfLine,
+    ]
+);
 
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(insert_after);
+    cx.add_action(insert_first_non_whitespace);
+    cx.add_action(insert_end_of_line);
+    cx.add_action(insert_line_above);
+    cx.add_action(insert_line_below);
+    cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::Left, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::Right, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            change_over(vim, Motion::EndOfLine, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::EndOfLine, cx);
+        })
+    });
+
     change_init(cx);
 }
 
@@ -33,6 +80,101 @@ fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     });
 }
 
+fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::Right.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_first_non_whitespace(
+    _: &mut Workspace,
+    _: &InsertFirstNonWhitespace,
+    cx: &mut ViewContext<Workspace>,
+) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::FirstNonWhitespace.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::EndOfLine.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                let (map, old_selections) = editor.display_selections(cx);
+                let selection_start_rows: HashSet<u32> = old_selections
+                    .into_iter()
+                    .map(|selection| selection.start.row())
+                    .collect();
+                let edits = selection_start_rows.into_iter().map(|row| {
+                    let (indent, _) = map.line_indent(row);
+                    let start_of_line = map
+                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
+                        .to_point(&map);
+                    let mut new_text = " ".repeat(indent as usize);
+                    new_text.push('\n');
+                    (start_of_line..start_of_line, new_text)
+                });
+                editor.edit(edits, cx);
+                editor.move_cursors(cx, |map, mut cursor, _| {
+                    *cursor.row_mut() -= 1;
+                    *cursor.column_mut() = map.line_len(cursor.row());
+                    (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                });
+            });
+        });
+    });
+}
+
+fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                let (map, old_selections) = editor.display_selections(cx);
+                let selection_end_rows: HashSet<u32> = old_selections
+                    .into_iter()
+                    .map(|selection| selection.end.row())
+                    .collect();
+                let edits = selection_end_rows.into_iter().map(|row| {
+                    let (indent, _) = map.line_indent(row);
+                    let end_of_line = map
+                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
+                        .to_point(&map);
+                    let mut new_text = "\n".to_string();
+                    new_text.push_str(&" ".repeat(indent as usize));
+                    (end_of_line..end_of_line, new_text)
+                });
+                editor.move_cursors(cx, |map, cursor, goal| {
+                    Motion::EndOfLine.move_point(map, cursor, goal)
+                });
+                editor.edit(edits, cx);
+            });
+        });
+    });
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;
@@ -63,18 +205,18 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_l(cx: &mut gpui::TestAppContext) {
+    async fn test_backspace(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");
+        let mut cx = cx.binding(["backspace"]);
+        cx.assert("The q|uick", "The |quick");
+        cx.assert("|The quick", "|The quick");
         cx.assert(
             indoc! {"
-                The quic|k
-                brown"},
+                The quick
+                |brown"},
             indoc! {"
-                The quic|k
-                brown"},
+                The quick
+                |brown"},
         );
     }
 
@@ -146,6 +288,22 @@ mod 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"},
+        );
+    }
+
     #[gpui::test]
     async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
@@ -242,7 +400,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
+    async fn test_w(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             The |quick|-|brown
@@ -289,7 +447,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
+    async fn test_e(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             Th|e quic|k|-brow|n
@@ -335,7 +493,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
+    async fn test_b(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             ||The |quick|-|brown
@@ -397,7 +555,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
+    async fn test_gg(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
         let mut cx = cx.binding(["g", "g"]);
         cx.assert(
@@ -449,4 +607,418 @@ mod test {
                 over the lazy dog"},
         );
     }
+
+    #[gpui::test]
+    async fn test_a(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["a"]).mode_after(Mode::Insert);
+
+        cx.assert("The q|uick", "The qu|ick");
+        cx.assert("The quic|k", "The quick|");
+    }
+
+    #[gpui::test]
+    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert);
+        cx.assert("The q|uick", "The quick|");
+        cx.assert("The q|uick ", "The quick |");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                The quick|
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-^"]);
+        cx.assert("The q|uick", "|The quick");
+        cx.assert(" The q|uick", " |The quick");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                |The quick
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+        cx.assert(
+            indoc! {"
+                    |
+                The quick"},
+            indoc! {"
+                    |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert);
+        cx.assert("The q|uick", "|The quick");
+        cx.assert(" The q|uick", " |The quick");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                |The quick
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-D"]);
+        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_x(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["x"]);
+        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_left(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-X"]);
+        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_o(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["o"]).mode_after(Mode::Insert);
+
+        cx.assert(
+            "|",
+            indoc! {"
+                
+                |"},
+        );
+        cx.assert(
+            "The |quick",
+            indoc! {"
+                The quick
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                brown fox
+                |
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown fox
+                jumps over
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                
+                |
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test() {
+                    println!(|);
+                }"},
+            indoc! {"
+                fn test() {
+                    println!();
+                    |
+                }"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test(|) {
+                    println!();
+                }"},
+            indoc! {"
+                fn test() {
+                |
+                    println!();
+                }"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert);
+
+        cx.assert(
+            "|",
+            indoc! {"
+                |
+                "},
+        );
+        cx.assert(
+            "The |quick",
+            indoc! {"
+                |
+                The quick"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown fox
+                |
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                |
+                The quick
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |
+                
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test() {
+                    println!(|);
+                }"},
+            indoc! {"
+                fn test() {
+                    |
+                    println!();
+                }"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test(|) {
+                    println!();
+                }"},
+            indoc! {"
+                |
+                fn test() {
+                    println!();
+                }"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_dd(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["d", "d"]);
+
+        cx.assert("|", "|");
+        cx.assert("The |quick", "|");
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                jumps |over"},
+        );
+        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! {"
+                brown| fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |brown fox"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_cc(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["c", "c"]).mode_after(Mode::Insert);
+
+        cx.assert("|", "|");
+        cx.assert("The |quick", "|");
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                jumps over"},
+        );
+        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! {"
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+        );
+    }
 }