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::{AutoIndentMode, 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                let bracket_anchors =
 567                    self.prepare_and_move_to_valid_bracket_pair(object, window, cx);
 568                if !bracket_anchors.is_empty() {
 569                    waiting_operator = Some(Operator::ChangeSurrounds {
 570                        target: Some(object),
 571                        opening,
 572                        bracket_anchors,
 573                    });
 574                }
 575            }
 576            _ => {
 577                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 578            }
 579        }
 580        self.clear_operator(window, cx);
 581        if let Some(operator) = waiting_operator {
 582            self.push_operator(operator, window, cx);
 583        }
 584    }
 585
 586    pub(crate) fn move_cursor(
 587        &mut self,
 588        motion: Motion,
 589        times: Option<usize>,
 590        window: &mut Window,
 591        cx: &mut Context<Self>,
 592    ) {
 593        self.update_editor(cx, |vim, editor, cx| {
 594            let text_layout_details = editor.text_layout_details(window, cx);
 595
 596            // If vim is in temporary mode and the motion being used is
 597            // `EndOfLine` ($), we'll want to disable clipping at line ends so
 598            // that the newline character can be selected so that, when moving
 599            // back to visual mode, the cursor will be placed after the last
 600            // character and not before it.
 601            let clip_at_line_ends = editor.clip_at_line_ends(cx);
 602            let should_disable_clip = matches!(motion, Motion::EndOfLine { .. }) && vim.temp_mode;
 603
 604            if should_disable_clip {
 605                editor.set_clip_at_line_ends(false, cx)
 606            };
 607
 608            editor.change_selections(
 609                SelectionEffects::default().nav_history(motion.push_to_jump_list()),
 610                window,
 611                cx,
 612                |s| {
 613                    s.move_cursors_with(&mut |map, cursor, goal| {
 614                        motion
 615                            .move_point(map, cursor, goal, times, &text_layout_details)
 616                            .unwrap_or((cursor, goal))
 617                    })
 618                },
 619            );
 620
 621            if should_disable_clip {
 622                editor.set_clip_at_line_ends(clip_at_line_ends, cx);
 623            };
 624        });
 625    }
 626
 627    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 628        self.start_recording(cx);
 629        self.switch_mode(Mode::Insert, false, window, cx);
 630        self.update_editor(cx, |_, editor, cx| {
 631            editor.change_selections(Default::default(), window, cx, |s| {
 632                s.move_cursors_with(&mut |map, cursor, _| {
 633                    (right(map, cursor, 1), SelectionGoal::None)
 634                });
 635            });
 636        });
 637    }
 638
 639    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 640        self.start_recording(cx);
 641        if self.mode.is_visual() {
 642            let current_mode = self.mode;
 643            self.update_editor(cx, |_, editor, cx| {
 644                editor.change_selections(Default::default(), window, cx, |s| {
 645                    s.move_with(&mut |map, selection| {
 646                        if current_mode == Mode::VisualLine {
 647                            let start_of_line = motion::start_of_line(map, false, selection.start);
 648                            selection.collapse_to(start_of_line, SelectionGoal::None)
 649                        } else {
 650                            selection.collapse_to(selection.start, SelectionGoal::None)
 651                        }
 652                    });
 653                });
 654            });
 655        }
 656        self.switch_mode(Mode::Insert, false, window, cx);
 657    }
 658
 659    fn insert_first_non_whitespace(
 660        &mut self,
 661        _: &InsertFirstNonWhitespace,
 662        window: &mut Window,
 663        cx: &mut Context<Self>,
 664    ) {
 665        self.start_recording(cx);
 666        self.switch_mode(Mode::Insert, false, window, cx);
 667        self.update_editor(cx, |_, editor, cx| {
 668            editor.change_selections(Default::default(), window, cx, |s| {
 669                s.move_cursors_with(&mut |map, cursor, _| {
 670                    (
 671                        first_non_whitespace(map, false, cursor),
 672                        SelectionGoal::None,
 673                    )
 674                });
 675            });
 676        });
 677    }
 678
 679    fn insert_end_of_line(
 680        &mut self,
 681        _: &InsertEndOfLine,
 682        window: &mut Window,
 683        cx: &mut Context<Self>,
 684    ) {
 685        self.start_recording(cx);
 686        self.switch_mode(Mode::Insert, false, window, cx);
 687        self.update_editor(cx, |_, editor, cx| {
 688            editor.change_selections(Default::default(), window, cx, |s| {
 689                s.move_cursors_with(&mut |map, cursor, _| {
 690                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 691                });
 692            });
 693        });
 694    }
 695
 696    fn insert_at_previous(
 697        &mut self,
 698        _: &InsertAtPrevious,
 699        window: &mut Window,
 700        cx: &mut Context<Self>,
 701    ) {
 702        self.start_recording(cx);
 703        self.switch_mode(Mode::Insert, false, window, cx);
 704        self.update_editor(cx, |vim, editor, cx| {
 705            if let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx)
 706                && !marks.is_empty()
 707            {
 708                editor.change_selections(Default::default(), window, cx, |s| {
 709                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 710                });
 711            }
 712        });
 713    }
 714
 715    fn insert_line_above(
 716        &mut self,
 717        _: &InsertLineAbove,
 718        window: &mut Window,
 719        cx: &mut Context<Self>,
 720    ) {
 721        self.start_recording(cx);
 722        self.switch_mode(Mode::Insert, false, window, cx);
 723        self.update_editor(cx, |_, editor, cx| {
 724            editor.transact(window, cx, |editor, window, cx| {
 725                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 726                let snapshot = editor.buffer().read(cx).snapshot(cx);
 727
 728                let selection_start_rows: BTreeSet<u32> = selections
 729                    .into_iter()
 730                    .map(|selection| selection.start.row)
 731                    .collect();
 732
 733                let mut auto_indent_edits = Vec::new();
 734                let mut plain_edits = Vec::new();
 735
 736                for row in selection_start_rows {
 737                    let auto_indent_mode = snapshot
 738                        .language_settings_at(Point::new(row, 0), cx)
 739                        .auto_indent;
 740                    let indent = if auto_indent_mode == AutoIndentMode::None {
 741                        String::new()
 742                    } else {
 743                        snapshot.indent_and_comment_for_line(MultiBufferRow(row), cx)
 744                    };
 745                    let start_of_line = Point::new(row, 0);
 746                    let edit = (start_of_line..start_of_line, indent + "\n");
 747                    if auto_indent_mode == AutoIndentMode::None {
 748                        plain_edits.push(edit);
 749                    } else {
 750                        auto_indent_edits.push(edit);
 751                    }
 752                }
 753
 754                if !plain_edits.is_empty() {
 755                    editor.edit(plain_edits, cx);
 756                }
 757                if !auto_indent_edits.is_empty() {
 758                    editor.edit_with_autoindent(auto_indent_edits, cx);
 759                }
 760
 761                editor.change_selections(Default::default(), window, cx, |s| {
 762                    s.move_with(&mut |map, selection| {
 763                        let previous_line = map.start_of_relative_buffer_row(selection.start, -1);
 764                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 765                        selection.collapse_to(insert_point, SelectionGoal::None)
 766                    });
 767                });
 768            });
 769        });
 770    }
 771
 772    fn insert_line_below(
 773        &mut self,
 774        _: &InsertLineBelow,
 775        window: &mut Window,
 776        cx: &mut Context<Self>,
 777    ) {
 778        self.start_recording(cx);
 779        self.switch_mode(Mode::Insert, false, window, cx);
 780        self.update_editor(cx, |_, editor, cx| {
 781            editor.transact(window, cx, |editor, window, cx| {
 782                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 783                let snapshot = editor.buffer().read(cx).snapshot(cx);
 784
 785                let selection_end_rows: BTreeSet<u32> = selections
 786                    .into_iter()
 787                    .map(|selection| {
 788                        if !selection.is_empty() && selection.end.column == 0 {
 789                            selection.end.row.saturating_sub(1)
 790                        } else {
 791                            selection.end.row
 792                        }
 793                    })
 794                    .collect();
 795
 796                let mut auto_indent_edits = Vec::new();
 797                let mut plain_edits = Vec::new();
 798
 799                for row in selection_end_rows {
 800                    let auto_indent_mode = snapshot
 801                        .language_settings_at(Point::new(row, 0), cx)
 802                        .auto_indent;
 803                    let indent = if auto_indent_mode == AutoIndentMode::None {
 804                        String::new()
 805                    } else {
 806                        snapshot.indent_and_comment_for_line(MultiBufferRow(row), cx)
 807                    };
 808                    let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 809                    let edit = (end_of_line..end_of_line, "\n".to_string() + &indent);
 810                    if auto_indent_mode == AutoIndentMode::None {
 811                        plain_edits.push(edit);
 812                    } else {
 813                        auto_indent_edits.push(edit);
 814                    }
 815                }
 816
 817                editor.change_selections(Default::default(), window, cx, |s| {
 818                    s.move_with(&mut |map, selection| {
 819                        let current_line = if !selection.is_empty() && selection.end.column() == 0 {
 820                            // If this is an insert after a selection to the end of the line, the
 821                            // cursor needs to be bumped back, because it'll be at the start of the
 822                            // *next* line.
 823                            map.start_of_relative_buffer_row(selection.end, -1)
 824                        } else {
 825                            selection.end
 826                        };
 827                        let insert_point = motion::end_of_line(map, false, current_line, 1);
 828                        selection.collapse_to(insert_point, SelectionGoal::None)
 829                    });
 830                });
 831
 832                if !plain_edits.is_empty() {
 833                    editor.edit(plain_edits, cx);
 834                }
 835                if !auto_indent_edits.is_empty() {
 836                    editor.edit_with_autoindent(auto_indent_edits, cx);
 837                }
 838            });
 839        });
 840    }
 841
 842    fn insert_empty_line_above(
 843        &mut self,
 844        _: &InsertEmptyLineAbove,
 845        window: &mut Window,
 846        cx: &mut Context<Self>,
 847    ) {
 848        self.record_current_action(cx);
 849        let count = Vim::take_count(cx).unwrap_or(1);
 850        Vim::take_forced_motion(cx);
 851        self.update_editor(cx, |_, editor, cx| {
 852            editor.transact(window, cx, |editor, _, cx| {
 853                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 854
 855                let selection_start_rows: BTreeSet<u32> = selections
 856                    .into_iter()
 857                    .map(|selection| selection.start.row)
 858                    .collect();
 859                let edits = selection_start_rows
 860                    .into_iter()
 861                    .map(|row| {
 862                        let start_of_line = Point::new(row, 0);
 863                        (start_of_line..start_of_line, "\n".repeat(count))
 864                    })
 865                    .collect::<Vec<_>>();
 866                editor.edit(edits, cx);
 867            });
 868        });
 869    }
 870
 871    fn insert_empty_line_below(
 872        &mut self,
 873        _: &InsertEmptyLineBelow,
 874        window: &mut Window,
 875        cx: &mut Context<Self>,
 876    ) {
 877        self.record_current_action(cx);
 878        let count = Vim::take_count(cx).unwrap_or(1);
 879        Vim::take_forced_motion(cx);
 880        self.update_editor(cx, |_, editor, cx| {
 881            editor.transact(window, cx, |editor, window, cx| {
 882                let display_map = editor.display_snapshot(cx);
 883                let selections = editor.selections.all::<Point>(&display_map);
 884                let snapshot = editor.buffer().read(cx).snapshot(cx);
 885                let display_selections = editor.selections.all_display(&display_map);
 886                let original_positions = display_selections
 887                    .iter()
 888                    .map(|s| (s.id, s.head()))
 889                    .collect::<HashMap<_, _>>();
 890
 891                let selection_end_rows: BTreeSet<u32> = selections
 892                    .into_iter()
 893                    .map(|selection| selection.end.row)
 894                    .collect();
 895                let edits = selection_end_rows
 896                    .into_iter()
 897                    .map(|row| {
 898                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 899                        (end_of_line..end_of_line, "\n".repeat(count))
 900                    })
 901                    .collect::<Vec<_>>();
 902                editor.edit(edits, cx);
 903
 904                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 905                    s.move_with(&mut |_, selection| {
 906                        if let Some(position) = original_positions.get(&selection.id) {
 907                            selection.collapse_to(*position, SelectionGoal::None);
 908                        }
 909                    });
 910                });
 911            });
 912        });
 913    }
 914
 915    fn join_lines_impl(
 916        &mut self,
 917        insert_whitespace: bool,
 918        window: &mut Window,
 919        cx: &mut Context<Self>,
 920    ) {
 921        self.record_current_action(cx);
 922        let mut times = Vim::take_count(cx).unwrap_or(1);
 923        Vim::take_forced_motion(cx);
 924        if self.mode.is_visual() {
 925            times = 1;
 926        } else if times > 1 {
 927            // 2J joins two lines together (same as J or 1J)
 928            times -= 1;
 929        }
 930
 931        self.update_editor(cx, |_, editor, cx| {
 932            editor.transact(window, cx, |editor, window, cx| {
 933                for _ in 0..times {
 934                    editor.join_lines_impl(insert_whitespace, window, cx)
 935                }
 936            })
 937        });
 938        if self.mode.is_visual() {
 939            self.switch_mode(Mode::Normal, true, window, cx)
 940        }
 941    }
 942
 943    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 944        let count = Vim::take_count(cx);
 945        let forced_motion = Vim::take_forced_motion(cx);
 946        self.yank_motion(
 947            motion::Motion::CurrentLine,
 948            count,
 949            forced_motion,
 950            window,
 951            cx,
 952        )
 953    }
 954
 955    fn yank_to_end_of_line(
 956        &mut self,
 957        _: &YankToEndOfLine,
 958        window: &mut Window,
 959        cx: &mut Context<Self>,
 960    ) {
 961        let count = Vim::take_count(cx);
 962        let forced_motion = Vim::take_forced_motion(cx);
 963        self.yank_motion(
 964            motion::Motion::EndOfLine {
 965                display_lines: false,
 966            },
 967            count,
 968            forced_motion,
 969            window,
 970            cx,
 971        )
 972    }
 973
 974    fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
 975        let count = Vim::take_count(cx);
 976        Vim::take_forced_motion(cx);
 977        self.update_editor(cx, |vim, editor, cx| {
 978            let selection = editor.selections.newest_anchor();
 979            let Some((buffer, point)) = editor
 980                .buffer()
 981                .read(cx)
 982                .point_to_buffer_point(selection.head(), cx)
 983            else {
 984                return;
 985            };
 986            let filename = if let Some(file) = buffer.read(cx).file() {
 987                if count.is_some() {
 988                    if let Some(local) = file.as_local() {
 989                        local.abs_path(cx).to_string_lossy().into_owned()
 990                    } else {
 991                        file.full_path(cx).to_string_lossy().into_owned()
 992                    }
 993                } else {
 994                    file.path().display(file.path_style(cx)).into_owned()
 995                }
 996            } else {
 997                "[No Name]".into()
 998            };
 999            let buffer = buffer.read(cx);
1000            let lines = buffer.max_point().row + 1;
1001            let current_line = point.row;
1002            let percentage = current_line as f32 / lines as f32;
1003            let modified = if buffer.is_dirty() { " [modified]" } else { "" };
1004            vim.set_status_label(
1005                format!(
1006                    "{}{} {} lines --{:.0}%--",
1007                    filename,
1008                    modified,
1009                    lines,
1010                    percentage * 100.0,
1011                ),
1012                cx,
1013            );
1014        });
1015    }
1016
1017    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
1018        self.record_current_action(cx);
1019        self.store_visual_marks(window, cx);
1020        self.update_editor(cx, |vim, editor, cx| {
1021            editor.transact(window, cx, |editor, window, cx| {
1022                let original_positions = vim.save_selection_starts(editor, cx);
1023                editor.toggle_comments(&Default::default(), window, cx);
1024                vim.restore_selection_cursors(editor, window, cx, original_positions);
1025            });
1026        });
1027        if self.mode.is_visual() {
1028            self.switch_mode(Mode::Normal, true, window, cx)
1029        }
1030    }
1031
1032    fn toggle_block_comments(
1033        &mut self,
1034        _: &ToggleBlockComments,
1035        window: &mut Window,
1036        cx: &mut Context<Self>,
1037    ) {
1038        self.record_current_action(cx);
1039        self.store_visual_marks(window, cx);
1040        let is_visual_line = self.mode == Mode::VisualLine;
1041        self.update_editor(cx, |vim, editor, cx| {
1042            editor.transact(window, cx, |editor, window, cx| {
1043                let original_positions = vim.save_selection_starts(editor, cx);
1044                if is_visual_line {
1045                    editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1046                        s.move_with(&mut |map, selection| {
1047                            let start_row = selection.start.to_point(map).row;
1048                            let end_row = selection.end.to_point(map).row;
1049                            let end_col = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
1050                            selection.start = Point::new(start_row, 0).to_display_point(map);
1051                            selection.end = Point::new(end_row, end_col).to_display_point(map);
1052                        });
1053                    });
1054                }
1055                editor.toggle_block_comments(&Default::default(), window, cx);
1056                vim.restore_selection_cursors(editor, window, cx, original_positions);
1057            });
1058        });
1059        if self.mode.is_visual() {
1060            self.switch_mode(Mode::Normal, true, window, cx)
1061        }
1062    }
1063
1064    pub(crate) fn normal_replace(
1065        &mut self,
1066        text: Arc<str>,
1067        window: &mut Window,
1068        cx: &mut Context<Self>,
1069    ) {
1070        // We need to use `text.chars().count()` instead of `text.len()` here as
1071        // `len()` counts bytes, not characters.
1072        let char_count = text.chars().count();
1073        let count = Vim::take_count(cx).unwrap_or(char_count);
1074        let is_return_char = text == "\n".into() || text == "\r".into();
1075        let repeat_count = match (is_return_char, char_count) {
1076            (true, _) => 0,
1077            (_, 1) => count,
1078            (_, _) => 1,
1079        };
1080
1081        Vim::take_forced_motion(cx);
1082        self.stop_recording(cx);
1083        self.update_editor(cx, |_, editor, cx| {
1084            editor.transact(window, cx, |editor, window, cx| {
1085                editor.set_clip_at_line_ends(false, cx);
1086                let display_map = editor.display_snapshot(cx);
1087                let display_selections = editor.selections.all_display(&display_map);
1088
1089                let mut edits = Vec::with_capacity(display_selections.len());
1090                for selection in &display_selections {
1091                    let mut range = selection.range();
1092                    for _ in 0..count {
1093                        let new_point = movement::saturating_right(&display_map, range.end);
1094                        if range.end == new_point {
1095                            return;
1096                        }
1097                        range.end = new_point;
1098                    }
1099
1100                    edits.push((
1101                        range.start.to_offset(&display_map, Bias::Left)
1102                            ..range.end.to_offset(&display_map, Bias::Left),
1103                        text.repeat(repeat_count),
1104                    ));
1105                }
1106
1107                editor.edit(edits, cx);
1108                if is_return_char {
1109                    editor.newline(&editor::actions::Newline, window, cx);
1110                }
1111                editor.set_clip_at_line_ends(true, cx);
1112                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1113                    s.move_with(&mut |map, selection| {
1114                        let point = movement::saturating_left(map, selection.head());
1115                        selection.collapse_to(point, SelectionGoal::None)
1116                    });
1117                });
1118            });
1119        });
1120        self.pop_operator(window, cx);
1121    }
1122
1123    pub fn save_selection_starts(
1124        &self,
1125        editor: &Editor,
1126        cx: &mut Context<Editor>,
1127    ) -> HashMap<usize, Anchor> {
1128        let display_map = editor.display_snapshot(cx);
1129        let selections = editor.selections.all_display(&display_map);
1130        selections
1131            .iter()
1132            .map(|selection| {
1133                (
1134                    selection.id,
1135                    display_map.display_point_to_anchor(selection.start, Bias::Right),
1136                )
1137            })
1138            .collect::<HashMap<_, _>>()
1139    }
1140
1141    pub fn restore_selection_cursors(
1142        &self,
1143        editor: &mut Editor,
1144        window: &mut Window,
1145        cx: &mut Context<Editor>,
1146        mut positions: HashMap<usize, Anchor>,
1147    ) {
1148        editor.change_selections(Default::default(), window, cx, |s| {
1149            s.move_with(&mut |map, selection| {
1150                if let Some(anchor) = positions.remove(&selection.id) {
1151                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1152                }
1153            });
1154        });
1155    }
1156
1157    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1158        if self.temp_mode {
1159            self.switch_mode(Mode::Insert, true, window, cx);
1160        }
1161    }
1162}
1163
1164#[cfg(test)]
1165mod test {
1166    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1167    use indoc::indoc;
1168    use settings::SettingsStore;
1169
1170    use crate::{
1171        motion,
1172        state::Mode::{self},
1173        test::{NeovimBackedTestContext, VimTestContext},
1174    };
1175    use language;
1176
1177    #[gpui::test]
1178    async fn test_h(cx: &mut gpui::TestAppContext) {
1179        let mut cx = NeovimBackedTestContext::new(cx).await;
1180        cx.simulate_at_each_offset(
1181            "h",
1182            indoc! {"
1183            ˇThe qˇuick
1184            ˇbrown"
1185            },
1186        )
1187        .await
1188        .assert_matches();
1189    }
1190
1191    #[gpui::test]
1192    async fn test_backspace(cx: &mut gpui::TestAppContext) {
1193        let mut cx = NeovimBackedTestContext::new(cx).await;
1194        cx.simulate_at_each_offset(
1195            "backspace",
1196            indoc! {"
1197            ˇThe qˇuick
1198            ˇbrown"
1199            },
1200        )
1201        .await
1202        .assert_matches();
1203    }
1204
1205    #[gpui::test]
1206    async fn test_j(cx: &mut gpui::TestAppContext) {
1207        let mut cx = NeovimBackedTestContext::new(cx).await;
1208
1209        cx.set_shared_state(indoc! {"
1210            aaˇaa
1211            😃😃"
1212        })
1213        .await;
1214        cx.simulate_shared_keystrokes("j").await;
1215        cx.shared_state().await.assert_eq(indoc! {"
1216            aaaa
1217            😃ˇ😃"
1218        });
1219
1220        cx.simulate_at_each_offset(
1221            "j",
1222            indoc! {"
1223                ˇThe qˇuick broˇwn
1224                ˇfox jumps"
1225            },
1226        )
1227        .await
1228        .assert_matches();
1229    }
1230
1231    #[gpui::test]
1232    async fn test_enter(cx: &mut gpui::TestAppContext) {
1233        let mut cx = NeovimBackedTestContext::new(cx).await;
1234        cx.simulate_at_each_offset(
1235            "enter",
1236            indoc! {"
1237            ˇThe qˇuick broˇwn
1238            ˇfox jumps"
1239            },
1240        )
1241        .await
1242        .assert_matches();
1243    }
1244
1245    #[gpui::test]
1246    async fn test_k(cx: &mut gpui::TestAppContext) {
1247        let mut cx = NeovimBackedTestContext::new(cx).await;
1248        cx.simulate_at_each_offset(
1249            "k",
1250            indoc! {"
1251            ˇThe qˇuick
1252            ˇbrown fˇox jumˇps"
1253            },
1254        )
1255        .await
1256        .assert_matches();
1257    }
1258
1259    #[gpui::test]
1260    async fn test_l(cx: &mut gpui::TestAppContext) {
1261        let mut cx = NeovimBackedTestContext::new(cx).await;
1262        cx.simulate_at_each_offset(
1263            "l",
1264            indoc! {"
1265            ˇThe qˇuicˇk
1266            ˇbrowˇn"},
1267        )
1268        .await
1269        .assert_matches();
1270    }
1271
1272    #[gpui::test]
1273    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1274        let mut cx = NeovimBackedTestContext::new(cx).await;
1275        cx.simulate_at_each_offset(
1276            "$",
1277            indoc! {"
1278            ˇThe qˇuicˇk
1279            ˇbrowˇn"},
1280        )
1281        .await
1282        .assert_matches();
1283        cx.simulate_at_each_offset(
1284            "0",
1285            indoc! {"
1286                ˇThe qˇuicˇk
1287                ˇbrowˇn"},
1288        )
1289        .await
1290        .assert_matches();
1291    }
1292
1293    #[gpui::test]
1294    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1295        let mut cx = NeovimBackedTestContext::new(cx).await;
1296
1297        cx.simulate_at_each_offset(
1298            "shift-g",
1299            indoc! {"
1300                The ˇquick
1301
1302                brown fox jumps
1303                overˇ the lazy doˇg"},
1304        )
1305        .await
1306        .assert_matches();
1307        cx.simulate(
1308            "shift-g",
1309            indoc! {"
1310            The quiˇck
1311
1312            brown"},
1313        )
1314        .await
1315        .assert_matches();
1316        cx.simulate(
1317            "shift-g",
1318            indoc! {"
1319            The quiˇck
1320
1321            "},
1322        )
1323        .await
1324        .assert_matches();
1325    }
1326
1327    #[gpui::test]
1328    async fn test_w(cx: &mut gpui::TestAppContext) {
1329        let mut cx = NeovimBackedTestContext::new(cx).await;
1330        cx.simulate_at_each_offset(
1331            "w",
1332            indoc! {"
1333            The ˇquickˇ-ˇbrown
1334            ˇ
1335            ˇ
1336            ˇfox_jumps ˇover
1337            ˇthˇe"},
1338        )
1339        .await
1340        .assert_matches();
1341        cx.simulate_at_each_offset(
1342            "shift-w",
1343            indoc! {"
1344            The ˇquickˇ-ˇbrown
1345            ˇ
1346            ˇ
1347            ˇfox_jumps ˇover
1348            ˇthˇe"},
1349        )
1350        .await
1351        .assert_matches();
1352    }
1353
1354    #[gpui::test]
1355    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1356        let mut cx = NeovimBackedTestContext::new(cx).await;
1357        cx.simulate_at_each_offset(
1358            "e",
1359            indoc! {"
1360            Thˇe quicˇkˇ-browˇn
1361
1362
1363            fox_jumpˇs oveˇr
1364            thˇe"},
1365        )
1366        .await
1367        .assert_matches();
1368        cx.simulate_at_each_offset(
1369            "shift-e",
1370            indoc! {"
1371            Thˇe quicˇkˇ-browˇn
1372
1373
1374            fox_jumpˇs oveˇr
1375            thˇe"},
1376        )
1377        .await
1378        .assert_matches();
1379    }
1380
1381    #[gpui::test]
1382    async fn test_b(cx: &mut gpui::TestAppContext) {
1383        let mut cx = NeovimBackedTestContext::new(cx).await;
1384        cx.simulate_at_each_offset(
1385            "b",
1386            indoc! {"
1387            ˇThe ˇquickˇ-ˇbrown
1388            ˇ
1389            ˇ
1390            ˇfox_jumps ˇover
1391            ˇthe"},
1392        )
1393        .await
1394        .assert_matches();
1395        cx.simulate_at_each_offset(
1396            "shift-b",
1397            indoc! {"
1398            ˇThe ˇquickˇ-ˇbrown
1399            ˇ
1400            ˇ
1401            ˇfox_jumps ˇover
1402            ˇthe"},
1403        )
1404        .await
1405        .assert_matches();
1406    }
1407
1408    #[gpui::test]
1409    async fn test_gg(cx: &mut gpui::TestAppContext) {
1410        let mut cx = NeovimBackedTestContext::new(cx).await;
1411        cx.simulate_at_each_offset(
1412            "g g",
1413            indoc! {"
1414                The qˇuick
1415
1416                brown fox jumps
1417                over ˇthe laˇzy dog"},
1418        )
1419        .await
1420        .assert_matches();
1421        cx.simulate(
1422            "g g",
1423            indoc! {"
1424
1425
1426                brown fox jumps
1427                over the laˇzy dog"},
1428        )
1429        .await
1430        .assert_matches();
1431        cx.simulate(
1432            "2 g g",
1433            indoc! {"
1434                ˇ
1435
1436                brown fox jumps
1437                over the lazydog"},
1438        )
1439        .await
1440        .assert_matches();
1441    }
1442
1443    #[gpui::test]
1444    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1445        let mut cx = NeovimBackedTestContext::new(cx).await;
1446        cx.simulate_at_each_offset(
1447            "shift-g",
1448            indoc! {"
1449                The qˇuick
1450
1451                brown fox jumps
1452                over ˇthe laˇzy dog"},
1453        )
1454        .await
1455        .assert_matches();
1456        cx.simulate(
1457            "shift-g",
1458            indoc! {"
1459
1460
1461                brown fox jumps
1462                over the laˇzy dog"},
1463        )
1464        .await
1465        .assert_matches();
1466        cx.simulate(
1467            "2 shift-g",
1468            indoc! {"
1469                ˇ
1470
1471                brown fox jumps
1472                over the lazydog"},
1473        )
1474        .await
1475        .assert_matches();
1476    }
1477
1478    #[gpui::test]
1479    async fn test_a(cx: &mut gpui::TestAppContext) {
1480        let mut cx = NeovimBackedTestContext::new(cx).await;
1481        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1482            .await
1483            .assert_matches();
1484    }
1485
1486    #[gpui::test]
1487    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1488        let mut cx = NeovimBackedTestContext::new(cx).await;
1489        cx.simulate_at_each_offset(
1490            "shift-a",
1491            indoc! {"
1492            ˇ
1493            The qˇuick
1494            brown ˇfox "},
1495        )
1496        .await
1497        .assert_matches();
1498    }
1499
1500    #[gpui::test]
1501    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1502        let mut cx = NeovimBackedTestContext::new(cx).await;
1503        cx.simulate("^", "The qˇuick").await.assert_matches();
1504        cx.simulate("^", " The qˇuick").await.assert_matches();
1505        cx.simulate("^", "ˇ").await.assert_matches();
1506        cx.simulate(
1507            "^",
1508            indoc! {"
1509                The qˇuick
1510                brown fox"},
1511        )
1512        .await
1513        .assert_matches();
1514        cx.simulate(
1515            "^",
1516            indoc! {"
1517                ˇ
1518                The quick"},
1519        )
1520        .await
1521        .assert_matches();
1522        // Indoc disallows trailing whitespace.
1523        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1524    }
1525
1526    #[gpui::test]
1527    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1528        let mut cx = NeovimBackedTestContext::new(cx).await;
1529        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1530        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1531        cx.simulate("shift-i", "ˇ").await.assert_matches();
1532        cx.simulate(
1533            "shift-i",
1534            indoc! {"
1535                The qˇuick
1536                brown fox"},
1537        )
1538        .await
1539        .assert_matches();
1540        cx.simulate(
1541            "shift-i",
1542            indoc! {"
1543                ˇ
1544                The quick"},
1545        )
1546        .await
1547        .assert_matches();
1548    }
1549
1550    #[gpui::test]
1551    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1552        let mut cx = NeovimBackedTestContext::new(cx).await;
1553        cx.simulate(
1554            "shift-d",
1555            indoc! {"
1556                The qˇuick
1557                brown fox"},
1558        )
1559        .await
1560        .assert_matches();
1561        cx.simulate(
1562            "shift-d",
1563            indoc! {"
1564                The quick
1565                ˇ
1566                brown fox"},
1567        )
1568        .await
1569        .assert_matches();
1570    }
1571
1572    #[gpui::test]
1573    async fn test_x(cx: &mut gpui::TestAppContext) {
1574        let mut cx = NeovimBackedTestContext::new(cx).await;
1575        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1576            .await
1577            .assert_matches();
1578        cx.simulate(
1579            "x",
1580            indoc! {"
1581                Tesˇt
1582                test"},
1583        )
1584        .await
1585        .assert_matches();
1586    }
1587
1588    #[gpui::test]
1589    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1590        let mut cx = NeovimBackedTestContext::new(cx).await;
1591        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1592            .await
1593            .assert_matches();
1594        cx.simulate(
1595            "shift-x",
1596            indoc! {"
1597                Test
1598                ˇtest"},
1599        )
1600        .await
1601        .assert_matches();
1602    }
1603
1604    #[gpui::test]
1605    async fn test_o(cx: &mut gpui::TestAppContext) {
1606        let mut cx = NeovimBackedTestContext::new(cx).await;
1607        cx.simulate("o", "ˇ").await.assert_matches();
1608        cx.simulate("o", "The ˇquick").await.assert_matches();
1609        cx.simulate_at_each_offset(
1610            "o",
1611            indoc! {"
1612                The qˇuick
1613                brown ˇfox
1614                jumps ˇover"},
1615        )
1616        .await
1617        .assert_matches();
1618        cx.simulate(
1619            "o",
1620            indoc! {"
1621                The quick
1622                ˇ
1623                brown fox"},
1624        )
1625        .await
1626        .assert_matches();
1627
1628        cx.assert_binding(
1629            "o",
1630            indoc! {"
1631                fn test() {
1632                    println!(ˇ);
1633                }"},
1634            Mode::Normal,
1635            indoc! {"
1636                fn test() {
1637                    println!();
1638                    ˇ
1639                }"},
1640            Mode::Insert,
1641        );
1642
1643        cx.assert_binding(
1644            "o",
1645            indoc! {"
1646                fn test(ˇ) {
1647                    println!();
1648                }"},
1649            Mode::Normal,
1650            indoc! {"
1651                fn test() {
1652                    ˇ
1653                    println!();
1654                }"},
1655            Mode::Insert,
1656        );
1657    }
1658
1659    #[gpui::test]
1660    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1661        let mut cx = NeovimBackedTestContext::new(cx).await;
1662        cx.simulate("shift-o", "ˇ").await.assert_matches();
1663        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1664        cx.simulate_at_each_offset(
1665            "shift-o",
1666            indoc! {"
1667            The qˇuick
1668            brown ˇfox
1669            jumps ˇover"},
1670        )
1671        .await
1672        .assert_matches();
1673        cx.simulate(
1674            "shift-o",
1675            indoc! {"
1676            The quick
1677            ˇ
1678            brown fox"},
1679        )
1680        .await
1681        .assert_matches();
1682
1683        // Our indentation is smarter than vims. So we don't match here
1684        cx.assert_binding(
1685            "shift-o",
1686            indoc! {"
1687                fn test() {
1688                    println!(ˇ);
1689                }"},
1690            Mode::Normal,
1691            indoc! {"
1692                fn test() {
1693                    ˇ
1694                    println!();
1695                }"},
1696            Mode::Insert,
1697        );
1698        cx.assert_binding(
1699            "shift-o",
1700            indoc! {"
1701                fn test(ˇ) {
1702                    println!();
1703                }"},
1704            Mode::Normal,
1705            indoc! {"
1706                ˇ
1707                fn test() {
1708                    println!();
1709                }"},
1710            Mode::Insert,
1711        );
1712    }
1713
1714    #[gpui::test]
1715    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1716        let mut cx = NeovimBackedTestContext::new(cx).await;
1717        cx.simulate("[ space", "ˇ").await.assert_matches();
1718        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1719        cx.simulate_at_each_offset(
1720            "3 [ space",
1721            indoc! {"
1722            The qˇuick
1723            brown ˇfox
1724            jumps ˇover"},
1725        )
1726        .await
1727        .assert_matches();
1728        cx.simulate_at_each_offset(
1729            "[ space",
1730            indoc! {"
1731            The qˇuick
1732            brown ˇfox
1733            jumps ˇover"},
1734        )
1735        .await
1736        .assert_matches();
1737        cx.simulate(
1738            "[ space",
1739            indoc! {"
1740            The quick
1741            ˇ
1742            brown fox"},
1743        )
1744        .await
1745        .assert_matches();
1746
1747        cx.simulate("] space", "ˇ").await.assert_matches();
1748        cx.simulate("] space", "The ˇquick").await.assert_matches();
1749        cx.simulate_at_each_offset(
1750            "3 ] space",
1751            indoc! {"
1752            The qˇuick
1753            brown ˇfox
1754            jumps ˇover"},
1755        )
1756        .await
1757        .assert_matches();
1758        cx.simulate_at_each_offset(
1759            "] space",
1760            indoc! {"
1761            The qˇuick
1762            brown ˇfox
1763            jumps ˇover"},
1764        )
1765        .await
1766        .assert_matches();
1767        cx.simulate(
1768            "] space",
1769            indoc! {"
1770            The quick
1771            ˇ
1772            brown fox"},
1773        )
1774        .await
1775        .assert_matches();
1776    }
1777
1778    #[gpui::test]
1779    async fn test_dd(cx: &mut gpui::TestAppContext) {
1780        let mut cx = NeovimBackedTestContext::new(cx).await;
1781        cx.simulate("d d", "ˇ").await.assert_matches();
1782        cx.simulate("d d", "The ˇquick").await.assert_matches();
1783        cx.simulate_at_each_offset(
1784            "d d",
1785            indoc! {"
1786            The qˇuick
1787            brown ˇfox
1788            jumps ˇover"},
1789        )
1790        .await
1791        .assert_matches();
1792        cx.simulate(
1793            "d d",
1794            indoc! {"
1795                The quick
1796                ˇ
1797                brown fox"},
1798        )
1799        .await
1800        .assert_matches();
1801    }
1802
1803    #[gpui::test]
1804    async fn test_cc(cx: &mut gpui::TestAppContext) {
1805        let mut cx = NeovimBackedTestContext::new(cx).await;
1806        cx.simulate("c c", "ˇ").await.assert_matches();
1807        cx.simulate("c c", "The ˇquick").await.assert_matches();
1808        cx.simulate_at_each_offset(
1809            "c c",
1810            indoc! {"
1811                The quˇick
1812                brown ˇfox
1813                jumps ˇover"},
1814        )
1815        .await
1816        .assert_matches();
1817        cx.simulate(
1818            "c c",
1819            indoc! {"
1820                The quick
1821                ˇ
1822                brown fox"},
1823        )
1824        .await
1825        .assert_matches();
1826    }
1827
1828    #[gpui::test]
1829    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1830        let mut cx = NeovimBackedTestContext::new(cx).await;
1831
1832        for count in 1..=5 {
1833            cx.simulate_at_each_offset(
1834                &format!("{count} w"),
1835                indoc! {"
1836                    ˇThe quˇickˇ browˇn
1837                    ˇ
1838                    ˇfox ˇjumpsˇ-ˇoˇver
1839                    ˇthe lazy dog
1840                "},
1841            )
1842            .await
1843            .assert_matches();
1844        }
1845    }
1846
1847    #[gpui::test]
1848    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1849        let mut cx = NeovimBackedTestContext::new(cx).await;
1850        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1851            .await
1852            .assert_matches();
1853    }
1854
1855    #[gpui::test]
1856    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1857        let mut cx = NeovimBackedTestContext::new(cx).await;
1858
1859        for count in 1..=3 {
1860            let test_case = indoc! {"
1861                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1862                ˇ    ˇbˇaaˇa ˇbˇbˇb
1863                ˇ
1864                ˇb
1865            "};
1866
1867            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1868                .await
1869                .assert_matches();
1870
1871            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1872                .await
1873                .assert_matches();
1874        }
1875    }
1876
1877    #[gpui::test]
1878    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1879        let mut cx = NeovimBackedTestContext::new(cx).await;
1880        let test_case = indoc! {"
1881            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1882            ˇ    ˇbˇaaˇa ˇbˇbˇb
1883            ˇ•••
1884            ˇb
1885            "
1886        };
1887
1888        for count in 1..=3 {
1889            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1890                .await
1891                .assert_matches();
1892
1893            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1894                .await
1895                .assert_matches();
1896        }
1897    }
1898
1899    #[gpui::test]
1900    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1901        let mut cx = VimTestContext::new(cx, true).await;
1902        cx.update_global(|store: &mut SettingsStore, cx| {
1903            store.update_user_settings(cx, |s| {
1904                s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1905            });
1906        });
1907
1908        cx.assert_binding(
1909            "f p",
1910            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1911            Mode::Normal,
1912            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1913            Mode::Normal,
1914        );
1915
1916        cx.assert_binding(
1917            "shift-f p",
1918            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1919            Mode::Normal,
1920            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1921            Mode::Normal,
1922        );
1923
1924        cx.assert_binding(
1925            "t p",
1926            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1927            Mode::Normal,
1928            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1929            Mode::Normal,
1930        );
1931
1932        cx.assert_binding(
1933            "shift-t p",
1934            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1935            Mode::Normal,
1936            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1937            Mode::Normal,
1938        );
1939    }
1940
1941    #[gpui::test]
1942    async fn test_percent(cx: &mut TestAppContext) {
1943        let mut cx = NeovimBackedTestContext::new(cx).await;
1944        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1945            .await
1946            .assert_matches();
1947        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1948            .await
1949            .assert_matches();
1950        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1951            .await
1952            .assert_matches();
1953    }
1954
1955    #[gpui::test]
1956    async fn test_percent_in_comment(cx: &mut TestAppContext) {
1957        let mut cx = NeovimBackedTestContext::new(cx).await;
1958        cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1959            .await
1960            .assert_matches();
1961        cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
1962            .await
1963            .assert_matches();
1964        // Template-style brackets (like Liquid {% %} and {{ }})
1965        cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
1966            .await
1967            .assert_matches();
1968        cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
1969            .await
1970            .assert_matches();
1971    }
1972
1973    #[gpui::test]
1974    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1975        let mut cx = NeovimBackedTestContext::new(cx).await;
1976
1977        // goes to current line end
1978        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1979        cx.simulate_shared_keystrokes("$").await;
1980        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1981
1982        // goes to next line end
1983        cx.simulate_shared_keystrokes("2 $").await;
1984        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1985
1986        // try to exceed the final line.
1987        cx.simulate_shared_keystrokes("4 $").await;
1988        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1989    }
1990
1991    #[gpui::test]
1992    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1993        let mut cx = VimTestContext::new(cx, true).await;
1994        cx.update(|_, cx| {
1995            cx.bind_keys(vec![
1996                KeyBinding::new(
1997                    "w",
1998                    motion::NextSubwordStart {
1999                        ignore_punctuation: false,
2000                    },
2001                    Some("Editor && VimControl && !VimWaiting && !menu"),
2002                ),
2003                KeyBinding::new(
2004                    "b",
2005                    motion::PreviousSubwordStart {
2006                        ignore_punctuation: false,
2007                    },
2008                    Some("Editor && VimControl && !VimWaiting && !menu"),
2009                ),
2010                KeyBinding::new(
2011                    "e",
2012                    motion::NextSubwordEnd {
2013                        ignore_punctuation: false,
2014                    },
2015                    Some("Editor && VimControl && !VimWaiting && !menu"),
2016                ),
2017                KeyBinding::new(
2018                    "g e",
2019                    motion::PreviousSubwordEnd {
2020                        ignore_punctuation: false,
2021                    },
2022                    Some("Editor && VimControl && !VimWaiting && !menu"),
2023                ),
2024            ]);
2025        });
2026
2027        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
2028        // Special case: In 'cw', 'w' acts like 'e'
2029        cx.assert_binding(
2030            "c w",
2031            indoc! {"ˇassert_binding"},
2032            Mode::Normal,
2033            indoc! {"ˇ_binding"},
2034            Mode::Insert,
2035        );
2036
2037        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
2038
2039        // Subword end should stop at EOL
2040        cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"});
2041
2042        // Already at subword end, should move to next subword on next line
2043        cx.assert_binding_normal(
2044            "e",
2045            indoc! {"foo_barˇ\nbaz_qux"},
2046            indoc! {"foo_bar\nbaˇz_qux"},
2047        );
2048
2049        // CamelCase at EOL
2050        cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"});
2051
2052        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
2053
2054        cx.assert_binding_normal(
2055            "g e",
2056            indoc! {"assert_bindinˇg"},
2057            indoc! {"asserˇt_binding"},
2058        );
2059    }
2060
2061    #[gpui::test]
2062    async fn test_r(cx: &mut gpui::TestAppContext) {
2063        let mut cx = NeovimBackedTestContext::new(cx).await;
2064
2065        cx.set_shared_state("ˇhello\n").await;
2066        cx.simulate_shared_keystrokes("r -").await;
2067        cx.shared_state().await.assert_eq("ˇ-ello\n");
2068
2069        cx.set_shared_state("ˇhello\n").await;
2070        cx.simulate_shared_keystrokes("3 r -").await;
2071        cx.shared_state().await.assert_eq("--ˇ-lo\n");
2072
2073        cx.set_shared_state("ˇhello\n").await;
2074        cx.simulate_shared_keystrokes("r - 2 l .").await;
2075        cx.shared_state().await.assert_eq("-eˇ-lo\n");
2076
2077        cx.set_shared_state("ˇhello world\n").await;
2078        cx.simulate_shared_keystrokes("2 r - f w .").await;
2079        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
2080
2081        cx.set_shared_state("ˇhello world\n").await;
2082        cx.simulate_shared_keystrokes("2 0 r - ").await;
2083        cx.shared_state().await.assert_eq("ˇhello world\n");
2084
2085        cx.set_shared_state("  helloˇ world\n").await;
2086        cx.simulate_shared_keystrokes("r enter").await;
2087        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
2088
2089        cx.set_shared_state("  helloˇ world\n").await;
2090        cx.simulate_shared_keystrokes("2 r enter").await;
2091        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
2092    }
2093
2094    #[gpui::test]
2095    async fn test_gq(cx: &mut gpui::TestAppContext) {
2096        let mut cx = NeovimBackedTestContext::new(cx).await;
2097        cx.set_neovim_option("textwidth=5").await;
2098
2099        cx.update(|_, cx| {
2100            SettingsStore::update_global(cx, |settings, cx| {
2101                settings.update_user_settings(cx, |settings| {
2102                    settings
2103                        .project
2104                        .all_languages
2105                        .defaults
2106                        .preferred_line_length = Some(5);
2107                });
2108            })
2109        });
2110
2111        cx.set_shared_state("ˇth th th th th th\n").await;
2112        cx.simulate_shared_keystrokes("g q q").await;
2113        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
2114
2115        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
2116            .await;
2117        cx.simulate_shared_keystrokes("v j g q").await;
2118        cx.shared_state()
2119            .await
2120            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
2121    }
2122
2123    #[gpui::test]
2124    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
2125        let mut cx = NeovimBackedTestContext::new(cx).await;
2126        cx.set_neovim_option("filetype=rust").await;
2127
2128        cx.set_shared_state("// helloˇ\n").await;
2129        cx.simulate_shared_keystrokes("o").await;
2130        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
2131        cx.simulate_shared_keystrokes("x escape shift-o").await;
2132        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
2133    }
2134
2135    #[gpui::test]
2136    async fn test_o_auto_indent_none(cx: &mut gpui::TestAppContext) {
2137        let mut cx = VimTestContext::new(cx, true).await;
2138        cx.update_global(|store: &mut SettingsStore, cx| {
2139            store.update_user_settings(cx, |s| {
2140                s.project.all_languages.defaults.auto_indent = Some(language::AutoIndentMode::None);
2141            });
2142        });
2143
2144        // o: new line below starts at column 0 regardless of current indentation
2145        cx.set_state("    let xˇ = 1;", Mode::Normal);
2146        cx.simulate_keystrokes("o");
2147        cx.assert_state("    let x = 1;\nˇ", Mode::Insert);
2148
2149        // O: new line above starts at column 0 regardless of current indentation
2150        cx.set_state("    let xˇ = 1;", Mode::Normal);
2151        cx.simulate_keystrokes("shift-o");
2152        cx.assert_state("ˇ\n    let x = 1;", Mode::Insert);
2153
2154        // o on the first line: no crash and column 0
2155        cx.set_state("ˇfoo", Mode::Normal);
2156        cx.simulate_keystrokes("o");
2157        cx.assert_state("foo\nˇ", Mode::Insert);
2158
2159        // O on the first line: no crash and column 0
2160        cx.set_state("ˇfoo", Mode::Normal);
2161        cx.simulate_keystrokes("shift-o");
2162        cx.assert_state("ˇ\nfoo", Mode::Insert);
2163
2164        // o on an already-empty line: stays at column 0
2165        cx.set_state("fooˇ\n\nbar", Mode::Normal);
2166        cx.simulate_keystrokes("j o");
2167        cx.assert_state("foo\n\nˇ\nbar", Mode::Insert);
2168    }
2169
2170    #[gpui::test]
2171    async fn test_o_preserve_indent(cx: &mut gpui::TestAppContext) {
2172        let mut cx = VimTestContext::new(cx, true).await;
2173        cx.update_global(|store: &mut SettingsStore, cx| {
2174            store.update_user_settings(cx, |s| {
2175                s.project.all_languages.defaults.auto_indent =
2176                    Some(language::AutoIndentMode::PreserveIndent);
2177            });
2178        });
2179
2180        // o: new line below copies current line's indentation
2181        cx.set_state("    let xˇ = 1;", Mode::Normal);
2182        cx.simulate_keystrokes("o");
2183        cx.assert_state("    let x = 1;\n    ˇ", Mode::Insert);
2184
2185        // O: new line above copies current line's indentation
2186        cx.set_state("    let xˇ = 1;", Mode::Normal);
2187        cx.simulate_keystrokes("shift-o");
2188        cx.assert_state("    ˇ\n    let x = 1;", Mode::Insert);
2189    }
2190
2191    #[gpui::test]
2192    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2193        let mut cx = NeovimBackedTestContext::new(cx).await;
2194        cx.set_shared_state("heˇllo\n").await;
2195        cx.simulate_shared_keystrokes("y y p").await;
2196        cx.shared_state().await.assert_eq("hello\nˇhello\n");
2197    }
2198
2199    #[gpui::test]
2200    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2201        let mut cx = NeovimBackedTestContext::new(cx).await;
2202        cx.set_shared_state("heˇllo").await;
2203        cx.simulate_shared_keystrokes("y y p").await;
2204        cx.shared_state().await.assert_eq("hello\nˇhello");
2205    }
2206
2207    #[gpui::test]
2208    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2209        let mut cx = NeovimBackedTestContext::new(cx).await;
2210        cx.set_shared_state("heˇllo\nhello").await;
2211        cx.simulate_shared_keystrokes("2 y y p").await;
2212        cx.shared_state()
2213            .await
2214            .assert_eq("hello\nˇhello\nhello\nhello");
2215    }
2216
2217    #[gpui::test]
2218    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2219        let mut cx = NeovimBackedTestContext::new(cx).await;
2220        cx.set_shared_state("heˇllo").await;
2221        cx.simulate_shared_keystrokes("d d").await;
2222        cx.shared_state().await.assert_eq("ˇ");
2223        cx.simulate_shared_keystrokes("p p").await;
2224        cx.shared_state().await.assert_eq("\nhello\nˇhello");
2225    }
2226
2227    #[gpui::test]
2228    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2229        let mut cx = NeovimBackedTestContext::new(cx).await;
2230
2231        cx.set_shared_state("heˇllo").await;
2232        cx.simulate_shared_keystrokes("v i w shift-i").await;
2233        cx.shared_state().await.assert_eq("ˇhello");
2234
2235        cx.set_shared_state(indoc! {"
2236            The quick brown
2237            fox ˇjumps over
2238            the lazy dog"})
2239            .await;
2240        cx.simulate_shared_keystrokes("shift-v shift-i").await;
2241        cx.shared_state().await.assert_eq(indoc! {"
2242            The quick brown
2243            ˇfox jumps over
2244            the lazy dog"});
2245
2246        cx.set_shared_state(indoc! {"
2247            The quick brown
2248            fox ˇjumps over
2249            the lazy dog"})
2250            .await;
2251        cx.simulate_shared_keystrokes("shift-v shift-a").await;
2252        cx.shared_state().await.assert_eq(indoc! {"
2253            The quick brown
2254            fox jˇumps over
2255            the lazy dog"});
2256    }
2257
2258    #[gpui::test]
2259    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2260        let mut cx = NeovimBackedTestContext::new(cx).await;
2261
2262        cx.set_shared_state(indoc! {"
2263            ˇfn a() { }
2264
2265
2266
2267
2268
2269            fn b() { }
2270
2271
2272
2273
2274
2275            fn b() { }"})
2276            .await;
2277        cx.simulate_shared_keystrokes("3 }").await;
2278        cx.shared_state().await.assert_matches();
2279        cx.simulate_shared_keystrokes("ctrl-o").await;
2280        cx.shared_state().await.assert_matches();
2281        cx.simulate_shared_keystrokes("ctrl-i").await;
2282        cx.shared_state().await.assert_matches();
2283        cx.simulate_shared_keystrokes("1 1 k").await;
2284        cx.shared_state().await.assert_matches();
2285        cx.simulate_shared_keystrokes("ctrl-o").await;
2286        cx.shared_state().await.assert_matches();
2287    }
2288
2289    #[gpui::test]
2290    async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2291        let mut cx = NeovimBackedTestContext::new(cx).await;
2292
2293        cx.set_shared_state(indoc! {"
2294            ˇfn a() { }
2295            fn a() { }
2296            fn a() { }
2297        "})
2298            .await;
2299        // do a jump to reset vim's undo grouping
2300        cx.simulate_shared_keystrokes("shift-g").await;
2301        cx.shared_state().await.assert_matches();
2302        cx.simulate_shared_keystrokes("r a").await;
2303        cx.shared_state().await.assert_matches();
2304        cx.simulate_shared_keystrokes("shift-u").await;
2305        cx.shared_state().await.assert_matches();
2306        cx.simulate_shared_keystrokes("shift-u").await;
2307        cx.shared_state().await.assert_matches();
2308        cx.simulate_shared_keystrokes("g g shift-u").await;
2309        cx.shared_state().await.assert_matches();
2310    }
2311
2312    #[gpui::test]
2313    async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2314        let mut cx = NeovimBackedTestContext::new(cx).await;
2315
2316        cx.set_shared_state(indoc! {"
2317            ˇfn a() { }
2318            fn a() { }
2319            fn a() { }
2320        "})
2321            .await;
2322        // do a jump to reset vim's undo grouping
2323        cx.simulate_shared_keystrokes("shift-g k").await;
2324        cx.shared_state().await.assert_matches();
2325        cx.simulate_shared_keystrokes("o h e l l o escape").await;
2326        cx.shared_state().await.assert_matches();
2327        cx.simulate_shared_keystrokes("shift-u").await;
2328        cx.shared_state().await.assert_matches();
2329        cx.simulate_shared_keystrokes("shift-u").await;
2330    }
2331
2332    #[gpui::test]
2333    async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2334        let mut cx = NeovimBackedTestContext::new(cx).await;
2335
2336        cx.set_shared_state(indoc! {"
2337            ˇfn a() { }
2338            fn a() { }
2339            fn a() { }
2340        "})
2341            .await;
2342        // do a jump to reset vim's undo grouping
2343        cx.simulate_shared_keystrokes("x shift-g k").await;
2344        cx.shared_state().await.assert_matches();
2345        cx.simulate_shared_keystrokes("x f a x f { x").await;
2346        cx.shared_state().await.assert_matches();
2347        cx.simulate_shared_keystrokes("shift-u").await;
2348        cx.shared_state().await.assert_matches();
2349        cx.simulate_shared_keystrokes("shift-u").await;
2350        cx.shared_state().await.assert_matches();
2351        cx.simulate_shared_keystrokes("shift-u").await;
2352        cx.shared_state().await.assert_matches();
2353        cx.simulate_shared_keystrokes("shift-u").await;
2354        cx.shared_state().await.assert_matches();
2355    }
2356
2357    #[gpui::test]
2358    async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2359        let mut cx = VimTestContext::new(cx, true).await;
2360
2361        cx.set_state(
2362            indoc! {"
2363            ˇone two ˇone
2364            two ˇone two
2365        "},
2366            Mode::Normal,
2367        );
2368        cx.simulate_keystrokes("3 r a");
2369        cx.assert_state(
2370            indoc! {"
2371            aaˇa two aaˇa
2372            two aaˇa two
2373        "},
2374            Mode::Normal,
2375        );
2376        cx.simulate_keystrokes("escape escape");
2377        cx.simulate_keystrokes("shift-u");
2378        cx.set_state(
2379            indoc! {"
2380            onˇe two onˇe
2381            two onˇe two
2382        "},
2383            Mode::Normal,
2384        );
2385    }
2386
2387    #[gpui::test]
2388    async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2389        let mut cx = VimTestContext::new(cx, true).await;
2390
2391        // Open 4 tabs.
2392        cx.simulate_keystrokes(": tabnew");
2393        cx.simulate_keystrokes("enter");
2394        cx.simulate_keystrokes(": tabnew");
2395        cx.simulate_keystrokes("enter");
2396        cx.simulate_keystrokes(": tabnew");
2397        cx.simulate_keystrokes("enter");
2398        cx.workspace(|workspace, _, cx| {
2399            assert_eq!(workspace.items(cx).count(), 4);
2400            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2401        });
2402
2403        cx.simulate_keystrokes("1 g t");
2404        cx.workspace(|workspace, _, cx| {
2405            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2406        });
2407
2408        cx.simulate_keystrokes("3 g t");
2409        cx.workspace(|workspace, _, cx| {
2410            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2411        });
2412
2413        cx.simulate_keystrokes("4 g t");
2414        cx.workspace(|workspace, _, cx| {
2415            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2416        });
2417
2418        cx.simulate_keystrokes("1 g t");
2419        cx.simulate_keystrokes("g t");
2420        cx.workspace(|workspace, _, cx| {
2421            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2422        });
2423    }
2424
2425    #[gpui::test]
2426    async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2427        let mut cx = VimTestContext::new(cx, true).await;
2428
2429        // Open 4 tabs.
2430        cx.simulate_keystrokes(": tabnew");
2431        cx.simulate_keystrokes("enter");
2432        cx.simulate_keystrokes(": tabnew");
2433        cx.simulate_keystrokes("enter");
2434        cx.simulate_keystrokes(": tabnew");
2435        cx.simulate_keystrokes("enter");
2436        cx.workspace(|workspace, _, cx| {
2437            assert_eq!(workspace.items(cx).count(), 4);
2438            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2439        });
2440
2441        cx.simulate_keystrokes("2 g shift-t");
2442        cx.workspace(|workspace, _, cx| {
2443            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2444        });
2445
2446        cx.simulate_keystrokes("g shift-t");
2447        cx.workspace(|workspace, _, cx| {
2448            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2449        });
2450
2451        // Wraparound: gT from first tab should go to last.
2452        cx.simulate_keystrokes("g shift-t");
2453        cx.workspace(|workspace, _, cx| {
2454            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2455        });
2456
2457        cx.simulate_keystrokes("6 g shift-t");
2458        cx.workspace(|workspace, _, cx| {
2459            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2460        });
2461    }
2462
2463    #[gpui::test]
2464    async fn test_temporary_mode(cx: &mut gpui::TestAppContext) {
2465        let mut cx = NeovimBackedTestContext::new(cx).await;
2466
2467        // Test jumping to the end of the line ($).
2468        cx.set_shared_state(indoc! {"lorem ˇipsum"}).await;
2469        cx.simulate_shared_keystrokes("i").await;
2470        cx.shared_state().await.assert_matches();
2471        cx.simulate_shared_keystrokes("ctrl-o $").await;
2472        cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"});
2473
2474        // Test jumping to the next word.
2475        cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await;
2476        cx.simulate_shared_keystrokes("a").await;
2477        cx.shared_state().await.assert_matches();
2478        cx.simulate_shared_keystrokes("a n d space ctrl-o w").await;
2479        cx.shared_state()
2480            .await
2481            .assert_eq(indoc! {"lorem and ipsum ˇdolor"});
2482
2483        // Test yanking to end of line ($).
2484        cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await;
2485        cx.simulate_shared_keystrokes("i").await;
2486        cx.shared_state().await.assert_matches();
2487        cx.simulate_shared_keystrokes("a n d space ctrl-o y $")
2488            .await;
2489        cx.shared_state()
2490            .await
2491            .assert_eq(indoc! {"lorem and ˇipsum dolor"});
2492    }
2493}