normal.rs

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