Fix vim selection to include entire range (#2787)

Conrad Irwin created

Update vim mode to have vim selection and editor selections match.
Before this we had to adjust between vim selections and real selections
when making changes; now we have to adjust when making selections.

Release Notes:

- vim: Ensure editor selection matches the vim selection
([#1796](https://github.com/zed-industries/community/issues/1796)).
- vim: Fix `s` in visual line mode
- vim: Add `o` and `shift-o` to toggle direction of visual selection
- vim: Fix `v` and `shift-v` to toggle back to normal mode
- vim: Fix block selections like `vi}` to contain correct whitespace

Change summary

assets/keymaps/vim.json                                                |  22 
assets/settings/default.json                                           |   4 
crates/editor/src/display_map.rs                                       |  31 
crates/editor/src/element.rs                                           | 274 
crates/editor/src/movement.rs                                          |  12 
crates/editor/src/multi_buffer.rs                                      |  19 
crates/vim/src/mode_indicator.rs                                       |   2 
crates/vim/src/motion.rs                                               |   7 
crates/vim/src/normal.rs                                               |   2 
crates/vim/src/normal/substitute.rs                                    | 114 
crates/vim/src/object.rs                                               |  85 
crates/vim/src/state.rs                                                |  20 
crates/vim/src/test.rs                                                 |   2 
crates/vim/src/test/neovim_backed_test_context.rs                      |  97 
crates/vim/src/test/neovim_connection.rs                               | 102 
crates/vim/src/test/vim_test_context.rs                                |   5 
crates/vim/src/vim.rs                                                  |  23 
crates/vim/src/visual.rs                                               | 440 
crates/vim/test_data/test_enter_visual_line_mode.json                  |  15 
crates/vim/test_data/test_enter_visual_mode.json                       |  34 
crates/vim/test_data/test_multiline_surrounding_character_objects.json |  10 
crates/vim/test_data/test_visual_change.json                           |  32 
crates/vim/test_data/test_visual_delete.json                           |   4 
crates/vim/test_data/test_visual_line_delete.json                      |   5 
crates/vim/test_data/test_visual_word_object.json                      |  98 
25 files changed, 1,006 insertions(+), 453 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -101,6 +101,8 @@
         "vim::SwitchMode",
         "Normal"
       ],
+      "v": "vim::ToggleVisual",
+      "shift-v": "vim::ToggleVisualLine",
       "*": "vim::MoveToNext",
       "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
@@ -274,22 +276,6 @@
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
       "~": "vim::ChangeCase",
-      "v": [
-        "vim::SwitchMode",
-        {
-          "Visual": {
-            "line": false
-          }
-        }
-      ],
-      "shift-v": [
-        "vim::SwitchMode",
-        {
-          "Visual": {
-            "line": true
-          }
-        }
-      ],
       "p": "vim::Paste",
       "u": "editor::Undo",
       "ctrl-r": "editor::Redo",
@@ -382,12 +368,14 @@
     "context": "Editor && vim_mode == visual && !VimWaiting",
     "bindings": {
       "u": "editor::Undo",
-      "c": "vim::VisualChange",
+      "o": "vim::OtherEnd",
+      "shift-o": "vim::OtherEnd",
       "d": "vim::VisualDelete",
       "x": "vim::VisualDelete",
       "y": "vim::VisualYank",
       "p": "vim::VisualPaste",
       "s": "vim::Substitute",
+      "c": "vim::Substitute",
       "~": "vim::ChangeCase",
       "r": [
         "vim::PushOperator",

assets/settings/default.json 🔗

@@ -214,7 +214,9 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [".env"]
+    "disabled_globs": [
+      ".env"
+    ]
   },
   // Settings specific to journaling
   "journal": {

crates/editor/src/display_map.rs 🔗

@@ -353,19 +353,26 @@ impl DisplaySnapshot {
         }
     }
 
+    // used by line_mode selections and tries to match vim behaviour
     pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
-        let mut new_start = self.prev_line_boundary(range.start).0;
-        let mut new_end = self.next_line_boundary(range.end).0;
-
-        if new_start.row == range.start.row && new_end.row == range.end.row {
-            if new_end.row < self.buffer_snapshot.max_point().row {
-                new_end.row += 1;
-                new_end.column = 0;
-            } else if new_start.row > 0 {
-                new_start.row -= 1;
-                new_start.column = self.buffer_snapshot.line_len(new_start.row);
-            }
-        }
+        let new_start = if range.start.row == 0 {
+            Point::new(0, 0)
+        } else if range.start.row == self.max_buffer_row()
+            || (range.end.column > 0 && range.end.row == self.max_buffer_row())
+        {
+            Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
+        } else {
+            self.prev_line_boundary(range.start).0
+        };
+
+        let new_end = if range.end.column == 0 {
+            range.end
+        } else if range.end.row < self.max_buffer_row() {
+            self.buffer_snapshot
+                .clip_point(Point::new(range.end.row + 1, 0), Bias::Left)
+        } else {
+            self.buffer_snapshot.max_point()
+        };
 
         new_start..new_end
     }

crates/editor/src/element.rs 🔗

@@ -63,6 +63,7 @@ struct SelectionLayout {
     cursor_shape: CursorShape,
     is_newest: bool,
     range: Range<DisplayPoint>,
+    active_rows: Range<u32>,
 }
 
 impl SelectionLayout {
@@ -73,25 +74,44 @@ impl SelectionLayout {
         map: &DisplaySnapshot,
         is_newest: bool,
     ) -> Self {
+        let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
+        let display_selection = point_selection.map(|p| p.to_display_point(map));
+        let mut range = display_selection.range();
+        let mut head = display_selection.head();
+        let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
+            ..map.next_line_boundary(point_selection.end).1.row();
+
+        // vim visual line mode
         if line_mode {
-            let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
-            let point_range = map.expand_to_line(selection.range());
-            Self {
-                head: selection.head().to_display_point(map),
-                cursor_shape,
-                is_newest,
-                range: point_range.start.to_display_point(map)
-                    ..point_range.end.to_display_point(map),
-            }
-        } else {
-            let selection = selection.map(|p| p.to_display_point(map));
-            Self {
-                head: selection.head(),
-                cursor_shape,
-                is_newest,
-                range: selection.range(),
+            let point_range = map.expand_to_line(point_selection.range());
+            range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
+        }
+
+        // any vim visual mode (including line mode)
+        if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
+            if head.column() > 0 {
+                head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
+            } else if head.row() > 0 && head != map.max_point() {
+                head = map.clip_point(
+                    DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)),
+                    Bias::Left,
+                );
+                // updating range.end is a no-op unless you're cursor is
+                // on the newline containing a multi-buffer divider
+                // in which case the clip_point may have moved the head up
+                // an additional row.
+                range.end = DisplayPoint::new(head.row() + 1, 0);
+                active_rows.end = head.row();
             }
         }
+
+        Self {
+            head,
+            cursor_shape,
+            is_newest,
+            range,
+            active_rows,
+        }
     }
 }
 
@@ -2152,22 +2172,37 @@ impl Element<Editor> for EditorElement {
         }
         selections.extend(remote_selections);
 
