normal.rs

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