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