normal.rs

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