normal.rs

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