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