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