normal.rs

   1mod case;
   2mod change;
   3mod delete;
   4mod increment;
   5mod indent;
   6pub(crate) mod mark;
   7mod paste;
   8pub(crate) mod repeat;
   9mod scroll;
  10pub(crate) mod search;
  11pub mod substitute;
  12mod toggle_comments;
  13pub(crate) mod yank;
  14
  15use std::collections::HashMap;
  16use std::sync::Arc;
  17
  18use crate::{
  19    motion::{self, first_non_whitespace, next_line_end, right, Motion},
  20    object::Object,
  21    state::{Mode, Operator},
  22    surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType},
  23    Vim,
  24};
  25use case::{change_case_motion, change_case_object, CaseTarget};
  26use collections::BTreeSet;
  27use editor::scroll::Autoscroll;
  28use editor::Anchor;
  29use editor::Bias;
  30use editor::Editor;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{actions, ViewContext, WindowContext};
  33use language::{Point, SelectionGoal};
  34use log::error;
  35use multi_buffer::MultiBufferRow;
  36use workspace::Workspace;
  37
  38use self::{
  39    case::{change_case, convert_to_lower_case, convert_to_upper_case},
  40    change::{change_motion, change_object},
  41    delete::{delete_motion, delete_object},
  42    indent::{indent_motion, indent_object, IndentDirection},
  43    toggle_comments::{toggle_comments_motion, toggle_comments_object},
  44    yank::{yank_motion, yank_object},
  45};
  46
  47actions!(
  48    vim,
  49    [
  50        InsertAfter,
  51        InsertBefore,
  52        InsertFirstNonWhitespace,
  53        InsertEndOfLine,
  54        InsertLineAbove,
  55        InsertLineBelow,
  56        InsertAtPrevious,
  57        DeleteLeft,
  58        DeleteRight,
  59        ChangeToEndOfLine,
  60        DeleteToEndOfLine,
  61        Yank,
  62        YankLine,
  63        YankToEndOfLine,
  64        ChangeCase,
  65        ConvertToUpperCase,
  66        ConvertToLowerCase,
  67        JoinLines,
  68        Indent,
  69        Outdent,
  70        ToggleComments,
  71        Undo,
  72        Redo,
  73    ]
  74);
  75
  76pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
  77    workspace.register_action(insert_after);
  78    workspace.register_action(insert_before);
  79    workspace.register_action(insert_first_non_whitespace);
  80    workspace.register_action(insert_end_of_line);
  81    workspace.register_action(insert_line_above);
  82    workspace.register_action(insert_line_below);
  83    workspace.register_action(insert_at_previous);
  84    workspace.register_action(change_case);
  85    workspace.register_action(convert_to_upper_case);
  86    workspace.register_action(convert_to_lower_case);
  87    workspace.register_action(yank_line);
  88    workspace.register_action(yank_to_end_of_line);
  89    workspace.register_action(toggle_comments);
  90
  91    workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
  92        Vim::update(cx, |vim, cx| {
  93            vim.record_current_action(cx);
  94            let times = vim.take_count(cx);
  95            delete_motion(vim, Motion::Left, times, cx);
  96        })
  97    });
  98    workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
  99        Vim::update(cx, |vim, cx| {
 100            vim.record_current_action(cx);
 101            let times = vim.take_count(cx);
 102            delete_motion(vim, Motion::Right, times, cx);
 103        })
 104    });
 105    workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
 106        Vim::update(cx, |vim, cx| {
 107            vim.start_recording(cx);
 108            let times = vim.take_count(cx);
 109            change_motion(
 110                vim,
 111                Motion::EndOfLine {
 112                    display_lines: false,
 113                },
 114                times,
 115                cx,
 116            );
 117        })
 118    });
 119    workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
 120        Vim::update(cx, |vim, cx| {
 121            vim.record_current_action(cx);
 122            let times = vim.take_count(cx);
 123            delete_motion(
 124                vim,
 125                Motion::EndOfLine {
 126                    display_lines: false,
 127                },
 128                times,
 129                cx,
 130            );
 131        })
 132    });
 133    workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
 134        Vim::update(cx, |vim, cx| {
 135            vim.record_current_action(cx);
 136            let mut times = vim.take_count(cx).unwrap_or(1);
 137            if vim.state().mode.is_visual() {
 138                times = 1;
 139            } else if times > 1 {
 140                // 2J joins two lines together (same as J or 1J)
 141                times -= 1;
 142            }
 143
 144            vim.update_active_editor(cx, |_, editor, cx| {
 145                editor.transact(cx, |editor, cx| {
 146                    for _ in 0..times {
 147                        editor.join_lines(&Default::default(), cx)
 148                    }
 149                })
 150            });
 151            if vim.state().mode.is_visual() {
 152                vim.switch_mode(Mode::Normal, false, cx)
 153            }
 154        });
 155    });
 156
 157    workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
 158        Vim::update(cx, |vim, cx| {
 159            vim.record_current_action(cx);
 160            vim.update_active_editor(cx, |_, editor, cx| {
 161                editor.transact(cx, |editor, cx| {
 162                    let mut original_positions = save_selection_starts(editor, cx);
 163                    editor.indent(&Default::default(), cx);
 164                    restore_selection_cursors(editor, cx, &mut original_positions);
 165                });
 166            });
 167            if vim.state().mode.is_visual() {
 168                vim.switch_mode(Mode::Normal, false, cx)
 169            }
 170        });
 171    });
 172
 173    workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
 174        Vim::update(cx, |vim, cx| {
 175            vim.record_current_action(cx);
 176            vim.update_active_editor(cx, |_, editor, cx| {
 177                editor.transact(cx, |editor, cx| {
 178                    let mut original_positions = save_selection_starts(editor, cx);
 179                    editor.outdent(&Default::default(), cx);
 180                    restore_selection_cursors(editor, cx, &mut original_positions);
 181                });
 182            });
 183            if vim.state().mode.is_visual() {
 184                vim.switch_mode(Mode::Normal, false, cx)
 185            }
 186        });
 187    });
 188
 189    workspace.register_action(|_: &mut Workspace, _: &Undo, cx| {
 190        Vim::update(cx, |vim, cx| {
 191            let times = vim.take_count(cx);
 192            vim.update_active_editor(cx, |_, editor, cx| {
 193                for _ in 0..times.unwrap_or(1) {
 194                    editor.undo(&editor::actions::Undo, cx);
 195                }
 196            });
 197        })
 198    });
 199    workspace.register_action(|_: &mut Workspace, _: &Redo, cx| {
 200        Vim::update(cx, |vim, cx| {
 201            let times = vim.take_count(cx);
 202            vim.update_active_editor(cx, |_, editor, cx| {
 203                for _ in 0..times.unwrap_or(1) {
 204                    editor.redo(&editor::actions::Redo, cx);
 205                }
 206            });
 207        })
 208    });
 209
 210    paste::register(workspace, cx);
 211    repeat::register(workspace, cx);
 212    scroll::register(workspace, cx);
 213    search::register(workspace, cx);
 214    substitute::register(workspace, cx);
 215    increment::register(workspace, cx);
 216}
 217
 218pub fn normal_motion(
 219    motion: Motion,
 220    operator: Option<Operator>,
 221    times: Option<usize>,
 222    cx: &mut WindowContext,
 223) {
 224    Vim::update(cx, |vim, cx| {
 225        match operator {
 226            None => move_cursor(vim, motion, times, cx),
 227            Some(Operator::Change) => change_motion(vim, motion, times, cx),
 228            Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
 229            Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
 230            Some(Operator::AddSurrounds { target: None }) => {}
 231            Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx),
 232            Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx),
 233            Some(Operator::Lowercase) => {
 234                change_case_motion(vim, motion, times, CaseTarget::Lowercase, cx)
 235            }
 236            Some(Operator::Uppercase) => {
 237                change_case_motion(vim, motion, times, CaseTarget::Uppercase, cx)
 238            }
 239            Some(Operator::OppositeCase) => {
 240                change_case_motion(vim, motion, times, CaseTarget::OppositeCase, cx)
 241            }
 242            Some(Operator::ToggleComments) => toggle_comments_motion(vim, motion, times, cx),
 243            Some(operator) => {
 244                // Can't do anything for text objects, Ignoring
 245                error!("Unexpected normal mode motion operator: {:?}", operator)
 246            }
 247        }
 248    });
 249}
 250
 251pub fn normal_object(object: Object, cx: &mut WindowContext) {
 252    Vim::update(cx, |vim, cx| {
 253        let mut waiting_operator: Option<Operator> = None;
 254        match vim.maybe_pop_operator() {
 255            Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
 256                Some(Operator::Change) => change_object(vim, object, around, cx),
 257                Some(Operator::Delete) => delete_object(vim, object, around, cx),
 258                Some(Operator::Yank) => yank_object(vim, object, around, cx),
 259                Some(Operator::Indent) => {
 260                    indent_object(vim, object, around, IndentDirection::In, cx)
 261                }
 262                Some(Operator::Outdent) => {
 263                    indent_object(vim, object, around, IndentDirection::Out, cx)
 264                }
 265                Some(Operator::Lowercase) => {
 266                    change_case_object(vim, object, around, CaseTarget::Lowercase, cx)
 267                }
 268                Some(Operator::Uppercase) => {
 269                    change_case_object(vim, object, around, CaseTarget::Uppercase, cx)
 270                }
 271                Some(Operator::OppositeCase) => {
 272                    change_case_object(vim, object, around, CaseTarget::OppositeCase, cx)
 273                }
 274                Some(Operator::AddSurrounds { target: None }) => {
 275                    waiting_operator = Some(Operator::AddSurrounds {
 276                        target: Some(SurroundsType::Object(object)),
 277                    });
 278                }
 279                Some(Operator::ToggleComments) => toggle_comments_object(vim, object, around, cx),
 280                _ => {
 281                    // Can't do anything for namespace operators. Ignoring
 282                }
 283            },
 284            Some(Operator::DeleteSurrounds) => {
 285                waiting_operator = Some(Operator::DeleteSurrounds);
 286            }
 287            Some(Operator::ChangeSurrounds { target: None }) => {
 288                if check_and_move_to_valid_bracket_pair(vim, object, cx) {
 289                    waiting_operator = Some(Operator::ChangeSurrounds {
 290                        target: Some(object),
 291                    });
 292                }
 293            }
 294            _ => {
 295                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 296            }
 297        }
 298        vim.clear_operator(cx);
 299        if let Some(operator) = waiting_operator {
 300            vim.push_operator(operator, cx);
 301        }
 302    });
 303}
 304
 305pub(crate) fn move_cursor(
 306    vim: &mut Vim,
 307    motion: Motion,
 308    times: Option<usize>,
 309    cx: &mut WindowContext,
 310) {
 311    vim.update_active_editor(cx, |_, editor, cx| {
 312        let text_layout_details = editor.text_layout_details(cx);
 313        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 314            s.move_cursors_with(|map, cursor, goal| {
 315                motion
 316                    .move_point(map, cursor, goal, times, &text_layout_details)
 317                    .unwrap_or((cursor, goal))
 318            })
 319        })
 320    });
 321}
 322
 323fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
 324    Vim::update(cx, |vim, cx| {
 325        vim.start_recording(cx);
 326        vim.switch_mode(Mode::Insert, false, cx);
 327        vim.update_active_editor(cx, |_, editor, cx| {
 328            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 329                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 330            });
 331        });
 332    });
 333}
 334
 335fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
 336    Vim::update(cx, |vim, cx| {
 337        vim.start_recording(cx);
 338        vim.switch_mode(Mode::Insert, false, cx);
 339    });
 340}
 341
 342fn insert_first_non_whitespace(
 343    _: &mut Workspace,
 344    _: &InsertFirstNonWhitespace,
 345    cx: &mut ViewContext<Workspace>,
 346) {
 347    Vim::update(cx, |vim, cx| {
 348        vim.start_recording(cx);
 349        vim.switch_mode(Mode::Insert, false, cx);
 350        vim.update_active_editor(cx, |_, editor, cx| {
 351            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 352                s.move_cursors_with(|map, cursor, _| {
 353                    (
 354                        first_non_whitespace(map, false, cursor),
 355                        SelectionGoal::None,
 356                    )
 357                });
 358            });
 359        });
 360    });
 361}
 362
 363fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
 364    Vim::update(cx, |vim, cx| {
 365        vim.start_recording(cx);
 366        vim.switch_mode(Mode::Insert, false, cx);
 367        vim.update_active_editor(cx, |_, editor, cx| {
 368            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 369                s.move_cursors_with(|map, cursor, _| {
 370                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 371                });
 372            });
 373        });
 374    });
 375}
 376
 377fn insert_at_previous(_: &mut Workspace, _: &InsertAtPrevious, cx: &mut ViewContext<Workspace>) {
 378    Vim::update(cx, |vim, cx| {
 379        vim.start_recording(cx);
 380        vim.switch_mode(Mode::Insert, false, cx);
 381        vim.update_active_editor(cx, |vim, editor, cx| {
 382            if let Some(marks) = vim.state().marks.get("^") {
 383                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 384                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 385                });
 386            }
 387        });
 388    });
 389}
 390
 391fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
 392    Vim::update(cx, |vim, cx| {
 393        vim.start_recording(cx);
 394        vim.switch_mode(Mode::Insert, false, cx);
 395        vim.update_active_editor(cx, |_, editor, cx| {
 396            editor.transact(cx, |editor, cx| {
 397                let selections = editor.selections.all::<Point>(cx);
 398                let snapshot = editor.buffer().read(cx).snapshot(cx);
 399
 400                let selection_start_rows: BTreeSet<u32> = selections
 401                    .into_iter()
 402                    .map(|selection| selection.start.row)
 403                    .collect();
 404                let edits = selection_start_rows.into_iter().map(|row| {
 405                    let indent = snapshot
 406                        .indent_size_for_line(MultiBufferRow(row))
 407                        .chars()
 408                        .collect::<String>();
 409                    let start_of_line = Point::new(row, 0);
 410                    (start_of_line..start_of_line, indent + "\n")
 411                });
 412                editor.edit_with_autoindent(edits, cx);
 413                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 414                    s.move_cursors_with(|map, cursor, _| {
 415                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 416                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 417                        (insert_point, SelectionGoal::None)
 418                    });
 419                });
 420            });
 421        });
 422    });
 423}
 424
 425fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
 426    Vim::update(cx, |vim, cx| {
 427        vim.start_recording(cx);
 428        vim.switch_mode(Mode::Insert, false, cx);
 429        vim.update_active_editor(cx, |_, editor, cx| {
 430            let text_layout_details = editor.text_layout_details(cx);
 431            editor.transact(cx, |editor, cx| {
 432                let selections = editor.selections.all::<Point>(cx);
 433                let snapshot = editor.buffer().read(cx).snapshot(cx);
 434
 435                let selection_end_rows: BTreeSet<u32> = selections
 436                    .into_iter()
 437                    .map(|selection| selection.end.row)
 438                    .collect();
 439                let edits = selection_end_rows.into_iter().map(|row| {
 440                    let indent = snapshot
 441                        .indent_size_for_line(MultiBufferRow(row))
 442                        .chars()
 443                        .collect::<String>();
 444                    let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 445                    (end_of_line..end_of_line, "\n".to_string() + &indent)
 446                });
 447                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 448                    s.maybe_move_cursors_with(|map, cursor, goal| {
 449                        Motion::CurrentLine.move_point(
 450                            map,
 451                            cursor,
 452                            goal,
 453                            None,
 454                            &text_layout_details,
 455                        )
 456                    });
 457                });
 458                editor.edit_with_autoindent(edits, cx);
 459            });
 460        });
 461    });
 462}
 463
 464fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
 465    Vim::update(cx, |vim, cx| {
 466        let count = vim.take_count(cx);
 467        yank_motion(vim, motion::Motion::CurrentLine, count, cx)
 468    })
 469}
 470
 471fn yank_to_end_of_line(_: &mut Workspace, _: &YankToEndOfLine, cx: &mut ViewContext<Workspace>) {
 472    Vim::update(cx, |vim, cx| {
 473        vim.record_current_action(cx);
 474        let count = vim.take_count(cx);
 475        yank_motion(
 476            vim,
 477            motion::Motion::EndOfLine {
 478                display_lines: false,
 479            },
 480            count,
 481            cx,
 482        )
 483    })
 484}
 485
 486fn toggle_comments(_: &mut Workspace, _: &ToggleComments, cx: &mut ViewContext<Workspace>) {
 487    Vim::update(cx, |vim, cx| {
 488        vim.record_current_action(cx);
 489        vim.update_active_editor(cx, |_, editor, cx| {
 490            editor.transact(cx, |editor, cx| {
 491                let mut original_positions = save_selection_starts(editor, cx);
 492                editor.toggle_comments(&Default::default(), cx);
 493                restore_selection_cursors(editor, cx, &mut original_positions);
 494            });
 495        });
 496        if vim.state().mode.is_visual() {
 497            vim.switch_mode(Mode::Normal, false, cx)
 498        }
 499    });
 500}
 501
 502fn save_selection_starts(editor: &Editor, cx: &mut ViewContext<Editor>) -> HashMap<usize, Anchor> {
 503    let (map, selections) = editor.selections.all_display(cx);
 504    selections
 505        .iter()
 506        .map(|selection| {
 507            (
 508                selection.id,
 509                map.display_point_to_anchor(selection.start, Bias::Right),
 510            )
 511        })
 512        .collect::<HashMap<_, _>>()
 513}
 514
 515fn restore_selection_cursors(
 516    editor: &mut Editor,
 517    cx: &mut ViewContext<Editor>,
 518    positions: &mut HashMap<usize, Anchor>,
 519) {
 520    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 521        s.move_with(|map, selection| {
 522            if let Some(anchor) = positions.remove(&selection.id) {
 523                selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 524            }
 525        });
 526    });
 527}
 528
 529pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
 530    Vim::update(cx, |vim, cx| {
 531        let count = vim.take_count(cx).unwrap_or(1);
 532        vim.stop_recording();
 533        vim.update_active_editor(cx, |_, editor, cx| {
 534            editor.transact(cx, |editor, cx| {
 535                editor.set_clip_at_line_ends(false, cx);
 536                let (map, display_selections) = editor.selections.all_display(cx);
 537
 538                let mut edits = Vec::new();
 539                for selection in display_selections {
 540                    let mut range = selection.range();
 541                    for _ in 0..count {
 542                        let new_point = movement::saturating_right(&map, range.end);
 543                        if range.end == new_point {
 544                            return;
 545                        }
 546                        range.end = new_point;
 547                    }
 548
 549                    edits.push((
 550                        range.start.to_offset(&map, Bias::Left)
 551                            ..range.end.to_offset(&map, Bias::Left),
 552                        text.repeat(count),
 553                    ))
 554                }
 555
 556                editor.buffer().update(cx, |buffer, cx| {
 557                    buffer.edit(edits, None, cx);
 558                });
 559                editor.set_clip_at_line_ends(true, cx);
 560                editor.change_selections(None, cx, |s| {
 561                    s.move_with(|map, selection| {
 562                        let point = movement::saturating_left(map, selection.head());
 563                        selection.collapse_to(point, SelectionGoal::None)
 564                    });
 565                });
 566            });
 567        });
 568        vim.pop_operator(cx)
 569    });
 570}
 571
 572#[cfg(test)]
 573mod test {
 574    use gpui::{KeyBinding, TestAppContext};
 575    use indoc::indoc;
 576    use settings::SettingsStore;
 577
 578    use crate::{
 579        motion,
 580        state::Mode::{self},
 581        test::{NeovimBackedTestContext, VimTestContext},
 582        VimSettings,
 583    };
 584
 585    #[gpui::test]
 586    async fn test_h(cx: &mut gpui::TestAppContext) {
 587        let mut cx = NeovimBackedTestContext::new(cx).await;
 588        cx.simulate_at_each_offset(
 589            "h",
 590            indoc! {"
 591            ˇThe qˇuick
 592            ˇbrown"
 593            },
 594        )
 595        .await
 596        .assert_matches();
 597    }
 598
 599    #[gpui::test]
 600    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 601        let mut cx = NeovimBackedTestContext::new(cx).await;
 602        cx.simulate_at_each_offset(
 603            "backspace",
 604            indoc! {"
 605            ˇThe qˇuick
 606            ˇbrown"
 607            },
 608        )
 609        .await
 610        .assert_matches();
 611    }
 612
 613    #[gpui::test]
 614    async fn test_j(cx: &mut gpui::TestAppContext) {
 615        let mut cx = NeovimBackedTestContext::new(cx).await;
 616
 617        cx.set_shared_state(indoc! {"
 618            aaˇaa
 619            😃😃"
 620        })
 621        .await;
 622        cx.simulate_shared_keystrokes("j").await;
 623        cx.shared_state().await.assert_eq(indoc! {"
 624            aaaa
 625            😃ˇ😃"
 626        });
 627
 628        cx.simulate_at_each_offset(
 629            "j",
 630            indoc! {"
 631                ˇThe qˇuick broˇwn
 632                ˇfox jumps"
 633            },
 634        )
 635        .await
 636        .assert_matches();
 637    }
 638
 639    #[gpui::test]
 640    async fn test_enter(cx: &mut gpui::TestAppContext) {
 641        let mut cx = NeovimBackedTestContext::new(cx).await;
 642        cx.simulate_at_each_offset(
 643            "enter",
 644            indoc! {"
 645            ˇThe qˇuick broˇwn
 646            ˇfox jumps"
 647            },
 648        )
 649        .await
 650        .assert_matches();
 651    }
 652
 653    #[gpui::test]
 654    async fn test_k(cx: &mut gpui::TestAppContext) {
 655        let mut cx = NeovimBackedTestContext::new(cx).await;
 656        cx.simulate_at_each_offset(
 657            "k",
 658            indoc! {"
 659            ˇThe qˇuick
 660            ˇbrown fˇox jumˇps"
 661            },
 662        )
 663        .await
 664        .assert_matches();
 665    }
 666
 667    #[gpui::test]
 668    async fn test_l(cx: &mut gpui::TestAppContext) {
 669        let mut cx = NeovimBackedTestContext::new(cx).await;
 670        cx.simulate_at_each_offset(
 671            "l",
 672            indoc! {"
 673            ˇThe qˇuicˇk
 674            ˇbrowˇn"},
 675        )
 676        .await
 677        .assert_matches();
 678    }
 679
 680    #[gpui::test]
 681    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 682        let mut cx = NeovimBackedTestContext::new(cx).await;
 683        cx.simulate_at_each_offset(
 684            "$",
 685            indoc! {"
 686            ˇThe qˇuicˇk
 687            ˇbrowˇn"},
 688        )
 689        .await
 690        .assert_matches();
 691        cx.simulate_at_each_offset(
 692            "0",
 693            indoc! {"
 694                ˇThe qˇuicˇk
 695                ˇbrowˇn"},
 696        )
 697        .await
 698        .assert_matches();
 699    }
 700
 701    #[gpui::test]
 702    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 703        let mut cx = NeovimBackedTestContext::new(cx).await;
 704
 705        cx.simulate_at_each_offset(
 706            "shift-g",
 707            indoc! {"
 708                The ˇquick
 709
 710                brown fox jumps
 711                overˇ the lazy doˇg"},
 712        )
 713        .await
 714        .assert_matches();
 715        cx.simulate(
 716            "shift-g",
 717            indoc! {"
 718            The quiˇck
 719
 720            brown"},
 721        )
 722        .await
 723        .assert_matches();
 724        cx.simulate(
 725            "shift-g",
 726            indoc! {"
 727            The quiˇck
 728
 729            "},
 730        )
 731        .await
 732        .assert_matches();
 733    }
 734
 735    #[gpui::test]
 736    async fn test_w(cx: &mut gpui::TestAppContext) {
 737        let mut cx = NeovimBackedTestContext::new(cx).await;
 738        cx.simulate_at_each_offset(
 739            "w",
 740            indoc! {"
 741            The ˇquickˇ-ˇbrown
 742            ˇ
 743            ˇ
 744            ˇfox_jumps ˇover
 745            ˇthˇe"},
 746        )
 747        .await
 748        .assert_matches();
 749        cx.simulate_at_each_offset(
 750            "shift-w",
 751            indoc! {"
 752            The ˇquickˇ-ˇbrown
 753            ˇ
 754            ˇ
 755            ˇfox_jumps ˇover
 756            ˇthˇe"},
 757        )
 758        .await
 759        .assert_matches();
 760    }
 761
 762    #[gpui::test]
 763    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 764        let mut cx = NeovimBackedTestContext::new(cx).await;
 765        cx.simulate_at_each_offset(
 766            "e",
 767            indoc! {"
 768            Thˇe quicˇkˇ-browˇn
 769
 770
 771            fox_jumpˇs oveˇr
 772            thˇe"},
 773        )
 774        .await
 775        .assert_matches();
 776        cx.simulate_at_each_offset(
 777            "shift-e",
 778            indoc! {"
 779            Thˇe quicˇkˇ-browˇn
 780
 781
 782            fox_jumpˇs oveˇr
 783            thˇe"},
 784        )
 785        .await
 786        .assert_matches();
 787    }
 788
 789    #[gpui::test]
 790    async fn test_b(cx: &mut gpui::TestAppContext) {
 791        let mut cx = NeovimBackedTestContext::new(cx).await;
 792        cx.simulate_at_each_offset(
 793            "b",
 794            indoc! {"
 795            ˇThe ˇquickˇ-ˇbrown
 796            ˇ
 797            ˇ
 798            ˇfox_jumps ˇover
 799            ˇthe"},
 800        )
 801        .await
 802        .assert_matches();
 803        cx.simulate_at_each_offset(
 804            "shift-b",
 805            indoc! {"
 806            ˇThe ˇquickˇ-ˇbrown
 807            ˇ
 808            ˇ
 809            ˇfox_jumps ˇover
 810            ˇthe"},
 811        )
 812        .await
 813        .assert_matches();
 814    }
 815
 816    #[gpui::test]
 817    async fn test_gg(cx: &mut gpui::TestAppContext) {
 818        let mut cx = NeovimBackedTestContext::new(cx).await;
 819        cx.simulate_at_each_offset(
 820            "g g",
 821            indoc! {"
 822                The qˇuick
 823
 824                brown fox jumps
 825                over ˇthe laˇzy dog"},
 826        )
 827        .await
 828        .assert_matches();
 829        cx.simulate(
 830            "g g",
 831            indoc! {"
 832
 833
 834                brown fox jumps
 835                over the laˇzy dog"},
 836        )
 837        .await
 838        .assert_matches();
 839        cx.simulate(
 840            "2 g g",
 841            indoc! {"
 842                ˇ
 843
 844                brown fox jumps
 845                over the lazydog"},
 846        )
 847        .await
 848        .assert_matches();
 849    }
 850
 851    #[gpui::test]
 852    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 853        let mut cx = NeovimBackedTestContext::new(cx).await;
 854        cx.simulate_at_each_offset(
 855            "shift-g",
 856            indoc! {"
 857                The qˇuick
 858
 859                brown fox jumps
 860                over ˇthe laˇzy dog"},
 861        )
 862        .await
 863        .assert_matches();
 864        cx.simulate(
 865            "shift-g",
 866            indoc! {"
 867
 868
 869                brown fox jumps
 870                over the laˇzy dog"},
 871        )
 872        .await
 873        .assert_matches();
 874        cx.simulate(
 875            "2 shift-g",
 876            indoc! {"
 877                ˇ
 878
 879                brown fox jumps
 880                over the lazydog"},
 881        )
 882        .await
 883        .assert_matches();
 884    }
 885
 886    #[gpui::test]
 887    async fn test_a(cx: &mut gpui::TestAppContext) {
 888        let mut cx = NeovimBackedTestContext::new(cx).await;
 889        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 890            .await
 891            .assert_matches();
 892    }
 893
 894    #[gpui::test]
 895    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 896        let mut cx = NeovimBackedTestContext::new(cx).await;
 897        cx.simulate_at_each_offset(
 898            "shift-a",
 899            indoc! {"
 900            ˇ
 901            The qˇuick
 902            brown ˇfox "},
 903        )
 904        .await
 905        .assert_matches();
 906    }
 907
 908    #[gpui::test]
 909    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 910        let mut cx = NeovimBackedTestContext::new(cx).await;
 911        cx.simulate("^", "The qˇuick").await.assert_matches();
 912        cx.simulate("^", " The qˇuick").await.assert_matches();
 913        cx.simulate("^", "ˇ").await.assert_matches();
 914        cx.simulate(
 915            "^",
 916            indoc! {"
 917                The qˇuick
 918                brown fox"},
 919        )
 920        .await
 921        .assert_matches();
 922        cx.simulate(
 923            "^",
 924            indoc! {"
 925                ˇ
 926                The quick"},
 927        )
 928        .await
 929        .assert_matches();
 930        // Indoc disallows trailing whitespace.
 931        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
 932    }
 933
 934    #[gpui::test]
 935    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 936        let mut cx = NeovimBackedTestContext::new(cx).await;
 937        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
 938        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
 939        cx.simulate("shift-i", "ˇ").await.assert_matches();
 940        cx.simulate(
 941            "shift-i",
 942            indoc! {"
 943                The qˇuick
 944                brown fox"},
 945        )
 946        .await
 947        .assert_matches();
 948        cx.simulate(
 949            "shift-i",
 950            indoc! {"
 951                ˇ
 952                The quick"},
 953        )
 954        .await
 955        .assert_matches();
 956    }
 957
 958    #[gpui::test]
 959    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 960        let mut cx = NeovimBackedTestContext::new(cx).await;
 961        cx.simulate(
 962            "shift-d",
 963            indoc! {"
 964                The qˇuick
 965                brown fox"},
 966        )
 967        .await
 968        .assert_matches();
 969        cx.simulate(
 970            "shift-d",
 971            indoc! {"
 972                The quick
 973                ˇ
 974                brown fox"},
 975        )
 976        .await
 977        .assert_matches();
 978    }
 979
 980    #[gpui::test]
 981    async fn test_x(cx: &mut gpui::TestAppContext) {
 982        let mut cx = NeovimBackedTestContext::new(cx).await;
 983        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
 984            .await
 985            .assert_matches();
 986        cx.simulate(
 987            "x",
 988            indoc! {"
 989                Tesˇt
 990                test"},
 991        )
 992        .await
 993        .assert_matches();
 994    }
 995
 996    #[gpui::test]
 997    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 998        let mut cx = NeovimBackedTestContext::new(cx).await;
 999        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1000            .await
1001            .assert_matches();
1002        cx.simulate(
1003            "shift-x",
1004            indoc! {"
1005                Test
1006                ˇtest"},
1007        )
1008        .await
1009        .assert_matches();
1010    }
1011
1012    #[gpui::test]
1013    async fn test_o(cx: &mut gpui::TestAppContext) {
1014        let mut cx = NeovimBackedTestContext::new(cx).await;
1015        cx.simulate("o", "ˇ").await.assert_matches();
1016        cx.simulate("o", "The ˇquick").await.assert_matches();
1017        cx.simulate_at_each_offset(
1018            "o",
1019            indoc! {"
1020                The qˇuick
1021                brown ˇfox
1022                jumps ˇover"},
1023        )
1024        .await
1025        .assert_matches();
1026        cx.simulate(
1027            "o",
1028            indoc! {"
1029                The quick
1030                ˇ
1031                brown fox"},
1032        )
1033        .await
1034        .assert_matches();
1035
1036        cx.assert_binding(
1037            "o",
1038            indoc! {"
1039                fn test() {
1040                    println!(ˇ);
1041                }"},
1042            Mode::Normal,
1043            indoc! {"
1044                fn test() {
1045                    println!();
1046                    ˇ
1047                }"},
1048            Mode::Insert,
1049        );
1050
1051        cx.assert_binding(
1052            "o",
1053            indoc! {"
1054                fn test(ˇ) {
1055                    println!();
1056                }"},
1057            Mode::Normal,
1058            indoc! {"
1059                fn test() {
1060                    ˇ
1061                    println!();
1062                }"},
1063            Mode::Insert,
1064        );
1065    }
1066
1067    #[gpui::test]
1068    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1069        let mut cx = NeovimBackedTestContext::new(cx).await;
1070        cx.simulate("shift-o", "ˇ").await.assert_matches();
1071        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1072        cx.simulate_at_each_offset(
1073            "shift-o",
1074            indoc! {"
1075            The qˇuick
1076            brown ˇfox
1077            jumps ˇover"},
1078        )
1079        .await
1080        .assert_matches();
1081        cx.simulate(
1082            "shift-o",
1083            indoc! {"
1084            The quick
1085            ˇ
1086            brown fox"},
1087        )
1088        .await
1089        .assert_matches();
1090
1091        // Our indentation is smarter than vims. So we don't match here
1092        cx.assert_binding(
1093            "shift-o",
1094            indoc! {"
1095                fn test() {
1096                    println!(ˇ);
1097                }"},
1098            Mode::Normal,
1099            indoc! {"
1100                fn test() {
1101                    ˇ
1102                    println!();
1103                }"},
1104            Mode::Insert,
1105        );
1106        cx.assert_binding(
1107            "shift-o",
1108            indoc! {"
1109                fn test(ˇ) {
1110                    println!();
1111                }"},
1112            Mode::Normal,
1113            indoc! {"
1114                ˇ
1115                fn test() {
1116                    println!();
1117                }"},
1118            Mode::Insert,
1119        );
1120    }
1121
1122    #[gpui::test]
1123    async fn test_dd(cx: &mut gpui::TestAppContext) {
1124        let mut cx = NeovimBackedTestContext::new(cx).await;
1125        cx.simulate("d d", "ˇ").await.assert_matches();
1126        cx.simulate("d d", "The ˇquick").await.assert_matches();
1127        cx.simulate_at_each_offset(
1128            "d d",
1129            indoc! {"
1130            The qˇuick
1131            brown ˇfox
1132            jumps ˇover"},
1133        )
1134        .await
1135        .assert_matches();
1136        cx.simulate(
1137            "d d",
1138            indoc! {"
1139                The quick
1140                ˇ
1141                brown fox"},
1142        )
1143        .await
1144        .assert_matches();
1145    }
1146
1147    #[gpui::test]
1148    async fn test_cc(cx: &mut gpui::TestAppContext) {
1149        let mut cx = NeovimBackedTestContext::new(cx).await;
1150        cx.simulate("c c", "ˇ").await.assert_matches();
1151        cx.simulate("c c", "The ˇquick").await.assert_matches();
1152        cx.simulate_at_each_offset(
1153            "c c",
1154            indoc! {"
1155                The quˇick
1156                brown ˇfox
1157                jumps ˇover"},
1158        )
1159        .await
1160        .assert_matches();
1161        cx.simulate(
1162            "c c",
1163            indoc! {"
1164                The quick
1165                ˇ
1166                brown fox"},
1167        )
1168        .await
1169        .assert_matches();
1170    }
1171
1172    #[gpui::test]
1173    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1174        let mut cx = NeovimBackedTestContext::new(cx).await;
1175
1176        for count in 1..=5 {
1177            cx.simulate_at_each_offset(
1178                &format!("{count} w"),
1179                indoc! {"
1180                    ˇThe quˇickˇ browˇn
1181                    ˇ
1182                    ˇfox ˇjumpsˇ-ˇoˇver
1183                    ˇthe lazy dog
1184                "},
1185            )
1186            .await
1187            .assert_matches();
1188        }
1189    }
1190
1191    #[gpui::test]
1192    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1193        let mut cx = NeovimBackedTestContext::new(cx).await;
1194        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1195            .await
1196            .assert_matches();
1197    }
1198
1199    #[gpui::test]
1200    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1201        let mut cx = NeovimBackedTestContext::new(cx).await;
1202
1203        for count in 1..=3 {
1204            let test_case = indoc! {"
1205                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1206                ˇ    ˇbˇaaˇa ˇbˇbˇb
1207                ˇ
1208                ˇb
1209            "};
1210
1211            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1212                .await
1213                .assert_matches();
1214
1215            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1216                .await
1217                .assert_matches();
1218        }
1219    }
1220
1221    #[gpui::test]
1222    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1223        let mut cx = NeovimBackedTestContext::new(cx).await;
1224        let test_case = indoc! {"
1225            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1226            ˇ    ˇbˇaaˇa ˇbˇbˇb
1227            ˇ•••
1228            ˇb
1229            "
1230        };
1231
1232        for count in 1..=3 {
1233            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1234                .await
1235                .assert_matches();
1236
1237            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1238                .await
1239                .assert_matches();
1240        }
1241    }
1242
1243    #[gpui::test]
1244    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1245        let mut cx = VimTestContext::new(cx, true).await;
1246        cx.update_global(|store: &mut SettingsStore, cx| {
1247            store.update_user_settings::<VimSettings>(cx, |s| {
1248                s.use_multiline_find = Some(true);
1249            });
1250        });
1251
1252        cx.assert_binding(
1253            "f l",
1254            indoc! {"
1255            ˇfunction print() {
1256                console.log('ok')
1257            }
1258            "},
1259            Mode::Normal,
1260            indoc! {"
1261            function print() {
1262                consoˇle.log('ok')
1263            }
1264            "},
1265            Mode::Normal,
1266        );
1267
1268        cx.assert_binding(
1269            "t l",
1270            indoc! {"
1271            ˇfunction print() {
1272                console.log('ok')
1273            }
1274            "},
1275            Mode::Normal,
1276            indoc! {"
1277            function print() {
1278                consˇole.log('ok')
1279            }
1280            "},
1281            Mode::Normal,
1282        );
1283    }
1284
1285    #[gpui::test]
1286    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1287        let mut cx = VimTestContext::new(cx, true).await;
1288        cx.update_global(|store: &mut SettingsStore, cx| {
1289            store.update_user_settings::<VimSettings>(cx, |s| {
1290                s.use_multiline_find = Some(true);
1291            });
1292        });
1293
1294        cx.assert_binding(
1295            "shift-f p",
1296            indoc! {"
1297            function print() {
1298                console.ˇlog('ok')
1299            }
1300            "},
1301            Mode::Normal,
1302            indoc! {"
1303            function ˇprint() {
1304                console.log('ok')
1305            }
1306            "},
1307            Mode::Normal,
1308        );
1309
1310        cx.assert_binding(
1311            "shift-t p",
1312            indoc! {"
1313            function print() {
1314                console.ˇlog('ok')
1315            }
1316            "},
1317            Mode::Normal,
1318            indoc! {"
1319            function pˇrint() {
1320                console.log('ok')
1321            }
1322            "},
1323            Mode::Normal,
1324        );
1325    }
1326
1327    #[gpui::test]
1328    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1329        let mut cx = VimTestContext::new(cx, true).await;
1330        cx.update_global(|store: &mut SettingsStore, cx| {
1331            store.update_user_settings::<VimSettings>(cx, |s| {
1332                s.use_smartcase_find = Some(true);
1333            });
1334        });
1335
1336        cx.assert_binding(
1337            "f p",
1338            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1339            Mode::Normal,
1340            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1341            Mode::Normal,
1342        );
1343
1344        cx.assert_binding(
1345            "shift-f p",
1346            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1347            Mode::Normal,
1348            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1349            Mode::Normal,
1350        );
1351
1352        cx.assert_binding(
1353            "t p",
1354            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1355            Mode::Normal,
1356            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1357            Mode::Normal,
1358        );
1359
1360        cx.assert_binding(
1361            "shift-t p",
1362            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1363            Mode::Normal,
1364            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1365            Mode::Normal,
1366        );
1367    }
1368
1369    #[gpui::test]
1370    async fn test_percent(cx: &mut TestAppContext) {
1371        let mut cx = NeovimBackedTestContext::new(cx).await;
1372        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1373            .await
1374            .assert_matches();
1375        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1376            .await
1377            .assert_matches();
1378        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1379            .await
1380            .assert_matches();
1381    }
1382
1383    #[gpui::test]
1384    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1385        let mut cx = NeovimBackedTestContext::new(cx).await;
1386
1387        // goes to current line end
1388        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1389        cx.simulate_shared_keystrokes("$").await;
1390        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1391
1392        // goes to next line end
1393        cx.simulate_shared_keystrokes("2 $").await;
1394        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1395
1396        // try to exceed the final line.
1397        cx.simulate_shared_keystrokes("4 $").await;
1398        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1399    }
1400
1401    #[gpui::test]
1402    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1403        let mut cx = VimTestContext::new(cx, true).await;
1404        cx.update(|cx| {
1405            cx.bind_keys(vec![
1406                KeyBinding::new(
1407                    "w",
1408                    motion::NextSubwordStart {
1409                        ignore_punctuation: false,
1410                    },
1411                    Some("Editor && VimControl && !VimWaiting && !menu"),
1412                ),
1413                KeyBinding::new(
1414                    "b",
1415                    motion::PreviousSubwordStart {
1416                        ignore_punctuation: false,
1417                    },
1418                    Some("Editor && VimControl && !VimWaiting && !menu"),
1419                ),
1420                KeyBinding::new(
1421                    "e",
1422                    motion::NextSubwordEnd {
1423                        ignore_punctuation: false,
1424                    },
1425                    Some("Editor && VimControl && !VimWaiting && !menu"),
1426                ),
1427                KeyBinding::new(
1428                    "g e",
1429                    motion::PreviousSubwordEnd {
1430                        ignore_punctuation: false,
1431                    },
1432                    Some("Editor && VimControl && !VimWaiting && !menu"),
1433                ),
1434            ]);
1435        });
1436
1437        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1438        // Special case: In 'cw', 'w' acts like 'e'
1439        cx.assert_binding(
1440            "c w",
1441            indoc! {"ˇassert_binding"},
1442            Mode::Normal,
1443            indoc! {"ˇ_binding"},
1444            Mode::Insert,
1445        );
1446
1447        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1448
1449        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1450
1451        cx.assert_binding_normal(
1452            "g e",
1453            indoc! {"assert_bindinˇg"},
1454            indoc! {"asserˇt_binding"},
1455        );
1456    }
1457
1458    #[gpui::test]
1459    async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1460        let mut cx = NeovimBackedTestContext::new(cx).await;
1461
1462        cx.set_shared_state("helˇlo\n").await;
1463        cx.simulate_shared_keystrokes("shift-y").await;
1464        cx.shared_clipboard().await.assert_eq("lo");
1465    }
1466
1467    #[gpui::test]
1468    async fn test_r(cx: &mut gpui::TestAppContext) {
1469        let mut cx = NeovimBackedTestContext::new(cx).await;
1470
1471        cx.set_shared_state("ˇhello\n").await;
1472        cx.simulate_shared_keystrokes("r -").await;
1473        cx.shared_state().await.assert_eq("ˇ-ello\n");
1474
1475        cx.set_shared_state("ˇhello\n").await;
1476        cx.simulate_shared_keystrokes("3 r -").await;
1477        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1478
1479        cx.set_shared_state("ˇhello\n").await;
1480        cx.simulate_shared_keystrokes("r - 2 l .").await;
1481        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1482
1483        cx.set_shared_state("ˇhello world\n").await;
1484        cx.simulate_shared_keystrokes("2 r - f w .").await;
1485        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1486
1487        cx.set_shared_state("ˇhello world\n").await;
1488        cx.simulate_shared_keystrokes("2 0 r - ").await;
1489        cx.shared_state().await.assert_eq("ˇhello world\n");
1490    }
1491}