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