normal.rs

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