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