Fix find_{,preceding}boundary to work on buffer text (#2912)

Conrad Irwin created

Fixes movement::find_boundary to work on the buffer, not on display
points.

The user-visible impact is that the "until end of word" commands now
correctly go to the end of a soft-wrapped word (instead of to the first
character of the wrapped line).

It also fixes a bug where the callback passed to these methods was
called with the content of inlay hints.

[[PR Description]]

Release Notes:

- fix finding end of word on soft-wrapped lines

Change summary

crates/editor/src/movement.rs                         | 167 ++++--------
crates/live_kit_client/LiveKitBridge/Package.resolved |   4 
crates/vim/src/motion.rs                              |  25 +
crates/vim/src/normal.rs                              |   2 
crates/vim/src/normal/change.rs                       |  19 
crates/vim/src/object.rs                              |  31 +
crates/vim/src/test.rs                                |  18 +
crates/vim/src/test/neovim_backed_test_context.rs     |   7 
crates/vim/test_data/test_end_of_word.json            |  32 ++
crates/vim/test_data/test_visual_word_object.json     |   6 
crates/vim/test_data/test_wrapped_lines.json          |   5 
11 files changed, 173 insertions(+), 143 deletions(-)

Detailed changes

crates/editor/src/movement.rs 🔗

@@ -1,8 +1,14 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToPoint};
+use crate::{char_kind, CharKind, ToOffset, ToPoint};
 use language::Point;
 use std::ops::Range;
 
