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