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