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