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