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