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.status_label = Some(
 953                format!(
 954                    "{}{} {} lines --{:.0}%--",
 955                    filename,
 956                    modified,
 957                    lines,
 958                    percentage * 100.0,
 959                )
 960                .into(),
 961            );
 962            cx.notify();
 963        });
 964    }
 965
 966    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 967        self.record_current_action(cx);
 968        self.store_visual_marks(window, cx);
 969        self.update_editor(cx, |vim, editor, cx| {
 970            editor.transact(window, cx, |editor, window, cx| {
 971                let original_positions = vim.save_selection_starts(editor, cx);
 972                editor.toggle_comments(&Default::default(), window, cx);
 973                vim.restore_selection_cursors(editor, window, cx, original_positions);
 974            });
 975        });
 976        if self.mode.is_visual() {
 977            self.switch_mode(Mode::Normal, true, window, cx)
 978        }
 979    }
 980
 981    pub(crate) fn normal_replace(
 982        &mut self,
 983        text: Arc<str>,
 984        window: &mut Window,
 985        cx: &mut Context<Self>,
 986    ) {
 987        // We need to use `text.chars().count()` instead of `text.len()` here as
 988        // `len()` counts bytes, not characters.
 989        let char_count = text.chars().count();
 990        let count = Vim::take_count(cx).unwrap_or(char_count);
 991        let is_return_char = text == "\n".into() || text == "\r".into();
 992        let repeat_count = match (is_return_char, char_count) {
 993            (true, _) => 0,
 994            (_, 1) => count,
 995            (_, _) => 1,
 996        };
 997
 998        Vim::take_forced_motion(cx);
 999        self.stop_recording(cx);
1000        self.update_editor(cx, |_, editor, cx| {
1001            editor.transact(window, cx, |editor, window, cx| {
1002                editor.set_clip_at_line_ends(false, cx);
1003                let display_map = editor.display_snapshot(cx);
1004                let display_selections = editor.selections.all_display(&display_map);
1005
1006                let mut edits = Vec::with_capacity(display_selections.len());
1007                for selection in &display_selections {
1008                    let mut range = selection.range();
1009                    for _ in 0..count {
1010                        let new_point = movement::saturating_right(&display_map, range.end);
1011                        if range.end == new_point {
1012                            return;
1013                        }
1014                        range.end = new_point;
1015                    }
1016
1017                    edits.push((
1018                        range.start.to_offset(&display_map, Bias::Left)
1019                            ..range.end.to_offset(&display_map, Bias::Left),
1020                        text.repeat(repeat_count),
1021                    ));
1022                }
1023
1024                editor.edit(edits, cx);
1025                if is_return_char {
1026                    editor.newline(&editor::actions::Newline, window, cx);
1027                }
1028                editor.set_clip_at_line_ends(true, cx);
1029                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1030                    s.move_with(&mut |map, selection| {
1031                        let point = movement::saturating_left(map, selection.head());
1032                        selection.collapse_to(point, SelectionGoal::None)
1033                    });
1034                });
1035            });
1036        });
1037        self.pop_operator(window, cx);
1038    }
1039
1040    pub fn save_selection_starts(
1041        &self,
1042        editor: &Editor,
1043        cx: &mut Context<Editor>,
1044    ) -> HashMap<usize, Anchor> {
1045        let display_map = editor.display_snapshot(cx);
1046        let selections = editor.selections.all_display(&display_map);
1047        selections
1048            .iter()
1049            .map(|selection| {
1050                (
1051                    selection.id,
1052                    display_map.display_point_to_anchor(selection.start, Bias::Right),
1053                )
1054            })
1055            .collect::<HashMap<_, _>>()
1056    }
1057
1058    pub fn restore_selection_cursors(
1059        &self,
1060        editor: &mut Editor,
1061        window: &mut Window,
1062        cx: &mut Context<Editor>,
1063        mut positions: HashMap<usize, Anchor>,
1064    ) {
1065        editor.change_selections(Default::default(), window, cx, |s| {
1066            s.move_with(&mut |map, selection| {
1067                if let Some(anchor) = positions.remove(&selection.id) {
1068                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
1069                }
1070            });
1071        });
1072    }
1073
1074    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1075        if self.temp_mode {
1076            self.switch_mode(Mode::Insert, true, window, cx);
1077        }
1078    }
1079}
1080
1081#[cfg(test)]
1082mod test {
1083    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
1084    use indoc::indoc;
1085    use settings::SettingsStore;
1086
1087    use crate::{
1088        motion,
1089        state::Mode::{self},
1090        test::{NeovimBackedTestContext, VimTestContext},
1091    };
1092
1093    #[gpui::test]
1094    async fn test_h(cx: &mut gpui::TestAppContext) {
1095        let mut cx = NeovimBackedTestContext::new(cx).await;
1096        cx.simulate_at_each_offset(
1097            "h",
1098            indoc! {"
1099            ˇThe qˇuick
1100            ˇbrown"
1101            },
1102        )
1103        .await
1104        .assert_matches();
1105    }
1106
1107    #[gpui::test]
1108    async fn test_backspace(cx: &mut gpui::TestAppContext) {
1109        let mut cx = NeovimBackedTestContext::new(cx).await;
1110        cx.simulate_at_each_offset(
1111            "backspace",
1112            indoc! {"
1113            ˇThe qˇuick
1114            ˇbrown"
1115            },
1116        )
1117        .await
1118        .assert_matches();
1119    }
1120
1121    #[gpui::test]
1122    async fn test_j(cx: &mut gpui::TestAppContext) {
1123        let mut cx = NeovimBackedTestContext::new(cx).await;
1124
1125        cx.set_shared_state(indoc! {"
1126            aaˇaa
1127            😃😃"
1128        })
1129        .await;
1130        cx.simulate_shared_keystrokes("j").await;
1131        cx.shared_state().await.assert_eq(indoc! {"
1132            aaaa
1133            😃ˇ😃"
1134        });
1135
1136        cx.simulate_at_each_offset(
1137            "j",
1138            indoc! {"
1139                ˇThe qˇuick broˇwn
1140                ˇfox jumps"
1141            },
1142        )
1143        .await
1144        .assert_matches();
1145    }
1146
1147    #[gpui::test]
1148    async fn test_enter(cx: &mut gpui::TestAppContext) {
1149        let mut cx = NeovimBackedTestContext::new(cx).await;
1150        cx.simulate_at_each_offset(
1151            "enter",
1152            indoc! {"
1153            ˇThe qˇuick broˇwn
1154            ˇfox jumps"
1155            },
1156        )
1157        .await
1158        .assert_matches();
1159    }
1160
1161    #[gpui::test]
1162    async fn test_k(cx: &mut gpui::TestAppContext) {
1163        let mut cx = NeovimBackedTestContext::new(cx).await;
1164        cx.simulate_at_each_offset(
1165            "k",
1166            indoc! {"
1167            ˇThe qˇuick
1168            ˇbrown fˇox jumˇps"
1169            },
1170        )
1171        .await
1172        .assert_matches();
1173    }
1174
1175    #[gpui::test]
1176    async fn test_l(cx: &mut gpui::TestAppContext) {
1177        let mut cx = NeovimBackedTestContext::new(cx).await;
1178        cx.simulate_at_each_offset(
1179            "l",
1180            indoc! {"
1181            ˇThe qˇuicˇk
1182            ˇbrowˇn"},
1183        )
1184        .await
1185        .assert_matches();
1186    }
1187
1188    #[gpui::test]
1189    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1190        let mut cx = NeovimBackedTestContext::new(cx).await;
1191        cx.simulate_at_each_offset(
1192            "$",
1193            indoc! {"
1194            ˇThe qˇuicˇk
1195            ˇbrowˇn"},
1196        )
1197        .await
1198        .assert_matches();
1199        cx.simulate_at_each_offset(
1200            "0",
1201            indoc! {"
1202                ˇThe qˇuicˇk
1203                ˇbrowˇn"},
1204        )
1205        .await
1206        .assert_matches();
1207    }
1208
1209    #[gpui::test]
1210    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1211        let mut cx = NeovimBackedTestContext::new(cx).await;
1212
1213        cx.simulate_at_each_offset(
1214            "shift-g",
1215            indoc! {"
1216                The ˇquick
1217
1218                brown fox jumps
1219                overˇ the lazy doˇg"},
1220        )
1221        .await
1222        .assert_matches();
1223        cx.simulate(
1224            "shift-g",
1225            indoc! {"
1226            The quiˇck
1227
1228            brown"},
1229        )
1230        .await
1231        .assert_matches();
1232        cx.simulate(
1233            "shift-g",
1234            indoc! {"
1235            The quiˇck
1236
1237            "},
1238        )
1239        .await
1240        .assert_matches();
1241    }
1242
1243    #[gpui::test]
1244    async fn test_w(cx: &mut gpui::TestAppContext) {
1245        let mut cx = NeovimBackedTestContext::new(cx).await;
1246        cx.simulate_at_each_offset(
1247            "w",
1248            indoc! {"
1249            The ˇquickˇ-ˇbrown
1250            ˇ
1251            ˇ
1252            ˇfox_jumps ˇover
1253            ˇthˇe"},
1254        )
1255        .await
1256        .assert_matches();
1257        cx.simulate_at_each_offset(
1258            "shift-w",
1259            indoc! {"
1260            The ˇquickˇ-ˇbrown
1261            ˇ
1262            ˇ
1263            ˇfox_jumps ˇover
1264            ˇthˇe"},
1265        )
1266        .await
1267        .assert_matches();
1268    }
1269
1270    #[gpui::test]
1271    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1272        let mut cx = NeovimBackedTestContext::new(cx).await;
1273        cx.simulate_at_each_offset(
1274            "e",
1275            indoc! {"
1276            Thˇe quicˇkˇ-browˇn
1277
1278
1279            fox_jumpˇs oveˇr
1280            thˇe"},
1281        )
1282        .await
1283        .assert_matches();
1284        cx.simulate_at_each_offset(
1285            "shift-e",
1286            indoc! {"
1287            Thˇe quicˇkˇ-browˇn
1288
1289
1290            fox_jumpˇs oveˇr
1291            thˇe"},
1292        )
1293        .await
1294        .assert_matches();
1295    }
1296
1297    #[gpui::test]
1298    async fn test_b(cx: &mut gpui::TestAppContext) {
1299        let mut cx = NeovimBackedTestContext::new(cx).await;
1300        cx.simulate_at_each_offset(
1301            "b",
1302            indoc! {"
1303            ˇThe ˇquickˇ-ˇbrown
1304            ˇ
1305            ˇ
1306            ˇfox_jumps ˇover
1307            ˇthe"},
1308        )
1309        .await
1310        .assert_matches();
1311        cx.simulate_at_each_offset(
1312            "shift-b",
1313            indoc! {"
1314            ˇThe ˇquickˇ-ˇbrown
1315            ˇ
1316            ˇ
1317            ˇfox_jumps ˇover
1318            ˇthe"},
1319        )
1320        .await
1321        .assert_matches();
1322    }
1323
1324    #[gpui::test]
1325    async fn test_gg(cx: &mut gpui::TestAppContext) {
1326        let mut cx = NeovimBackedTestContext::new(cx).await;
1327        cx.simulate_at_each_offset(
1328            "g g",
1329            indoc! {"
1330                The qˇuick
1331
1332                brown fox jumps
1333                over ˇthe laˇzy dog"},
1334        )
1335        .await
1336        .assert_matches();
1337        cx.simulate(
1338            "g g",
1339            indoc! {"
1340
1341
1342                brown fox jumps
1343                over the laˇzy dog"},
1344        )
1345        .await
1346        .assert_matches();
1347        cx.simulate(
1348            "2 g g",
1349            indoc! {"
1350                ˇ
1351
1352                brown fox jumps
1353                over the lazydog"},
1354        )
1355        .await
1356        .assert_matches();
1357    }
1358
1359    #[gpui::test]
1360    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1361        let mut cx = NeovimBackedTestContext::new(cx).await;
1362        cx.simulate_at_each_offset(
1363            "shift-g",
1364            indoc! {"
1365                The qˇuick
1366
1367                brown fox jumps
1368                over ˇthe laˇzy dog"},
1369        )
1370        .await
1371        .assert_matches();
1372        cx.simulate(
1373            "shift-g",
1374            indoc! {"
1375
1376
1377                brown fox jumps
1378                over the laˇzy dog"},
1379        )
1380        .await
1381        .assert_matches();
1382        cx.simulate(
1383            "2 shift-g",
1384            indoc! {"
1385                ˇ
1386
1387                brown fox jumps
1388                over the lazydog"},
1389        )
1390        .await
1391        .assert_matches();
1392    }
1393
1394    #[gpui::test]
1395    async fn test_a(cx: &mut gpui::TestAppContext) {
1396        let mut cx = NeovimBackedTestContext::new(cx).await;
1397        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1398            .await
1399            .assert_matches();
1400    }
1401
1402    #[gpui::test]
1403    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1404        let mut cx = NeovimBackedTestContext::new(cx).await;
1405        cx.simulate_at_each_offset(
1406            "shift-a",
1407            indoc! {"
1408            ˇ
1409            The qˇuick
1410            brown ˇfox "},
1411        )
1412        .await
1413        .assert_matches();
1414    }
1415
1416    #[gpui::test]
1417    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1418        let mut cx = NeovimBackedTestContext::new(cx).await;
1419        cx.simulate("^", "The qˇuick").await.assert_matches();
1420        cx.simulate("^", " The qˇuick").await.assert_matches();
1421        cx.simulate("^", "ˇ").await.assert_matches();
1422        cx.simulate(
1423            "^",
1424            indoc! {"
1425                The qˇuick
1426                brown fox"},
1427        )
1428        .await
1429        .assert_matches();
1430        cx.simulate(
1431            "^",
1432            indoc! {"
1433                ˇ
1434                The quick"},
1435        )
1436        .await
1437        .assert_matches();
1438        // Indoc disallows trailing whitespace.
1439        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1440    }
1441
1442    #[gpui::test]
1443    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1444        let mut cx = NeovimBackedTestContext::new(cx).await;
1445        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1446        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1447        cx.simulate("shift-i", "ˇ").await.assert_matches();
1448        cx.simulate(
1449            "shift-i",
1450            indoc! {"
1451                The qˇuick
1452                brown fox"},
1453        )
1454        .await
1455        .assert_matches();
1456        cx.simulate(
1457            "shift-i",
1458            indoc! {"
1459                ˇ
1460                The quick"},
1461        )
1462        .await
1463        .assert_matches();
1464    }
1465
1466    #[gpui::test]
1467    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1468        let mut cx = NeovimBackedTestContext::new(cx).await;
1469        cx.simulate(
1470            "shift-d",
1471            indoc! {"
1472                The qˇuick
1473                brown fox"},
1474        )
1475        .await
1476        .assert_matches();
1477        cx.simulate(
1478            "shift-d",
1479            indoc! {"
1480                The quick
1481                ˇ
1482                brown fox"},
1483        )
1484        .await
1485        .assert_matches();
1486    }
1487
1488    #[gpui::test]
1489    async fn test_x(cx: &mut gpui::TestAppContext) {
1490        let mut cx = NeovimBackedTestContext::new(cx).await;
1491        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1492            .await
1493            .assert_matches();
1494        cx.simulate(
1495            "x",
1496            indoc! {"
1497                Tesˇt
1498                test"},
1499        )
1500        .await
1501        .assert_matches();
1502    }
1503
1504    #[gpui::test]
1505    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1506        let mut cx = NeovimBackedTestContext::new(cx).await;
1507        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1508            .await
1509            .assert_matches();
1510        cx.simulate(
1511            "shift-x",
1512            indoc! {"
1513                Test
1514                ˇtest"},
1515        )
1516        .await
1517        .assert_matches();
1518    }
1519
1520    #[gpui::test]
1521    async fn test_o(cx: &mut gpui::TestAppContext) {
1522        let mut cx = NeovimBackedTestContext::new(cx).await;
1523        cx.simulate("o", "ˇ").await.assert_matches();
1524        cx.simulate("o", "The ˇquick").await.assert_matches();
1525        cx.simulate_at_each_offset(
1526            "o",
1527            indoc! {"
1528                The qˇuick
1529                brown ˇfox
1530                jumps ˇover"},
1531        )
1532        .await
1533        .assert_matches();
1534        cx.simulate(
1535            "o",
1536            indoc! {"
1537                The quick
1538                ˇ
1539                brown fox"},
1540        )
1541        .await
1542        .assert_matches();
1543
1544        cx.assert_binding(
1545            "o",
1546            indoc! {"
1547                fn test() {
1548                    println!(ˇ);
1549                }"},
1550            Mode::Normal,
1551            indoc! {"
1552                fn test() {
1553                    println!();
1554                    ˇ
1555                }"},
1556            Mode::Insert,
1557        );
1558
1559        cx.assert_binding(
1560            "o",
1561            indoc! {"
1562                fn test(ˇ) {
1563                    println!();
1564                }"},
1565            Mode::Normal,
1566            indoc! {"
1567                fn test() {
1568                    ˇ
1569                    println!();
1570                }"},
1571            Mode::Insert,
1572        );
1573    }
1574
1575    #[gpui::test]
1576    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1577        let mut cx = NeovimBackedTestContext::new(cx).await;
1578        cx.simulate("shift-o", "ˇ").await.assert_matches();
1579        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1580        cx.simulate_at_each_offset(
1581            "shift-o",
1582            indoc! {"
1583            The qˇuick
1584            brown ˇfox
1585            jumps ˇover"},
1586        )
1587        .await
1588        .assert_matches();
1589        cx.simulate(
1590            "shift-o",
1591            indoc! {"
1592            The quick
1593            ˇ
1594            brown fox"},
1595        )
1596        .await
1597        .assert_matches();
1598
1599        // Our indentation is smarter than vims. So we don't match here
1600        cx.assert_binding(
1601            "shift-o",
1602            indoc! {"
1603                fn test() {
1604                    println!(ˇ);
1605                }"},
1606            Mode::Normal,
1607            indoc! {"
1608                fn test() {
1609                    ˇ
1610                    println!();
1611                }"},
1612            Mode::Insert,
1613        );
1614        cx.assert_binding(
1615            "shift-o",
1616            indoc! {"
1617                fn test(ˇ) {
1618                    println!();
1619                }"},
1620            Mode::Normal,
1621            indoc! {"
1622                ˇ
1623                fn test() {
1624                    println!();
1625                }"},
1626            Mode::Insert,
1627        );
1628    }
1629
1630    #[gpui::test]
1631    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1632        let mut cx = NeovimBackedTestContext::new(cx).await;
1633        cx.simulate("[ space", "ˇ").await.assert_matches();
1634        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1635        cx.simulate_at_each_offset(
1636            "3 [ space",
1637            indoc! {"
1638            The qˇuick
1639            brown ˇfox
1640            jumps ˇover"},
1641        )
1642        .await
1643        .assert_matches();
1644        cx.simulate_at_each_offset(
1645            "[ space",
1646            indoc! {"
1647            The qˇuick
1648            brown ˇfox
1649            jumps ˇover"},
1650        )
1651        .await
1652        .assert_matches();
1653        cx.simulate(
1654            "[ space",
1655            indoc! {"
1656            The quick
1657            ˇ
1658            brown fox"},
1659        )
1660        .await
1661        .assert_matches();
1662
1663        cx.simulate("] space", "ˇ").await.assert_matches();
1664        cx.simulate("] space", "The ˇquick").await.assert_matches();
1665        cx.simulate_at_each_offset(
1666            "3 ] space",
1667            indoc! {"
1668            The qˇuick
1669            brown ˇfox
1670            jumps ˇover"},
1671        )
1672        .await
1673        .assert_matches();
1674        cx.simulate_at_each_offset(
1675            "] space",
1676            indoc! {"
1677            The qˇuick
1678            brown ˇfox
1679            jumps ˇover"},
1680        )
1681        .await
1682        .assert_matches();
1683        cx.simulate(
1684            "] space",
1685            indoc! {"
1686            The quick
1687            ˇ
1688            brown fox"},
1689        )
1690        .await
1691        .assert_matches();
1692    }
1693
1694    #[gpui::test]
1695    async fn test_dd(cx: &mut gpui::TestAppContext) {
1696        let mut cx = NeovimBackedTestContext::new(cx).await;
1697        cx.simulate("d d", "ˇ").await.assert_matches();
1698        cx.simulate("d d", "The ˇquick").await.assert_matches();
1699        cx.simulate_at_each_offset(
1700            "d d",
1701            indoc! {"
1702            The qˇuick
1703            brown ˇfox
1704            jumps ˇover"},
1705        )
1706        .await
1707        .assert_matches();
1708        cx.simulate(
1709            "d d",
1710            indoc! {"
1711                The quick
1712                ˇ
1713                brown fox"},
1714        )
1715        .await
1716        .assert_matches();
1717    }
1718
1719    #[gpui::test]
1720    async fn test_cc(cx: &mut gpui::TestAppContext) {
1721        let mut cx = NeovimBackedTestContext::new(cx).await;
1722        cx.simulate("c c", "ˇ").await.assert_matches();
1723        cx.simulate("c c", "The ˇquick").await.assert_matches();
1724        cx.simulate_at_each_offset(
1725            "c c",
1726            indoc! {"
1727                The quˇick
1728                brown ˇfox
1729                jumps ˇover"},
1730        )
1731        .await
1732        .assert_matches();
1733        cx.simulate(
1734            "c c",
1735            indoc! {"
1736                The quick
1737                ˇ
1738                brown fox"},
1739        )
1740        .await
1741        .assert_matches();
1742    }
1743
1744    #[gpui::test]
1745    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1746        let mut cx = NeovimBackedTestContext::new(cx).await;
1747
1748        for count in 1..=5 {
1749            cx.simulate_at_each_offset(
1750                &format!("{count} w"),
1751                indoc! {"
1752                    ˇThe quˇickˇ browˇn
1753                    ˇ
1754                    ˇfox ˇjumpsˇ-ˇoˇver
1755                    ˇthe lazy dog
1756                "},
1757            )
1758            .await
1759            .assert_matches();
1760        }
1761    }
1762
1763    #[gpui::test]
1764    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1765        let mut cx = NeovimBackedTestContext::new(cx).await;
1766        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1767            .await
1768            .assert_matches();
1769    }
1770
1771    #[gpui::test]
1772    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1773        let mut cx = NeovimBackedTestContext::new(cx).await;
1774
1775        for count in 1..=3 {
1776            let test_case = indoc! {"
1777                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1778                ˇ    ˇbˇaaˇa ˇbˇbˇb
1779                ˇ
1780                ˇb
1781            "};
1782
1783            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1784                .await
1785                .assert_matches();
1786
1787            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1788                .await
1789                .assert_matches();
1790        }
1791    }
1792
1793    #[gpui::test]
1794    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1795        let mut cx = NeovimBackedTestContext::new(cx).await;
1796        let test_case = indoc! {"
1797            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1798            ˇ    ˇbˇaaˇa ˇbˇbˇb
1799            ˇ•••
1800            ˇb
1801            "
1802        };
1803
1804        for count in 1..=3 {
1805            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1806                .await
1807                .assert_matches();
1808
1809            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1810                .await
1811                .assert_matches();
1812        }
1813    }
1814
1815    #[gpui::test]
1816    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1817        let mut cx = VimTestContext::new(cx, true).await;
1818        cx.update_global(|store: &mut SettingsStore, cx| {
1819            store.update_user_settings(cx, |s| {
1820                s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1821            });
1822        });
1823
1824        cx.assert_binding(
1825            "f p",
1826            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1827            Mode::Normal,
1828            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1829            Mode::Normal,
1830        );
1831
1832        cx.assert_binding(
1833            "shift-f p",
1834            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1835            Mode::Normal,
1836            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1837            Mode::Normal,
1838        );
1839
1840        cx.assert_binding(
1841            "t p",
1842            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1843            Mode::Normal,
1844            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1845            Mode::Normal,
1846        );
1847
1848        cx.assert_binding(
1849            "shift-t p",
1850            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1851            Mode::Normal,
1852            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1853            Mode::Normal,
1854        );
1855    }
1856
1857    #[gpui::test]
1858    async fn test_percent(cx: &mut TestAppContext) {
1859        let mut cx = NeovimBackedTestContext::new(cx).await;
1860        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1861            .await
1862            .assert_matches();
1863        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1864            .await
1865            .assert_matches();
1866        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1867            .await
1868            .assert_matches();
1869    }
1870
1871    #[gpui::test]
1872    async fn test_percent_in_comment(cx: &mut TestAppContext) {
1873        let mut cx = NeovimBackedTestContext::new(cx).await;
1874        cx.simulate_at_each_offset("%", "// ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1875            .await
1876            .assert_matches();
1877        cx.simulate_at_each_offset("%", "// ˇ{ ˇ{ˇ}ˇ }ˇ")
1878            .await
1879            .assert_matches();
1880        // Template-style brackets (like Liquid {% %} and {{ }})
1881        cx.simulate_at_each_offset("%", "ˇ{ˇ% block %ˇ}ˇ")
1882            .await
1883            .assert_matches();
1884        cx.simulate_at_each_offset("%", "ˇ{ˇ{ˇ var ˇ}ˇ}ˇ")
1885            .await
1886            .assert_matches();
1887    }
1888
1889    #[gpui::test]
1890    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1891        let mut cx = NeovimBackedTestContext::new(cx).await;
1892
1893        // goes to current line end
1894        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1895        cx.simulate_shared_keystrokes("$").await;
1896        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1897
1898        // goes to next line end
1899        cx.simulate_shared_keystrokes("2 $").await;
1900        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1901
1902        // try to exceed the final line.
1903        cx.simulate_shared_keystrokes("4 $").await;
1904        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1905    }
1906
1907    #[gpui::test]
1908    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1909        let mut cx = VimTestContext::new(cx, true).await;
1910        cx.update(|_, cx| {
1911            cx.bind_keys(vec![
1912                KeyBinding::new(
1913                    "w",
1914                    motion::NextSubwordStart {
1915                        ignore_punctuation: false,
1916                    },
1917                    Some("Editor && VimControl && !VimWaiting && !menu"),
1918                ),
1919                KeyBinding::new(
1920                    "b",
1921                    motion::PreviousSubwordStart {
1922                        ignore_punctuation: false,
1923                    },
1924                    Some("Editor && VimControl && !VimWaiting && !menu"),
1925                ),
1926                KeyBinding::new(
1927                    "e",
1928                    motion::NextSubwordEnd {
1929                        ignore_punctuation: false,
1930                    },
1931                    Some("Editor && VimControl && !VimWaiting && !menu"),
1932                ),
1933                KeyBinding::new(
1934                    "g e",
1935                    motion::PreviousSubwordEnd {
1936                        ignore_punctuation: false,
1937                    },
1938                    Some("Editor && VimControl && !VimWaiting && !menu"),
1939                ),
1940            ]);
1941        });
1942
1943        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1944        // Special case: In 'cw', 'w' acts like 'e'
1945        cx.assert_binding(
1946            "c w",
1947            indoc! {"ˇassert_binding"},
1948            Mode::Normal,
1949            indoc! {"ˇ_binding"},
1950            Mode::Insert,
1951        );
1952
1953        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1954
1955        // Subword end should stop at EOL
1956        cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"});
1957
1958        // Already at subword end, should move to next subword on next line
1959        cx.assert_binding_normal(
1960            "e",
1961            indoc! {"foo_barˇ\nbaz_qux"},
1962            indoc! {"foo_bar\nbaˇz_qux"},
1963        );
1964
1965        // CamelCase at EOL
1966        cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"});
1967
1968        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1969
1970        cx.assert_binding_normal(
1971            "g e",
1972            indoc! {"assert_bindinˇg"},
1973            indoc! {"asserˇt_binding"},
1974        );
1975    }
1976
1977    #[gpui::test]
1978    async fn test_r(cx: &mut gpui::TestAppContext) {
1979        let mut cx = NeovimBackedTestContext::new(cx).await;
1980
1981        cx.set_shared_state("ˇhello\n").await;
1982        cx.simulate_shared_keystrokes("r -").await;
1983        cx.shared_state().await.assert_eq("ˇ-ello\n");
1984
1985        cx.set_shared_state("ˇhello\n").await;
1986        cx.simulate_shared_keystrokes("3 r -").await;
1987        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1988
1989        cx.set_shared_state("ˇhello\n").await;
1990        cx.simulate_shared_keystrokes("r - 2 l .").await;
1991        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1992
1993        cx.set_shared_state("ˇhello world\n").await;
1994        cx.simulate_shared_keystrokes("2 r - f w .").await;
1995        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1996
1997        cx.set_shared_state("ˇhello world\n").await;
1998        cx.simulate_shared_keystrokes("2 0 r - ").await;
1999        cx.shared_state().await.assert_eq("ˇhello world\n");
2000
2001        cx.set_shared_state("  helloˇ world\n").await;
2002        cx.simulate_shared_keystrokes("r enter").await;
2003        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
2004
2005        cx.set_shared_state("  helloˇ world\n").await;
2006        cx.simulate_shared_keystrokes("2 r enter").await;
2007        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
2008    }
2009
2010    #[gpui::test]
2011    async fn test_gq(cx: &mut gpui::TestAppContext) {
2012        let mut cx = NeovimBackedTestContext::new(cx).await;
2013        cx.set_neovim_option("textwidth=5").await;
2014
2015        cx.update(|_, cx| {
2016            SettingsStore::update_global(cx, |settings, cx| {
2017                settings.update_user_settings(cx, |settings| {
2018                    settings
2019                        .project
2020                        .all_languages
2021                        .defaults
2022                        .preferred_line_length = Some(5);
2023                });
2024            })
2025        });
2026
2027        cx.set_shared_state("ˇth th th th th th\n").await;
2028        cx.simulate_shared_keystrokes("g q q").await;
2029        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
2030
2031        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
2032            .await;
2033        cx.simulate_shared_keystrokes("v j g q").await;
2034        cx.shared_state()
2035            .await
2036            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
2037    }
2038
2039    #[gpui::test]
2040    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
2041        let mut cx = NeovimBackedTestContext::new(cx).await;
2042        cx.set_neovim_option("filetype=rust").await;
2043
2044        cx.set_shared_state("// helloˇ\n").await;
2045        cx.simulate_shared_keystrokes("o").await;
2046        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
2047        cx.simulate_shared_keystrokes("x escape shift-o").await;
2048        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
2049    }
2050
2051    #[gpui::test]
2052    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
2053        let mut cx = NeovimBackedTestContext::new(cx).await;
2054        cx.set_shared_state("heˇllo\n").await;
2055        cx.simulate_shared_keystrokes("y y p").await;
2056        cx.shared_state().await.assert_eq("hello\nˇhello\n");
2057    }
2058
2059    #[gpui::test]
2060    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2061        let mut cx = NeovimBackedTestContext::new(cx).await;
2062        cx.set_shared_state("heˇllo").await;
2063        cx.simulate_shared_keystrokes("y y p").await;
2064        cx.shared_state().await.assert_eq("hello\nˇhello");
2065    }
2066
2067    #[gpui::test]
2068    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2069        let mut cx = NeovimBackedTestContext::new(cx).await;
2070        cx.set_shared_state("heˇllo\nhello").await;
2071        cx.simulate_shared_keystrokes("2 y y p").await;
2072        cx.shared_state()
2073            .await
2074            .assert_eq("hello\nˇhello\nhello\nhello");
2075    }
2076
2077    #[gpui::test]
2078    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
2079        let mut cx = NeovimBackedTestContext::new(cx).await;
2080        cx.set_shared_state("heˇllo").await;
2081        cx.simulate_shared_keystrokes("d d").await;
2082        cx.shared_state().await.assert_eq("ˇ");
2083        cx.simulate_shared_keystrokes("p p").await;
2084        cx.shared_state().await.assert_eq("\nhello\nˇhello");
2085    }
2086
2087    #[gpui::test]
2088    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
2089        let mut cx = NeovimBackedTestContext::new(cx).await;
2090
2091        cx.set_shared_state("heˇllo").await;
2092        cx.simulate_shared_keystrokes("v i w shift-i").await;
2093        cx.shared_state().await.assert_eq("ˇhello");
2094
2095        cx.set_shared_state(indoc! {"
2096            The quick brown
2097            fox ˇjumps over
2098            the lazy dog"})
2099            .await;
2100        cx.simulate_shared_keystrokes("shift-v shift-i").await;
2101        cx.shared_state().await.assert_eq(indoc! {"
2102            The quick brown
2103            ˇfox jumps over
2104            the lazy dog"});
2105
2106        cx.set_shared_state(indoc! {"
2107            The quick brown
2108            fox ˇjumps over
2109            the lazy dog"})
2110            .await;
2111        cx.simulate_shared_keystrokes("shift-v shift-a").await;
2112        cx.shared_state().await.assert_eq(indoc! {"
2113            The quick brown
2114            fox jˇumps over
2115            the lazy dog"});
2116    }
2117
2118    #[gpui::test]
2119    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
2120        let mut cx = NeovimBackedTestContext::new(cx).await;
2121
2122        cx.set_shared_state(indoc! {"
2123            ˇfn a() { }
2124
2125
2126
2127
2128
2129            fn b() { }
2130
2131
2132
2133
2134
2135            fn b() { }"})
2136            .await;
2137        cx.simulate_shared_keystrokes("3 }").await;
2138        cx.shared_state().await.assert_matches();
2139        cx.simulate_shared_keystrokes("ctrl-o").await;
2140        cx.shared_state().await.assert_matches();
2141        cx.simulate_shared_keystrokes("ctrl-i").await;
2142        cx.shared_state().await.assert_matches();
2143        cx.simulate_shared_keystrokes("1 1 k").await;
2144        cx.shared_state().await.assert_matches();
2145        cx.simulate_shared_keystrokes("ctrl-o").await;
2146        cx.shared_state().await.assert_matches();
2147    }
2148
2149    #[gpui::test]
2150    async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2151        let mut cx = NeovimBackedTestContext::new(cx).await;
2152
2153        cx.set_shared_state(indoc! {"
2154            ˇfn a() { }
2155            fn a() { }
2156            fn a() { }
2157        "})
2158            .await;
2159        // do a jump to reset vim's undo grouping
2160        cx.simulate_shared_keystrokes("shift-g").await;
2161        cx.shared_state().await.assert_matches();
2162        cx.simulate_shared_keystrokes("r a").await;
2163        cx.shared_state().await.assert_matches();
2164        cx.simulate_shared_keystrokes("shift-u").await;
2165        cx.shared_state().await.assert_matches();
2166        cx.simulate_shared_keystrokes("shift-u").await;
2167        cx.shared_state().await.assert_matches();
2168        cx.simulate_shared_keystrokes("g g shift-u").await;
2169        cx.shared_state().await.assert_matches();
2170    }
2171
2172    #[gpui::test]
2173    async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2174        let mut cx = NeovimBackedTestContext::new(cx).await;
2175
2176        cx.set_shared_state(indoc! {"
2177            ˇfn a() { }
2178            fn a() { }
2179            fn a() { }
2180        "})
2181            .await;
2182        // do a jump to reset vim's undo grouping
2183        cx.simulate_shared_keystrokes("shift-g k").await;
2184        cx.shared_state().await.assert_matches();
2185        cx.simulate_shared_keystrokes("o h e l l o escape").await;
2186        cx.shared_state().await.assert_matches();
2187        cx.simulate_shared_keystrokes("shift-u").await;
2188        cx.shared_state().await.assert_matches();
2189        cx.simulate_shared_keystrokes("shift-u").await;
2190    }
2191
2192    #[gpui::test]
2193    async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2194        let mut cx = NeovimBackedTestContext::new(cx).await;
2195
2196        cx.set_shared_state(indoc! {"
2197            ˇfn a() { }
2198            fn a() { }
2199            fn a() { }
2200        "})
2201            .await;
2202        // do a jump to reset vim's undo grouping
2203        cx.simulate_shared_keystrokes("x shift-g k").await;
2204        cx.shared_state().await.assert_matches();
2205        cx.simulate_shared_keystrokes("x f a x f { x").await;
2206        cx.shared_state().await.assert_matches();
2207        cx.simulate_shared_keystrokes("shift-u").await;
2208        cx.shared_state().await.assert_matches();
2209        cx.simulate_shared_keystrokes("shift-u").await;
2210        cx.shared_state().await.assert_matches();
2211        cx.simulate_shared_keystrokes("shift-u").await;
2212        cx.shared_state().await.assert_matches();
2213        cx.simulate_shared_keystrokes("shift-u").await;
2214        cx.shared_state().await.assert_matches();
2215    }
2216
2217    #[gpui::test]
2218    async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2219        let mut cx = VimTestContext::new(cx, true).await;
2220
2221        cx.set_state(
2222            indoc! {"
2223            ˇone two ˇone
2224            two ˇone two
2225        "},
2226            Mode::Normal,
2227        );
2228        cx.simulate_keystrokes("3 r a");
2229        cx.assert_state(
2230            indoc! {"
2231            aaˇa two aaˇa
2232            two aaˇa two
2233        "},
2234            Mode::Normal,
2235        );
2236        cx.simulate_keystrokes("escape escape");
2237        cx.simulate_keystrokes("shift-u");
2238        cx.set_state(
2239            indoc! {"
2240            onˇe two onˇe
2241            two onˇe two
2242        "},
2243            Mode::Normal,
2244        );
2245    }
2246
2247    #[gpui::test]
2248    async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) {
2249        let mut cx = VimTestContext::new(cx, true).await;
2250
2251        // Open 4 tabs.
2252        cx.simulate_keystrokes(": tabnew");
2253        cx.simulate_keystrokes("enter");
2254        cx.simulate_keystrokes(": tabnew");
2255        cx.simulate_keystrokes("enter");
2256        cx.simulate_keystrokes(": tabnew");
2257        cx.simulate_keystrokes("enter");
2258        cx.workspace(|workspace, _, cx| {
2259            assert_eq!(workspace.items(cx).count(), 4);
2260            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2261        });
2262
2263        cx.simulate_keystrokes("1 g t");
2264        cx.workspace(|workspace, _, cx| {
2265            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2266        });
2267
2268        cx.simulate_keystrokes("3 g t");
2269        cx.workspace(|workspace, _, cx| {
2270            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2);
2271        });
2272
2273        cx.simulate_keystrokes("4 g t");
2274        cx.workspace(|workspace, _, cx| {
2275            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2276        });
2277
2278        cx.simulate_keystrokes("1 g t");
2279        cx.simulate_keystrokes("g t");
2280        cx.workspace(|workspace, _, cx| {
2281            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2282        });
2283    }
2284
2285    #[gpui::test]
2286    async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) {
2287        let mut cx = VimTestContext::new(cx, true).await;
2288
2289        // Open 4 tabs.
2290        cx.simulate_keystrokes(": tabnew");
2291        cx.simulate_keystrokes("enter");
2292        cx.simulate_keystrokes(": tabnew");
2293        cx.simulate_keystrokes("enter");
2294        cx.simulate_keystrokes(": tabnew");
2295        cx.simulate_keystrokes("enter");
2296        cx.workspace(|workspace, _, cx| {
2297            assert_eq!(workspace.items(cx).count(), 4);
2298            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2299        });
2300
2301        cx.simulate_keystrokes("2 g shift-t");
2302        cx.workspace(|workspace, _, cx| {
2303            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2304        });
2305
2306        cx.simulate_keystrokes("g shift-t");
2307        cx.workspace(|workspace, _, cx| {
2308            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0);
2309        });
2310
2311        // Wraparound: gT from first tab should go to last.
2312        cx.simulate_keystrokes("g shift-t");
2313        cx.workspace(|workspace, _, cx| {
2314            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3);
2315        });
2316
2317        cx.simulate_keystrokes("6 g shift-t");
2318        cx.workspace(|workspace, _, cx| {
2319            assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1);
2320        });
2321    }
2322
2323    #[gpui::test]
2324    async fn test_temporary_mode(cx: &mut gpui::TestAppContext) {
2325        let mut cx = NeovimBackedTestContext::new(cx).await;
2326
2327        // Test jumping to the end of the line ($).
2328        cx.set_shared_state(indoc! {"lorem ˇipsum"}).await;
2329        cx.simulate_shared_keystrokes("i").await;
2330        cx.shared_state().await.assert_matches();
2331        cx.simulate_shared_keystrokes("ctrl-o $").await;
2332        cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"});
2333
2334        // Test jumping to the next word.
2335        cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await;
2336        cx.simulate_shared_keystrokes("a").await;
2337        cx.shared_state().await.assert_matches();
2338        cx.simulate_shared_keystrokes("a n d space ctrl-o w").await;
2339        cx.shared_state()
2340            .await
2341            .assert_eq(indoc! {"lorem and ipsum ˇdolor"});
2342
2343        // Test yanking to end of line ($).
2344        cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await;
2345        cx.simulate_shared_keystrokes("i").await;
2346        cx.shared_state().await.assert_matches();
2347        cx.simulate_shared_keystrokes("a n d space ctrl-o y $")
2348            .await;
2349        cx.shared_state()
2350            .await
2351            .assert_eq(indoc! {"lorem and ˇipsum dolor"});
2352    }
2353}