+        let mut newest_selection_head = None;
+
         if editor.show_local_selections {
-            let mut local_selections = editor
+            let mut local_selections: Vec<Selection<Point>> = editor
                 .selections
                 .disjoint_in_range(start_anchor..end_anchor, cx);
             local_selections.extend(editor.selections.pending(cx));
+            let mut layouts = Vec::new();
             let newest = editor.selections.newest(cx);
-            for selection in &local_selections {
+            for selection in local_selections.drain(..) {
                 let is_empty = selection.start == selection.end;
-                let selection_start = snapshot.prev_line_boundary(selection.start).1;
-                let selection_end = snapshot.next_line_boundary(selection.end).1;
-                for row in cmp::max(selection_start.row(), start_row)
-                    ..=cmp::min(selection_end.row(), end_row)
+                let is_newest = selection == newest;
+
+                let layout = SelectionLayout::new(
+                    selection,
+                    editor.selections.line_mode,
+                    editor.cursor_shape,
+                    &snapshot.display_snapshot,
+                    is_newest,
+                );
+                if is_newest {
+                    newest_selection_head = Some(layout.head);
+                }
+
+                for row in cmp::max(layout.active_rows.start, start_row)
+                    ..=cmp::min(layout.active_rows.end, end_row)
                 {
                     let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
                     *contains_non_empty_selection |= !is_empty;
                 }
+                layouts.push(layout);
             }
 
             // Render the local selections in the leader's color when following.
@@ -2175,22 +2210,7 @@ impl Element<Editor> for EditorElement {
                 .leader_replica_id
                 .unwrap_or_else(|| editor.replica_id(cx));
 
-            selections.push((
-                local_replica_id,
-                local_selections
-                    .into_iter()
-                    .map(|selection| {
-                        let is_newest = selection == newest;
-                        SelectionLayout::new(
-                            selection,
-                            editor.selections.line_mode,
-                            editor.cursor_shape,
-                            &snapshot.display_snapshot,
-                            is_newest,
-                        )
-                    })
-                    .collect(),
-            ));
+            selections.push((local_replica_id, layouts));
         }
 
         let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2295,28 +2315,26 @@ impl Element<Editor> for EditorElement {
             snapshot = editor.snapshot(cx);
         }
 
-        let newest_selection_head = editor
-            .selections
-            .newest::<usize>(cx)
-            .head()
-            .to_display_point(&snapshot);
         let style = editor.style(cx);
 
         let mut context_menu = None;
         let mut code_actions_indicator = None;
-        if (start_row..end_row).contains(&newest_selection_head.row()) {
-            if editor.context_menu_visible() {
-                context_menu = editor.render_context_menu(newest_selection_head, style.clone(), cx);
-            }
+        if let Some(newest_selection_head) = newest_selection_head {
+            if (start_row..end_row).contains(&newest_selection_head.row()) {
+                if editor.context_menu_visible() {
+                    context_menu =
+                        editor.render_context_menu(newest_selection_head, style.clone(), cx);
+                }
 
-            let active = matches!(
-                editor.context_menu,
-                Some(crate::ContextMenu::CodeActions(_))
-            );
+                let active = matches!(
+                    editor.context_menu,
+                    Some(crate::ContextMenu::CodeActions(_))
+                );
 
-            code_actions_indicator = editor
-                .render_code_actions_indicator(&style, active, cx)
-                .map(|indicator| (newest_selection_head.row(), indicator));
+                code_actions_indicator = editor
+                    .render_code_actions_indicator(&style, active, cx)
+                    .map(|indicator| (newest_selection_head.row(), indicator));
+            }
         }
 
         let visible_rows = start_row..start_row + line_layouts.len() as u32;
@@ -2995,6 +3013,154 @@ mod tests {
         assert_eq!(layouts.len(), 6);
     }
 
+    #[gpui::test]
+    async fn test_vim_visual_selections(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (_, state) = editor.update(cx, |editor, cx| {
+            editor.cursor_shape = CursorShape::Block;
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(0, 0)..Point::new(1, 0),
+                    Point::new(3, 2)..Point::new(3, 3),
+                    Point::new(5, 6)..Point::new(6, 0),
+                ]);
+            });
+            let mut new_parents = Default::default();
+            let mut notify_views_if_parents_change = Default::default();
+            let mut layout_cx = LayoutContext::new(
+                cx,
+                &mut new_parents,
+                &mut notify_views_if_parents_change,
+                false,
+            );
+            element.layout(
+                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                editor,
+                &mut layout_cx,
+            )
+        });
+        assert_eq!(state.selections.len(), 1);
+        let local_selections = &state.selections[0].1;
+        assert_eq!(local_selections.len(), 3);
+        // moves cursor back one line
+        assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6));
+        assert_eq!(
+            local_selections[0].range,
+            DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0)
+        );
+
+        // moves cursor back one column
+        assert_eq!(
+            local_selections[1].range,
+            DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3)
+        );
+        assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2));
+
+        // leaves cursor on the max point
+        assert_eq!(
+            local_selections[2].range,
+            DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0)
+        );
+        assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0));
+
+        // active lines does not include 1 (even though the range of the selection does)
+        assert_eq!(
+            state.active_rows.keys().cloned().collect::<Vec<u32>>(),
+            vec![0, 3, 5, 6]
+        );
+
+        // multi-buffer support
+        // in DisplayPoint co-ordinates, this is what we're dealing with:
+        //  0: [[file
+        //  1:   header]]
+        //  2: aaaaaa
+        //  3: bbbbbb
+        //  4: cccccc
+        //  5:
+        //  6: ...
+        //  7: ffffff
+        //  8: gggggg
+        //  9: hhhhhh
+        // 10:
+        // 11: [[file
+        // 12:   header]]
+        // 13: bbbbbb
+        // 14: cccccc
+        // 15: dddddd
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_multi(
+                    [
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![
+                                Point::new(0, 0)..Point::new(3, 0),
+                                Point::new(4, 0)..Point::new(7, 0),
+                            ],
+                        ),
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![Point::new(1, 0)..Point::new(3, 0)],
+                        ),
+                    ],
+                    cx,
+                );
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
+        let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+        let (_, state) = editor.update(cx, |editor, cx| {
+            editor.cursor_shape = CursorShape::Block;
+            editor.change_selections(None, cx, |s| {
+                s.select_display_ranges([
+                    DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0),
+                    DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
+                ]);
+            });
+            let mut new_parents = Default::default();
+            let mut notify_views_if_parents_change = Default::default();
+            let mut layout_cx = LayoutContext::new(
+                cx,
+                &mut new_parents,
+                &mut notify_views_if_parents_change,
+                false,
+            );
+            element.layout(
+                SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+                editor,
+                &mut layout_cx,
+            )
+        });
+
+        assert_eq!(state.selections.len(), 1);
+        let local_selections = &state.selections[0].1;
+        assert_eq!(local_selections.len(), 2);
+
+        // moves cursor on excerpt boundary back a line
+        // and doesn't allow selection to bleed through
+        assert_eq!(
+            local_selections[0].range,
+            DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
+        );
+        assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
+
+        // moves cursor on buffer boundary back two lines
+        // and doesn't allow selection to bleed through
+        assert_eq!(
+            local_selections[1].range,
+            DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0)
+        );
+        assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0));
+    }
+
     #[gpui::test]
     fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
         init_test(cx, |_| {});

