normal.rs

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