normal.rs

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