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::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 = map
218                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
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, mut cursor, _| {
227                        *cursor.row_mut() -= 1;
228                        *cursor.column_mut() = map.line_len(cursor.row());
229                        (map.clip_point(cursor, Bias::Left), 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                let selection_end_rows: HashSet<u32> = old_selections
244                    .into_iter()
245                    .map(|selection| selection.end.row())
246                    .collect();
247                let edits = selection_end_rows.into_iter().map(|row| {
248                    let (indent, _) = map.line_indent(row);
249                    let end_of_line = map
250                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
251                        .to_point(&map);
252                    let mut new_text = "\n".to_string();
253                    new_text.push_str(&" ".repeat(indent as usize));
254                    (end_of_line..end_of_line, new_text)
255                });
256                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
257                    s.maybe_move_cursors_with(|map, cursor, goal| {
258                        Motion::CurrentLine.move_point(map, cursor, goal, None)
259                    });
260                });
261                editor.edit_with_autoindent(edits, cx);
262            });
263        });
264    });
265}
266
267pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
268    Vim::update(cx, |vim, cx| {
269        vim.update_active_editor(cx, |editor, cx| {
270            editor.transact(cx, |editor, cx| {
271                editor.set_clip_at_line_ends(false, cx);
272                let (map, display_selections) = editor.selections.all_display(cx);
273                // Selections are biased right at the start. So we need to store
274                // anchors that are biased left so that we can restore the selections
275                // after the change
276                let stable_anchors = editor
277                    .selections
278                    .disjoint_anchors()
279                    .into_iter()
280                    .map(|selection| {
281                        let start = selection.start.bias_left(&map.buffer_snapshot);
282                        start..start
283                    })
284                    .collect::<Vec<_>>();
285
286                let edits = display_selections
287                    .into_iter()
288                    .map(|selection| {
289                        let mut range = selection.range();
290                        *range.end.column_mut() += 1;
291                        range.end = map.clip_point(range.end, Bias::Right);
292
293                        (
294                            range.start.to_offset(&map, Bias::Left)
295                                ..range.end.to_offset(&map, Bias::Left),
296                            text.clone(),
297                        )
298                    })
299                    .collect::<Vec<_>>();
300
301                editor.buffer().update(cx, |buffer, cx| {
302                    buffer.edit(edits, None, cx);
303                });
304                editor.set_clip_at_line_ends(true, cx);
305                editor.change_selections(None, cx, |s| {
306                    s.select_anchor_ranges(stable_anchors);
307                });
308            });
309        });
310        vim.pop_operator(cx)
311    });
312}
313
314#[cfg(test)]
315mod test {
316    use gpui::TestAppContext;
317    use indoc::indoc;
318
319    use crate::{
320        state::Mode::{self},
321        test::{ExemptionFeatures, NeovimBackedTestContext},
322    };
323
324    #[gpui::test]
325    async fn test_h(cx: &mut gpui::TestAppContext) {
326        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
327        cx.assert_all(indoc! {"
328            ˇThe qˇuick
329            ˇbrown"
330        })
331        .await;
332    }
333
334    #[gpui::test]
335    async fn test_backspace(cx: &mut gpui::TestAppContext) {
336        let mut cx = NeovimBackedTestContext::new(cx)
337            .await
338            .binding(["backspace"]);
339        cx.assert_all(indoc! {"
340            ˇThe qˇuick
341            ˇbrown"
342        })
343        .await;
344    }
345
346    #[gpui::test]
347    async fn test_j(cx: &mut gpui::TestAppContext) {
348        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
349        cx.assert_all(indoc! {"
350            ˇThe qˇuick broˇwn
351            ˇfox jumps"
352        })
353        .await;
354    }
355
356    #[gpui::test]
357    async fn test_enter(cx: &mut gpui::TestAppContext) {
358        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
359        cx.assert_all(indoc! {"
360            ˇThe qˇuick broˇwn
361            ˇfox jumps"
362        })
363        .await;
364    }
365
366    #[gpui::test]
367    async fn test_k(cx: &mut gpui::TestAppContext) {
368        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
369        cx.assert_all(indoc! {"
370            ˇThe qˇuick
371            ˇbrown fˇox jumˇps"
372        })
373        .await;
374    }
375
376    #[gpui::test]
377    async fn test_l(cx: &mut gpui::TestAppContext) {
378        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
379        cx.assert_all(indoc! {"
380            ˇThe qˇuicˇk
381            ˇbrowˇn"})
382            .await;
383    }
384
385    #[gpui::test]
386    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
387        let mut cx = NeovimBackedTestContext::new(cx).await;
388        cx.assert_binding_matches_all(
389            ["$"],
390            indoc! {"
391            ˇThe qˇuicˇk
392            ˇbrowˇn"},
393        )
394        .await;
395        cx.assert_binding_matches_all(
396            ["0"],
397            indoc! {"
398                ˇThe qˇuicˇk
399                ˇbrowˇn"},
400        )
401        .await;
402    }
403
404    #[gpui::test]
405    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
406        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
407
408        cx.assert_all(indoc! {"
409                The ˇquick
410
411                brown fox jumps
412                overˇ the lazy doˇg"})
413            .await;
414        cx.assert(indoc! {"
415            The quiˇck
416
417            brown"})
418            .await;
419        cx.assert(indoc! {"
420            The quiˇck
421
422            "})
423            .await;
424    }
425
426    #[gpui::test]
427    async fn test_w(cx: &mut gpui::TestAppContext) {
428        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
429        cx.assert_all(indoc! {"
430            The ˇquickˇ-ˇbrown
431            ˇ
432            ˇ
433            ˇfox_jumps ˇover
434            ˇthˇe"})
435            .await;
436        let mut cx = cx.binding(["shift-w"]);
437        cx.assert_all(indoc! {"
438            The ˇquickˇ-ˇbrown
439            ˇ
440            ˇ
441            ˇfox_jumps ˇover
442            ˇthˇe"})
443            .await;
444    }
445
446    #[gpui::test]
447    async fn test_e(cx: &mut gpui::TestAppContext) {
448        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
449        cx.assert_all(indoc! {"
450            Thˇe quicˇkˇ-browˇn
451
452
453            fox_jumpˇs oveˇr
454            thˇe"})
455            .await;
456        let mut cx = cx.binding(["shift-e"]);
457        cx.assert_all(indoc! {"
458            Thˇe quicˇkˇ-browˇn
459
460
461            fox_jumpˇs oveˇr
462            thˇe"})
463            .await;
464    }
465
466    #[gpui::test]
467    async fn test_b(cx: &mut gpui::TestAppContext) {
468        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
469        cx.assert_all(indoc! {"
470            ˇThe ˇquickˇ-ˇbrown
471            ˇ
472            ˇ
473            ˇfox_jumps ˇover
474            ˇthe"})
475            .await;
476        let mut cx = cx.binding(["shift-b"]);
477        cx.assert_all(indoc! {"
478            ˇThe ˇquickˇ-ˇbrown
479            ˇ
480            ˇ
481            ˇfox_jumps ˇover
482            ˇthe"})
483            .await;
484    }
485
486    #[gpui::test]
487    async fn test_gg(cx: &mut gpui::TestAppContext) {
488        let mut cx = NeovimBackedTestContext::new(cx).await;
489        cx.assert_binding_matches_all(
490            ["g", "g"],
491            indoc! {"
492                The qˇuick
493
494                brown fox jumps
495                over ˇthe laˇzy dog"},
496        )
497        .await;
498        cx.assert_binding_matches(
499            ["g", "g"],
500            indoc! {"
501
502
503                brown fox jumps
504                over the laˇzy dog"},
505        )
506        .await;
507        cx.assert_binding_matches(
508            ["2", "g", "g"],
509            indoc! {"
510                ˇ
511
512                brown fox jumps
513                over the lazydog"},
514        )
515        .await;
516    }
517
518    #[gpui::test]
519    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
520        let mut cx = NeovimBackedTestContext::new(cx).await;
521        cx.assert_binding_matches_all(
522            ["shift-g"],
523            indoc! {"
524                The qˇuick
525
526                brown fox jumps
527                over ˇthe laˇzy dog"},
528        )
529        .await;
530        cx.assert_binding_matches(
531            ["shift-g"],
532            indoc! {"
533
534
535                brown fox jumps
536                over the laˇzy dog"},
537        )
538        .await;
539        cx.assert_binding_matches(
540            ["2", "shift-g"],
541            indoc! {"
542                ˇ
543
544                brown fox jumps
545                over the lazydog"},
546        )
547        .await;
548    }
549
550    #[gpui::test]
551    async fn test_a(cx: &mut gpui::TestAppContext) {
552        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
553        cx.assert_all("The qˇuicˇk").await;
554    }
555
556    #[gpui::test]
557    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
558        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
559        cx.assert_all(indoc! {"
560            ˇ
561            The qˇuick
562            brown ˇfox "})
563            .await;
564    }
565
566    #[gpui::test]
567    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
568        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
569        cx.assert("The qˇuick").await;
570        cx.assert(" The qˇuick").await;
571        cx.assert("ˇ").await;
572        cx.assert(indoc! {"
573                The qˇuick
574                brown fox"})
575            .await;
576        cx.assert(indoc! {"
577                ˇ
578                The quick"})
579            .await;
580        // Indoc disallows trailing whitespace.
581        cx.assert("   ˇ \nThe quick").await;
582    }
583
584    #[gpui::test]
585    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
586        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
587        cx.assert("The qˇuick").await;
588        cx.assert(" The qˇuick").await;
589        cx.assert("ˇ").await;
590        cx.assert(indoc! {"
591                The qˇuick
592                brown fox"})
593            .await;
594        cx.assert(indoc! {"
595                ˇ
596                The quick"})
597            .await;
598    }
599
600    #[gpui::test]
601    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
602        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
603        cx.assert(indoc! {"
604                The qˇuick
605                brown fox"})
606            .await;
607        cx.assert(indoc! {"
608                The quick
609                ˇ
610                brown fox"})
611            .await;
612    }
613
614    #[gpui::test]
615    async fn test_x(cx: &mut gpui::TestAppContext) {
616        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
617        cx.assert_all("ˇTeˇsˇt").await;
618        cx.assert(indoc! {"
619                Tesˇt
620                test"})
621            .await;
622    }
623
624    #[gpui::test]
625    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
626        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
627        cx.assert_all("ˇTˇeˇsˇt").await;
628        cx.assert(indoc! {"
629                Test
630                ˇtest"})
631            .await;
632    }
633
634    #[gpui::test]
635    async fn test_o(cx: &mut gpui::TestAppContext) {
636        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
637        cx.assert("ˇ").await;
638        cx.assert("The ˇquick").await;
639        cx.assert_all(indoc! {"
640                The qˇuick
641                brown ˇfox
642                jumps ˇover"})
643            .await;
644        cx.assert(indoc! {"
645                The quick
646                ˇ
647                brown fox"})
648            .await;
649
650        cx.assert_manual(
651            indoc! {"
652                fn test() {
653                    println!(ˇ);
654                }"},
655            Mode::Normal,
656            indoc! {"
657                fn test() {
658                    println!();
659                    ˇ
660                }"},
661            Mode::Insert,
662        );
663
664        cx.assert_manual(
665            indoc! {"
666                fn test(ˇ) {
667                    println!();
668                }"},
669            Mode::Normal,
670            indoc! {"
671                fn test() {
672                    ˇ
673                    println!();
674                }"},
675            Mode::Insert,
676        );
677    }
678
679    #[gpui::test]
680    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
681        let cx = NeovimBackedTestContext::new(cx).await;
682        let mut cx = cx.binding(["shift-o"]);
683        cx.assert("ˇ").await;
684        cx.assert("The ˇquick").await;
685        cx.assert_all(indoc! {"
686            The qˇuick
687            brown ˇfox
688            jumps ˇover"})
689            .await;
690        cx.assert(indoc! {"
691            The quick
692            ˇ
693            brown fox"})
694            .await;
695
696        // Our indentation is smarter than vims. So we don't match here
697        cx.assert_manual(
698            indoc! {"
699                fn test() {
700                    println!(ˇ);
701                }"},
702            Mode::Normal,
703            indoc! {"
704                fn test() {
705                    ˇ
706                    println!();
707                }"},
708            Mode::Insert,
709        );
710        cx.assert_manual(
711            indoc! {"
712                fn test(ˇ) {
713                    println!();
714                }"},
715            Mode::Normal,
716            indoc! {"
717                ˇ
718                fn test() {
719                    println!();
720                }"},
721            Mode::Insert,
722        );
723    }
724
725    #[gpui::test]
726    async fn test_dd(cx: &mut gpui::TestAppContext) {
727        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
728        cx.assert("ˇ").await;
729        cx.assert("The ˇquick").await;
730        cx.assert_all(indoc! {"
731                The qˇuick
732                brown ˇfox
733                jumps ˇover"})
734            .await;
735        cx.assert_exempted(
736            indoc! {"
737                The quick
738                ˇ
739                brown fox"},
740            ExemptionFeatures::DeletionOnEmptyLine,
741        )
742        .await;
743    }
744
745    #[gpui::test]
746    async fn test_cc(cx: &mut gpui::TestAppContext) {
747        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
748        cx.assert("ˇ").await;
749        cx.assert("The ˇquick").await;
750        cx.assert_all(indoc! {"
751                The quˇick
752                brown ˇfox
753                jumps ˇover"})
754            .await;
755        cx.assert(indoc! {"
756                The quick
757                ˇ
758                brown fox"})
759            .await;
760    }
761
762    #[gpui::test]
763    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
764        let mut cx = NeovimBackedTestContext::new(cx).await;
765
766        for count in 1..=5 {
767            cx.assert_binding_matches_all(
768                [&count.to_string(), "w"],
769                indoc! {"
770                    ˇThe quˇickˇ browˇn
771                    ˇ
772                    ˇfox ˇjumpsˇ-ˇoˇver
773                    ˇthe lazy dog
774                "},
775            )
776            .await;
777        }
778    }
779
780    #[gpui::test]
781    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
782        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
783        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
784    }
785
786    #[gpui::test]
787    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
788        let mut cx = NeovimBackedTestContext::new(cx).await;
789        for count in 1..=3 {
790            let test_case = indoc! {"
791                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
792                ˇ    ˇbˇaaˇa ˇbˇbˇb
793                ˇ
794                ˇb
795            "};
796
797            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
798                .await;
799
800            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
801                .await;
802        }
803    }
804
805    #[gpui::test]
806    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
807        let mut cx = NeovimBackedTestContext::new(cx).await;
808        let test_case = indoc! {"
809            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
810            ˇ    ˇbˇaaˇa ˇbˇbˇb
811            ˇ•••
812            ˇb
813            "
814        };
815
816        for count in 1..=3 {
817            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
818                .await;
819
820            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
821                .await;
822        }
823    }
824
825    #[gpui::test]
826    async fn test_percent(cx: &mut TestAppContext) {
827        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
828        cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
829        cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
830            .await;
831        cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
832    }
833}