normal.rs

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