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