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