crates/editor/src/movement.rs 🔗

@@ -13,6 +13,13 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     map.clip_point(point, Bias::Left)
 }
 
+pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    if point.column() > 0 {
+        *point.column_mut() -= 1;
+    }
+    map.clip_point(point, Bias::Left)
+}
+
 pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let max_column = map.line_len(point.row());
     if point.column() < max_column {
@@ -24,6 +31,11 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     map.clip_point(point, Bias::Right)
 }
 
+pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    *point.column_mut() += 1;
+    map.clip_point(point, Bias::Right)
+}
+
 pub fn up(
     map: &DisplaySnapshot,
     start: DisplayPoint,

crates/editor/src/multi_buffer.rs 🔗

@@ -1565,6 +1565,25 @@ impl MultiBuffer {
         cx.add_model(|cx| Self::singleton(buffer, cx))
     }
 
+    pub fn build_multi<const COUNT: usize>(
+        excerpts: [(&str, Vec<Range<Point>>); COUNT],
+        cx: &mut gpui::AppContext,
+    ) -> ModelHandle<Self> {
+        let multi = cx.add_model(|_| Self::new(0));
+        for (text, ranges) in excerpts {
+            let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+            let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
+                context: range,
+                primary: None,
+            });
+            multi.update(cx, |multi, cx| {
+                multi.push_excerpts(buffer, excerpt_ranges, cx)
+            });
+        }
+
+        multi
+    }
+
     pub fn build_from_buffer(
         buffer: ModelHandle<Buffer>,
         cx: &mut gpui::AppContext,

crates/vim/src/mode_indicator.rs 🔗

@@ -87,7 +87,7 @@ impl View for ModeIndicator {
             Mode::Normal => "-- NORMAL --",
             Mode::Insert => "-- INSERT --",
             Mode::Visual { line: false } => "-- VISUAL --",
-            Mode::Visual { line: true } => "VISUAL LINE ",
+            Mode::Visual { line: true } => "VISUAL  LINE",
         };
         Label::new(text, theme.vim_mode_indicator.text.clone())
             .contained()

crates/vim/src/motion.rs 🔗

@@ -383,8 +383,7 @@ impl Motion {
 
 fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
     for _ in 0..times {
-        *point.column_mut() = point.column().saturating_sub(1);
-        point = map.clip_point(point, Bias::Left);
+        point = movement::saturating_left(map, point);
         if point.column() == 0 {
             break;
         }
@@ -425,9 +424,7 @@ fn up(
 
 pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
     for _ in 0..times {
-        let mut new_point = point;
-        *new_point.column_mut() += 1;
-        let new_point = map.clip_point(new_point, Bias::Right);
+        let new_point = movement::saturating_right(map, point);
         if point == new_point {
             break;
         }

crates/vim/src/normal.rs 🔗

@@ -3,7 +3,7 @@ mod change;
 mod delete;
 mod scroll;
 mod search;
-mod substitute;
+pub mod substitute;
 mod yank;
 
 use std::{borrow::Cow, sync::Arc};

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

@@ -1,34 +1,45 @@
 use gpui::WindowContext;
 use language::Point;
 
-use crate::{motion::Motion, Mode, Vim};
+use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
 
 pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
+    let line_mode = vim.state.mode == Mode::Visual { line: true };
+    vim.switch_mode(Mode::Insert, true, cx);
     vim.update_active_editor(cx, |editor, cx| {
-        editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
                     if selection.start == selection.end {
                         Motion::Right.expand_selection(map, selection, count, true);
                     }
+                    if line_mode {
+                        Motion::CurrentLine.expand_selection(map, selection, None, false);
+                        if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
+                            map,
+                            selection.start,
+                            selection.goal,
+                            None,
+                        ) {
+                            selection.start = point;
+                        }
+                    }
                 })
             });
-            let selections = editor.selections.all::<Point>(cx);
-            for selection in selections.into_iter().rev() {
-                editor.buffer().update(cx, |buffer, cx| {
-                    buffer.edit([(selection.start..selection.end, "")], None, cx)
-                })
-            }
+            copy_selections_content(editor, line_mode, cx);
+            let selections = editor.selections.all::<Point>(cx).into_iter();
+            let edits = selections.map(|selection| (selection.start..selection.end, ""));
+            editor.edit(edits, cx);
         });
-        editor.set_clip_at_line_ends(true, cx);
     });
-    vim.switch_mode(Mode::Insert, true, cx)
 }
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
     use indoc::indoc;
 
     #[gpui::test]
