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