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