@@ -69,5 +80,86 @@ mod test {
         // should transactionally undo selection changes
         cx.simulate_keystrokes(["escape", "u"]);
         cx.assert_editor_state("ˇcàfé\n");
+
+        // it handles visual line mode
+        cx.set_state(
+            indoc! {"
+            alpha
+              beˇta
+            gamma"},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["shift-v", "s"]);
+        cx.assert_editor_state(indoc! {"
+            alpha
+              ˇ
+            gamma"});
+    }
+
+    #[gpui::test]
+    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("The quick ˇbrown").await;
+        cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
+        cx.assert_shared_state("The quick ˇ").await;
+
+        cx.set_shared_state(indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
+        cx.assert_shared_state(indoc! {"
+            The ˇver
+            the lazy dog"})
+            .await;
+
+        let cases = cx.each_marked_position(indoc! {"
+            The ˇquick brown
+            fox jumps ˇover
+            the ˇlazy dog"});
+        for initial_state in cases {
+            cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
+                .await;
+            cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
+                .await;
+        }
+    }
+
+    #[gpui::test]
+    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["shift-v", "c"]);
+        cx.assert(indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        // Test pasting code copied on change
+        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
+        cx.assert_state_matches().await;
+
+        cx.assert_all(indoc! {"
+            The quick brown
+            fox juˇmps over
+            the laˇzy dog"})
+            .await;
+        let mut cx = cx.binding(["shift-v", "j", "c"]);
+        cx.assert(indoc! {"
+            The quˇick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        // Test pasting code copied on delete
+        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
+        cx.assert_state_matches().await;
+
+        cx.assert_all(indoc! {"
+            The quick brown
+            fox juˇmps over
+            the laˇzy dog"})
+            .await;
     }
 }

crates/vim/src/object.rs 🔗

@@ -369,7 +369,7 @@ fn surrounding_markers(
                     start = Some(point)
                 } else {
                     *point.column_mut() += char.len_utf8() as u32;
-                    start = Some(point);
+                    start = Some(point)
                 }
                 break;
             }
@@ -420,11 +420,38 @@ fn surrounding_markers(
         }
     }
 
-    if let (Some(start), Some(end)) = (start, end) {
-        Some(start..end)
-    } else {
-        None
+    let (Some(mut start), Some(mut end)) = (start, end) else {
+        return None;
+    };
+
+    if !around {
+        // if a block starts with a newline, move the start to after the newline.
+        let mut was_newline = false;
+        for (char, point) in map.chars_at(start) {
+            if was_newline {
+                start = point;
+            } else if char == '\n' {
+                was_newline = true;
+                continue;
+            }
+            break;
+        }
+        // if a block ends with a newline, then whitespace, then the delimeter,
+        // move the end to after the newline.
+        let mut new_end = end;
+        for (char, point) in map.reverse_chars_at(end) {
+            if char == '\n' {
+                end = new_end;
+                break;
+            }
+            if !char.is_whitespace() {
+                break;
+            }
+            new_end = point
+        }
     }
+
+    Some(start..end)
 }
 
 #[cfg(test)]
@@ -481,6 +508,12 @@ mod test {
     async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
+        cx.set_shared_state("The quick ˇbrown\nfox").await;
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+        cx.simulate_shared_keystrokes(["i", "w"]).await;
+        cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+
         cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
             .await;
         cx.assert_binding_matches_all_exempted(
@@ -675,6 +708,48 @@ mod test {
         }
     }
 
+    #[gpui::test]
+    async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "func empty(a string) bool {
+               if a == \"\" {
+                  return true
+               }
+               ˇreturn false
+            }"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
+        cx.assert_shared_state(indoc! {"
+            func empty(a string) bool {
+            «   if a == \"\" {
+                  return true
+               }
+               return false
+            ˇ»}"})
+            .await;
+        cx.set_shared_state(indoc! {
+            "func empty(a string) bool {
+                 if a == \"\" {
+                     ˇreturn true
+                 }
+                 return false
+            }"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
+        cx.assert_shared_state(indoc! {"
+            func empty(a string) bool {
+                 if a == \"\" {
+            «         return true
+            ˇ»     }
+                 return false
+            }"})
+            .await;
+    }
+
     #[gpui::test]
     async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/state.rs 🔗

@@ -12,6 +12,15 @@ pub enum Mode {
     Visual { line: bool },
 }
 
+impl Mode {
+    pub fn is_visual(&self) -> bool {
+        match self {
+            Mode::Normal | Mode::Insert => false,
+            Mode::Visual { .. } => true,
+        }
+    }
+}
+
 impl Default for Mode {
     fn default() -> Self {
         Self::Normal
@@ -78,12 +87,11 @@ impl VimState {
             )
     }
 
-    pub fn clip_at_line_end(&self) -> bool {
-        !matches!(self.mode, Mode::Insert | Mode::Visual { .. })
-    }
-
-    pub fn empty_selections_only(&self) -> bool {
-        !matches!(self.mode, Mode::Visual { .. })
+    pub fn clip_at_line_ends(&self) -> bool {
+        match self.mode {
+            Mode::Insert | Mode::Visual { .. } => false,
+            Mode::Normal => true,
+        }
     }
 
     pub fn keymap_context_layer(&self) -> KeymapContext {

crates/vim/src/test.rs 🔗

@@ -141,7 +141,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
 
     // works in visuial mode
     cx.simulate_keystrokes(["shift-v", "down", ">"]);
-    cx.assert_editor_state("aa\n    b«b\n    cˇ»c");
+    cx.assert_editor_state("aa\n    b«b\n    ccˇ»");
 }
 
 #[gpui::test]

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

@@ -61,6 +61,9 @@ pub struct NeovimBackedTestContext<'a> {
     // bindings are exempted. If None, all bindings are ignored for that insertion text.
     exemptions: HashMap<String, Option<HashSet<String>>>,
     neovim: NeovimConnection,
+
+    last_set_state: Option<String>,
+    recent_keystrokes: Vec<String>,
 }
 
 impl<'a> NeovimBackedTestContext<'a> {
@@ -71,6 +74,9 @@ impl<'a> NeovimBackedTestContext<'a> {
             cx,
             exemptions: Default::default(),
             neovim: NeovimConnection::new(function_name).await,
+
+            last_set_state: None,
+            recent_keystrokes: Default::default(),
         }
     }
 
@@ -102,13 +108,21 @@ impl<'a> NeovimBackedTestContext<'a> {
         keystroke_texts: [&str; COUNT],
     ) -> ContextHandle {
         for keystroke_text in keystroke_texts.into_iter() {
+            self.recent_keystrokes.push(keystroke_text.to_string());
             self.neovim.send_keystroke(keystroke_text).await;
         }
         self.simulate_keystrokes(keystroke_texts)
     }
 
     pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
-        let context_handle = self.set_state(marked_text, Mode::Normal);
+        let mode = if marked_text.contains("»") {
+            Mode::Visual { line: false }
+        } else {
+            Mode::Normal
+        };
+        let context_handle = self.set_state(marked_text, mode);
+        self.last_set_state = Some(marked_text.to_string());
+        self.recent_keystrokes = Vec::new();
         self.neovim.set_state(marked_text).await;
         context_handle
     }
@@ -116,15 +130,25 @@ impl<'a> NeovimBackedTestContext<'a> {
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
         let neovim = self.neovim_state().await;
         if neovim != marked_text {
+            let initial_state = self
+                .last_set_state
+                .as_ref()
+                .unwrap_or(&"N/A".to_string())
+                .clone();
             panic!(
                 indoc! {"Test is incorrect (currently expected != neovim state)
-
+                # initial state:
+                {}
+                # keystrokes:
+                {}
                 # currently expected:
                 {}
                 # neovim state:
                 {}
                 # zed state:
                 {}"},
+                initial_state,
+                self.recent_keystrokes.join(" "),
                 marked_text,
                 neovim,
                 self.editor_state(),
@@ -141,28 +165,40 @@ impl<'a> NeovimBackedTestContext<'a> {
         )
     }
 
+    pub async fn neovim_mode(&mut self) -> Mode {
+        self.neovim.mode().await.unwrap()
+    }
+
     async fn neovim_selection(&mut self) -> Range<usize> {
-        let mut neovim_selection = self.neovim.selection().await;
-        // Zed selections adjust themselves to make the end point visually make sense
-        if neovim_selection.start > neovim_selection.end {
-            neovim_selection.start.column += 1;
-        }
+        let neovim_selection = self.neovim.selection().await;
         neovim_selection.to_offset(&self.buffer_snapshot())
     }
 
     pub async fn assert_state_matches(&mut self) {
-        assert_eq!(
-            self.neovim.text().await,
-            self.buffer_text(),
-            "{}",
-            self.assertion_context()
-        );
-
-        let selections = vec![self.neovim_selection().await];
-        self.assert_editor_selections(selections);
-
-        if let Some(neovim_mode) = self.neovim.mode().await {
-            assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
+        let neovim = self.neovim_state().await;
+        let editor = self.editor_state();
+        let initial_state = self
+            .last_set_state
+            .as_ref()
+            .unwrap_or(&"N/A".to_string())
+            .clone();
+
+        if neovim != editor {
+            panic!(
+                indoc! {"Test failed (zed does not match nvim behaviour)
+                    # initial state:
+                    {}
+                    # keystrokes:
+                    {}
+                    # neovim state:
+                    {}
+                    # zed state:
+                    {}"},
+                initial_state,
+                self.recent_keystrokes.join(" "),
+                neovim,
+                editor,
+            )
         }
     }
 
@@ -207,6 +243,29 @@ impl<'a> NeovimBackedTestContext<'a> {
         }
     }
 
+    pub fn each_marked_position(&self, marked_positions: &str) -> Vec<String> {
+        let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
+        let mut ret = Vec::with_capacity(cursor_offsets.len());
+
+        for cursor_offset in cursor_offsets.iter() {
+            let mut marked_text = unmarked_text.clone();
+            marked_text.insert(*cursor_offset, 'ˇ');
+            ret.push(marked_text)
+        }
+
+        ret
+    }
+
+    pub async fn assert_neovim_compatible<const COUNT: usize>(
+        &mut self,
+        marked_positions: &str,
+        keystrokes: [&str; COUNT],
+    ) {
+        self.set_shared_state(&marked_positions).await;
+        self.simulate_shared_keystrokes(keystrokes).await;
+        self.assert_state_matches().await;
+    }
+
     pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
         &mut self,
         keystrokes: [&str; COUNT],

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

@@ -213,6 +213,16 @@ impl NeovimConnection {
         );
     }
 
+    #[cfg(feature = "neovim")]
+    async fn read_position(&mut self, cmd: &str) -> u32 {
+        self.nvim
+            .command_output(cmd)
+            .await
+            .unwrap()
+            .parse::<u32>()
+            .unwrap()
+    }
+
     #[cfg(feature = "neovim")]
     pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
         let nvim_buffer = self
@@ -226,22 +236,12 @@ impl NeovimConnection {
             .expect("Could not get buffer text")
             .join("\n");
 
-        let cursor_row: u32 = self
-            .nvim
-            .command_output("echo line('.')")
-            .await
-            .unwrap()
-            .parse::<u32>()
-            .unwrap()
-            - 1; // Neovim rows start at 1
-        let cursor_col: u32 = self
-            .nvim
-            .command_output("echo col('.')")
-            .await
-            .unwrap()
-            .parse::<u32>()
-            .unwrap()
-            - 1; // Neovim columns start at 1
+        // nvim columns are 1-based, so -1.
+        let mut cursor_row = self.read_position("echo line('.')").await - 1;
+        let mut cursor_col = self.read_position("echo col('.')").await - 1;
+        let mut selection_row = self.read_position("echo line('v')").await - 1;
+        let mut selection_col = self.read_position("echo col('v')").await - 1;
+        let total_rows = self.read_position("echo line('$')").await - 1;
 
         let nvim_mode_text = self
             .nvim
@@ -266,46 +266,38 @@ impl NeovimConnection {
             _ => None,
         };
 
-        let (start, end) = if let Some(Mode::Visual { .. }) = mode {
-            self.nvim
-                .input("<escape>")
-                .await
-                .expect("Could not exit visual mode");
-            let nvim_buffer = self
-                .nvim
-                .get_current_buf()
-                .await
-                .expect("Could not get neovim buffer");
-            let (start_row, start_col) = nvim_buffer
-                .get_mark("<")
-                .await
-                .expect("Could not get selection start");
-            let (end_row, end_col) = nvim_buffer
-                .get_mark(">")
-                .await
-                .expect("Could not get selection end");
-            self.nvim
-                .input("gv")
-                .await
-                .expect("Could not reselect visual selection");
-
-            if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
-                (
-                    Point::new(end_row as u32 - 1, end_col as u32),
-                    Point::new(start_row as u32 - 1, start_col as u32),
-                )
-            } else {
-                (
-                    Point::new(start_row as u32 - 1, start_col as u32),
-                    Point::new(end_row as u32 - 1, end_col as u32),
-                )
+        // Vim uses the index of the first and last character in the selection
+        // Zed uses the index of the positions between the characters, so we need
+        // to add one to the end in visual mode.
+        match mode {
+            Some(Mode::Visual { .. }) => {
+                if selection_col > cursor_col {
+                    let selection_line_length =
+                        self.read_position("echo strlen(getline(line('v')))").await;
+                    if selection_line_length > selection_col {
+                        selection_col += 1;
+                    } else if selection_row < total_rows {
+                        selection_col = 0;
+                        selection_row += 1;
+                    }
+                } else {
+                    let cursor_line_length =
+                        self.read_position("echo strlen(getline(line('.')))").await;
+                    if cursor_line_length > cursor_col {
+                        cursor_col += 1;
+                    } else if cursor_row < total_rows {
+                        cursor_col = 0;
+                        cursor_row += 1;
+                    }
+                }
             }
-        } else {
-            (
-                Point::new(cursor_row, cursor_col),
-                Point::new(cursor_row, cursor_col),
-            )
-        };
+            Some(Mode::Insert) | Some(Mode::Normal) | None => {}
+        }
+
+        let (start, end) = (
+            Point::new(selection_row, selection_col),
+            Point::new(cursor_row, cursor_col),
+        );
 
         let state = NeovimData::Get {
             mode,

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

@@ -86,12 +86,13 @@ impl<'a> VimTestContext<'a> {
 
     pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
         let window = self.window;
+        let context_handle = self.cx.set_state(text);
         window.update(self.cx.cx.cx, |cx| {
             Vim::update(cx, |vim, cx| {
-                vim.switch_mode(mode, false, cx);
+                vim.switch_mode(mode, true, cx);
             })
         });
-        self.cx.set_state(text)
+        context_handle
     }
 
     #[track_caller]

crates/vim/src/vim.rs 🔗

@@ -13,7 +13,7 @@ mod visual;
 
 use anyhow::Result;
 use collections::CommandPaletteFilter;
-use editor::{Bias, Editor, EditorMode, Event};
+use editor::{movement, Editor, EditorMode, Event};
 use gpui::{
     actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
     Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@@ -181,6 +181,7 @@ impl Vim {
     }
 
     fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
+        let last_mode = self.state.mode;
         self.state.mode = mode;
         self.state.operator_stack.clear();
 
@@ -197,12 +198,16 @@ impl Vim {
         self.update_active_editor(cx, |editor, cx| {
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
-                    if self.state.empty_selections_only() {
-                        let new_head = map.clip_point(selection.head(), Bias::Left);
-                        selection.collapse_to(new_head, selection.goal)
-                    } else {
-                        selection
-                            .set_head(map.clip_point(selection.head(), Bias::Left), selection.goal);
+                    if last_mode.is_visual() && !mode.is_visual() {
+                        let mut point = selection.head();
+                        if !selection.reversed {
+                            point = movement::left(map, selection.head());
+                        }
+                        selection.collapse_to(point, selection.goal)
+                    } else if !last_mode.is_visual() && mode.is_visual() {
+                        if selection.is_empty() {
+                            selection.end = movement::right(map, selection.start);
+                        }
                     }
                 });
             })
@@ -265,7 +270,7 @@ impl Vim {
             }
             Some(Operator::Replace) => match Vim::read(cx).state.mode {
                 Mode::Normal => normal_replace(text, cx),
-                Mode::Visual { line } => visual_replace(text, line, cx),
+                Mode::Visual { .. } => visual_replace(text, cx),
                 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
             },
             _ => {}
@@ -309,7 +314,7 @@ impl Vim {
         self.update_active_editor(cx, |editor, cx| {
             if self.enabled && editor.mode() == EditorMode::Full {
                 editor.set_cursor_shape(cursor_shape, cx);
-                editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
+                editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
                 editor.set_collapse_matches(true);
                 editor.set_input_enabled(!state.vim_controlled());
                 editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });

crates/vim/src/visual.rs 🔗

@@ -16,10 +16,22 @@ use crate::{
     Vim,
 };
 
-actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
+actions!(
+    vim,
+    [
+        ToggleVisual,
+        ToggleVisualLine,
+        VisualDelete,
+        VisualYank,
+        VisualPaste,
+        OtherEnd,
+    ]
+);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(change);
+    cx.add_action(toggle_visual);
+    cx.add_action(toggle_visual_line);
+    cx.add_action(other_end);
     cx.add_action(delete);
     cx.add_action(yank);
     cx.add_action(paste);
@@ -32,24 +44,45 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
                 s.move_with(|map, selection| {
                     let was_reversed = selection.reversed;
 
-                    if let Some((new_head, goal)) =
-                        motion.move_point(map, selection.head(), selection.goal, times)
+                    let mut current_head = selection.head();
+
+                    // our motions assume the current character is after the cursor,
+                    // but in (forward) visual mode the current character is just
+                    // before the end of the selection.
+
+                    // If the file ends with a newline (which is common) we don't do this.
+                    // so that if you go to the end of such a file you can use "up" to go
+                    // to the previous line and have it work somewhat as expected.
+                    if !selection.reversed
+                        && !selection.is_empty()
+                        && !(selection.end.column() == 0 && selection.end == map.max_point())
                     {
-                        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);
+                        current_head = movement::left(map, selection.end)
+                    }
+
+                    let Some((new_head, goal)) =
+                        motion.move_point(map, current_head, selection.goal, times) else { return };
+
+                    selection.set_head(new_head, goal);
+
+                    // ensure the current character is included in the selection.
+                    if !selection.reversed {
+                        // TODO: maybe try clipping left for multi-buffers
+                        let next_point = movement::right(map, selection.end);
+
+                        if !(next_point.column() == 0 && next_point == map.max_point()) {
+                            selection.end = movement::right(map, selection.end)
                         }
                     }
+
+                    // vim always ensures the anchor character stays selected.
+                    // if our selection has reversed, we need to move the opposite end
+                    // to ensure the anchor is still selected.
+                    if was_reversed && !selection.reversed {
+                        selection.start = movement::left(map, selection.start);
+                    } else if !was_reversed && selection.reversed {
+                        selection.end = movement::right(map, selection.end);
+                    }
                 });
             });
         });
@@ -64,14 +97,29 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
             vim.update_active_editor(cx, |editor, cx| {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_with(|map, selection| {
-                        let head = selection.head();
-                        if let Some(mut range) = object.range(map, head, around) {
+                        let mut head = selection.head();
+
+                        // all our motions assume that the current character is
+                        // after the cursor; however in the case of a visual selection
+                        // the current character is before the cursor.
+                        if !selection.reversed {
+                            head = movement::left(map, head);
+                        }
+
+                        if let Some(range) = object.range(map, head, around) {
                             if !range.is_empty() {
-                                if let Some((_, end)) = map.reverse_chars_at(range.end).next() {
-                                    range.end = end;
-                                }
+                                let expand_both_ways = if selection.is_empty() {
+                                    true
+                                // contains only one character
+                                } else if let Some((_, start)) =
+                                    map.reverse_chars_at(selection.end).next()
+                                {
+                                    selection.start == start
+                                } else {
+                                    false
+                                };
 
-                                if selection.is_empty() {
+                                if expand_both_ways {
                                     selection.start = range.start;
                                     selection.end = range.end;
                                 } else if selection.reversed {
@@ -88,72 +136,58 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
     });
 }
 
-pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
+pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| match vim.state.mode {
+        Mode::Normal | Mode::Insert | Mode::Visual { line: true } => {
+            vim.switch_mode(Mode::Visual { line: false }, false, cx);
+        }
+        Mode::Visual { line: false } => {
+            vim.switch_mode(Mode::Normal, false, cx);
+        }
+    })
+}
+
+pub fn toggle_visual_line(
+    _: &mut Workspace,
+    _: &ToggleVisualLine,
+    cx: &mut ViewContext<Workspace>,
+) {
+    Vim::update(cx, |vim, cx| match vim.state.mode {
+        Mode::Normal | Mode::Insert | Mode::Visual { line: false } => {
+            vim.switch_mode(Mode::Visual { line: true }, false, cx);
+        }
+        Mode::Visual { line: true } => {
+            vim.switch_mode(Mode::Normal, false, cx);
+        }
+    })
+}
+
+pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
-            editor.set_clip_at_line_ends(false, cx);
-            // Compute edits and resulting anchor selections. If in line mode, adjust
-            // the anchor location and additional newline
-            let mut edits = Vec::new();
-            let mut new_selections = Vec::new();
-            let line_mode = editor.selections.line_mode;
             editor.change_selections(None, cx, |s| {
-                s.move_with(|map, selection| {
-                    if !selection.reversed {
-                        // Head is at the end of the selection. Adjust the end position to
-                        // to include the character under the cursor.
-                        *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Right);
-                    }
-
-                    if line_mode {
-                        let range = selection.map(|p| p.to_point(map)).range();
-                        let expanded_range = map.expand_to_line(range);
-                        // If we are at the last line, the anchor needs to be after the newline so that
-                        // it is on a line of its own. Otherwise, the anchor may be after the newline
-                        let anchor = if expanded_range.end == map.buffer_snapshot.max_point() {
-                            map.buffer_snapshot.anchor_after(expanded_range.end)
-                        } else {
-                            map.buffer_snapshot.anchor_before(expanded_range.start)
-                        };
-
-                        edits.push((expanded_range, "\n"));
-                        new_selections.push(selection.map(|_| anchor));
-                    } else {
-                        let range = selection.map(|p| p.to_point(map)).range();
-                        let anchor = map.buffer_snapshot.anchor_after(range.end);
-                        edits.push((range, ""));
-                        new_selections.push(selection.map(|_| anchor));
-                    }
-                    selection.goal = SelectionGoal::None;
-                });
-            });
-            copy_selections_content(editor, editor.selections.line_mode, cx);
-            editor.edit_with_autoindent(edits, cx);
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.select_anchors(new_selections);
-            });
-        });
-        vim.switch_mode(Mode::Insert, false, cx);
+                s.move_with(|_, selection| {
+                    selection.reversed = !selection.reversed;
+                })
+            })
+        })
     });
 }
 
 pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
-            editor.set_clip_at_line_ends(false, cx);
             let mut original_columns: HashMap<_, _> = Default::default();
             let line_mode = editor.selections.line_mode;
+
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.move_with(|map, selection| {
                     if line_mode {
-                        original_columns
-                            .insert(selection.id, selection.head().to_point(map).column);
-                    } else if !selection.reversed {
-                        // Head is at the end of the selection. Adjust the end position to
-                        // to include the character under the cursor.
-                        *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = map.clip_point(selection.end, Bias::Right);
+                        let mut position = selection.head();
+                        if !selection.reversed {
+                            position = movement::left(map, position);
+                        }
+                        original_columns.insert(selection.id, position.to_point(map).column);
                     }
                     selection.goal = SelectionGoal::None;
                 });
@@ -175,27 +209,14 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
                 });
             });
         });
