normal.rs

  1mod change;
  2mod delete;
  3mod yank;
  4
  5use std::borrow::Cow;
  6
  7use crate::{
  8    motion::Motion,
  9    object::Object,
 10    state::{Mode, Operator},
 11    Vim,
 12};
 13use collections::{HashMap, HashSet};
 14use editor::{
 15    display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint,
 16};
 17use gpui::{actions, MutableAppContext, ViewContext};
 18use language::{AutoindentMode, Point, SelectionGoal};
 19use workspace::Workspace;
 20
 21use self::{
 22    change::{change_motion, change_object},
 23    delete::{delete_motion, delete_object},
 24    yank::{yank_motion, yank_object},
 25};
 26
 27actions!(
 28    vim,
 29    [
 30        InsertAfter,
 31        InsertFirstNonWhitespace,
 32        InsertEndOfLine,
 33        InsertLineAbove,
 34        InsertLineBelow,
 35        DeleteLeft,
 36        DeleteRight,
 37        ChangeToEndOfLine,
 38        DeleteToEndOfLine,
 39        Paste,
 40        Yank,
 41    ]
 42);
 43
 44pub fn init(cx: &mut MutableAppContext) {
 45    cx.add_action(insert_after);
 46    cx.add_action(insert_first_non_whitespace);
 47    cx.add_action(insert_end_of_line);
 48    cx.add_action(insert_line_above);
 49    cx.add_action(insert_line_below);
 50    cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
 51        Vim::update(cx, |vim, cx| {
 52            let times = vim.pop_number_operator(cx);
 53            delete_motion(vim, Motion::Left, times, cx);
 54        })
 55    });
 56    cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
 57        Vim::update(cx, |vim, cx| {
 58            let times = vim.pop_number_operator(cx);
 59            delete_motion(vim, Motion::Right, times, cx);
 60        })
 61    });
 62    cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
 63        Vim::update(cx, |vim, cx| {
 64            let times = vim.pop_number_operator(cx);
 65            change_motion(vim, Motion::EndOfLine, times, cx);
 66        })
 67    });
 68    cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
 69        Vim::update(cx, |vim, cx| {
 70            let times = vim.pop_number_operator(cx);
 71            delete_motion(vim, Motion::EndOfLine, times, cx);
 72        })
 73    });
 74    cx.add_action(paste);
 75}
 76
 77pub fn normal_motion(
 78    motion: Motion,
 79    operator: Option<Operator>,
 80    times: usize,
 81    cx: &mut MutableAppContext,
 82) {
 83    Vim::update(cx, |vim, cx| {
 84        match operator {
 85            None => move_cursor(vim, motion, times, cx),
 86            Some(Operator::Change) => change_motion(vim, motion, times, cx),
 87            Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
 88            Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
 89            _ => {
 90                // Can't do anything for text objects or namespace operators. Ignoring
 91            }
 92        }
 93    });
 94}
 95
 96pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
 97    Vim::update(cx, |vim, cx| {
 98        match vim.state.operator_stack.pop() {
 99            Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
100                Some(Operator::Change) => change_object(vim, object, around, cx),
101                Some(Operator::Delete) => delete_object(vim, object, around, cx),
102                Some(Operator::Yank) => yank_object(vim, object, around, cx),
103                _ => {
104                    // Can't do anything for namespace operators. Ignoring
105                }
106            },
107            _ => {
108                // Can't do anything with change/delete/yank and text objects. Ignoring
109            }
110        }
111        vim.clear_operator(cx);
112    })
113}
114
115fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
116    vim.update_active_editor(cx, |editor, cx| {
117        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
118            s.move_cursors_with(|map, cursor, goal| {
119                motion
120                    .move_point(map, cursor, goal, times)
121                    .unwrap_or((cursor, goal))
122            })
123        })
124    });
125}
126
127fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
128    Vim::update(cx, |vim, cx| {
129        vim.switch_mode(Mode::Insert, false, cx);
130        vim.update_active_editor(cx, |editor, cx| {
131            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
132                s.maybe_move_cursors_with(|map, cursor, goal| {
133                    Motion::Right.move_point(map, cursor, goal, 1)
134                });
135            });
136        });
137    });
138}
139
140fn insert_first_non_whitespace(
141    _: &mut Workspace,
142    _: &InsertFirstNonWhitespace,
143    cx: &mut ViewContext<Workspace>,
144) {
145    Vim::update(cx, |vim, cx| {
146        vim.switch_mode(Mode::Insert, false, cx);
147        vim.update_active_editor(cx, |editor, cx| {
148            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
149                s.maybe_move_cursors_with(|map, cursor, goal| {
150                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
151                });
152            });
153        });
154    });
155}
156
157fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
158    Vim::update(cx, |vim, cx| {
159        vim.switch_mode(Mode::Insert, false, cx);
160        vim.update_active_editor(cx, |editor, cx| {
161            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
162                s.maybe_move_cursors_with(|map, cursor, goal| {
163                    Motion::EndOfLine.move_point(map, cursor, goal, 1)
164                });
165            });
166        });
167    });
168}
169
170fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
171    Vim::update(cx, |vim, cx| {
172        vim.switch_mode(Mode::Insert, false, cx);
173        vim.update_active_editor(cx, |editor, cx| {
174            editor.transact(cx, |editor, cx| {
175                let (map, old_selections) = editor.selections.all_display(cx);
176                let selection_start_rows: HashSet<u32> = old_selections
177                    .into_iter()
178                    .map(|selection| selection.start.row())
179                    .collect();
180                let edits = selection_start_rows.into_iter().map(|row| {
181                    let (indent, _) = map.line_indent(row);
182                    let start_of_line = map
183                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
184                        .to_point(&map);
185                    let mut new_text = " ".repeat(indent as usize);
186                    new_text.push('\n');
187                    (start_of_line..start_of_line, new_text)
188                });
189                editor.edit_with_autoindent(edits, cx);
190                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
191                    s.move_cursors_with(|map, mut cursor, _| {
192                        *cursor.row_mut() -= 1;
193                        *cursor.column_mut() = map.line_len(cursor.row());
194                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
195                    });
196                });
197            });
198        });
199    });
200}
201
202fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
203    Vim::update(cx, |vim, cx| {
204        vim.switch_mode(Mode::Insert, false, cx);
205        vim.update_active_editor(cx, |editor, cx| {
206            editor.transact(cx, |editor, cx| {
207                let (map, old_selections) = editor.selections.all_display(cx);
208                let selection_end_rows: HashSet<u32> = old_selections
209                    .into_iter()
210                    .map(|selection| selection.end.row())
211                    .collect();
212                let edits = selection_end_rows.into_iter().map(|row| {
213                    let (indent, _) = map.line_indent(row);
214                    let end_of_line = map
215                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
216                        .to_point(&map);
217                    let mut new_text = "\n".to_string();
218                    new_text.push_str(&" ".repeat(indent as usize));
219                    (end_of_line..end_of_line, new_text)
220                });
221                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
222                    s.maybe_move_cursors_with(|map, cursor, goal| {
223                        Motion::EndOfLine.move_point(map, cursor, goal, 1)
224                    });
225                });
226                editor.edit_with_autoindent(edits, cx);
227            });
228        });
229    });
230}
231
232fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
233    Vim::update(cx, |vim, cx| {
234        vim.update_active_editor(cx, |editor, cx| {
235            editor.transact(cx, |editor, cx| {
236                editor.set_clip_at_line_ends(false, cx);
237                if let Some(item) = cx.as_mut().read_from_clipboard() {
238                    let mut clipboard_text = Cow::Borrowed(item.text());
239                    if let Some(mut clipboard_selections) =
240                        item.metadata::<Vec<ClipboardSelection>>()
241                    {
242                        let (display_map, selections) = editor.selections.all_display(cx);
243                        let all_selections_were_entire_line =
244                            clipboard_selections.iter().all(|s| s.is_entire_line);
245                        if clipboard_selections.len() != selections.len() {
246                            let mut newline_separated_text = String::new();
247                            let mut clipboard_selections =
248                                clipboard_selections.drain(..).peekable();
249                            let mut ix = 0;
250                            while let Some(clipboard_selection) = clipboard_selections.next() {
251                                newline_separated_text
252                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
253                                ix += clipboard_selection.len;
254                                if clipboard_selections.peek().is_some() {
255                                    newline_separated_text.push('\n');
256                                }
257                            }
258                            clipboard_text = Cow::Owned(newline_separated_text);
259                        }
260
261                        // If the pasted text is a single line, the cursor should be placed after
262                        // the newly pasted text. This is easiest done with an anchor after the
263                        // insertion, and then with a fixup to move the selection back one position.
264                        // However if the pasted text is linewise, the cursor should be placed at the start
265                        // of the new text on the following line. This is easiest done with a manually adjusted
266                        // point.
267                        // This enum lets us represent both cases
268                        enum NewPosition {
269                            Inside(Point),
270                            After(Anchor),
271                        }
272                        let mut new_selections: HashMap<usize, NewPosition> = Default::default();
273                        editor.buffer().update(cx, |buffer, cx| {
274                            let snapshot = buffer.snapshot(cx);
275                            let mut start_offset = 0;
276                            let mut edits = Vec::new();
277                            for (ix, selection) in selections.iter().enumerate() {
278                                let to_insert;
279                                let linewise;
280                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
281                                    let end_offset = start_offset + clipboard_selection.len;
282                                    to_insert = &clipboard_text[start_offset..end_offset];
283                                    linewise = clipboard_selection.is_entire_line;
284                                    start_offset = end_offset;
285                                } else {
286                                    to_insert = clipboard_text.as_str();
287                                    linewise = all_selections_were_entire_line;
288                                }
289
290                                // If the clipboard text was copied linewise, and the current selection
291                                // is empty, then paste the text after this line and move the selection
292                                // to the start of the pasted text
293                                let insert_at = if linewise {
294                                    let (point, _) = display_map
295                                        .next_line_boundary(selection.start.to_point(&display_map));
296
297                                    if !to_insert.starts_with('\n') {
298                                        // Add newline before pasted text so that it shows up
299                                        edits.push((point..point, "\n"));
300                                    }
301                                    // Drop selection at the start of the next line
302                                    new_selections.insert(
303                                        selection.id,
304                                        NewPosition::Inside(Point::new(point.row + 1, 0)),
305                                    );
306                                    point
307                                } else {
308                                    let mut point = selection.end;
309                                    // Paste the text after the current selection
310                                    *point.column_mut() = point.column() + 1;
311                                    let point = display_map
312                                        .clip_point(point, Bias::Right)
313                                        .to_point(&display_map);
314
315                                    new_selections.insert(
316                                        selection.id,
317                                        if to_insert.contains('\n') {
318                                            NewPosition::Inside(point)
319                                        } else {
320                                            NewPosition::After(snapshot.anchor_after(point))
321                                        },
322                                    );
323                                    point
324                                };
325
326                                if linewise && to_insert.ends_with('\n') {
327                                    edits.push((
328                                        insert_at..insert_at,
329                                        &to_insert[0..to_insert.len().saturating_sub(1)],
330                                    ))
331                                } else {
332                                    edits.push((insert_at..insert_at, to_insert));
333                                }
334                            }
335                            drop(snapshot);
336                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
337                        });
338
339                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
340                            s.move_with(|map, selection| {
341                                if let Some(new_position) = new_selections.get(&selection.id) {
342                                    match new_position {
343                                        NewPosition::Inside(new_point) => {
344                                            selection.collapse_to(
345                                                new_point.to_display_point(map),
346                                                SelectionGoal::None,
347                                            );
348                                        }
349                                        NewPosition::After(after_point) => {
350                                            let mut new_point = after_point.to_display_point(map);
351                                            *new_point.column_mut() =
352                                                new_point.column().saturating_sub(1);
353                                            new_point = map.clip_point(new_point, Bias::Left);
354                                            selection.collapse_to(new_point, SelectionGoal::None);
355                                        }
356                                    }
357                                }
358                            });
359                        });
360                    } else {
361                        editor.insert(&clipboard_text, cx);
362                    }
363                }
364                editor.set_clip_at_line_ends(true, cx);
365            });
366        });
367    });
368}
369
370#[cfg(test)]
371mod test {
372    use indoc::indoc;
373
374    use crate::{
375        state::{
376            Mode::{self, *},
377            Namespace, Operator,
378        },
379        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
380    };
381
382    #[gpui::test]
383    async fn test_h(cx: &mut gpui::TestAppContext) {
384        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
385        cx.assert_all(indoc! {"
386            ˇThe qˇuick
387            ˇbrown"
388        })
389        .await;
390    }
391
392    #[gpui::test]
393    async fn test_backspace(cx: &mut gpui::TestAppContext) {
394        let mut cx = NeovimBackedTestContext::new(cx)
395            .await
396            .binding(["backspace"]);
397        cx.assert_all(indoc! {"
398            ˇThe qˇuick
399            ˇbrown"
400        })
401        .await;
402    }
403
404    #[gpui::test]
405    async fn test_j(cx: &mut gpui::TestAppContext) {
406        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
407        cx.assert_all(indoc! {"
408            ˇThe qˇuick broˇwn
409            ˇfox jumps"
410        })
411        .await;
412    }
413
414    #[gpui::test]
415    async fn test_k(cx: &mut gpui::TestAppContext) {
416        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
417        cx.assert_all(indoc! {"
418            ˇThe qˇuick
419            ˇbrown fˇox jumˇps"
420        })
421        .await;
422    }
423
424    #[gpui::test]
425    async fn test_l(cx: &mut gpui::TestAppContext) {
426        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
427        cx.assert_all(indoc! {"
428            ˇThe qˇuicˇk
429            ˇbrowˇn"})
430            .await;
431    }
432
433    #[gpui::test]
434    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
435        let mut cx = NeovimBackedTestContext::new(cx).await;
436        cx.assert_binding_matches_all(
437            ["$"],
438            indoc! {"
439            ˇThe qˇuicˇk
440            ˇbrowˇn"},
441        )
442        .await;
443        cx.assert_binding_matches_all(
444            ["0"],
445            indoc! {"
446                ˇThe qˇuicˇk
447                ˇbrowˇn"},
448        )
449        .await;
450    }
451
452    #[gpui::test]
453    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
454        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
455
456        cx.assert_all(indoc! {"
457                The ˇquick
458                
459                brown fox jumps
460                overˇ the lazy doˇg"})
461            .await;
462        cx.assert(indoc! {"
463            The quiˇck
464            
465            brown"})
466            .await;
467        cx.assert(indoc! {"
468            The quiˇck
469            
470            "})
471            .await;
472    }
473
474    #[gpui::test]
475    async fn test_w(cx: &mut gpui::TestAppContext) {
476        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
477        cx.assert_all(indoc! {"
478            The ˇquickˇ-ˇbrown
479            ˇ
480            ˇ
481            ˇfox_jumps ˇover
482            ˇthˇe"})
483            .await;
484        let mut cx = cx.binding(["shift-w"]);
485        cx.assert_all(indoc! {"
486            The ˇquickˇ-ˇbrown
487            ˇ
488            ˇ
489            ˇfox_jumps ˇover
490            ˇthˇe"})
491            .await;
492    }
493
494    #[gpui::test]
495    async fn test_e(cx: &mut gpui::TestAppContext) {
496        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
497        cx.assert_all(indoc! {"
498            Thˇe quicˇkˇ-browˇn
499            
500            
501            fox_jumpˇs oveˇr
502            thˇe"})
503            .await;
504        let mut cx = cx.binding(["shift-e"]);
505        cx.assert_all(indoc! {"
506            Thˇe quicˇkˇ-browˇn
507            
508            
509            fox_jumpˇs oveˇr
510            thˇe"})
511            .await;
512    }
513
514    #[gpui::test]
515    async fn test_b(cx: &mut gpui::TestAppContext) {
516        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
517        cx.assert_all(indoc! {"
518            ˇThe ˇquickˇ-ˇbrown
519            ˇ
520            ˇ
521            ˇfox_jumps ˇover
522            ˇthe"})
523            .await;
524        let mut cx = cx.binding(["shift-b"]);
525        cx.assert_all(indoc! {"
526            ˇThe ˇquickˇ-ˇbrown
527            ˇ
528            ˇ
529            ˇfox_jumps ˇover
530            ˇthe"})
531            .await;
532    }
533
534    #[gpui::test]
535    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
536        let mut cx = VimTestContext::new(cx, true).await;
537
538        // Can abort with escape to get back to normal mode
539        cx.simulate_keystroke("g");
540        assert_eq!(cx.mode(), Normal);
541        assert_eq!(
542            cx.active_operator(),
543            Some(Operator::Namespace(Namespace::G))
544        );
545        cx.simulate_keystroke("escape");
546        assert_eq!(cx.mode(), Normal);
547        assert_eq!(cx.active_operator(), None);
548    }
549
550    #[gpui::test]
551    async fn test_gg(cx: &mut gpui::TestAppContext) {
552        let mut cx = NeovimBackedTestContext::new(cx).await;
553        cx.assert_binding_matches_all(
554            ["g", "g"],
555            indoc! {"
556                The qˇuick
557            
558                brown fox jumps
559                over ˇthe laˇzy dog"},
560        )
561        .await;
562        cx.assert_binding_matches(
563            ["g", "g"],
564            indoc! {"
565                
566            
567                brown fox jumps
568                over the laˇzy dog"},
569        )
570        .await;
571        cx.assert_binding_matches(
572            ["2", "g", "g"],
573            indoc! {"
574                ˇ
575                
576                brown fox jumps
577                over the lazydog"},
578        )
579        .await;
580    }
581
582    #[gpui::test]
583    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
584        let mut cx = NeovimBackedTestContext::new(cx).await;
585        cx.assert_binding_matches_all(
586            ["shift-g"],
587            indoc! {"
588                The qˇuick
589                
590                brown fox jumps
591                over ˇthe laˇzy dog"},
592        )
593        .await;
594        cx.assert_binding_matches(
595            ["shift-g"],
596            indoc! {"
597                
598                
599                brown fox jumps
600                over the laˇzy dog"},
601        )
602        .await;
603        cx.assert_binding_matches(
604            ["2", "shift-g"],
605            indoc! {"
606                ˇ
607                
608                brown fox jumps
609                over the lazydog"},
610        )
611        .await;
612    }
613
614    #[gpui::test]
615    async fn test_a(cx: &mut gpui::TestAppContext) {
616        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
617        cx.assert_all("The qˇuicˇk").await;
618    }
619
620    #[gpui::test]
621    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
622        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
623        cx.assert_all(indoc! {"
624            ˇ
625            The qˇuick
626            brown ˇfox "})
627            .await;
628    }
629
630    #[gpui::test]
631    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
632        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
633        cx.assert("The qˇuick").await;
634        cx.assert(" The qˇuick").await;
635        cx.assert("ˇ").await;
636        cx.assert(indoc! {"
637                The qˇuick
638                brown fox"})
639            .await;
640        cx.assert(indoc! {"
641                ˇ
642                The quick"})
643            .await;
644        // Indoc disallows trailing whitspace.
645        cx.assert("   ˇ \nThe quick").await;
646    }
647
648    #[gpui::test]
649    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
650        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
651        cx.assert("The qˇuick").await;
652        cx.assert(" The qˇuick").await;
653        cx.assert("ˇ").await;
654        cx.assert(indoc! {"
655                The qˇuick
656                brown fox"})
657            .await;
658        cx.assert(indoc! {"
659                ˇ
660                The quick"})
661            .await;
662    }
663
664    #[gpui::test]
665    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
666        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
667        cx.assert(indoc! {"
668                The qˇuick
669                brown fox"})
670            .await;
671        cx.assert(indoc! {"
672                The quick
673                ˇ
674                brown fox"})
675            .await;
676    }
677
678    #[gpui::test]
679    async fn test_x(cx: &mut gpui::TestAppContext) {
680        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
681        cx.assert_all("ˇTeˇsˇt").await;
682        cx.assert(indoc! {"
683                Tesˇt
684                test"})
685            .await;
686    }
687
688    #[gpui::test]
689    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
690        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
691        cx.assert_all("ˇTˇeˇsˇt").await;
692        cx.assert(indoc! {"
693                Test
694                ˇtest"})
695            .await;
696    }
697
698    #[gpui::test]
699    async fn test_o(cx: &mut gpui::TestAppContext) {
700        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
701        cx.assert("ˇ").await;
702        cx.assert("The ˇquick").await;
703        cx.assert_all(indoc! {"
704                The qˇuick
705                brown ˇfox
706                jumps ˇover"})
707            .await;
708        cx.assert(indoc! {"
709                The quick
710                ˇ
711                brown fox"})
712            .await;
713        cx.assert(indoc! {"
714                fn test() {
715                    println!(ˇ);
716                }
717            "})
718            .await;
719        cx.assert(indoc! {"
720                fn test(ˇ) {
721                    println!();
722                }"})
723            .await;
724    }
725
726    #[gpui::test]
727    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
728        let cx = NeovimBackedTestContext::new(cx).await;
729        let mut cx = cx.binding(["shift-o"]);
730        cx.assert("ˇ").await;
731        cx.assert("The ˇquick").await;
732        cx.assert_all(indoc! {"
733            The qˇuick
734            brown ˇfox
735            jumps ˇover"})
736            .await;
737        cx.assert(indoc! {"
738            The quick
739            ˇ
740            brown fox"})
741            .await;
742
743        // Our indentation is smarter than vims. So we don't match here
744        cx.assert_manual(
745            indoc! {"
746                fn test()
747                    println!(ˇ);"},
748            Mode::Normal,
749            indoc! {"
750                fn test()
751                    ˇ
752                    println!();"},
753            Mode::Insert,
754        );
755        cx.assert_manual(
756            indoc! {"
757                fn test(ˇ) {
758                    println!();
759                }"},
760            Mode::Normal,
761            indoc! {"
762                ˇ
763                fn test() {
764                    println!();
765                }"},
766            Mode::Insert,
767        );
768    }
769
770    #[gpui::test]
771    async fn test_dd(cx: &mut gpui::TestAppContext) {
772        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
773        cx.assert("ˇ").await;
774        cx.assert("The ˇquick").await;
775        cx.assert_all(indoc! {"
776                The qˇuick
777                brown ˇfox
778                jumps ˇover"})
779            .await;
780        cx.assert_exempted(
781            indoc! {"
782                The quick
783                ˇ
784                brown fox"},
785            ExemptionFeatures::DeletionOnEmptyLine,
786        )
787        .await;
788    }
789
790    #[gpui::test]
791    async fn test_cc(cx: &mut gpui::TestAppContext) {
792        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
793        cx.assert("ˇ").await;
794        cx.assert("The ˇquick").await;
795        cx.assert_all(indoc! {"
796                The quˇick
797                brown ˇfox
798                jumps ˇover"})
799            .await;
800        cx.assert(indoc! {"
801                The quick
802                ˇ
803                brown fox"})
804            .await;
805    }
806
807    #[gpui::test]
808    async fn test_p(cx: &mut gpui::TestAppContext) {
809        let mut cx = NeovimBackedTestContext::new(cx).await;
810        cx.set_shared_state(indoc! {"
811                The quick brown
812                fox juˇmps over
813                the lazy dog"})
814            .await;
815
816        cx.simulate_shared_keystrokes(["d", "d"]).await;
817        cx.assert_state_matches().await;
818
819        cx.simulate_shared_keystroke("p").await;
820        cx.assert_state_matches().await;
821
822        cx.set_shared_state(indoc! {"
823                The quick brown
824                fox ˇjumps over
825                the lazy dog"})
826            .await;
827        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
828        cx.set_shared_state(indoc! {"
829                The quick brown
830                fox jumps oveˇr
831                the lazy dog"})
832            .await;
833        cx.simulate_shared_keystroke("p").await;
834        cx.assert_state_matches().await;
835    }
836
837    #[gpui::test]
838    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
839        let mut cx = NeovimBackedTestContext::new(cx).await;
840
841        for count in 1..=5 {
842            cx.assert_binding_matches_all(
843                [&count.to_string(), "w"],
844                indoc! {"
845                    ˇThe quˇickˇ browˇn
846                    ˇ
847                    ˇfox ˇjumpsˇ-ˇoˇver
848                    ˇthe lazy dog
849                "},
850            )
851            .await;
852        }
853    }
854
855    #[gpui::test]
856    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
857        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
858        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
859    }
860}