normal.rs

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