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