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