normal.rs

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