Merge pull request #1845 from zed-industries/vim-dd-fix

Kay Simmons created

Vim dd fix

Change summary

crates/editor/src/selections_collection.rs            | 13 ++
crates/vim/src/motion.rs                              | 80 ++++++-----
crates/vim/src/normal.rs                              | 14 +
crates/vim/src/normal/change.rs                       | 82 ++++++++----
crates/vim/src/normal/delete.rs                       |  4 
crates/vim/src/test/neovim_backed_test_context.rs     |  9 +
crates/vim/src/visual.rs                              | 31 ++--
crates/vim/test_data/test_change_end_of_document.json |  2 
crates/vim/test_data/test_change_gg.json              |  2 
crates/vim/test_data/test_change_j.json               |  2 
crates/vim/test_data/test_change_k.json               |  2 
crates/vim/test_data/test_dd.json                     |  2 
crates/vim/test_data/test_delete_end_of_document.json |  2 
crates/vim/test_data/test_delete_gg.json              |  2 
crates/vim/test_data/test_repeated_cb.json            |  0 
15 files changed, 152 insertions(+), 95 deletions(-)

Detailed changes

crates/editor/src/selections_collection.rs 🔗

@@ -677,6 +677,19 @@ impl<'a> MutableSelectionsCollection<'a> {
         });
     }
 
+    pub fn maybe_move_cursors_with(
+        &mut self,
+        mut update_cursor_position: impl FnMut(
+            &DisplaySnapshot,
+            DisplayPoint,
+            SelectionGoal,
+        ) -> Option<(DisplayPoint, SelectionGoal)>,
+    ) {
+        self.move_cursors_with(|map, point, goal| {
+            update_cursor_position(map, point, goal).unwrap_or((point, goal))
+        })
+    }
+
     pub fn replace_cursors_with(
         &mut self,
         mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,

crates/vim/src/motion.rs 🔗

@@ -137,6 +137,11 @@ impl Motion {
         )
     }
 
+    pub fn infallible(self) -> bool {
+        use Motion::*;
+        matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
+    }
+
     pub fn inclusive(self) -> bool {
         use Motion::*;
         match self {
@@ -164,9 +169,9 @@ impl Motion {
         point: DisplayPoint,
         goal: SelectionGoal,
         times: usize,
-    ) -> (DisplayPoint, SelectionGoal) {
+    ) -> Option<(DisplayPoint, SelectionGoal)> {
         use Motion::*;
-        match self {
+        let (new_point, goal) = match self {
             Left => (left(map, point, times), SelectionGoal::None),
             Backspace => (backspace(map, point, times), SelectionGoal::None),
             Down => down(map, point, goal, times),
@@ -191,7 +196,9 @@ impl Motion {
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
             Matching => (matching(map, point), SelectionGoal::None),
-        }
+        };
+
+        (new_point != point || self.infallible()).then_some((new_point, goal))
     }
 
     // Expands a selection using self motion for an operator
@@ -201,12 +208,13 @@ impl Motion {
         selection: &mut Selection<DisplayPoint>,
         times: usize,
         expand_to_surrounding_newline: bool,
-    ) {
-        let (new_head, goal) = self.move_point(map, selection.head(), selection.goal, times);
-        selection.set_head(new_head, goal);
+    ) -> bool {
+        if let Some((new_head, goal)) =
+            self.move_point(map, selection.head(), selection.goal, times)
+        {
+            selection.set_head(new_head, goal);
 
-        if self.linewise() {
-            if selection.start != selection.end {
+            if self.linewise() {
                 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
 
                 if expand_to_surrounding_newline {
@@ -215,7 +223,7 @@ impl Motion {
                         *selection.end.column_mut() = 0;
                         selection.end = map.clip_point(selection.end, Bias::Right);
                         // Don't reset the end here
-                        return;
+                        return true;
                     } else if selection.start.row() > 0 {
                         *selection.start.row_mut() -= 1;
                         *selection.start.column_mut() = map.line_len(selection.start.row());
@@ -224,31 +232,33 @@ impl Motion {
                 }
 
                 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
-            }
-        } 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
-                && self != Motion::Backspace
-                && 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,
-                );
-            }
+            } 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
+                    && self != Motion::Backspace
+                    && selection.end.row() > selection.start.row()
+                    && selection.end.column() == 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;
+                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
+                    *selection.end.column_mut() += 1;
+                }
             }
+            true
+        } else {
+            false
         }
     }
 }
