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