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