@@ -325,9 +335,7 @@ pub(crate) fn next_word_start(
                 || at_newline && crossed_newline
                 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 
-            if at_newline {
-                crossed_newline = true;
-            }
+            crossed_newline |= at_newline;
             found
         })
     }
@@ -350,7 +358,7 @@ 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
+        // we have backtracked already
         if !map
             .chars_at(point)
             .nth(1)

crates/vim/src/normal.rs 🔗

@@ -115,7 +115,11 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
 fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal, times))
+            s.move_cursors_with(|map, cursor, goal| {
+                motion
+                    .move_point(map, cursor, goal, times)
+                    .unwrap_or((cursor, goal))
+            })
         })
     });
 }
@@ -125,7 +129,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
+                s.maybe_move_cursors_with(|map, cursor, goal| {
                     Motion::Right.move_point(map, cursor, goal, 1)
                 });
             });
@@ -142,7 +146,7 @@ fn insert_first_non_whitespace(
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
+                s.maybe_move_cursors_with(|map, cursor, goal| {
                     Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
                 });
             });
@@ -155,7 +159,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
+                s.maybe_move_cursors_with(|map, cursor, goal| {
                     Motion::EndOfLine.move_point(map, cursor, goal, 1)
                 });
             });
@@ -215,7 +219,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                     (end_of_line..end_of_line, new_text)
                 });
                 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                    s.move_cursors_with(|map, cursor, goal| {
+                    s.maybe_move_cursors_with(|map, cursor, goal| {
                         Motion::EndOfLine.move_point(map, cursor, goal, 1)
                     });
                 });

crates/vim/src/normal/change.rs 🔗

@@ -1,27 +1,40 @@
 use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
-use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
+use editor::{
+    char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint,
+};
 use gpui::MutableAppContext;
 use language::Selection;
 
 pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
+    // Some motions ignore failure when switching to normal mode
+    let mut motion_succeeded = matches!(
+        motion,
+        Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
+    );
     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.change_selections(Some(Autoscroll::Fit), cx, |s| {
                 s.move_with(|map, selection| {
-                    if let Motion::NextWordStart { ignore_punctuation } = motion {
-                        expand_changed_word_selection(map, selection, times, ignore_punctuation);
+                    motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
+                    {
+                        expand_changed_word_selection(map, selection, times, ignore_punctuation)
                     } else {
-                        motion.expand_selection(map, selection, times, false);
-                    }
+                        motion.expand_selection(map, selection, times, false)
+                    };
                 });
             });
             copy_selections_content(editor, motion.linewise(), cx);
             editor.insert("", cx);
         });
     });
-    vim.switch_mode(Mode::Insert, false, cx)
+
+    if motion_succeeded {
+        vim.switch_mode(Mode::Insert, false, cx)
+    } else {
+        vim.switch_mode(Mode::Normal, false, cx)
+    }
 }
 
 pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
@@ -49,36 +62,45 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
     }
 }
 
-// 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.
+// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
+// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
+// on a non-blank.  This is because "cw" is interpreted as change-word, and a
+// word does not include the following white space.  {Vi: "cw" when on a blank
+//     followed by other blanks changes only the first blank; this is probably a
+//     bug, because "dw" deletes all the blanks}
+//
+// NOT HANDLED YET
+// Another special case: When using the "w" motion in combination with an
+// operator and the last word moved over is at the end of a line, the end of
+// that word becomes the end of the operated text, not the first word in the
+// next line.
 fn expand_changed_word_selection(
     map: &DisplaySnapshot,
     selection: &mut Selection<DisplayPoint>,
     times: usize,
     ignore_punctuation: bool,
-) {
-    if times > 1 {
-        Motion::NextWordStart { ignore_punctuation }.expand_selection(
-            map,
-            selection,
-            times - 1,
-            false,
-        );
-    }
-
-    if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
-        return;
+) -> bool {
+    if times == 1 {
+        let in_word = map
+            .chars_at(selection.head())
+            .next()
+            .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+            .unwrap_or_default();
+
+        if in_word {
+            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_kind != CharKind::Whitespace
+            });
+            true
+        } else {
+            Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
+        }
+    } else {
+        Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
     }
