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