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