normal.rs

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