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