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