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