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