-        vim.switch_mode(Mode::Normal, false, cx);
+        vim.switch_mode(Mode::Normal, true, cx);
     });
 }
 
 pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
-            editor.set_clip_at_line_ends(false, cx);
             let line_mode = editor.selections.line_mode;
-            if !line_mode {
-                editor.change_selections(None, cx, |s| {
-                    s.move_with(|map, selection| {
-                        if !selection.reversed {
-                            // Head is at the end of the selection. Adjust the end position to
-                            // to include the character under the cursor.
-                            *selection.end.column_mut() = selection.end.column() + 1;
-                            selection.end = map.clip_point(selection.end, Bias::Right);
-                        }
-                    });
-                });
-            }
             copy_selections_content(editor, line_mode, cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|_, selection| {
@@ -203,7 +224,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
                 });
             });
         });
-        vim.switch_mode(Mode::Normal, false, cx);
+        vim.switch_mode(Mode::Normal, true, cx);
     });
 }
 
@@ -256,11 +277,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
 
                                 let mut selection = selection.clone();
                                 if !selection.reversed {
-                                    let mut adjusted = selection.end;
-                                    // Head is at the end of the selection. Adjust the end position to
-                                    // to include the character under the cursor.
-                                    *adjusted.column_mut() = adjusted.column() + 1;
-                                    adjusted = display_map.clip_point(adjusted, Bias::Right);
+                                    let adjusted = selection.end;
                                     // If the selection is empty, move both the start and end forward one
                                     // character
                                     if selection.is_empty() {
@@ -311,11 +328,11 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
                 }
             });
         });
