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