normal.rs

   1mod change;
   2mod convert;
   3mod delete;
   4mod increment;
   5pub(crate) mod mark;
   6pub(crate) mod 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 collections::BTreeSet;
  26use convert::ConvertTarget;
  27use editor::Editor;
  28use editor::{Anchor, SelectionEffects};
  29use editor::{Bias, ToPoint};
  30use editor::{display_map::ToDisplayPoint, movement};
  31use gpui::{Context, Window, actions};
  32use language::{Point, SelectionGoal};
  33use log::error;
  34use multi_buffer::MultiBufferRow;
  35
  36actions!(
  37    vim,
  38    [
  39        /// Inserts text after the cursor.
  40        InsertAfter,
  41        /// Inserts text before the cursor.
  42        InsertBefore,
  43        /// Inserts at the first non-whitespace character.
  44        InsertFirstNonWhitespace,
  45        /// Inserts at the end of the line.
  46        InsertEndOfLine,
  47        /// Inserts a new line above the current line.
  48        InsertLineAbove,
  49        /// Inserts a new line below the current line.
  50        InsertLineBelow,
  51        /// Inserts an empty line above without entering insert mode.
  52        InsertEmptyLineAbove,
  53        /// Inserts an empty line below without entering insert mode.
  54        InsertEmptyLineBelow,
  55        /// Inserts at the previous insert position.
  56        InsertAtPrevious,
  57        /// Joins the current line with the next line.
  58        JoinLines,
  59        /// Joins lines without adding whitespace.
  60        JoinLinesNoWhitespace,
  61        /// Deletes character to the left.
  62        DeleteLeft,
  63        /// Deletes character to the right.
  64        DeleteRight,
  65        /// Deletes using Helix-style behavior.
  66        HelixDelete,
  67        /// Collapse the current selection
  68        HelixCollapseSelection,
  69        /// Changes from cursor to end of line.
  70        ChangeToEndOfLine,
  71        /// Deletes from cursor to end of line.
  72        DeleteToEndOfLine,
  73        /// Yanks (copies) the selected text.
  74        Yank,
  75        /// Yanks the entire line.
  76        YankLine,
  77        /// Yanks from cursor to end of line.
  78        YankToEndOfLine,
  79        /// Toggles the case of selected text.
  80        ChangeCase,
  81        /// Converts selected text to uppercase.
  82        ConvertToUpperCase,
  83        /// Converts selected text to lowercase.
  84        ConvertToLowerCase,
  85        /// Applies ROT13 cipher to selected text.
  86        ConvertToRot13,
  87        /// Applies ROT47 cipher to selected text.
  88        ConvertToRot47,
  89        /// Toggles comments for selected lines.
  90        ToggleComments,
  91        /// Toggles block comments for selected lines.
  92        ToggleBlockComments,
  93        /// Shows the current location in the file.
  94        ShowLocation,
  95        /// Undoes the last change.
  96        Undo,
  97        /// Redoes the last undone change.
  98        Redo,
  99        /// Undoes all changes to the most recently changed line.
 100        UndoLastLine,
 101        /// Go to tab page (with count support).
 102        GoToTab,
 103        /// Go to previous tab page (with count support).
 104        GoToPreviousTab,
 105        /// Goes to the previous reference to the symbol under the cursor.
 106        GoToPreviousReference,
 107        /// Goes to the next reference to the symbol under the cursor.
 108        GoToNextReference,
 109    ]
 110);
 111
 112pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 113    Vim::action(editor, cx, Vim::insert_after);
 114    Vim::action(editor, cx, Vim::insert_before);
 115    Vim::action(editor, cx, Vim::insert_first_non_whitespace);
 116    Vim::action(editor, cx, Vim::insert_end_of_line);
 117    Vim::action(editor, cx, Vim::insert_line_above);
 118    Vim::action(editor, cx, Vim::insert_line_below);
 119    Vim::action(editor, cx, Vim::insert_empty_line_above);
 120    Vim::action(editor, cx, Vim::insert_empty_line_below);
 121    Vim::action(editor, cx, Vim::insert_at_previous);
 122    Vim::action(editor, cx, Vim::change_case);
 123    Vim::action(editor, cx, Vim::convert_to_upper_case);
 124    Vim::action(editor, cx, Vim::convert_to_lower_case);
 125    Vim::action(editor, cx, Vim::convert_to_rot13);
 126    Vim::action(editor, cx, Vim::convert_to_rot47);
 127    Vim::action(editor, cx, Vim::yank_line);
 128    Vim::action(editor, cx, Vim::yank_to_end_of_line);
 129    Vim::action(editor, cx, Vim::toggle_comments);
 130    Vim::action(editor, cx, Vim::toggle_block_comments);
 131    Vim::action(editor, cx, Vim::paste);
 132    Vim::action(editor, cx, Vim::show_location);
 133
 134    Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
 135        vim.record_current_action(cx);
 136        let times = Vim::take_count(cx);
 137        let forced_motion = Vim::take_forced_motion(cx);
 138        vim.delete_motion(Motion::Left, times, forced_motion, window, cx);
 139    });
 140    Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
 141        vim.record_current_action(cx);
 142        let times = Vim::take_count(cx);
 143        let forced_motion = Vim::take_forced_motion(cx);
 144        vim.delete_motion(Motion::Right, times, forced_motion, window, cx);
 145    });
 146
 147    Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
 148        vim.record_current_action(cx);
 149        vim.update_editor(cx, |_, editor, cx| {
 150            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 151                s.move_with(&mut |map, selection| {
 152                    if selection.is_empty() {
 153                        selection.end = movement::right(map, selection.end)
 154                    }
 155                })
 156            })
 157        });
 158        vim.visual_delete(false, window, cx);
 159        vim.switch_mode(Mode::HelixNormal, true, window, cx);
 160    });
 161
 162    Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| {
 163        vim.update_editor(cx, |_, editor, cx| {
 164            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 165                s.move_with(&mut |map, selection| {
 166                    let mut point = selection.head();
 167                    if !selection.reversed && !selection.is_empty() {
 168                        point = movement::left(map, selection.head());
 169                    }
 170                    selection.collapse_to(point, selection.goal)
 171                });
 172            });
 173        });
 174    });
 175
 176    Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
 177        vim.start_recording(cx);
 178        let times = Vim::take_count(cx);
 179        let forced_motion = Vim::take_forced_motion(cx);
 180        vim.change_motion(
 181            Motion::EndOfLine {
 182                display_lines: false,
 183            },
 184            times,
 185            forced_motion,
 186            window,
 187            cx,
 188        );
 189    });
 190    Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
 191        vim.record_current_action(cx);
 192        let times = Vim::take_count(cx);
 193        let forced_motion = Vim::take_forced_motion(cx);
 194        vim.delete_motion(
 195            Motion::EndOfLine {
 196                display_lines: false,
 197            },
 198            times,
 199            forced_motion,
 200            window,
 201            cx,
 202        );
 203    });
 204    Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| {
 205        vim.join_lines_impl(true, window, cx);
 206    });
 207
 208    Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| {
 209        vim.join_lines_impl(false, window, cx);
 210    });
 211
 212    Vim::action(editor, cx, |vim, _: &GoToPreviousReference, window, cx| {
 213        let count = Vim::take_count(cx);
 214        vim.update_editor(cx, |_, editor, cx| {
 215            let task = editor.go_to_reference_before_or_after_position(
 216                editor::Direction::Prev,
 217                count.unwrap_or(1),
 218                window,
 219                cx,
 220            );
 221            if let Some(task) = task {
 222                task.detach_and_log_err(cx);
 223            };
 224        });
 225    });
 226
 227    Vim::action(editor, cx, |vim, _: &GoToNextReference, window, cx| {
 228        let count = Vim::take_count(cx);
 229        vim.update_editor(cx, |_, editor, cx| {
 230            let task = editor.go_to_reference_before_or_after_position(
 231                editor::Direction::Next,
 232                count.unwrap_or(1),
 233                window,
 234                cx,
 235            );
 236            if let Some(task) = task {
 237                task.detach_and_log_err(cx);
 238            };
 239        });
 240    });
 241
 242    Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
 243        let times = Vim::take_count(cx);
 244        Vim::take_forced_motion(cx);
 245        vim.update_editor(cx, |_, editor, cx| {
 246            for _ in 0..times.unwrap_or(1) {
 247                editor.undo(&editor::actions::Undo, window, cx);
 248            }
 249        });
 250    });
 251    Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
 252        let times = Vim::take_count(cx);
 253        Vim::take_forced_motion(cx);
 254        vim.update_editor(cx, |_, editor, cx| {
 255            for _ in 0..times.unwrap_or(1) {
 256                editor.redo(&editor::actions::Redo, window, cx);
 257            }
 258        });
 259    });
 260    Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
 261        Vim::take_forced_motion(cx);
 262        vim.update_editor(cx, |vim, editor, cx| {
 263            let snapshot = editor.buffer().read(cx).snapshot(cx);
 264            let Some(last_change) = editor.change_list.last_before_grouping() else {
 265                return;
 266            };
 267
 268            let anchors = last_change.to_vec();
 269            let mut last_row = None;
 270            let ranges: Vec<_> = anchors
 271                .iter()
 272                .filter_map(|anchor| {
 273                    let point = anchor.to_point(&snapshot);
 274                    if last_row == Some(point.row) {
 275                        return None;
 276                    }
 277                    last_row = Some(point.row);
 278                    let line_range = Point::new(point.row, 0)
 279                        ..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row)));
 280                    Some((
 281                        snapshot.anchor_before(line_range.start)
 282                            ..snapshot.anchor_after(line_range.end),
 283                        line_range,
 284                    ))
 285                })
 286                .collect();
 287
 288            let edits = editor.buffer().update(cx, |buffer, cx| {
 289                let current_content = ranges
 290                    .iter()
 291                    .map(|(anchors, _)| {
 292                        buffer
 293                            .snapshot(cx)
 294                            .text_for_range(anchors.clone())
 295                            .collect::<String>()
 296                    })
 297                    .collect::<Vec<_>>();
 298                let mut content_before_undo = current_content.clone();
 299                let mut undo_count = 0;
 300
 301                loop {
 302                    let undone_tx = buffer.undo(cx);
 303                    undo_count += 1;
 304                    let mut content_after_undo = Vec::new();
 305
 306                    let mut line_changed = false;
 307                    for ((anchors, _), text_before_undo) in
 308                        ranges.iter().zip(content_before_undo.iter())
 309                    {
 310                        let snapshot = buffer.snapshot(cx);
 311                        let text_after_undo =
 312                            snapshot.text_for_range(anchors.clone()).collect::<String>();
 313
 314                        if &text_after_undo != text_before_undo {
 315                            line_changed = true;
 316                        }
 317                        content_after_undo.push(text_after_undo);
 318                    }
 319
 320                    content_before_undo = content_after_undo;
 321                    if !line_changed {
 322                        break;
 323                    }
 324                    if undone_tx == vim.undo_last_line_tx {
 325                        break;
 326                    }
 327                }
 328
 329                let edits = ranges
 330                    .into_iter()
 331                    .zip(content_before_undo.into_iter().zip(current_content))
 332                    .filter_map(|((_, mut points), (mut old_text, new_text))| {
 333                        if new_text == old_text {
 334                            return None;
 335                        }
 336                        let common_suffix_starts_at = old_text
 337                            .char_indices()
 338                            .rev()
 339                            .zip(new_text.chars().rev())
 340                            .find_map(
 341                                |((i, a), b)| {
 342                                    if a != b { Some(i + a.len_utf8()) } else { None }
 343                                },
 344                            )
 345                            .unwrap_or(old_text.len());
 346                        points.end.column -= (old_text.len() - common_suffix_starts_at) as u32;
 347                        old_text = old_text.split_at(common_suffix_starts_at).0.to_string();
 348                        let common_prefix_len = old_text
 349                            .char_indices()
 350                            .zip(new_text.chars())
 351                            .find_map(|((i, a), b)| if a != b { Some(i) } else { None })
 352                            .unwrap_or(0);
 353                        points.start.column = common_prefix_len as u32;
 354                        old_text = old_text.split_at(common_prefix_len).1.to_string();
 355
 356                        Some((points, old_text))
 357                    })
 358                    .collect::<Vec<_>>();
 359
 360                for _ in 0..undo_count {
 361                    buffer.redo(cx);
 362                }
 363                edits
 364            });
 365            vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| {
 366                editor.change_list.invert_last_group();
 367                editor.edit(edits, cx);
 368                editor.change_selections(SelectionEffects::default(), window, cx, |s| {
 369                    s.select_anchor_ranges(anchors.into_iter().map(|a| a..a));
 370                })
 371            });
 372        });
 373    });
 374
 375    repeat::register(editor, cx);
 376    scroll::register(editor, cx);
 377    search::register(editor, cx);
 378    substitute::register(editor, cx);
 379    increment::register(editor, cx);
 380}
 381
 382impl Vim {
 383    pub fn normal_motion(
 384        &mut self,
 385        motion: Motion,
 386        operator: Option<Operator>,
 387        times: Option<usize>,
 388        forced_motion: bool,
 389        window: &mut Window,
 390        cx: &mut Context<Self>,
 391    ) {
 392        match operator {
 393            None => self.move_cursor(motion, times, window, cx),
 394            Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx),
 395            Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx),
 396            Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx),
 397            Some(Operator::AddSurrounds { target: None }) => {}
 398            Some(Operator::Indent) => self.indent_motion(
 399                motion,
 400                times,
 401                forced_motion,
 402                IndentDirection::In,
 403                window,
 404                cx,
 405            ),
 406            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx),
 407            Some(Operator::Outdent) => self.indent_motion(
 408                motion,
 409                times,
 410                forced_motion,
 411                IndentDirection::Out,
 412                window,
 413                cx,
 414            ),
 415            Some(Operator::AutoIndent) => self.indent_motion(
 416                motion,
 417                times,
 418                forced_motion,
 419                IndentDirection::Auto,
 420                window,
 421                cx,
 422            ),
 423            Some(Operator::ShellCommand) => {
 424                self.shell_command_motion(motion, times, forced_motion, window, cx)
 425            }
 426            Some(Operator::Lowercase) => self.convert_motion(
 427                motion,
 428                times,
 429                forced_motion,
 430                ConvertTarget::LowerCase,
 431                window,
 432                cx,
 433            ),
 434            Some(Operator::Uppercase) => self.convert_motion(
 435                motion,
 436                times,
 437                forced_motion,
 438                ConvertTarget::UpperCase,
 439                window,
 440                cx,
 441            ),
 442            Some(Operator::OppositeCase) => self.convert_motion(
 443                motion,
 444                times,
 445                forced_motion,
 446                ConvertTarget::OppositeCase,
 447                window,
 448                cx,
 449            ),
 450            Some(Operator::Rot13) => self.convert_motion(
 451                motion,
 452                times,
 453                forced_motion,
 454                ConvertTarget::Rot13,
 455                window,
 456                cx,
 457            ),
 458            Some(Operator::Rot47) => self.convert_motion(
 459                motion,
 460                times,
 461                forced_motion,
 462                ConvertTarget::Rot47,
 463                window,
 464                cx,
 465            ),
 466            Some(Operator::ToggleComments) => {
 467                self.toggle_comments_motion(motion, times, forced_motion, window, cx)
 468            }
 469            Some(Operator::ToggleBlockComments) => {
 470                self.toggle_block_comments_motion(motion, times, forced_motion, window, cx)
 471            }
 472            Some(Operator::ReplaceWithRegister) => {
 473                self.replace_with_register_motion(motion, times, forced_motion, window, cx)
 474            }
 475            Some(Operator::Exchange) => {
 476                self.exchange_motion(motion, times, forced_motion, window, cx)
 477            }
 478            Some(operator) => {
 479                // Can't do anything for text objects, Ignoring
 480                error!("Unexpected normal mode motion operator: {:?}", operator)
 481            }
 482        }
 483        // Exit temporary normal mode (if active).
 484        self.exit_temporary_normal(window, cx);
 485    }
 486
 487    pub fn normal_object(
 488        &mut self,
 489        object: Object,
 490        times: Option<usize>,
 491        opening: bool,
 492        window: &mut Window,
 493        cx: &mut Context<Self>,
 494    ) {
 495        let mut waiting_operator: Option<Operator> = None;
 496        match self.maybe_pop_operator() {
 497            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
 498                Some(Operator::Change) => self.change_object(object, around, times, window, cx),
 499                Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
 500                Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
 501                Some(Operator::Indent) => {
 502                    self.indent_object(object, around, IndentDirection::In, times, window, cx)
 503                }
 504                Some(Operator::Outdent) => {
 505                    self.indent_object(object, around, IndentDirection::Out, times, window, cx)
 506                }
 507                Some(Operator::AutoIndent) => {
 508                    self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
 509                }
 510                Some(Operator::ShellCommand) => {
 511                    self.shell_command_object(object, around, window, cx);
 512                }
 513                Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
 514                Some(Operator::Lowercase) => {
 515                    self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
 516                }
 517                Some(Operator::Uppercase) => {
 518                    self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
 519                }
 520                Some(Operator::OppositeCase) => self.convert_object(
 521                    object,
 522                    around,
 523                    ConvertTarget::OppositeCase,
 524                    times,
 525                    window,
 526                    cx,
 527                ),
 528                Some(Operator::Rot13) => {
 529                    self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
 530                }
 531                Some(Operator::Rot47) => {
 532                    self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
 533                }
 534                Some(Operator::AddSurrounds { target: None }) => {
 535                    waiting_operator = Some(Operator::AddSurrounds {
 536                        target: Some(SurroundsType::Object(object, around)),
 537                    });
 538                }
 539                Some(Operator::ToggleComments) => {
 540                    self.toggle_comments_object(object, around, times, window, cx)
 541                }
 542                Some(Operator::ToggleBlockComments) => {
 543                    self.toggle_block_comments_object(object, around, times, window, cx)
 544                }
 545                Some(Operator::ReplaceWithRegister) => {
 546                    self.replace_with_register_object(object, around, window, cx)
 547                }
 548                Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
 549                Some(Operator::HelixMatch) => {
 550                    self.select_current_object(object, around, window, cx)
 551                }
 552                _ => {
 553                    // Can't do anything for namespace operators. Ignoring
 554                }
 555            },
 556            Some(Operator::HelixNext { around }) => {
 557                self.select_next_object(object, around, window, cx);
 558            }
 559            Some(Operator::HelixPrevious { around }) => {
 560                self.select_previous_object(object, around, window, cx);
 561            }
 562            Some(Operator::DeleteSurrounds) => {
 563                waiting_operator = Some(Operator::DeleteSurrounds);
 564            }
 565            Some(Operator::ChangeSurrounds { target: None, .. }) => {
 566                if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
 567                    waiting_operator = Some(Operator::ChangeSurrounds {
 568                        target: Some(object),
 569                        opening,
 570                    });
 571                }
 572            }
 573            _ => {
 574                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 575            }
 576        }
 577        self.clear_operator(window, cx);
 578        if let Some(operator) = waiting_operator {
 579            self.push_operator(operator, window, cx);
 580        }
 581    }
 582
 583    pub(crate) fn move_cursor(
 584        &mut self,
 585        motion: Motion,
 586        times: Option<usize>,
 587        window: &mut Window,
 588        cx: &mut Context<Self>,
 589    ) {
 590        self.update_editor(cx, |vim, editor, cx| {
 591            let text_layout_details = editor.text_layout_details(window, cx);
 592
 593            // If vim is in temporary mode and the motion being used is
 594            // `EndOfLine` ($), we'll want to disable clipping at line ends so
 595            // that the newline character can be selected so that, when moving
 596            // back to visual mode, the cursor will be placed after the last
 597            // character and not before it.
 598            let clip_at_line_ends = editor.clip_at_line_ends(cx);
 599            let should_disable_clip = matches!(motion, Motion::EndOfLine { .. }) && vim.temp_mode;
 600
 601            if should_disable_clip {
 602                editor.set_clip_at_line_ends(false, cx)
 603            };
 604
 605            editor.change_selections(
 606                SelectionEffects::default().nav_history(motion.push_to_jump_list()),
 607                window,
 608                cx,
 609                |s| {
 610                    s.move_cursors_with(&mut |map, cursor, goal| {
 611                        motion
 612                            .move_point(map, cursor, goal, times, &text_layout_details)
 613                            .unwrap_or((cursor, goal))
 614                    })
 615                },
 616            );
 617
 618            if should_disable_clip {
 619                editor.set_clip_at_line_ends(clip_at_line_ends, cx);
 620            };
 621        });
 622    }
 623
 624    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 625        self.start_recording(cx);
 626        self.switch_mode(Mode::Insert, false, window, cx);
 627        self.update_editor(cx, |_, editor, cx| {
 628            editor.change_selections(Default::default(), window, cx, |s| {
 629                s.move_cursors_with(&mut |map, cursor, _| {
 630                    (right(map, cursor, 1), SelectionGoal::None)
 631                });
 632            });
 633        });
 634    }
 635
 636    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 637        self.start_recording(cx);
 638        if self.mode.is_visual() {
 639            let current_mode = self.mode;
 640            self.update_editor(cx, |_, editor, cx| {
 641                editor.change_selections(Default::default(), window, cx, |s| {
 642                    s.move_with(&mut |map, selection| {
 643                        if current_mode == Mode::VisualLine {
 644                            let start_of_line = motion::start_of_line(map, false, selection.start);
 645                            selection.collapse_to(start_of_line, SelectionGoal::None)
 646                        } else {
 647                            selection.collapse_to(selection.start, SelectionGoal::None)
 648                        }
 649                    });
 650                });
 651            });
 652        }
 653        self.switch_mode(Mode::Insert, false, window, cx);
 654    }
 655
 656    fn insert_first_non_whitespace(
 657        &mut self,
 658        _: &InsertFirstNonWhitespace,
 659        window: &mut Window,
 660        cx: &mut Context<Self>,
 661    ) {
 662        self.start_recording(cx);
 663        self.switch_mode(Mode::Insert, false, window, cx);
 664        self.update_editor(cx, |_, editor, cx| {
 665            editor.change_selections(Default::default(), window, cx, |s| {
 666                s.move_cursors_with(&mut |map, cursor, _| {
 667                    (
 668                        first_non_whitespace(map, false, cursor),
 669                        SelectionGoal::None,
 670                    )
 671                });
 672            });
 673        });
 674    }
 675
 676    fn insert_end_of_line(
 677        &mut self,
 678        _: &InsertEndOfLine,
 679        window: &mut Window,
 680        cx: &mut Context<Self>,
 681    ) {
 682        self.start_recording(cx);
 683        self.switch_mode(Mode::Insert, false, window, cx);
 684        self.update_editor(cx, |_, editor, cx| {
 685            editor.change_selections(Default::default(), window, cx, |s| {
 686                s.move_cursors_with(&mut |map, cursor, _| {
 687                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 688                });
 689            });
 690        });
 691    }
 692
 693    fn insert_at_previous(
 694        &mut self,
 695        _: &InsertAtPrevious,
 696        window: &mut Window,
 697        cx: &mut Context<Self>,
 698    ) {
 699        self.start_recording(cx);
 700        self.switch_mode(Mode::Insert, false, window, cx);
 701        self.update_editor(cx, |vim, editor, cx| {
 702            if let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx)
 703                && !marks.is_empty()
 704            {
 705                editor.change_selections(Default::default(), window, cx, |s| {
 706                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 707                });
 708            }
 709        });
 710    }
 711
 712    fn insert_line_above(
 713        &mut self,
 714        _: &InsertLineAbove,
 715        window: &mut Window,
 716        cx: &mut Context<Self>,
 717    ) {
 718        self.start_recording(cx);
 719        self.switch_mode(Mode::Insert, false, window, cx);
 720        self.update_editor(cx, |_, editor, cx| {
 721            editor.transact(window, cx, |editor, window, cx| {
 722                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 723                let snapshot = editor.buffer().read(cx).snapshot(cx);
 724
 725                let selection_start_rows: BTreeSet<u32> = selections
 726                    .into_iter()
 727                    .map(|selection| selection.start.row)
 728                    .collect();
 729                let edits = selection_start_rows
 730                    .into_iter()
 731                    .map(|row| {
 732                        let indent = snapshot
 733                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 734                            .chars()
 735                            .collect::<String>();
 736
 737                        let start_of_line = Point::new(row, 0);
 738                        (start_of_line..start_of_line, indent + "\n")
 739                    })
 740                    .collect::<Vec<_>>();
 741                editor.edit_with_autoindent(edits, cx);
 742                editor.change_selections(Default::default(), window, cx, |s| {
 743                    s.move_with(&mut |map, selection| {
 744                        let previous_line = map.start_of_relative_buffer_row(selection.start, -1);
 745                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 746                        selection.collapse_to(insert_point, SelectionGoal::None)
 747                    });
 748                });
 749            });
 750        });
 751    }
 752
 753    fn insert_line_below(
 754        &mut self,
 755        _: &InsertLineBelow,
 756        window: &mut Window,
 757        cx: &mut Context<Self>,
 758    ) {
 759        self.start_recording(cx);
 760        self.switch_mode(Mode::Insert, false, window, cx);
 761        self.update_editor(cx, |_, editor, cx| {
 762            editor.transact(window, cx, |editor, window, cx| {
 763                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 764                let snapshot = editor.buffer().read(cx).snapshot(cx);
 765
 766                let selection_end_rows: BTreeSet<u32> = selections
 767                    .into_iter()
 768                    .map(|selection| {
 769                        if !selection.is_empty() && selection.end.column == 0 {
 770                            selection.end.row.saturating_sub(1)
 771                        } else {
 772                            selection.end.row
 773                        }
 774                    })
 775                    .collect();
 776                let edits = selection_end_rows
 777                    .into_iter()
 778                    .map(|row| {
 779                        let indent = snapshot
 780                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 781                            .chars()
 782                            .collect::<String>();
 783
 784                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 785                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 786                    })
 787                    .collect::<Vec<_>>();
 788                editor.change_selections(Default::default(), window, cx, |s| {
 789                    s.move_with(&mut |map, selection| {
 790                        let current_line = if !selection.is_empty() && selection.end.column() == 0 {
 791                            // If this is an insert after a selection to the end of the line, the
 792                            // cursor needs to be bumped back, because it'll be at the start of the
 793                            // *next* line.
 794                            map.start_of_relative_buffer_row(selection.end, -1)
 795                        } else {
 796                            selection.end
 797                        };
 798                        let insert_point = motion::end_of_line(map, false, current_line, 1);
 799                        selection.collapse_to(insert_point, SelectionGoal::None)
 800                    });
 801                });
 802                editor.edit_with_autoindent(edits, cx);
 803            });
 804        });
 805    }
 806
 807    fn insert_empty_line_above(
 808        &mut self,
 809        _: &InsertEmptyLineAbove,
 810        window: &mut Window,
 811        cx: &mut Context<Self>,
 812    ) {
 813        self.record_current_action(cx);
 814        let count = Vim::take_count(cx).unwrap_or(1);
 815        Vim::take_forced_motion(cx);
 816        self.update_editor(cx, |_, editor, cx| {
 817            editor.transact(window, cx, |editor, _, cx| {
 818                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 819
 820                let selection_start_rows: BTreeSet<u32> = selections
 821                    .into_iter()
 822                    .map(|selection| selection.start.row)
 823                    .collect();
 824                let edits = selection_start_rows
 825                    .into_iter()
 826                    .map(|row| {
 827                        let start_of_line = Point::new(row, 0);
 828                        (start_of_line..start_of_line, "\n".repeat(count))
 829                    })
 830                    .collect::<Vec<_>>();
 831                editor.edit(edits, cx);
 832            });
 833        });
 834    }
 835
 836    fn insert_empty_line_below(
 837        &mut self,
 838        _: &InsertEmptyLineBelow,
 839        window: &mut Window,
 840        cx: &mut Context<Self>,
 841    ) {
 842        self.record_current_action(cx);
 843        let count = Vim::take_count(cx).unwrap_or(1);
 844        Vim::take_forced_motion(cx);
 845        self.update_editor(cx, |_, editor, cx| {
 846            editor.transact(window, cx, |editor, window, cx| {
 847                let display_map = editor.display_snapshot(cx);
 848                let selections = editor.selections.all::<Point>(&display_map);
 849                let snapshot = editor.buffer().read(cx).snapshot(cx);
 850                let display_selections = editor.selections.all_display(&display_map);
 851                let original_positions = display_selections
 852                    .iter()
 853                    .map(|s| (s.id, s.head()))
 854                    .collect::<HashMap<_, _>>();
 855
 856                let selection_end_rows: BTreeSet<u32> = selections
 857                    .into_iter()
 858                    .map(|selection| selection.end.row)
 859                    .collect();
 860                let edits = selection_end_rows
 861                    .into_iter()
 862                    .map(|row| {
 863                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 864                        (end_of_line..end_of_line, "\n".repeat(count))
 865                    })
 866                    .collect::<Vec<_>>();
 867                editor.edit(edits, cx);
 868
 869                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 870                    s.move_with(&mut |_, selection| {
 871                        if let Some(position) = original_positions.get(&selection.id) {
 872                            selection.collapse_to(*position, SelectionGoal::None);
 873                        }
 874                    });
 875                });
 876            });
 877        });
 878    }
 879
 880    fn join_lines_impl(
 881        &mut self,
 882        insert_whitespace: bool,
 883        window: &mut Window,
 884        cx: &mut Context<Self>,
 885    ) {
 886        self.record_current_action(cx);
 887        let mut times = Vim::take_count(cx).unwrap_or(1);
 888        Vim::take_forced_motion(cx);
 889        if self.mode.is_visual() {
 890            times = 1;
 891        } else if times > 1 {
 892            // 2J joins two lines together (same as J or 1J)
 893            times -= 1;
 894        }
 895
 896        self.update_editor(cx, |_, editor, cx| {
 897            editor.transact(window, cx, |editor, window, cx| {
 898                for _ in 0..times {
 899                    editor.join_lines_impl(insert_whitespace, window, cx)
 900                }
 901            })
 902        });
 903        if self.mode.is_visual() {
 904            self.switch_mode(Mode::Normal, true, window, cx)
 905        }
 906    }
 907
 908    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 909        let count = Vim::take_count(cx);
 910        let forced_motion = Vim::take_forced_motion(cx);
 911        self.yank_motion(
 912            motion::Motion::CurrentLine,
 913            count,
 914            forced_motion,
 915            window,
 916            cx,
 917        )
 918    }
 919
 920    fn yank_to_end_of_line(
 921        &mut self,
 922        _: &YankToEndOfLine,
 923        window: &mut Window,
 924        cx: &mut Context<Self>,
 925    ) {
 926        let count = Vim::take_count(cx);
 927        let forced_motion = Vim::take_forced_motion(cx);
 928        self.yank_motion(
 929            motion::Motion::EndOfLine {
 930                display_lines: false,
 931            },
 932            count,
 933            forced_motion,
 934            window,
 935            cx,
 936        )
 937    }
 938
 939    fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
 940        let count = Vim::take_count(cx);
 941        Vim::take_forced_motion(cx);
 942        self.update_editor(cx, |vim, editor, cx| {
 943            let selection = editor.selections.newest_anchor();
 944            let Some((buffer, point)) = editor
 945                .buffer()
 946                .read(cx)
 947                .point_to_buffer_point(selection.head(), cx)
 948            else {
 949                return;
 950            };
 951            let filename = if let Some(file) = buffer.read(cx).file() {
 952                if count.is_some() {
 953                    if let Some(local) = file.as_local() {
 954                        local.abs_path(cx).to_string_lossy().into_owned()
 955                    } else {
 956                        file.full_path(cx).to_string_lossy().into_owned()
 957                    }
 958                } else {
 959                    file.path().display(file.path_style(cx)).into_owned()
 960                }
 961            } else {
 962                "[No Name]".into()
 963            };
 964            let buffer = buffer.read(cx);
 965            let lines = buffer.max_point().row + 1;
 966            let current_line = point.row;
 967            let percentage = current_line as f32 / lines as f32;
 968            let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 969            vim.set_status_label(
 970                format!(
 971                    "{}{} {} lines --{:.0}%--",
 972                    filename,
 973                    modified,
 974                    lines,
 975                    percentage * 100.0,
 976                ),
 977                cx,
 978            );
 979        });
 980    }
 981
 982    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 983        self.record_current_action(cx);
 984        self.store_visual_marks(window, cx);
 985        self.update_editor(cx, |vim, editor, cx| {
 986            editor.transact(window, cx, |editor, window, cx| {
 987                let original_positions = vim.save_selection_starts(editor, cx);
 988                editor.toggle_comments(&Default::default(), window, cx);
 989                vim.restore_selection_cursors(editor, window, cx, original_positions);
 990            });
 991        });
 992        if self.mode.is_visual() {
 993            self.switch_mode(Mode::Normal, true, window, cx)
 994        }
 995    }
 996
 997    fn toggle_block_comments(
 998        &mut self,
 999        _: &ToggleBlockComments,
1000        window: &mut Window,
1001        cx: &mut Context<Self>,
1002    ) {
1003        self.record_current_action(cx);
1004        self.store_visual_marks(window, cx);
1005        let is_visual_line = self.mode == Mode::VisualLine;
1006        self.update_editor(cx, |vim, editor, cx| {
1007            editor.transact(window, cx, |editor, window, cx| {
1008                let original_positions = vim.save_selection_starts(editor, cx);
1009                if is_visual_line {
1010                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1011                        s.move_with(&mut |map, selection| {
1012                            let start_row = selection.start.to_point(map).row;
1013                            let end_row = selection.end.to_point(map).row;
1014                            let end_col = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1015                            selection.start = Point::new(start_row, 0).to_display_point(map);
1016                            selection.end = Point::new(end_row, end_col).to_display_point(map);
1017                        });
1018                    });
1019                }
1020                editor.toggle_block_comments(&Default::default(), window, cx);
1021                vim.restore_selection_cursors(editor, window, cx, original_positions);
1022            });
1023        });
1024        if self.mode.is_visual() {
1025            self.switch_mode(Mode::Normal, true, window, cx)
1026        }
1027    }
1028
1029    pub(crate) fn normal_replace(
1030        &mut self,
1031        text: Arc<str>,
1032        window: &mut Window,
1033        cx: &mut Context<Self>,
1034    ) {
1035        // We need to use `text.chars().count()` instead of `text.len()` here as
1036        // `len()` counts bytes, not characters.
1037        let char_count = text.chars().count();
1038        let count = Vim::take_count(cx).unwrap_or(char_count);
1039        let is_return_char = text == "\n".into() || text == "\r".into();
1040        let repeat_count = match (is_return_char, char_count) {
1041            (true, _) => 0,
1042            (_, 1) => count,
1043            (_, _) => 1,
1044        };
1045
1046        Vim::take_forced_motion(cx);
1047        self.stop_recording(cx);
1048        self.update_editor(cx, |_, editor, cx| {
1049            editor.transact(window, cx, |editor, window, cx| {
1050                editor.set_clip_at_line_ends(false, cx);
1051                let display_map = editor.display_snapshot(cx);
1052                let display_selections = editor.selections.all_display(&display_map);
1053
1054                let mut edits = Vec::with_capacity(display_selections.len());
1055                for selection in &display_selections {
1056                    let mut range = selection.range();
1057                    for _ in 0..count {
1058                        let new_point = movement::saturating_right(&display_map, range.end);
1059                        if range.end == new_point {
1060                            return;
1061                        }
1062                        range.end = new_point;
1063                    }
1064
1065                    edits.push((
1066                        range.start.to_offset(&display_map, Bias::Left)
1067                            ..range.end.to_offset(&display_map, Bias::Left),
1068                        text.repeat(repeat_count),
1069                    ));
1070                }
1071
1072                editor.edit(edits, cx);
1073                if is_return_char {
1074                    editor.newline(&editor::actions::Newline, window, cx);
1075                }
1076                editor.set_clip_at_line_ends(true, cx);
1077                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1078                    s.move_with(&mut |map, selection| {
1079                        let point = movement::saturating_left(map, selection.head());
1080                        selection.collapse_to(point, SelectionGoal::None)
1081                    });
1082                });
1083            });
1084        });
1085        self.pop_operator(window, cx);
1086    }
1087
1088    pub fn save_selection_starts(
1089        &self,
1090        editor: &Editor,
1091        cx: &mut Context<Editor>,
1092    ) -> HashMap<usize, Anchor> {
1093        let display_map = editor.display_snapshot(cx);
1094        let selections = editor.selections.all_display(&display_map);
1095        selections
1096            .iter()
1097            .map(|selection| {
1098                (
1099                    selection.id,
1100                    display_map.display_point_to_anchor(selection.start, Bias::Right),
1101                )
1102            })
1103            .collect::<HashMap<_, _>>()
1104    }
1105
1106    pub fn restore_selection_cursors(
1107        &self,
1108        editor: &mut Editor,
1109        window: &mut Window,
1110        cx: &mut Context<Editor>,
1111        mut positions: HashMap<usize, Anchor>,
1112    ) {
1113        editor.change_selections(Default::default(), window, cx, |s| {
1114            s.move_with(&mut |map, selection| {
1115                if let Some(anchor) = positions.remove(&selection.id) {
1116                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1117                }
1118            });
1119        });
1120    }
1121
1122    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1123        if self.temp_mode {
1124            self.switch_mode(Mode::Insert, true, window, cx);
1125        }
1126    }
1127}
1128
1129#[cfg(test)]
1130mod test {
1131    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1132    use indoc::indoc;
1133    use settings::SettingsStore;
1134
1135    use crate::{
1136        motion,
1137        state::Mode::{self},
1138        test::{NeovimBackedTestContext, VimTestContext},
1139    };
1140
1141    #[gpui::test]
1142    async fn test_h(cx: &mut gpui::TestAppContext) {
1143        let mut cx = NeovimBackedTestContext::new(cx).await;
1144        cx.simulate_at_each_offset(
1145            "h",
1146            indoc! {"
1147            ˇThe qˇuick
1148            ˇbrown"
1149            },
1150        )
1151        .await
1152        .assert_matches();
1153    }
1154
1155    #[gpui::test]
1156    async fn test_backspace(cx: &mut gpui::TestAppContext) {
1157        let mut cx = NeovimBackedTestContext::new(cx).await;
1158        cx.simulate_at_each_offset(
1159            "backspace",
1160            indoc! {"
1161            ˇThe qˇuick
1162            ˇbrown"
1163            },
1164        )
1165        .await
1166        .assert_matches();
1167    }
1168
1169    #[gpui::test]
1170    async fn test_j(cx: &mut gpui::TestAppContext) {
1171        let mut cx = NeovimBackedTestContext::new(cx).await;
1172
1173        cx.set_shared_state(indoc! {"
1174            aaˇaa
1175            😃😃"
1176        })
1177        .await;
1178        cx.simulate_shared_keystrokes("j").await;
1179        cx.shared_state().await.assert_eq(indoc! {"
1180            aaaa
1181            😃ˇ😃"
1182        });
1183
1184        cx.simulate_at_each_offset(
1185            "j",
1186            indoc! {"
1187                ˇThe qˇuick broˇwn
1188                ˇfox jumps"
1189            },
1190        )
1191        .await
1192        .assert_matches();
1193    }
1194
1195    #[gpui::test]
1196    async fn test_enter(cx: &mut gpui::TestAppContext) {
1197        let mut cx = NeovimBackedTestContext::new(cx).await;
1198        cx.simulate_at_each_offset(
1199            "enter",
1200            indoc! {"
1201            ˇThe qˇuick broˇwn
1202            ˇfox jumps"
1203            },
1204        )
1205        .await
1206        .assert_matches();
1207    }
1208
1209    #[gpui::test]
1210    async fn test_k(cx: &mut gpui::TestAppContext) {
1211        let mut cx = NeovimBackedTestContext::new(cx).await;
1212        cx.simulate_at_each_offset(
1213            "k",
1214            indoc! {"
1215            ˇThe qˇuick
1216            ˇbrown fˇox jumˇps"
1217            },
1218        )
1219        .await
1220        .assert_matches();
1221    }
1222
1223    #[gpui::test]
1224    async fn test_l(cx: &mut gpui::TestAppContext) {
1225        let mut cx = NeovimBackedTestContext::new(cx).await;
1226        cx.simulate_at_each_offset(
1227            "l",
1228            indoc! {"
1229            ˇThe qˇuicˇk
1230            ˇbrowˇn"},
1231        )
1232        .await
1233        .assert_matches();
1234    }
1235
1236    #[gpui::test]
1237    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1238        let mut cx = NeovimBackedTestContext::new(cx).await;
1239        cx.simulate_at_each_offset(
1240            "$",
1241            indoc! {"
1242            ˇThe qˇuicˇk
1243            ˇbrowˇn"},
1244        )
1245        .await
1246        .assert_matches();
1247        cx.simulate_at_each_offset(
1248            "0",
1249            indoc! {"
1250                ˇThe qˇuicˇk
1251                ˇbrowˇn"},
1252        )
1253        .await
1254        .assert_matches();
1255    }
1256
1257    #[gpui::test]
1258    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1259        let mut cx = NeovimBackedTestContext::new(cx).await;
1260
1261        cx.simulate_at_each_offset(
1262            "shift-g",
1263            indoc! {"
1264                The ˇquick
1265
1266                brown fox jumps
1267                overˇ the lazy doˇg"},
1268        )
1269        .await
1270        .assert_matches();
1271        cx.simulate(
1272            "shift-g",
1273            indoc! {"
1274            The quiˇck
1275
1276            brown"},
1277        )
1278        .await
1279        .assert_matches();
1280        cx.simulate(
1281            "shift-g",
1282            indoc! {"
1283            The quiˇck
1284
1285            "},
1286        )
1287        .await
1288        .assert_matches();
1289    }
1290
1291    #[gpui::test]
1292    async fn test_w(cx: &mut gpui::TestAppContext) {
1293        let mut cx = NeovimBackedTestContext::new(cx).await;
1294        cx.simulate_at_each_offset(
1295            "w",
1296            indoc! {"
1297            The ˇquickˇ-ˇbrown
1298            ˇ
1299            ˇ
1300            ˇfox_jumps ˇover
1301            ˇthˇe"},
1302        )
1303        .await
1304        .assert_matches();
1305        cx.simulate_at_each_offset(
1306            "shift-w",
1307            indoc! {"
1308            The ˇquickˇ-ˇbrown
1309            ˇ
1310            ˇ
1311            ˇfox_jumps ˇover
1312            ˇthˇe"},
1313        )
1314        .await
1315        .assert_matches();
1316    }
1317
1318    #[gpui::test]
1319    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1320        let mut cx = NeovimBackedTestContext::new(cx).await;
1321        cx.simulate_at_each_offset(
1322            "e",
1323            indoc! {"
1324            Thˇe quicˇkˇ-browˇn
1325
1326
1327            fox_jumpˇs oveˇr
1328            thˇe"},
1329        )
1330        .await
1331        .assert_matches();
1332        cx.simulate_at_each_offset(
1333            "shift-e",
1334            indoc! {"
1335            Thˇe quicˇkˇ-browˇn
1336
1337
1338            fox_jumpˇs oveˇr
1339            thˇe"},
1340        )
1341        .await
1342        .assert_matches();
1343    }
1344
1345    #[gpui::test]
1346    async fn test_b(cx: &mut gpui::TestAppContext) {
1347        let mut cx = NeovimBackedTestContext::new(cx).await;
1348        cx.simulate_at_each_offset(
1349            "b",
1350            indoc! {"
1351            ˇThe ˇquickˇ-ˇbrown
1352            ˇ
1353            ˇ
1354            ˇfox_jumps ˇover
1355            ˇthe"},
1356        )
1357        .await
1358        .assert_matches();
1359        cx.simulate_at_each_offset(
1360            "shift-b",
1361            indoc! {"
1362            ˇThe ˇquickˇ-ˇbrown
1363            ˇ
1364            ˇ
1365            ˇfox_jumps ˇover
1366            ˇthe"},
1367        )
1368        .await
1369        .assert_matches();
1370    }
1371
1372    #[gpui::test]
1373    async fn test_gg(cx: &mut gpui::TestAppContext) {
1374        let mut cx = NeovimBackedTestContext::new(cx).await;
1375        cx.simulate_at_each_offset(
1376            "g g",
1377            indoc! {"
1378                The qˇuick
1379
1380                brown fox jumps
1381                over ˇthe laˇzy dog"},
1382        )
1383        .await
1384        .assert_matches();
1385        cx.simulate(
1386            "g g",
1387            indoc! {"
1388
1389
1390                brown fox jumps
1391                over the laˇzy dog"},
1392        )
1393        .await
1394        .assert_matches();
1395        cx.simulate(
1396            "2 g g",
1397            indoc! {"
1398                ˇ
1399
1400                brown fox jumps
1401                over the lazydog"},
1402        )
1403        .await
1404        .assert_matches();
1405    }
1406
1407    #[gpui::test]
1408    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1409        let mut cx = NeovimBackedTestContext::new(cx).await;
1410        cx.simulate_at_each_offset(
1411            "shift-g",
1412            indoc! {"
1413                The qˇuick
1414
1415                brown fox jumps
1416                over ˇthe laˇzy dog"},
1417        )
1418        .await
1419        .assert_matches();
1420        cx.simulate(
1421            "shift-g",
1422            indoc! {"
1423
1424
1425                brown fox jumps
1426                over the laˇzy dog"},
1427        )
1428        .await
1429        .assert_matches();
1430        cx.simulate(
1431            "2 shift-g",
1432            indoc! {"
1433                ˇ
1434
1435                brown fox jumps
1436                over the lazydog"},
1437        )
1438        .await
1439        .assert_matches();
1440    }
1441
1442    #[gpui::test]
1443    async fn test_a(cx: &mut gpui::TestAppContext) {
1444        let mut cx = NeovimBackedTestContext::new(cx).await;
1445        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1446            .await
1447            .assert_matches();
1448    }
1449
1450    #[gpui::test]
1451    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1452        let mut cx = NeovimBackedTestContext::new(cx).await;
1453        cx.simulate_at_each_offset(
1454            "shift-a",
1455            indoc! {"
1456            ˇ
1457            The qˇuick
1458            brown ˇfox "},
1459        )
1460        .await
1461        .assert_matches();
1462    }
1463
1464    #[gpui::test]
1465    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1466        let mut cx = NeovimBackedTestContext::new(cx).await;
1467        cx.simulate("^", "The qˇuick").await.assert_matches();
1468        cx.simulate("^", " The qˇuick").await.assert_matches();
1469        cx.simulate("^", "ˇ").await.assert_matches();
1470        cx.simulate(
1471            "^",
1472            indoc! {"
1473                The qˇuick
1474                brown fox"},
1475        )
1476        .await
1477        .assert_matches();
1478        cx.simulate(
1479            "^",
1480            indoc! {"
1481                ˇ
1482                The quick"},
1483        )
1484        .await
1485        .assert_matches();
1486        // Indoc disallows trailing whitespace.
1487        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1488    }
1489
1490    #[gpui::test]
1491    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1492        let mut cx = NeovimBackedTestContext::new(cx).await;
1493        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1494        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1495        cx.simulate("shift-i", "ˇ").await.assert_matches();
1496        cx.simulate(
1497            "shift-i",
1498            indoc! {"
1499                The qˇuick
1500                brown fox"},
1501        )
1502        .await
1503        .assert_matches();
1504        cx.simulate(
1505            "shift-i",
1506            indoc! {"
1507                ˇ
1508                The quick"},
1509        )
1510        .await
1511        .assert_matches();
1512    }
1513
1514    #[gpui::test]
1515    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1516        let mut cx = NeovimBackedTestContext::new(cx).await;
1517        cx.simulate(
1518            "shift-d",
1519            indoc! {"
1520                The qˇuick
1521                brown fox"},
1522        )
1523        .await
1524        .assert_matches();
1525        cx.simulate(
1526            "shift-d",
1527            indoc! {"
1528                The quick
1529                ˇ
1530                brown fox"},
1531        )
1532        .await
1533        .assert_matches();
1534    }
1535
1536    #[gpui::test]
1537    async fn test_x(cx: &mut gpui::TestAppContext) {
1538        let mut cx = NeovimBackedTestContext::new(cx).await;
1539        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1540            .await
1541            .assert_matches();
1542        cx.simulate(
1543            "x",
1544            indoc! {"
1545                Tesˇt
1546                test"},
1547        )
1548        .await
1549        .assert_matches();
1550    }
1551
1552    #[gpui::test]
1553    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1554        let mut cx = NeovimBackedTestContext::new(cx).await;
1555        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1556            .await
1557            .assert_matches();
1558        cx.simulate(
1559            "shift-x",
1560            indoc! {"
1561                Test
1562                ˇtest"},
1563        )
1564        .await
1565        .assert_matches();
1566    }
1567
1568    #[gpui::test]
1569    async fn test_o(cx: &mut gpui::TestAppContext) {
1570        let mut cx = NeovimBackedTestContext::new(cx).await;
1571        cx.simulate("o", "ˇ").await.assert_matches();
1572        cx.simulate("o", "The ˇquick").await.assert_matches();
1573        cx.simulate_at_each_offset(
1574            "o",
1575            indoc! {"
1576                The qˇuick
1577                brown ˇfox
1578                jumps ˇover"},
1579        )
1580        .await
1581        .assert_matches();
1582        cx.simulate(
1583            "o",
1584            indoc! {"
1585                The quick
1586                ˇ
1587                brown fox"},
1588        )
1589        .await
1590        .assert_matches();
1591
1592        cx.assert_binding(
1593            "o",
1594            indoc! {"
1595                fn test() {
1596                    println!(ˇ);
1597                }"},
1598            Mode::Normal,
1599            indoc! {"
1600                fn test() {
1601                    println!();
1602                    ˇ
1603                }"},
1604            Mode::Insert,
1605        );
1606
1607        cx.assert_binding(
1608            "o",
1609            indoc! {"
1610                fn test(ˇ) {
1611                    println!();
1612                }"},
1613            Mode::Normal,
1614            indoc! {"
1615                fn test() {
1616                    ˇ
1617                    println!();
1618                }"},
1619            Mode::Insert,
1620        );
1621    }
1622
1623    #[gpui::test]
1624    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1625        let mut cx = NeovimBackedTestContext::new(cx).await;
1626        cx.simulate("shift-o", "ˇ").await.assert_matches();
1627        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1628        cx.simulate_at_each_offset(
1629            "shift-o",
1630            indoc! {"
1631            The qˇuick
1632            brown ˇfox
1633            jumps ˇover"},
1634        )
1635        .await
1636        .assert_matches();
1637        cx.simulate(
1638            "shift-o",
1639            indoc! {"
1640            The quick
1641            ˇ
1642            brown fox"},
1643        )
1644        .await
1645        .assert_matches();
1646
1647        // Our indentation is smarter than vims. So we don't match here
1648        cx.assert_binding(
1649            "shift-o",
1650            indoc! {"
1651                fn test() {
1652                    println!(ˇ);
1653                }"},
1654            Mode::Normal,
1655            indoc! {"
1656                fn test() {
1657                    ˇ
1658                    println!();
1659                }"},
1660            Mode::Insert,
1661        );
1662        cx.assert_binding(
1663            "shift-o",
1664            indoc! {"
1665                fn test(ˇ) {
1666                    println!();
1667                }"},
1668            Mode::Normal,
1669            indoc! {"
1670                ˇ
1671                fn test() {
1672                    println!();
1673                }"},
1674            Mode::Insert,
1675        );
1676    }
1677
1678    #[gpui::test]
1679    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1680        let mut cx = NeovimBackedTestContext::new(cx).await;
1681        cx.simulate("[ space", "ˇ").await.assert_matches();
1682        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1683        cx.simulate_at_each_offset(
1684            "3 [ space",
1685            indoc! {"
1686            The qˇuick
1687            brown ˇfox
1688            jumps ˇover"},
1689        )
1690        .await
1691        .assert_matches();
1692        cx.simulate_at_each_offset(
1693            "[ space",
1694            indoc! {"
1695            The qˇuick
1696            brown ˇfox
1697            jumps ˇover"},
1698        )
1699        .await
1700        .assert_matches();
1701        cx.simulate(
1702            "[ space",
1703            indoc! {"
1704            The quick
1705            ˇ
1706            brown fox"},
1707        )
1708        .await
1709        .assert_matches();
1710
1711        cx.simulate("] space", "ˇ").await.assert_matches();
1712        cx.simulate("] space", "The ˇquick").await.assert_matches();
1713        cx.simulate_at_each_offset(
1714            "3 ] space",
1715            indoc! {"
1716            The qˇuick
1717            brown ˇfox
1718            jumps ˇover"},
1719        )
1720        .await
1721        .assert_matches();
1722        cx.simulate_at_each_offset(
1723            "] space",
1724            indoc! {"
1725            The qˇuick
1726            brown ˇfox
1727            jumps ˇover"},
1728        )
1729        .await
1730        .assert_matches();
1731        cx.simulate(
1732            "] space",
1733            indoc! {"
1734            The quick
1735            ˇ
1736            brown fox"},
1737        )
1738        .await
1739        .assert_matches();
1740    }
1741
1742    #[gpui::test]
1743    async fn test_dd(cx: &mut gpui::TestAppContext) {
1744        let mut cx = NeovimBackedTestContext::new(cx).await;
1745        cx.simulate("d d", "ˇ").await.assert_matches();
1746        cx.simulate("d d", "The ˇquick").await.assert_matches();
1747        cx.simulate_at_each_offset(
1748            "d d",
1749            indoc! {"
1750            The qˇuick
1751            brown ˇfox
1752            jumps ˇover"},
1753        )
1754        .await
1755        .assert_matches();
1756        cx.simulate(
1757            "d d",
1758            indoc! {"
1759                The quick
1760                ˇ
1761                brown fox"},
1762        )
1763        .await
1764        .assert_matches();
1765    }
1766
1767    #[gpui::test]
1768    async fn test_cc(cx: &mut gpui::TestAppContext) {
1769        let mut cx = NeovimBackedTestContext::new(cx).await;
1770        cx.simulate("c c", "ˇ").await.assert_matches();
1771        cx.simulate("c c", "The ˇquick").await.assert_matches();
1772        cx.simulate_at_each_offset(
1773            "c c",
1774            indoc! {"
1775                The quˇick
1776                brown ˇfox
1777                jumps ˇover"},
1778        )
1779        .await
1780        .assert_matches();
1781        cx.simulate(
1782            "c c",
1783            indoc! {"
1784                The quick
1785                ˇ
1786                brown fox"},
1787        )
1788        .await
1789        .assert_matches();
1790    }
1791
1792    #[gpui::test]
1793    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1794        let mut cx = NeovimBackedTestContext::new(cx).await;
1795
1796        for count in 1..=5 {
1797            cx.simulate_at_each_offset(
1798                &format!("{count} w"),
1799                indoc! {"
1800                    ˇThe quˇickˇ browˇn
1801                    ˇ
1802                    ˇfox ˇjumpsˇ-ˇoˇver
1803                    ˇthe lazy dog
1804                "},
1805            )
1806            .await
1807            .assert_matches();
1808        }
1809    }
1810
1811    #[gpui::test]
1812    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1813        let mut cx = NeovimBackedTestContext::new(cx).await;
1814        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1815            .await
1816            .assert_matches();
1817    }
1818
1819    #[gpui::test]
1820    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1821        let mut cx = NeovimBackedTestContext::new(cx).await;
1822
1823        for count in 1..=3 {
1824            let test_case = indoc! {"
1825                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1826                ˇ    ˇbˇaaˇa ˇbˇbˇb
1827                ˇ
1828                ˇb
1829            "};
1830
1831            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1832                .await
1833                .assert_matches();
1834
1835            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1836                .await
1837                .assert_matches();
1838        }
1839    }
1840
1841    #[gpui::test]
1842    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1843        let mut cx = NeovimBackedTestContext::new(cx).await;
1844        let test_case = indoc! {"
1845            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1846            ˇ    ˇbˇaaˇa ˇbˇbˇb
1847            ˇ•••
1848            ˇb
1849            "
1850        };
1851
1852        for count in 1..=3 {
1853            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1854                .await
1855                .assert_matches();
1856
1857            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1858                .await
1859                .assert_matches();
1860        }
1861    }
1862
1863    #[gpui::test]
1864    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1865        let mut cx = VimTestContext::new(cx, true).await;
1866        cx.update_global(|store: &mut SettingsStore, cx| {
1867            store.update_user_settings(cx, |s| {
1868                s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1869            });
1870        });
1871
1872        cx.assert_binding(
1873            "f p",
1874            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1875            Mode::Normal,
1876            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1877            Mode::Normal,
1878        );
1879
1880        cx.assert_binding(
1881            "shift-f p",
1882            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1883            Mode::Normal,
1884            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1885            Mode::Normal,
1886        );
1887
1888        cx.assert_binding(
1889            "t p",
1890            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1891            Mode::Normal,
1892            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1893            Mode::Normal,
1894        );
1895
1896        cx.assert_binding(
1897            "shift-t p",
1898            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1899            Mode::Normal,
1900            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1901            Mode::Normal,
1902        );
1903    }
1904
1905    #[gpui::test]
1906    async fn test_percent(cx: &mut TestAppContext) {
1907        let mut cx = NeovimBackedTestContext::new(cx).await;
1908        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1909            .await
1910            .assert_matches();
1911        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1912            .await
1913            .assert_matches();
1914        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1915            .await
1916            .assert_matches();
1917    }
1918
1919    #[gpui::test]
1920    async fn test_percent_in_comment(cx: &mut TestAppContext) {
1921        let mut cx = NeovimBackedTestContext::new(cx).await;
1922        cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1923            .await
1924            .assert_matches();
1925        cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
1926            .await
1927            .assert_matches();
1928        // Template-style brackets (like Liquid {% %} and {{ }})
1929        cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
1930            .await
1931            .assert_matches();
1932        cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
1933            .await
1934            .assert_matches();
1935    }
1936
1937    #[gpui::test]
1938    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1939        let mut cx = NeovimBackedTestContext::new(cx).await;
1940
1941        // goes to current line end
1942        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1943        cx.simulate_shared_keystrokes("$").await;
1944        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1945
1946        // goes to next line end
1947        cx.simulate_shared_keystrokes("2 $").await;
1948        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1949
1950        // try to exceed the final line.
1951        cx.simulate_shared_keystrokes("4 $").await;
1952        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1953    }
1954
1955    #[gpui::test]
1956    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1957        let mut cx = VimTestContext::new(cx, true).await;
1958        cx.update(|_, cx| {
1959            cx.bind_keys(vec![
1960                KeyBinding::new(
1961                    "w",
1962                    motion::NextSubwordStart {
1963                        ignore_punctuation: false,
1964                    },
1965                    Some("Editor && VimControl && !VimWaiting && !menu"),
1966                ),
1967                KeyBinding::new(
1968                    "b",
1969                    motion::PreviousSubwordStart {
1970                        ignore_punctuation: false,
1971                    },
1972                    Some("Editor && VimControl && !VimWaiting && !menu"),
1973                ),
1974                KeyBinding::new(
1975                    "e",
1976                    motion::NextSubwordEnd {
1977                        ignore_punctuation: false,
1978                    },
1979                    Some("Editor && VimControl && !VimWaiting && !menu"),
1980                ),
1981                KeyBinding::new(
1982                    "g e",
1983                    motion::PreviousSubwordEnd {
1984                        ignore_punctuation: false,
1985                    },
1986                    Some("Editor && VimControl && !VimWaiting && !menu"),
1987                ),
1988            ]);
1989        });
1990
1991        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1992        // Special case: In 'cw', 'w' acts like 'e'
1993        cx.assert_binding(
1994            "c w",
1995            indoc! {"ˇassert_binding"},
1996            Mode::Normal,
1997            indoc! {"ˇ_binding"},
1998            Mode::Insert,
1999        );
2000
2001        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
2002
2003        // Subword end should stop at EOL
2004        cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"});
2005
2006        // Already at subword end, should move to next subword on next line
2007        cx.assert_binding_normal(
2008            "e",
2009            indoc! {"foo_barˇ\nbaz_qux"},
2010            indoc! {"foo_bar\nbaˇz_qux"},
2011        );
2012
2013        // CamelCase at EOL
2014        cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"});
2015
2016        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
2017
2018        cx.assert_binding_normal(
2019            "g e",
2020            indoc! {"assert_bindinˇg"},
2021            indoc! {"asserˇt_binding"},
2022        );
2023    }
2024
2025    #[gpui::test]
2026    async fn test_r(cx: &mut gpui::TestAppContext) {
2027        let mut cx = NeovimBackedTestContext::new(cx).await;
2028
2029        cx.set_shared_state("ˇhello\n").await;
2030        cx.simulate_shared_keystrokes("r -").await;
2031        cx.shared_state().await.assert_eq("ˇ-ello\n");
2032
2033        cx.set_shared_state("ˇhello\n").await;
2034        cx.simulate_shared_keystrokes("3 r -").await;
2035        cx.shared_state().await.assert_eq("--ˇ-lo\n");
2036
2037        cx.set_shared_state("ˇhello\n").await;
2038        cx.simulate_shared_keystrokes("r - 2 l .").await;
2039        cx.shared_state().await.assert_eq("-eˇ-lo\n");
2040
2041        cx.set_shared_state("ˇhello world\n").await;
2042        cx.simulate_shared_keystrokes("2 r - f w .").await;
2043        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
2044
2045        cx.set_shared_state("ˇhello world\n").await;
2046        cx.simulate_shared_keystrokes("2 0 r - ").await;
2047        cx.shared_state().await.assert_eq("ˇhello world\n");
2048
2049        cx.set_shared_state("  helloˇ world\n").await;
2050        cx.simulate_shared_keystrokes("r enter").await;
2051        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
2052
2053        cx.set_shared_state("  helloˇ world\n").await;
2054        cx.simulate_shared_keystrokes("2 r enter").await;
2055        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
2056    }
2057
2058    #[gpui::test]
2059    async fn test_gq(cx: &mut gpui::TestAppContext) {
2060        let mut cx = NeovimBackedTestContext::new(cx).await;
2061        cx.set_neovim_option("textwidth=5").await;
2062
2063        cx.update(|_, cx| {
2064            SettingsStore::update_global(cx, |settings, cx| {
2065                settings.update_user_settings(cx, |settings| {
2066                    settings
2067                        .project
2068                        .all_languages
2069                        .defaults
2070                        .preferred_line_length = Some(5);
2071                });
2072            })
2073        });
2074
2075        cx.set_shared_state("ˇth th th th th th\n").await;
2076        cx.simulate_shared_keystrokes("g q q").await;
2077        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
2078
2079        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
2080            .await;
2081        cx.simulate_shared_keystrokes("v j g q").await;
2082        cx.shared_state()
2083            .await
2084            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
2085    }
2086
2087    #[gpui::test]
2088    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
2089        let mut cx = NeovimBackedTestContext::new(cx).await;
2090        cx.set_neovim_option("filetype=rust").await;
2091
2092        cx.set_shared_state("// helloˇ\n").await;
2093        cx.simulate_shared_keystrokes("o").await;
2094        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
2095        cx.simulate_shared_keystrokes("x escape shift-o").await;
2096        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
2097    }
2098
2099    #[gpui::test]
2100    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2101        let mut cx = NeovimBackedTestContext::new(cx).await;
2102        cx.set_shared_state("heˇllo\n").await;
2103        cx.simulate_shared_keystrokes("y y p").await;
2104        cx.shared_state().await.assert_eq("hello\nˇhello\n");
2105    }
2106
2107    #[gpui::test]
2108    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2109        let mut cx = NeovimBackedTestContext::new(cx).await;
2110        cx.set_shared_state("heˇllo").await;
2111        cx.simulate_shared_keystrokes("y y p").await;
2112        cx.shared_state().await.assert_eq("hello\nˇhello");
2113    }
2114
2115    #[gpui::test]
2116    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2117        let mut cx = NeovimBackedTestContext::new(cx).await;
2118        cx.set_shared_state("heˇllo\nhello").await;
2119        cx.simulate_shared_keystrokes("2 y y p").await;
2120        cx.shared_state()
2121            .await
2122            .assert_eq("hello\nˇhello\nhello\nhello");
2123    }
2124
2125    #[gpui::test]
2126    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2127        let mut cx = NeovimBackedTestContext::new(cx).await;
2128        cx.set_shared_state("heˇllo").await;
2129        cx.simulate_shared_keystrokes("d d").await;
2130        cx.shared_state().await.assert_eq("ˇ");
2131        cx.simulate_shared_keystrokes("p p").await;
2132        cx.shared_state().await.assert_eq("\nhello\nˇhello");
2133    }
2134
2135    #[gpui::test]
2136    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2137        let mut cx = NeovimBackedTestContext::new(cx).await;
2138
2139        cx.set_shared_state("heˇllo").await;
2140        cx.simulate_shared_keystrokes("v i w shift-i").await;
2141        cx.shared_state().await.assert_eq("ˇhello");
2142
2143        cx.set_shared_state(indoc! {"
2144            The quick brown
2145            fox ˇjumps over
2146            the lazy dog"})
2147            .await;
2148        cx.simulate_shared_keystrokes("shift-v shift-i").await;
2149        cx.shared_state().await.assert_eq(indoc! {"
2150            The quick brown
2151            ˇfox jumps over
2152            the lazy dog"});
2153
2154        cx.set_shared_state(indoc! {"
2155            The quick brown
2156            fox ˇjumps over
2157            the lazy dog"})
2158            .await;
2159        cx.simulate_shared_keystrokes("shift-v shift-a").await;
2160        cx.shared_state().await.assert_eq(indoc! {"
2161            The quick brown
2162            fox jˇumps over
2163            the lazy dog"});
2164    }
2165
2166    #[gpui::test]
2167    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2168        let mut cx = NeovimBackedTestContext::new(cx).await;
2169
2170        cx.set_shared_state(indoc! {"
2171            ˇfn a() { }
2172
2173
2174
2175
2176
2177            fn b() { }
2178
2179
2180
2181
2182
2183            fn b() { }"})
2184            .await;
2185        cx.simulate_shared_keystrokes("3 }").await;
2186        cx.shared_state().await.assert_matches();
2187        cx.simulate_shared_keystrokes("ctrl-o").await;
2188        cx.shared_state().await.assert_matches();
2189        cx.simulate_shared_keystrokes("ctrl-i").await;
2190        cx.shared_state().await.assert_matches();
2191        cx.simulate_shared_keystrokes("1 1 k").await;
2192        cx.shared_state().await.assert_matches();
2193        cx.simulate_shared_keystrokes("ctrl-o").await;
2194        cx.shared_state().await.assert_matches();
2195    }
2196
2197    #[gpui::test]
2198    async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2199        let mut cx = NeovimBackedTestContext::new(cx).await;
2200
2201        cx.set_shared_state(indoc! {"
2202            ˇfn a() { }
2203            fn a() { }
2204            fn a() { }
2205        "})
2206            .await;
2207        // do a jump to reset vim's undo grouping
2208        cx.simulate_shared_keystrokes("shift-g").await;
2209        cx.shared_state().await.assert_matches();
2210        cx.simulate_shared_keystrokes("r a").await;
2211        cx.shared_state().await.assert_matches();
2212        cx.simulate_shared_keystrokes("shift-u").await;
2213        cx.shared_state().await.assert_matches();
2214        cx.simulate_shared_keystrokes("shift-u").await;
2215        cx.shared_state().await.assert_matches();
2216        cx.simulate_shared_keystrokes("g g shift-u").await;
2217        cx.shared_state().await.assert_matches();
2218    }
2219
2220    #[gpui::test]
2221    async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2222        let mut cx = NeovimBackedTestContext::new(cx).await;
2223
2224        cx.set_shared_state(indoc! {"
2225            ˇfn a() { }
2226            fn a() { }
2227            fn a() { }
2228        "})
2229            .await;
2230        // do a jump to reset vim's undo grouping
2231        cx.simulate_shared_keystrokes("shift-g k").await;
2232        cx.shared_state().await.assert_matches();
2233        cx.simulate_shared_keystrokes("o h e l l o escape").await;
2234        cx.shared_state().await.assert_matches();
2235        cx.simulate_shared_keystrokes("shift-u").await;
2236        cx.shared_state().await.assert_matches();
2237        cx.simulate_shared_keystrokes("shift-u").await;
2238    }
2239
2240    #[gpui::test]
2241    async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2242        let mut cx = NeovimBackedTestContext::new(cx).await;
2243
2244        cx.set_shared_state(indoc! {"
2245            ˇfn a() { }
2246            fn a() { }
2247            fn a() { }
2248        "})
2249            .await;
2250        // do a jump to reset vim's undo grouping
2251        cx.simulate_shared_keystrokes("x shift-g k").await;
2252        cx.shared_state().await.assert_matches();
2253        cx.simulate_shared_keystrokes("x f a x f { x").await;
2254        cx.shared_state().await.assert_matches();
2255        cx.simulate_shared_keystrokes("shift-u").await;
2256        cx.shared_state().await.assert_matches();
2257        cx.simulate_shared_keystrokes("shift-u").await;
2258        cx.shared_state().await.assert_matches();
2259        cx.simulate_shared_keystrokes("shift-u").await;
2260        cx.shared_state().await.assert_matches();
2261        cx.simulate_shared_keystrokes("shift-u").await;
2262        cx.shared_state().await.assert_matches();
2263    }
2264
2265    #[gpui::test]
2266    async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2267        let mut cx = VimTestContext::new(cx, true).await;
2268
2269        cx.set_state(
2270            indoc! {"
2271            ˇone two ˇone
2272            two ˇone two
2273        "},
2274            Mode::Normal,
2275        );
2276        cx.simulate_keystrokes("3 r a");
2277        cx.assert_state(
2278            indoc! {"
2279            aaˇa two aaˇa
2280            two aaˇa two
2281        "},
2282            Mode::Normal,
2283        );
2284        cx.simulate_keystrokes("escape escape");
2285        cx.simulate_keystrokes("shift-u");
2286        cx.set_state(
2287            indoc! {"
2288            onˇe two onˇe
2289            two onˇe two
2290        "},
2291            Mode::Normal,
2292        );
2293    }
2294
2295    #[gpui::test]
2296    async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2297        let mut cx = VimTestContext::new(cx, true).await;
2298
2299        // Open 4 tabs.
2300        cx.simulate_keystrokes(": tabnew");
2301        cx.simulate_keystrokes("enter");
2302        cx.simulate_keystrokes(": tabnew");
2303        cx.simulate_keystrokes("enter");
2304        cx.simulate_keystrokes(": tabnew");
2305        cx.simulate_keystrokes("enter");
2306        cx.workspace(|workspace, _, cx| {
2307            assert_eq!(workspace.items(cx).count(), 4);
2308            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2309        });
2310
2311        cx.simulate_keystrokes("1 g t");
2312        cx.workspace(|workspace, _, cx| {
2313            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2314        });
2315
2316        cx.simulate_keystrokes("3 g t");
2317        cx.workspace(|workspace, _, cx| {
2318            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2319        });
2320
2321        cx.simulate_keystrokes("4 g t");
2322        cx.workspace(|workspace, _, cx| {
2323            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2324        });
2325
2326        cx.simulate_keystrokes("1 g t");
2327        cx.simulate_keystrokes("g t");
2328        cx.workspace(|workspace, _, cx| {
2329            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2330        });
2331    }
2332
2333    #[gpui::test]
2334    async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2335        let mut cx = VimTestContext::new(cx, true).await;
2336
2337        // Open 4 tabs.
2338        cx.simulate_keystrokes(": tabnew");
2339        cx.simulate_keystrokes("enter");
2340        cx.simulate_keystrokes(": tabnew");
2341        cx.simulate_keystrokes("enter");
2342        cx.simulate_keystrokes(": tabnew");
2343        cx.simulate_keystrokes("enter");
2344        cx.workspace(|workspace, _, cx| {
2345            assert_eq!(workspace.items(cx).count(), 4);
2346            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2347        });
2348
2349        cx.simulate_keystrokes("2 g shift-t");
2350        cx.workspace(|workspace, _, cx| {
2351            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2352        });
2353
2354        cx.simulate_keystrokes("g shift-t");
2355        cx.workspace(|workspace, _, cx| {
2356            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2357        });
2358
2359        // Wraparound: gT from first tab should go to last.
2360        cx.simulate_keystrokes("g shift-t");
2361        cx.workspace(|workspace, _, cx| {
2362            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2363        });
2364
2365        cx.simulate_keystrokes("6 g shift-t");
2366        cx.workspace(|workspace, _, cx| {
2367            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2368        });
2369    }
2370
2371    #[gpui::test]
2372    async fn test_temporary_mode(cx: &mut gpui::TestAppContext) {
2373        let mut cx = NeovimBackedTestContext::new(cx).await;
2374
2375        // Test jumping to the end of the line ($).
2376        cx.set_shared_state(indoc! {"lorem ˇipsum"}).await;
2377        cx.simulate_shared_keystrokes("i").await;
2378        cx.shared_state().await.assert_matches();
2379        cx.simulate_shared_keystrokes("ctrl-o $").await;
2380        cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"});
2381
2382        // Test jumping to the next word.
2383        cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await;
2384        cx.simulate_shared_keystrokes("a").await;
2385        cx.shared_state().await.assert_matches();
2386        cx.simulate_shared_keystrokes("a n d space ctrl-o w").await;
2387        cx.shared_state()
2388            .await
2389            .assert_eq(indoc! {"lorem and ipsum ˇdolor"});
2390
2391        // Test yanking to end of line ($).
2392        cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await;
2393        cx.simulate_shared_keystrokes("i").await;
2394        cx.shared_state().await.assert_matches();
2395        cx.simulate_shared_keystrokes("a n d space ctrl-o y $")
2396            .await;
2397        cx.shared_state()
2398            .await
2399            .assert_eq(indoc! {"lorem and ˇipsum dolor"});
2400    }
2401}