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