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    Vim,
  19    indent::IndentDirection,
  20    motion::{self, Motion, first_non_whitespace, next_line_end, right},
  21    object::Object,
  22    state::{Mark, Mode, Operator},
  23    surrounds::SurroundsType,
  24};
  25use case::CaseTarget;
  26use collections::BTreeSet;
  27use editor::Anchor;
  28use editor::Bias;
  29use editor::Editor;
  30use editor::scroll::Autoscroll;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{Context, Window, actions};
  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        VimSettings, motion,
 634        state::Mode::{self},
 635        test::{NeovimBackedTestContext, VimTestContext},
 636    };
 637
 638    #[gpui::test]
 639    async fn test_h(cx: &mut gpui::TestAppContext) {
 640        let mut cx = NeovimBackedTestContext::new(cx).await;
 641        cx.simulate_at_each_offset(
 642            "h",
 643            indoc! {"
 644            ˇThe qˇuick
 645            ˇbrown"
 646            },
 647        )
 648        .await
 649        .assert_matches();
 650    }
 651
 652    #[gpui::test]
 653    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 654        let mut cx = NeovimBackedTestContext::new(cx).await;
 655        cx.simulate_at_each_offset(
 656            "backspace",
 657            indoc! {"
 658            ˇThe qˇuick
 659            ˇbrown"
 660            },
 661        )
 662        .await
 663        .assert_matches();
 664    }
 665
 666    #[gpui::test]
 667    async fn test_j(cx: &mut gpui::TestAppContext) {
 668        let mut cx = NeovimBackedTestContext::new(cx).await;
 669
 670        cx.set_shared_state(indoc! {"
 671            aaˇaa
 672            😃😃"
 673        })
 674        .await;
 675        cx.simulate_shared_keystrokes("j").await;
 676        cx.shared_state().await.assert_eq(indoc! {"
 677            aaaa
 678            😃ˇ😃"
 679        });
 680
 681        cx.simulate_at_each_offset(
 682            "j",
 683            indoc! {"
 684                ˇThe qˇuick broˇwn
 685                ˇfox jumps"
 686            },
 687        )
 688        .await
 689        .assert_matches();
 690    }
 691
 692    #[gpui::test]
 693    async fn test_enter(cx: &mut gpui::TestAppContext) {
 694        let mut cx = NeovimBackedTestContext::new(cx).await;
 695        cx.simulate_at_each_offset(
 696            "enter",
 697            indoc! {"
 698            ˇThe qˇuick broˇwn
 699            ˇfox jumps"
 700            },
 701        )
 702        .await
 703        .assert_matches();
 704    }
 705
 706    #[gpui::test]
 707    async fn test_k(cx: &mut gpui::TestAppContext) {
 708        let mut cx = NeovimBackedTestContext::new(cx).await;
 709        cx.simulate_at_each_offset(
 710            "k",
 711            indoc! {"
 712            ˇThe qˇuick
 713            ˇbrown fˇox jumˇps"
 714            },
 715        )
 716        .await
 717        .assert_matches();
 718    }
 719
 720    #[gpui::test]
 721    async fn test_l(cx: &mut gpui::TestAppContext) {
 722        let mut cx = NeovimBackedTestContext::new(cx).await;
 723        cx.simulate_at_each_offset(
 724            "l",
 725            indoc! {"
 726            ˇThe qˇuicˇk
 727            ˇbrowˇn"},
 728        )
 729        .await
 730        .assert_matches();
 731    }
 732
 733    #[gpui::test]
 734    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 735        let mut cx = NeovimBackedTestContext::new(cx).await;
 736        cx.simulate_at_each_offset(
 737            "$",
 738            indoc! {"
 739            ˇThe qˇuicˇk
 740            ˇbrowˇn"},
 741        )
 742        .await
 743        .assert_matches();
 744        cx.simulate_at_each_offset(
 745            "0",
 746            indoc! {"
 747                ˇThe qˇuicˇk
 748                ˇbrowˇn"},
 749        )
 750        .await
 751        .assert_matches();
 752    }
 753
 754    #[gpui::test]
 755    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 756        let mut cx = NeovimBackedTestContext::new(cx).await;
 757
 758        cx.simulate_at_each_offset(
 759            "shift-g",
 760            indoc! {"
 761                The ˇquick
 762
 763                brown fox jumps
 764                overˇ the lazy doˇg"},
 765        )
 766        .await
 767        .assert_matches();
 768        cx.simulate(
 769            "shift-g",
 770            indoc! {"
 771            The quiˇck
 772
 773            brown"},
 774        )
 775        .await
 776        .assert_matches();
 777        cx.simulate(
 778            "shift-g",
 779            indoc! {"
 780            The quiˇck
 781
 782            "},
 783        )
 784        .await
 785        .assert_matches();
 786    }
 787
 788    #[gpui::test]
 789    async fn test_w(cx: &mut gpui::TestAppContext) {
 790        let mut cx = NeovimBackedTestContext::new(cx).await;
 791        cx.simulate_at_each_offset(
 792            "w",
 793            indoc! {"
 794            The ˇquickˇ-ˇbrown
 795            ˇ
 796            ˇ
 797            ˇfox_jumps ˇover
 798            ˇthˇe"},
 799        )
 800        .await
 801        .assert_matches();
 802        cx.simulate_at_each_offset(
 803            "shift-w",
 804            indoc! {"
 805            The ˇquickˇ-ˇbrown
 806            ˇ
 807            ˇ
 808            ˇfox_jumps ˇover
 809            ˇthˇe"},
 810        )
 811        .await
 812        .assert_matches();
 813    }
 814
 815    #[gpui::test]
 816    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 817        let mut cx = NeovimBackedTestContext::new(cx).await;
 818        cx.simulate_at_each_offset(
 819            "e",
 820            indoc! {"
 821            Thˇe quicˇkˇ-browˇn
 822
 823
 824            fox_jumpˇs oveˇr
 825            thˇe"},
 826        )
 827        .await
 828        .assert_matches();
 829        cx.simulate_at_each_offset(
 830            "shift-e",
 831            indoc! {"
 832            Thˇe quicˇkˇ-browˇn
 833
 834
 835            fox_jumpˇs oveˇr
 836            thˇe"},
 837        )
 838        .await
 839        .assert_matches();
 840    }
 841
 842    #[gpui::test]
 843    async fn test_b(cx: &mut gpui::TestAppContext) {
 844        let mut cx = NeovimBackedTestContext::new(cx).await;
 845        cx.simulate_at_each_offset(
 846            "b",
 847            indoc! {"
 848            ˇThe ˇquickˇ-ˇbrown
 849            ˇ
 850            ˇ
 851            ˇfox_jumps ˇover
 852            ˇthe"},
 853        )
 854        .await
 855        .assert_matches();
 856        cx.simulate_at_each_offset(
 857            "shift-b",
 858            indoc! {"
 859            ˇThe ˇquickˇ-ˇbrown
 860            ˇ
 861            ˇ
 862            ˇfox_jumps ˇover
 863            ˇthe"},
 864        )
 865        .await
 866        .assert_matches();
 867    }
 868
 869    #[gpui::test]
 870    async fn test_gg(cx: &mut gpui::TestAppContext) {
 871        let mut cx = NeovimBackedTestContext::new(cx).await;
 872        cx.simulate_at_each_offset(
 873            "g g",
 874            indoc! {"
 875                The qˇuick
 876
 877                brown fox jumps
 878                over ˇthe laˇzy dog"},
 879        )
 880        .await
 881        .assert_matches();
 882        cx.simulate(
 883            "g g",
 884            indoc! {"
 885
 886
 887                brown fox jumps
 888                over the laˇzy dog"},
 889        )
 890        .await
 891        .assert_matches();
 892        cx.simulate(
 893            "2 g g",
 894            indoc! {"
 895                ˇ
 896
 897                brown fox jumps
 898                over the lazydog"},
 899        )
 900        .await
 901        .assert_matches();
 902    }
 903
 904    #[gpui::test]
 905    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 906        let mut cx = NeovimBackedTestContext::new(cx).await;
 907        cx.simulate_at_each_offset(
 908            "shift-g",
 909            indoc! {"
 910                The qˇuick
 911
 912                brown fox jumps
 913                over ˇthe laˇzy dog"},
 914        )
 915        .await
 916        .assert_matches();
 917        cx.simulate(
 918            "shift-g",
 919            indoc! {"
 920
 921
 922                brown fox jumps
 923                over the laˇzy dog"},
 924        )
 925        .await
 926        .assert_matches();
 927        cx.simulate(
 928            "2 shift-g",
 929            indoc! {"
 930                ˇ
 931
 932                brown fox jumps
 933                over the lazydog"},
 934        )
 935        .await
 936        .assert_matches();
 937    }
 938
 939    #[gpui::test]
 940    async fn test_a(cx: &mut gpui::TestAppContext) {
 941        let mut cx = NeovimBackedTestContext::new(cx).await;
 942        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 943            .await
 944            .assert_matches();
 945    }
 946
 947    #[gpui::test]
 948    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 949        let mut cx = NeovimBackedTestContext::new(cx).await;
 950        cx.simulate_at_each_offset(
 951            "shift-a",
 952            indoc! {"
 953            ˇ
 954            The qˇuick
 955            brown ˇfox "},
 956        )
 957        .await
 958        .assert_matches();
 959    }
 960
 961    #[gpui::test]
 962    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 963        let mut cx = NeovimBackedTestContext::new(cx).await;
 964        cx.simulate("^", "The qˇuick").await.assert_matches();
 965        cx.simulate("^", " The qˇuick").await.assert_matches();
 966        cx.simulate("^", "ˇ").await.assert_matches();
 967        cx.simulate(
 968            "^",
 969            indoc! {"
 970                The qˇuick
 971                brown fox"},
 972        )
 973        .await
 974        .assert_matches();
 975        cx.simulate(
 976            "^",
 977            indoc! {"
 978                ˇ
 979                The quick"},
 980        )
 981        .await
 982        .assert_matches();
 983        // Indoc disallows trailing whitespace.
 984        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
 985    }
 986
 987    #[gpui::test]
 988    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 989        let mut cx = NeovimBackedTestContext::new(cx).await;
 990        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
 991        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
 992        cx.simulate("shift-i", "ˇ").await.assert_matches();
 993        cx.simulate(
 994            "shift-i",
 995            indoc! {"
 996                The qˇuick
 997                brown fox"},
 998        )
 999        .await
1000        .assert_matches();
1001        cx.simulate(
1002            "shift-i",
1003            indoc! {"
1004                ˇ
1005                The quick"},
1006        )
1007        .await
1008        .assert_matches();
1009    }
1010
1011    #[gpui::test]
1012    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1013        let mut cx = NeovimBackedTestContext::new(cx).await;
1014        cx.simulate(
1015            "shift-d",
1016            indoc! {"
1017                The qˇuick
1018                brown fox"},
1019        )
1020        .await
1021        .assert_matches();
1022        cx.simulate(
1023            "shift-d",
1024            indoc! {"
1025                The quick
1026                ˇ
1027                brown fox"},
1028        )
1029        .await
1030        .assert_matches();
1031    }
1032
1033    #[gpui::test]
1034    async fn test_x(cx: &mut gpui::TestAppContext) {
1035        let mut cx = NeovimBackedTestContext::new(cx).await;
1036        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1037            .await
1038            .assert_matches();
1039        cx.simulate(
1040            "x",
1041            indoc! {"
1042                Tesˇt
1043                test"},
1044        )
1045        .await
1046        .assert_matches();
1047    }
1048
1049    #[gpui::test]
1050    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1051        let mut cx = NeovimBackedTestContext::new(cx).await;
1052        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1053            .await
1054            .assert_matches();
1055        cx.simulate(
1056            "shift-x",
1057            indoc! {"
1058                Test
1059                ˇtest"},
1060        )
1061        .await
1062        .assert_matches();
1063    }
1064
1065    #[gpui::test]
1066    async fn test_o(cx: &mut gpui::TestAppContext) {
1067        let mut cx = NeovimBackedTestContext::new(cx).await;
1068        cx.simulate("o", "ˇ").await.assert_matches();
1069        cx.simulate("o", "The ˇquick").await.assert_matches();
1070        cx.simulate_at_each_offset(
1071            "o",
1072            indoc! {"
1073                The qˇuick
1074                brown ˇfox
1075                jumps ˇover"},
1076        )
1077        .await
1078        .assert_matches();
1079        cx.simulate(
1080            "o",
1081            indoc! {"
1082                The quick
1083                ˇ
1084                brown fox"},
1085        )
1086        .await
1087        .assert_matches();
1088
1089        cx.assert_binding(
1090            "o",
1091            indoc! {"
1092                fn test() {
1093                    println!(ˇ);
1094                }"},
1095            Mode::Normal,
1096            indoc! {"
1097                fn test() {
1098                    println!();
1099                    ˇ
1100                }"},
1101            Mode::Insert,
1102        );
1103
1104        cx.assert_binding(
1105            "o",
1106            indoc! {"
1107                fn test(ˇ) {
1108                    println!();
1109                }"},
1110            Mode::Normal,
1111            indoc! {"
1112                fn test() {
1113                    ˇ
1114                    println!();
1115                }"},
1116            Mode::Insert,
1117        );
1118    }
1119
1120    #[gpui::test]
1121    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1122        let mut cx = NeovimBackedTestContext::new(cx).await;
1123        cx.simulate("shift-o", "ˇ").await.assert_matches();
1124        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1125        cx.simulate_at_each_offset(
1126            "shift-o",
1127            indoc! {"
1128            The qˇuick
1129            brown ˇfox
1130            jumps ˇover"},
1131        )
1132        .await
1133        .assert_matches();
1134        cx.simulate(
1135            "shift-o",
1136            indoc! {"
1137            The quick
1138            ˇ
1139            brown fox"},
1140        )
1141        .await
1142        .assert_matches();
1143
1144        // Our indentation is smarter than vims. So we don't match here
1145        cx.assert_binding(
1146            "shift-o",
1147            indoc! {"
1148                fn test() {
1149                    println!(ˇ);
1150                }"},
1151            Mode::Normal,
1152            indoc! {"
1153                fn test() {
1154                    ˇ
1155                    println!();
1156                }"},
1157            Mode::Insert,
1158        );
1159        cx.assert_binding(
1160            "shift-o",
1161            indoc! {"
1162                fn test(ˇ) {
1163                    println!();
1164                }"},
1165            Mode::Normal,
1166            indoc! {"
1167                ˇ
1168                fn test() {
1169                    println!();
1170                }"},
1171            Mode::Insert,
1172        );
1173    }
1174
1175    #[gpui::test]
1176    async fn test_dd(cx: &mut gpui::TestAppContext) {
1177        let mut cx = NeovimBackedTestContext::new(cx).await;
1178        cx.simulate("d d", "ˇ").await.assert_matches();
1179        cx.simulate("d d", "The ˇquick").await.assert_matches();
1180        cx.simulate_at_each_offset(
1181            "d d",
1182            indoc! {"
1183            The qˇuick
1184            brown ˇfox
1185            jumps ˇover"},
1186        )
1187        .await
1188        .assert_matches();
1189        cx.simulate(
1190            "d d",
1191            indoc! {"
1192                The quick
1193                ˇ
1194                brown fox"},
1195        )
1196        .await
1197        .assert_matches();
1198    }
1199
1200    #[gpui::test]
1201    async fn test_cc(cx: &mut gpui::TestAppContext) {
1202        let mut cx = NeovimBackedTestContext::new(cx).await;
1203        cx.simulate("c c", "ˇ").await.assert_matches();
1204        cx.simulate("c c", "The ˇquick").await.assert_matches();
1205        cx.simulate_at_each_offset(
1206            "c c",
1207            indoc! {"
1208                The quˇick
1209                brown ˇfox
1210                jumps ˇover"},
1211        )
1212        .await
1213        .assert_matches();
1214        cx.simulate(
1215            "c c",
1216            indoc! {"
1217                The quick
1218                ˇ
1219                brown fox"},
1220        )
1221        .await
1222        .assert_matches();
1223    }
1224
1225    #[gpui::test]
1226    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1227        let mut cx = NeovimBackedTestContext::new(cx).await;
1228
1229        for count in 1..=5 {
1230            cx.simulate_at_each_offset(
1231                &format!("{count} w"),
1232                indoc! {"
1233                    ˇThe quˇickˇ browˇn
1234                    ˇ
1235                    ˇfox ˇjumpsˇ-ˇoˇver
1236                    ˇthe lazy dog
1237                "},
1238            )
1239            .await
1240            .assert_matches();
1241        }
1242    }
1243
1244    #[gpui::test]
1245    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1246        let mut cx = NeovimBackedTestContext::new(cx).await;
1247        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1248            .await
1249            .assert_matches();
1250    }
1251
1252    #[gpui::test]
1253    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1254        let mut cx = NeovimBackedTestContext::new(cx).await;
1255
1256        for count in 1..=3 {
1257            let test_case = indoc! {"
1258                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1259                ˇ    ˇbˇaaˇa ˇbˇbˇb
1260                ˇ
1261                ˇb
1262            "};
1263
1264            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1265                .await
1266                .assert_matches();
1267
1268            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1269                .await
1270                .assert_matches();
1271        }
1272    }
1273
1274    #[gpui::test]
1275    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1276        let mut cx = NeovimBackedTestContext::new(cx).await;
1277        let test_case = indoc! {"
1278            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1279            ˇ    ˇbˇaaˇa ˇbˇbˇb
1280            ˇ•••
1281            ˇb
1282            "
1283        };
1284
1285        for count in 1..=3 {
1286            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1287                .await
1288                .assert_matches();
1289
1290            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1291                .await
1292                .assert_matches();
1293        }
1294    }
1295
1296    #[gpui::test]
1297    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1298        let mut cx = VimTestContext::new(cx, true).await;
1299        cx.update_global(|store: &mut SettingsStore, cx| {
1300            store.update_user_settings::<VimSettings>(cx, |s| {
1301                s.use_multiline_find = Some(true);
1302            });
1303        });
1304
1305        cx.assert_binding(
1306            "f l",
1307            indoc! {"
1308            ˇfunction print() {
1309                console.log('ok')
1310            }
1311            "},
1312            Mode::Normal,
1313            indoc! {"
1314            function print() {
1315                consoˇle.log('ok')
1316            }
1317            "},
1318            Mode::Normal,
1319        );
1320
1321        cx.assert_binding(
1322            "t l",
1323            indoc! {"
1324            ˇfunction print() {
1325                console.log('ok')
1326            }
1327            "},
1328            Mode::Normal,
1329            indoc! {"
1330            function print() {
1331                consˇole.log('ok')
1332            }
1333            "},
1334            Mode::Normal,
1335        );
1336    }
1337
1338    #[gpui::test]
1339    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1340        let mut cx = VimTestContext::new(cx, true).await;
1341        cx.update_global(|store: &mut SettingsStore, cx| {
1342            store.update_user_settings::<VimSettings>(cx, |s| {
1343                s.use_multiline_find = Some(true);
1344            });
1345        });
1346
1347        cx.assert_binding(
1348            "shift-f p",
1349            indoc! {"
1350            function print() {
1351                console.ˇlog('ok')
1352            }
1353            "},
1354            Mode::Normal,
1355            indoc! {"
1356            function ˇprint() {
1357                console.log('ok')
1358            }
1359            "},
1360            Mode::Normal,
1361        );
1362
1363        cx.assert_binding(
1364            "shift-t p",
1365            indoc! {"
1366            function print() {
1367                console.ˇlog('ok')
1368            }
1369            "},
1370            Mode::Normal,
1371            indoc! {"
1372            function pˇrint() {
1373                console.log('ok')
1374            }
1375            "},
1376            Mode::Normal,
1377        );
1378    }
1379
1380    #[gpui::test]
1381    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1382        let mut cx = VimTestContext::new(cx, true).await;
1383        cx.update_global(|store: &mut SettingsStore, cx| {
1384            store.update_user_settings::<VimSettings>(cx, |s| {
1385                s.use_smartcase_find = Some(true);
1386            });
1387        });
1388
1389        cx.assert_binding(
1390            "f p",
1391            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1392            Mode::Normal,
1393            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1394            Mode::Normal,
1395        );
1396
1397        cx.assert_binding(
1398            "shift-f p",
1399            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1400            Mode::Normal,
1401            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1402            Mode::Normal,
1403        );
1404
1405        cx.assert_binding(
1406            "t p",
1407            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1408            Mode::Normal,
1409            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1410            Mode::Normal,
1411        );
1412
1413        cx.assert_binding(
1414            "shift-t p",
1415            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1416            Mode::Normal,
1417            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1418            Mode::Normal,
1419        );
1420    }
1421
1422    #[gpui::test]
1423    async fn test_percent(cx: &mut TestAppContext) {
1424        let mut cx = NeovimBackedTestContext::new(cx).await;
1425        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1426            .await
1427            .assert_matches();
1428        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1429            .await
1430            .assert_matches();
1431        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1432            .await
1433            .assert_matches();
1434    }
1435
1436    #[gpui::test]
1437    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1438        let mut cx = NeovimBackedTestContext::new(cx).await;
1439
1440        // goes to current line end
1441        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1442        cx.simulate_shared_keystrokes("$").await;
1443        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1444
1445        // goes to next line end
1446        cx.simulate_shared_keystrokes("2 $").await;
1447        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1448
1449        // try to exceed the final line.
1450        cx.simulate_shared_keystrokes("4 $").await;
1451        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1452    }
1453
1454    #[gpui::test]
1455    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1456        let mut cx = VimTestContext::new(cx, true).await;
1457        cx.update(|_, cx| {
1458            cx.bind_keys(vec![
1459                KeyBinding::new(
1460                    "w",
1461                    motion::NextSubwordStart {
1462                        ignore_punctuation: false,
1463                    },
1464                    Some("Editor && VimControl && !VimWaiting && !menu"),
1465                ),
1466                KeyBinding::new(
1467                    "b",
1468                    motion::PreviousSubwordStart {
1469                        ignore_punctuation: false,
1470                    },
1471                    Some("Editor && VimControl && !VimWaiting && !menu"),
1472                ),
1473                KeyBinding::new(
1474                    "e",
1475                    motion::NextSubwordEnd {
1476                        ignore_punctuation: false,
1477                    },
1478                    Some("Editor && VimControl && !VimWaiting && !menu"),
1479                ),
1480                KeyBinding::new(
1481                    "g e",
1482                    motion::PreviousSubwordEnd {
1483                        ignore_punctuation: false,
1484                    },
1485                    Some("Editor && VimControl && !VimWaiting && !menu"),
1486                ),
1487            ]);
1488        });
1489
1490        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1491        // Special case: In 'cw', 'w' acts like 'e'
1492        cx.assert_binding(
1493            "c w",
1494            indoc! {"ˇassert_binding"},
1495            Mode::Normal,
1496            indoc! {"ˇ_binding"},
1497            Mode::Insert,
1498        );
1499
1500        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1501
1502        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1503
1504        cx.assert_binding_normal(
1505            "g e",
1506            indoc! {"assert_bindinˇg"},
1507            indoc! {"asserˇt_binding"},
1508        );
1509    }
1510
1511    #[gpui::test]
1512    async fn test_r(cx: &mut gpui::TestAppContext) {
1513        let mut cx = NeovimBackedTestContext::new(cx).await;
1514
1515        cx.set_shared_state("ˇhello\n").await;
1516        cx.simulate_shared_keystrokes("r -").await;
1517        cx.shared_state().await.assert_eq("ˇ-ello\n");
1518
1519        cx.set_shared_state("ˇhello\n").await;
1520        cx.simulate_shared_keystrokes("3 r -").await;
1521        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1522
1523        cx.set_shared_state("ˇhello\n").await;
1524        cx.simulate_shared_keystrokes("r - 2 l .").await;
1525        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1526
1527        cx.set_shared_state("ˇhello world\n").await;
1528        cx.simulate_shared_keystrokes("2 r - f w .").await;
1529        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1530
1531        cx.set_shared_state("ˇhello world\n").await;
1532        cx.simulate_shared_keystrokes("2 0 r - ").await;
1533        cx.shared_state().await.assert_eq("ˇhello world\n");
1534    }
1535
1536    #[gpui::test]
1537    async fn test_gq(cx: &mut gpui::TestAppContext) {
1538        let mut cx = NeovimBackedTestContext::new(cx).await;
1539        cx.set_neovim_option("textwidth=5").await;
1540
1541        cx.update(|_, cx| {
1542            SettingsStore::update_global(cx, |settings, cx| {
1543                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1544                    settings.defaults.preferred_line_length = Some(5);
1545                });
1546            })
1547        });
1548
1549        cx.set_shared_state("ˇth th th th th th\n").await;
1550        cx.simulate_shared_keystrokes("g q q").await;
1551        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1552
1553        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1554            .await;
1555        cx.simulate_shared_keystrokes("v j g q").await;
1556        cx.shared_state()
1557            .await
1558            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1559    }
1560
1561    #[gpui::test]
1562    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1563        let mut cx = NeovimBackedTestContext::new(cx).await;
1564        cx.set_neovim_option("filetype=rust").await;
1565
1566        cx.set_shared_state("// helloˇ\n").await;
1567        cx.simulate_shared_keystrokes("o").await;
1568        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1569        cx.simulate_shared_keystrokes("x escape shift-o").await;
1570        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1571    }
1572
1573    #[gpui::test]
1574    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1575        let mut cx = NeovimBackedTestContext::new(cx).await;
1576        cx.set_shared_state("heˇllo\n").await;
1577        cx.simulate_shared_keystrokes("y y p").await;
1578        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1579    }
1580
1581    #[gpui::test]
1582    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1583        let mut cx = NeovimBackedTestContext::new(cx).await;
1584        cx.set_shared_state("heˇllo").await;
1585        cx.simulate_shared_keystrokes("y y p").await;
1586        cx.shared_state().await.assert_eq("hello\nˇhello");
1587    }
1588
1589    #[gpui::test]
1590    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1591        let mut cx = NeovimBackedTestContext::new(cx).await;
1592        cx.set_shared_state("heˇllo\nhello").await;
1593        cx.simulate_shared_keystrokes("2 y y p").await;
1594        cx.shared_state()
1595            .await
1596            .assert_eq("hello\nˇhello\nhello\nhello");
1597    }
1598
1599    #[gpui::test]
1600    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1601        let mut cx = NeovimBackedTestContext::new(cx).await;
1602        cx.set_shared_state("heˇllo").await;
1603        cx.simulate_shared_keystrokes("d d").await;
1604        cx.shared_state().await.assert_eq("ˇ");
1605        cx.simulate_shared_keystrokes("p p").await;
1606        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1607    }
1608
1609    #[gpui::test]
1610    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1611        let mut cx = NeovimBackedTestContext::new(cx).await;
1612
1613        cx.set_shared_state("heˇllo").await;
1614        cx.simulate_shared_keystrokes("v i w shift-i").await;
1615        cx.shared_state().await.assert_eq("ˇhello");
1616
1617        cx.set_shared_state(indoc! {"
1618            The quick brown
1619            fox ˇjumps over
1620            the lazy dog"})
1621            .await;
1622        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1623        cx.shared_state().await.assert_eq(indoc! {"
1624            The quick brown
1625            ˇfox jumps over
1626            the lazy dog"});
1627
1628        cx.set_shared_state(indoc! {"
1629            The quick brown
1630            fox ˇjumps over
1631            the lazy dog"})
1632            .await;
1633        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1634        cx.shared_state().await.assert_eq(indoc! {"
1635            The quick brown
1636            fox jˇumps over
1637            the lazy dog"});
1638    }
1639}