+#[derive(Debug, PartialEq)]
+pub enum FindRange {
+    SingleLine,
+    MultiLine,
+}
+
 pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     if point.column() > 0 {
         *point.column_mut() -= 1;
@@ -179,7 +185,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
 
-    find_preceding_boundary(map, point, |left, right| {
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
         (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
             || left == '\n'
     })
@@ -188,7 +194,8 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
 pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_preceding_boundary(map, point, |left, right| {
+
+    find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
         let is_word_start =
             char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
         let is_subword_start =
@@ -200,7 +207,8 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
 pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_boundary(map, point, |left, right| {
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
         (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
             || right == '\n'
     })
@@ -209,7 +217,8 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
 pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
-    find_boundary(map, point, |left, right| {
+
+    find_boundary(map, point, FindRange::MultiLine, |left, right| {
         let is_word_end =
             (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
         let is_subword_end =
@@ -272,79 +281,34 @@ pub fn end_of_paragraph(
     map.max_point()
 }
 
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line.
+/// Scans for a boundary preceding the given start point `from` until a boundary is found,
+/// indicated by the given predicate returning true.
+/// The predicate is called with the character to the left and right of the candidate boundary location.
+/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
 pub fn find_preceding_boundary(
     map: &DisplaySnapshot,
     from: DisplayPoint,
+    find_range: FindRange,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
-    let mut start_column = 0;
-    let mut soft_wrap_row = from.row() + 1;
-
-    let mut prev = None;
-    for (ch, point) in map.reverse_chars_at(from) {
-        // Recompute soft_wrap_indent if the row has changed
-        if point.row() != soft_wrap_row {
-            soft_wrap_row = point.row();
-
-            if point.row() == 0 {
-                start_column = 0;
-            } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
-                start_column = indent;
-            }
-        }
-
-        // If the current point is in the soft_wrap, skip comparing it
-        if point.column() < start_column {
-            continue;
-        }
-
-        if let Some((prev_ch, prev_point)) = prev {
-            if is_boundary(ch, prev_ch) {
-                return map.clip_point(prev_point, Bias::Left);
-            }
-        }
-
-        prev = Some((ch, point));
-    }
-    map.clip_point(DisplayPoint::zero(), Bias::Left)
-}
+    let mut prev_ch = None;
+    let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
 
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the start of the line is returned.
-pub fn find_preceding_boundary_in_line(
-    map: &DisplaySnapshot,
-    from: DisplayPoint,
-    mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
-    let mut start_column = 0;
-    if from.row() > 0 {
-        if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
-            start_column = indent;
+    for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
         }
-    }
-
-    let mut prev = None;
-    for (ch, point) in map.reverse_chars_at(from) {
-        if let Some((prev_ch, prev_point)) = prev {
+        if let Some(prev_ch) = prev_ch {
             if is_boundary(ch, prev_ch) {
-                return map.clip_point(prev_point, Bias::Left);
+                break;
             }
         }
 
-        if ch == '\n' || point.column() < start_column {
-            break;
-        }
-
-        prev = Some((ch, point));
+        offset -= ch.len_utf8();
+        prev_ch = Some(ch);
     }
 
-    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
+    map.clip_point(offset.to_display_point(map), Bias::Left)
 }
 
 /// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -354,47 +318,26 @@ pub fn find_preceding_boundary_in_line(
 pub fn find_boundary(
     map: &DisplaySnapshot,
     from: DisplayPoint,
+    find_range: FindRange,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
+    let mut offset = from.to_offset(&map, Bias::Right);
     let mut prev_ch = None;
-    for (ch, point) in map.chars_at(from) {
+
+    for ch in map.buffer_snapshot.chars_at(offset) {
+        if find_range == FindRange::SingleLine && ch == '\n' {
+            break;
+        }
         if let Some(prev_ch) = prev_ch {
             if is_boundary(prev_ch, ch) {
-                return map.clip_point(point, Bias::Right);
+                break;
             }
         }
 
+        offset += ch.len_utf8();
         prev_ch = Some(ch);
     }
-    map.clip_point(map.max_point(), Bias::Right)
-}
-
-/// Scans for a boundary following the given start point until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the end of the line is returned
-pub fn find_boundary_in_line(
-    map: &DisplaySnapshot,
-    from: DisplayPoint,
-    mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
-    let mut prev = None;
-    for (ch, point) in map.chars_at(from) {
-        if let Some((prev_ch, _)) = prev {
-            if is_boundary(prev_ch, ch) {
-                return map.clip_point(point, Bias::Right);
-            }
-        }
-
-        prev = Some((ch, point));
-
-        if ch == '\n' {
-            break;
-        }
-    }
-
-    // Return the last position checked so that we give a point right before the newline or eof.
-    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
+    map.clip_point(offset.to_display_point(map), Bias::Right)
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@@ -533,7 +476,12 @@ mod tests {
         ) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
-                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
+                find_preceding_boundary(
+                    &snapshot,
+                    display_points[1],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
                 display_points[0]
             );
         }
@@ -612,21 +560,15 @@ mod tests {
             find_preceding_boundary(
                 &snapshot,
                 buffer_snapshot.len().to_display_point(&snapshot),
-                |left, _| left == 'a',
+                FindRange::MultiLine,
+                |left, _| left == 'e',
             ),
-            0.to_display_point(&snapshot),
+            snapshot
+                .buffer_snapshot
+                .offset_to_point(5)
+                .to_display_point(&snapshot),
             "Should not stop at inlays when looking for boundaries"
         );
-
-        assert_eq!(
-            find_preceding_boundary_in_line(
-                &snapshot,
-                buffer_snapshot.len().to_display_point(&snapshot),
-                |left, _| left == 'a',
-            ),
-            0.to_display_point(&snapshot),
-            "Should not stop at inlays when looking for boundaries in line"
-        );
     }
 
     #[gpui::test]
@@ -699,7 +641,12 @@ mod tests {
         ) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
-                find_boundary(&snapshot, display_points[0], is_boundary),
+                find_boundary(
+                    &snapshot,
+                    display_points[0],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
                 display_points[1]
             );
         }

crates/live_kit_client/LiveKitBridge/Package.resolved 🔗

@@ -42,8 +42,8 @@
         "repositoryURL": "https://github.com/apple/swift-protobuf.git",
         "state": {
           "branch": null,
-          "revision": "ce20dc083ee485524b802669890291c0d8090170",
-          "version": "1.22.1"
+          "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
+          "version": "1.21.0"
         }
       }
     ]

crates/vim/src/motion.rs 🔗

@@ -3,7 +3,8 @@ use std::{cmp, sync::Arc};
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
-    movement, Bias, CharKind, DisplayPoint, ToOffset,
+    movement::{self, FindRange},
+    Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::{Point, Selection, SelectionGoal};
@@ -592,7 +593,7 @@ pub(crate) fn next_word_start(
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
         let mut crossed_newline = false;
-        point = movement::find_boundary(map, point, |left, right| {
+        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
             let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
             let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
             let at_newline = right == '\n';
@@ -616,8 +617,13 @@ fn next_word_end(
 ) -> DisplayPoint {
     let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
     for _ in 0..times {
-        *point.column_mut() += 1;
-        point = movement::find_boundary(map, point, |left, right| {
+        if point.column() < map.line_len(point.row()) {
+            *point.column_mut() += 1;
+        } else if point.row() < map.max_buffer_row() {
+            *point.row_mut() += 1;
+            *point.column_mut() = 0;
+        }
+        point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
             let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
             let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
@@ -649,12 +655,13 @@ fn previous_word_start(
     for _ in 0..times {
         // This works even though find_preceding_boundary is called for every character in the line containing
         // cursor because the newline is checked only once.
-        point = movement::find_preceding_boundary(map, point, |left, right| {
-            let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
-            let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
+        point =
+            movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
-            (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
-        });
+                (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
+            });
     }
     point
 }

crates/vim/src/normal.rs 🔗

@@ -445,7 +445,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_e(cx: &mut gpui::TestAppContext) {
+    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
         cx.assert_all(indoc! {"
             Thˇe quicˇkˇ-browˇn

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

@@ -1,7 +1,10 @@
 use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
 use editor::{
-    char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind,
-    DisplayPoint,
+    char_kind,
+    display_map::DisplaySnapshot,
+    movement::{self, FindRange},
+    scroll::autoscroll::Autoscroll,
+    CharKind, DisplayPoint,
 };
 use gpui::WindowContext;
 use language::Selection;
@@ -96,12 +99,14 @@ fn expand_changed_word_selection(
             .unwrap_or_default();
 
         if in_word {
-            selection.end = movement::find_boundary(map, selection.end, |left, right| {
-                let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
-                let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
+            selection.end =
+                movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
+                    let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+                    let right_kind =
+                        char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
-                left_kind != right_kind && left_kind != CharKind::Whitespace
-            });
+                    left_kind != right_kind && left_kind != CharKind::Whitespace
+                });
             true
         } else {
             Motion::NextWordStart { ignore_punctuation }

crates/vim/src/object.rs 🔗

@@ -1,6 +1,11 @@
 use std::ops::Range;
 
-use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
+use editor::{
+    char_kind,
+    display_map::DisplaySnapshot,
+    movement::{self, FindRange},
+    Bias, CharKind, DisplayPoint,
+};
 use gpui::{actions, impl_actions, AppContext, WindowContext};
 use language::Selection;
 use serde::Deserialize;
@@ -180,15 +185,17 @@ fn in_word(
     let scope = map
         .buffer_snapshot
         .language_scope_at(relative_to.to_point(map));
-    let start = movement::find_preceding_boundary_in_line(
+    let start = movement::find_preceding_boundary(
         map,
         right(map, relative_to, 1),
+        movement::FindRange::SingleLine,
         |left, right| {
             char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
                 != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
         },
     );
-    let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
+
+    let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
         char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
             != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
     });
@@ -247,9 +254,10 @@ fn around_next_word(
         .buffer_snapshot
         .language_scope_at(relative_to.to_point(map));
     // Get the start of the word
-    let start = movement::find_preceding_boundary_in_line(
+    let start = movement::find_preceding_boundary(
         map,
         right(map, relative_to, 1),
+        FindRange::SingleLine,
         |left, right| {
             char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
                 != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
@@ -257,7 +265,7 @@ fn around_next_word(
     );
 
     let mut word_found = false;
-    let end = movement::find_boundary(map, relative_to, |left, right| {
+    let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
         let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
         let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
 
@@ -572,11 +580,18 @@ 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.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.set_shared_state("The quick brown\nˇ\nfox").await;
         cx.simulate_shared_keystrokes(["v"]).await;
-        cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
         cx.simulate_shared_keystrokes(["i", "w"]).await;
-        cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+        cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
 
         cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
             .await;

crates/vim/src/test.rs 🔗

@@ -431,6 +431,24 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
         twelve char
     "})
         .await;
+
+    // line wraps as:
+    // fourteen ch
+    // ar
+    // fourteen ch
+    // ar
+    cx.set_shared_state(indoc! { "
+        fourteen chaˇr
+        fourteen char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
+    cx.assert_shared_state(indoc! {"
+        fourteenˇ•
+        fourteen char
+    "})
+        .await;
 }
 
 #[gpui::test]

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

@@ -153,6 +153,7 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        let marked_text = marked_text.replace("•", " ");
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
         if neovim == marked_text && neovim == editor {
@@ -184,9 +185,9 @@ impl<'a> NeovimBackedTestContext<'a> {
             message,
             initial_state,
             self.recent_keystrokes.join(" "),
-            marked_text,
-            neovim,
-            editor
+            marked_text.replace(" \n", "•\n"),
+            neovim.replace(" \n", "•\n"),
+            editor.replace(" \n", "•\n")
         )
     }
 

crates/vim/test_data/test_end_of_word.json 🔗

@@ -0,0 +1,32 @@
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"e"}
+{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}

crates/vim/test_data/test_visual_word_object.json 🔗

@@ -1,9 +1,9 @@
-{"Put":{"state":"The quick ˇbrown\nfox"}}
+{"Put":{"state":"The quick brown\nˇ\nfox"}}
 {"Key":"v"}
-{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
 {"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"}

crates/vim/test_data/test_wrapped_lines.json 🔗

@@ -48,3 +48,8 @@
 {"Key":"o"}
 {"Key":"escape"}
 {"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}