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