-        vim.switch_mode(Mode::Normal, false, cx);
+        vim.switch_mode(Mode::Normal, true, cx);
     });
 }
 
-pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut WindowContext) {
+pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
@@ -336,14 +353,7 @@ pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut WindowContext)
 
                 let mut edits = Vec::new();
                 for selection in selections.iter() {
-                    let mut selection = selection.clone();
-                    if !line && !selection.reversed {
-                        // Head is at the end of the selection. Adjust the end position to
-                        // to include the character under the cursor.
-                        *selection.end.column_mut() = selection.end.column() + 1;
-                        selection.end = display_map.clip_point(selection.end, Bias::Right);
-                    }
-
+                    let selection = selection.clone();
                     for row_range in
                         movement::split_display_range_by_lines(&display_map, selection.range())
                     {
@@ -367,6 +377,7 @@ pub(crate) fn visual_replace(text: Arc<str>, line: bool, cx: &mut WindowContext)
 #[cfg(test)]
 mod test {
     use indoc::indoc;
+    use workspace::item::Item;
 
     use crate::{
         state::Mode,
@@ -375,19 +386,146 @@ mod test {
 
     #[gpui::test]
     async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["v", "w", "j"]);
-        cx.assert_all(indoc! {"
-                The ˇquick brown
-                fox jumps ˇover
-                the ˇlazy dog"})
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "The ˇquick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+
+        // entering visual mode should select the character
+        // under cursor
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
+            fox jumps over
+            the lazy dog"})
             .await;
-        let mut cx = cx.binding(["v", "b", "k"]);
-        cx.assert_all(indoc! {"
-                The ˇquick brown
-                fox jumps ˇover
-                the ˇlazy dog"})
+        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+
+        // forwards motions should extend the selection
+        cx.simulate_shared_keystrokes(["w", "j"]).await;
+        cx.assert_shared_state(indoc! { "The «quick brown
+            fox jumps oˇ»ver
+            the lazy dog"})
+            .await;
+
+        cx.simulate_shared_keystrokes(["escape"]).await;
+        assert_eq!(Mode::Normal, cx.neovim_mode().await);
+        cx.assert_shared_state(indoc! { "The quick brown
+            fox jumps ˇover
+            the lazy dog"})
+            .await;
+
+        // motions work backwards
+        cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
+        cx.assert_shared_state(indoc! { "The «ˇquick brown
+            fox jumps o»ver
+            the lazy dog"})
+            .await;
+
+        // works on empty lines
+        cx.set_shared_state(indoc! {"
+            a
+            ˇ
+            b
+            "})
+            .await;
+        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            «
+            ˇ»b
+        "})
+            .await;
+        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+
+        // toggles off again
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            ˇ
+            b
+            "})
+            .await;
+
+        // works at the end of a document
+        cx.set_shared_state(indoc! {"
+            a
+            b
+            ˇ"})
+            .await;
+
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            b
+            ˇ"})
+            .await;
+        assert_eq!(cx.mode(), cx.neovim_mode().await);
+    }
+
+    #[gpui::test]
+    async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "The ˇquick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["shift-v"]).await;
+        cx.assert_shared_state(indoc! { "The «qˇ»uick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        assert_eq!(cx.mode(), cx.neovim_mode().await);
+        cx.simulate_shared_keystrokes(["x"]).await;
+        cx.assert_shared_state(indoc! { "fox ˇjumps over
+        the lazy dog"})
+            .await;
+
+        // it should work on empty lines
+        cx.set_shared_state(indoc! {"
+            a
+            ˇ
+            b"})
+            .await;
+        cx.simulate_shared_keystrokes(["shift-v"]).await;
+        cx.assert_shared_state(indoc! { "
+            a
+            «
+            ˇ»b"})
+            .await;
+        cx.simulate_shared_keystrokes(["x"]).await;
+        cx.assert_shared_state(indoc! { "
+            a
+            ˇb"})
+            .await;
+
+        // it should work at the end of the document
+        cx.set_shared_state(indoc! {"
+            a
+            b
+            ˇ"})
+            .await;
+        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+        cx.simulate_shared_keystrokes(["shift-v"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            b
+            ˇ"})
+            .await;
+        assert_eq!(cx.mode(), cx.neovim_mode().await);
+        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+        cx.simulate_shared_keystrokes(["x"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            ˇb"})
             .await;
     }
 
@@ -395,6 +533,9 @@ mod test {
     async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
+        cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
+            .await;
+
         cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
             .await;
         cx.assert_binding_matches(
@@ -457,62 +598,15 @@ mod test {
                 fox juˇmps over
                 the laˇzy dog"})
             .await;
-    }
-
-    #[gpui::test]
-    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["v", "w", "c"]);
-        cx.assert("The quick ˇbrown").await;
-        let mut cx = cx.binding(["v", "w", "j", "c"]);
-        cx.assert_all(indoc! {"
-                The ˇquick brown
-                fox jumps ˇover
-                the ˇlazy dog"})
-            .await;
-        let mut cx = cx.binding(["v", "b", "k", "c"]);
-        cx.assert_all(indoc! {"
-                The ˇquick brown
-                fox jumps ˇover
-                the ˇlazy dog"})
-            .await;
-    }
-
-    #[gpui::test]
-    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["shift-v", "c"]);
-        cx.assert(indoc! {"
-                The quˇick brown
-                fox jumps over
-                the lazy dog"})
-            .await;
-        // Test pasting code copied on change
-        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
-        cx.assert_state_matches().await;
 
-        cx.assert_all(indoc! {"
-                The quick brown
-                fox juˇmps over
-                the laˇzy dog"})
-            .await;
-        let mut cx = cx.binding(["shift-v", "j", "c"]);
-        cx.assert(indoc! {"
-                The quˇick brown
-                fox jumps over
-                the lazy dog"})
+        cx.set_shared_state(indoc! {"
+            The ˇlong line
+            should not
+            crash
+            "})
             .await;
-        // Test pasting code copied on delete
-        cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
+        cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
         cx.assert_state_matches().await;
-
-        cx.assert_all(indoc! {"
-                The quick brown
-                fox juˇmps over
-                the laˇzy dog"})
-            .await;
     }
 
     #[gpui::test]
@@ -605,7 +699,7 @@ mod test {
         cx.set_state(
             indoc! {"
                 The quick brown
-                fox «jumpˇ»s over
+                fox «jumpsˇ» over
                 the lazy dog"},
             Mode::Visual { line: false },
         );
@@ -629,7 +723,7 @@ mod test {
         cx.set_state(
             indoc! {"
                 The quick brown
-                fox juˇmps over
+                fox ju«mˇ»ps over
                 the lazy dog"},
             Mode::Visual { line: true },
         );
@@ -643,7 +737,7 @@ mod test {
         cx.set_state(
             indoc! {"
                 The quick brown
-                the «lazˇ»y dog"},
+                the «lazyˇ» dog"},
             Mode::Visual { line: false },
         );
         cx.simulate_keystroke("p");

crates/vim/test_data/test_enter_visual_line_mode.json 🔗

@@ -0,0 +1,15 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}}
+{"Key":"x"}
+{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"a\nˇ\nb"}}
+{"Key":"shift-v"}
+{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}}
+{"Key":"x"}
+{"Get":{"state":"a\nˇb","mode":"Normal"}}
+{"Put":{"state":"a\nb\nˇ"}}
+{"Key":"shift-v"}
+{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}}
+{"Key":"x"}
+{"Get":{"state":"a\nˇb","mode":"Normal"}}

crates/vim/test_data/test_enter_visual_mode.json 🔗

@@ -1,30 +1,20 @@
 {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"v"}
+{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
 {"Key":"w"}
 {"Key":"j"}
-{"Get":{"state":"The «quick brown\nfox jumps ˇ»over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
-{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
+{"Key":"escape"}
+{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}}
 {"Key":"v"}
-{"Key":"w"}
-{"Key":"j"}
-{"Get":{"state":"The quick brown\nfox jumps «over\nˇ»the lazy dog","mode":{"Visual":{"line":false}}}}
-{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
-{"Key":"v"}
-{"Key":"w"}
-{"Key":"j"}
-{"Get":{"state":"The quick brown\nfox jumps over\nthe «lazy ˇ»dog","mode":{"Visual":{"line":false}}}}
-{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
-{"Key":"v"}
-{"Key":"b"}
 {"Key":"k"}
-{"Get":{"state":"«ˇThe »quick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
-{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
-{"Key":"v"}
 {"Key":"b"}
-{"Key":"k"}
-{"Get":{"state":"The «ˇquick brown\nfox jumps »over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
-{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
+{"Put":{"state":"a\nˇ\nb\n"}}
 {"Key":"v"}
-{"Key":"b"}
-{"Key":"k"}
-{"Get":{"state":"The quick brown\n«ˇfox jumps over\nthe »lazy dog","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}}
+{"Key":"v"}
+{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}}
+{"Put":{"state":"a\nb\nˇ"}}
+{"Key":"v"}
+{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}}

crates/vim/test_data/test_multiline_surrounding_character_objects.json 🔗

@@ -0,0 +1,10 @@
+{"Put":{"state":"func empty(a string) bool {\n   if a == \"\" {\n      return true\n   }\n   ˇreturn false\n}"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"{"}
+{"Get":{"state":"func empty(a string) bool {\n«   if a == \"\" {\n      return true\n   }\n   return false\nˇ»}","mode":{"Visual":{"line":false}}}}
+{"Put":{"state":"func empty(a string) bool {\n     if a == \"\" {\n         ˇreturn true\n     }\n     return false\n}"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"{"}
+{"Get":{"state":"func empty(a string) bool {\n     if a == \"\" {\n«         return true\nˇ»     }\n     return false\n}","mode":{"Visual":{"line":false}}}}

crates/vim/test_data/test_visual_change.json 🔗

@@ -9,33 +9,39 @@
 {"Key":"j"}
 {"Key":"c"}
 {"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
-{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"v"}
 {"Key":"w"}
 {"Key":"j"}
 {"Key":"c"}
-{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}}
-{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"v"}
 {"Key":"w"}
-{"Key":"j"}
+{"Key":"k"}
 {"Key":"c"}
-{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}}
-{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Get":{"state":"The ˇrown\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
 {"Key":"v"}
-{"Key":"b"}
-{"Key":"k"}
+{"Key":"w"}
+{"Key":"j"}
 {"Key":"c"}
-{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}}
 {"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
 {"Key":"v"}
-{"Key":"b"}
+{"Key":"w"}
 {"Key":"k"}
 {"Key":"c"}
-{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Get":{"state":"The quick brown\nˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}}
 {"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
 {"Key":"v"}
-{"Key":"b"}
+{"Key":"w"}
 {"Key":"k"}
 {"Key":"c"}
-{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}}
+{"Get":{"state":"The quick brown\nfox jumpsˇazy dog","mode":"Insert"}}

crates/vim/test_data/test_visual_delete.json 🔗

@@ -1,6 +1,10 @@
 {"Put":{"state":"The quick ˇbrown"}}
 {"Key":"v"}
 {"Key":"w"}
+{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}}
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
 {"Key":"x"}
 {"Get":{"state":"The quickˇ ","mode":"Normal"}}
 {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}

crates/vim/test_data/test_visual_line_delete.json 🔗

@@ -29,3 +29,8 @@
 {"Key":"j"}
 {"Key":"x"}
 {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}}
+{"Key":"shift-v"}
+{"Key":"$"}
+{"Key":"x"}
+{"Get":{"state":"should noˇt\ncrash\n","mode":"Normal"}}

crates/vim/test_data/test_visual_word_object.json 🔗

@@ -1,230 +1,236 @@
+{"Put":{"state":"The quick ˇbrown\nfox"}}
+{"Key":"v"}
+{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick ˇbrown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «browˇ»n   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick browˇn   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «browˇ»n   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brownˇ   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown«  ˇ» \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox ˇjumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox «jumpˇ»s over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox juˇmps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox «jumpˇ»s over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dogˇ  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThˇe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«Thˇ»e-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«Theˇ»-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nTheˇ-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nTheˇ-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe«-ˇ»quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-ˇquick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quˇick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick ˇbrown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «browˇ»n \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \nˇ  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n« ˇ» \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \nˇ  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n« ˇ» \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \nˇ  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumpˇs over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-«jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick ˇbrown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick «browˇ»n   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick browˇn   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick «browˇ»n   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brownˇ   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown«  ˇ» \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox ˇjumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox «jumpˇ»s over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox juˇmps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox «jumpˇ»s over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dogˇ  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThˇe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nTheˇ-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-ˇquick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quˇick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quicˇ»k brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick ˇbrown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «browˇ»n \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \nˇ  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n« ˇ» \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \nˇ  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n« ˇ» \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \nˇ  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumpˇs over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  «fox-jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}}