WIP just missing insert line above and below

Keith Simmons created

Change summary

assets/keymaps/vim.json     |  63 ++-
crates/editor/src/editor.rs |   1 
crates/vim/src/motion.rs    |  32 +
crates/vim/src/normal.rs    | 587 ++++++++++++++++++++++++++++++++++++++
4 files changed, 648 insertions(+), 35 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

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

crates/editor/src/editor.rs 🔗

@@ -1456,6 +1456,7 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
         let newest_selection = self.newest_anchor_selection().clone();
+        let position = display_map.clip_point(position, Bias::Left);
 
         let start;
         let end;

crates/vim/src/motion.rs 🔗

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

crates/vim/src/normal.rs 🔗

@@ -1,15 +1,60 @@
 mod change;
 mod delete;
 
-use crate::{motion::Motion, state::Operator, Vim};
+use crate::{
+    motion::Motion,
+    state::{Mode, Operator},
+    Vim,
+};
 use change::init as change_init;
-use gpui::{actions, MutableAppContext};
+use gpui::{actions, MutableAppContext, ViewContext};
+use language::SelectionGoal;
+use workspace::Workspace;
 
 use self::{change::change_over, delete::delete_over};
 
-actions!(vim, [InsertLineAbove, InsertLineBelow, InsertAfter]);
+actions!(
+    vim,
+    [
+        InsertAfter,
+        InsertFirstNonWhitespace,
+        InsertEndOfLine,
+        InsertLineAbove,
+        InsertLineBelow,
+        DeleteLeft,
+        DeleteRight,
+        ChangeToEndOfLine,
+        DeleteToEndOfLine,
+    ]
+);
 
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(insert_after);
+    cx.add_action(insert_first_non_whitespace);
+    cx.add_action(insert_end_of_line);
+    cx.add_action(insert_line_above);
+    cx.add_action(insert_line_below);
+    cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::Left, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::Right, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            change_over(vim, Motion::EndOfLine, cx);
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            delete_over(vim, Motion::EndOfLine, cx);
+        })
+    });
+
     change_init(cx);
 }
 
@@ -33,6 +78,88 @@ fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     });
 }
 
+fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::Right.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_first_non_whitespace(
+    _: &mut Workspace,
+    _: &InsertFirstNonWhitespace,
+    cx: &mut ViewContext<Workspace>,
+) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::FirstNonWhitespace.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.move_cursors(cx, |map, cursor, goal| {
+                Motion::EndOfLine.move_point(map, cursor, goal)
+            });
+        });
+    });
+}
+
+fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                editor.move_cursors(cx, |map, cursor, goal| {
+                    let (indent, _) = map.line_indent(cursor.row());
+                    let (cursor, _) = Motion::EndOfLine.move_point(map, cursor, goal);
+                    (cursor, SelectionGoal::Column(indent))
+                });
+                editor.insert("\n", cx);
+                editor.move_cursors(cx, |_, mut cursor, goal| {
+                    if let SelectionGoal::Column(column) = goal {
+                        *cursor.column_mut() = column;
+                    }
+                    (cursor, SelectionGoal::None)
+                });
+            });
+        });
+    });
+}
+
+fn insert_line_below(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.switch_mode(Mode::Insert, cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                editor.move_cursors(cx, |map, cursor, goal| {
+                    let (indent, _) = map.line_indent(cursor.row());
+                    let (cursor, _) = Motion::StartOfLine.move_point(map, cursor, goal);
+                    (cursor, SelectionGoal::Column(indent))
+                });
+                editor.insert("\n", cx);
+                editor.move_cursors(cx, |_, mut cursor, goal| {
+                    *cursor.row_mut() -= 1;
+                    if let SelectionGoal::Column(column) = goal {
+                        *cursor.column_mut() = column;
+                    }
+                    (cursor, SelectionGoal::None)
+                });
+            });
+        });
+    });
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;
@@ -63,18 +190,18 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_l(cx: &mut gpui::TestAppContext) {
+    async fn test_backspace(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["l"]);
-        cx.assert("The q|uick", "The qu|ick");
-        cx.assert("The quic|k", "The quic|k");
+        let mut cx = cx.binding(["backspace"]);
+        cx.assert("The q|uick", "The |quick");
+        cx.assert("|The quick", "|The quick");
         cx.assert(
             indoc! {"
-                The quic|k
-                brown"},
+                The quick
+                |brown"},
             indoc! {"
-                The quic|k
-                brown"},
+                The quick
+                |brown"},
         );
     }
 
@@ -146,6 +273,22 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_l(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["l"]);
+        cx.assert("The q|uick", "The qu|ick");
+        cx.assert("The quic|k", "The quic|k");
+        cx.assert(
+            indoc! {"
+                The quic|k
+                brown"},
+            indoc! {"
+                The quic|k
+                brown"},
+        );
+    }
+
     #[gpui::test]
     async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
@@ -242,7 +385,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
+    async fn test_w(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             The |quick|-|brown
@@ -289,7 +432,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
+    async fn test_e(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             Th|e quic|k|-brow|n
@@ -335,7 +478,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
+    async fn test_b(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         let (_, cursor_offsets) = marked_text(indoc! {"
             ||The |quick|-|brown
@@ -397,7 +540,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
+    async fn test_gg(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
         let mut cx = cx.binding(["g", "g"]);
         cx.assert(
@@ -449,4 +592,418 @@ mod test {
                 over the lazy dog"},
         );
     }
+
+    #[gpui::test]
+    async fn test_a(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["a"]).mode_after(Mode::Insert);
+
+        cx.assert("The q|uick", "The qu|ick");
+        cx.assert("The quic|k", "The quick|");
+    }
+
+    #[gpui::test]
+    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert);
+        cx.assert("The q|uick", "The quick|");
+        cx.assert("The q|uick ", "The quick |");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                The quick|
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-^"]);
+        cx.assert("The q|uick", "|The quick");
+        cx.assert(" The q|uick", " |The quick");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                |The quick
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+        cx.assert(
+            indoc! {"
+                    |
+                The quick"},
+            indoc! {"
+                    |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert);
+        cx.assert("The q|uick", "|The quick");
+        cx.assert(" The q|uick", " |The quick");
+        cx.assert("|", "|");
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                |The quick
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                |
+                The quick"},
+            indoc! {"
+                |
+                The quick"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-D"]);
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox"},
+            indoc! {"
+                The |q
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_x(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["x"]);
+        cx.assert("|Test", "|est");
+        cx.assert("Te|st", "Te|t");
+        cx.assert("Tes|t", "Te|s");
+        cx.assert(
+            indoc! {"
+                Tes|t
+                test"},
+            indoc! {"
+                Te|s
+                test"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-X"]);
+        cx.assert("Te|st", "T|st");
+        cx.assert("T|est", "|est");
+        cx.assert("|Test", "|Test");
+        cx.assert(
+            indoc! {"
+                Test
+                |test"},
+            indoc! {"
+                Test
+                |test"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_o(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["o"]).mode_after(Mode::Insert);
+
+        cx.assert(
+            "|",
+            indoc! {"
+                
+                |"},
+        );
+        cx.assert(
+            "The |quick",
+            indoc! {"
+                The quick
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                brown fox
+                |
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown fox
+                jumps over
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                
+                |
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test() {
+                    println!(|);
+                }"},
+            indoc! {"
+                fn test() {
+                    println!();
+                    |
+                }"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test(|) {
+                    println!();
+                }"},
+            indoc! {"
+                fn test() {
+                |
+                    println!();
+                }"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert);
+
+        cx.assert(
+            "|",
+            indoc! {"
+                |
+                "},
+        );
+        cx.assert(
+            "The |quick",
+            indoc! {"
+                |
+                The quick"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown fox
+                |
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                |
+                The quick
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |
+                
+                brown fox"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test() {
+                    println!(|);
+                }"},
+            indoc! {"
+                fn test() {
+                    |
+                    println!();
+                }"},
+        );
+        cx.assert(
+            indoc! {"
+                fn test(|) {
+                    println!();
+                }"},
+            indoc! {"
+                |
+                fn test() {
+                    println!();
+                }"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_dd(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["d", "d"]);
+
+        cx.assert("|", "|");
+        cx.assert("The |quick", "|");
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                jumps |over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown |fox"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                brown| fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |brown fox"},
+        );
+    }
+
+    #[gpui::test]
+    async fn test_cc(cx: &mut gpui::TestAppContext) {
+        let cx = VimTestContext::new(cx, true).await;
+        let mut cx = cx.binding(["c", "c"]).mode_after(Mode::Insert);
+
+        cx.assert("|", "|");
+        cx.assert("The |quick", "|");
+        cx.assert(
+            indoc! {"
+                The quick
+                brown |fox
+                jumps over"},
+            indoc! {"
+                The quick
+                |
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                brown fox
+                jumps |over"},
+            indoc! {"
+                The quick
+                brown fox
+                |"},
+        );
+        cx.assert(
+            indoc! {"
+                The q|uick
+                brown fox
+                jumps over"},
+            indoc! {"
+                |
+                brown fox
+                jumps over"},
+        );
+        cx.assert(
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+            indoc! {"
+                The quick
+                |
+                brown fox"},
+        );
+    }
 }