-
-    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'
-    });
 }
 
 #[cfg(test)]

crates/vim/src/normal/delete.rs 🔗

@@ -143,7 +143,7 @@ mod test {
             Test test
             ˇ
             test"},
-            ExemptionFeatures::DeletionOnEmptyLine,
+            ExemptionFeatures::DeleteWordOnEmptyLine,
         )
         .await;
 
@@ -169,7 +169,7 @@ mod test {
             Test test
             ˇ
             test"},
-            ExemptionFeatures::DeletionOnEmptyLine,
+            ExemptionFeatures::OperatorLastNewlineRemains,
         )
         .await;
 

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -8,7 +8,10 @@ use util::test::marked_text_offsets;
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 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
 /// to add exemptions and automatically
@@ -19,6 +22,10 @@ pub enum ExemptionFeatures {
     DeletionOnEmptyLine,
     // When a motion fails, it should should not apply linewise operations
     OperatorAbortsOnFailedMotion,
+    // When an operator completes at the end of the file, an extra newline is left
+    OperatorLastNewlineRemains,
+    // Deleting a word on an empty line doesn't remove the newline
+    DeleteWordOnEmptyLine,
 
     // OBJECTS
     // Resulting position after the operation is slightly incorrect for unintuitive reasons.

crates/vim/src/visual.rs 🔗

@@ -30,20 +30,23 @@ pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
                 s.move_with(|map, selection| {
                     let was_reversed = selection.reversed;
 
-                    let (new_head, goal) =
-                        motion.move_point(map, selection.head(), selection.goal, times);
-                    selection.set_head(new_head, goal);
-
-                    if was_reversed && !selection.reversed {
-                        // Head was at the start of the selection, and now is at the end. We need to move the start
-                        // back by one if possible in order to compensate for this change.
-                        *selection.start.column_mut() = selection.start.column().saturating_sub(1);
-                        selection.start = map.clip_point(selection.start, Bias::Left);
-                    } else if !was_reversed && selection.reversed {
-                        // Head was at the end of the selection, and now is at the start. We need to move the end
-                        // forward by one if possible in order to compensate for this change.
-                        *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Right);
+                    if let Some((new_head, goal)) =
+                        motion.move_point(map, selection.head(), selection.goal, times)
+                    {
+                        selection.set_head(new_head, goal);
+
+                        if was_reversed && !selection.reversed {
+                            // Head was at the start of the selection, and now is at the end. We need to move the start
+                            // back by one if possible in order to compensate for this change.
+                            *selection.start.column_mut() =
+                                selection.start.column().saturating_sub(1);
+                            selection.start = map.clip_point(selection.start, Bias::Left);
+                        } else if !was_reversed && selection.reversed {
+                            // Head was at the end of the selection, and now is at the start. We need to move the end
+                            // forward by one if possible in order to compensate for this change.
+                            *selection.end.column_mut() = selection.end.column() + 1;
+                            selection.end = map.clip_point(selection.end, Bias::Right);
+                        }
                     }
                 });
             });

crates/vim/test_data/test_change_end_of_document.json 🔗

@@ -1 +1 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_gg.json 🔗

@@ -1 +1 @@
-[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_j.json 🔗

@@ -1 +1 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_change_k.json 🔗

@@ -1 +1 @@
-[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_dd.json 🔗

@@ -1 +1 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
+[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_end_of_document.json 🔗

@@ -1 +1 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
+[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_gg.json 🔗

@@ -1 +1 @@
-[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]