normal.rs

   1mod change;
   2mod convert;
   3mod delete;
   4mod increment;
   5pub(crate) mod mark;
   6mod paste;
   7pub(crate) mod repeat;
   8mod scroll;
   9pub(crate) mod search;
  10pub mod substitute;
  11mod toggle_comments;
  12pub(crate) mod yank;
  13
  14use std::collections::HashMap;
  15use std::sync::Arc;
  16
  17use crate::{
  18    Vim,
  19    indent::IndentDirection,
  20    motion::{self, Motion, first_non_whitespace, next_line_end, right},
  21    object::Object,
  22    state::{Mark, Mode, Operator},
  23    surrounds::SurroundsType,
  24};
  25use collections::BTreeSet;
  26use convert::ConvertTarget;
  27use editor::Bias;
  28use editor::Editor;
  29use editor::{Anchor, SelectionEffects};
  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        /// Changes from cursor to end of line.
  68        ChangeToEndOfLine,
  69        /// Deletes from cursor to end of line.
  70        DeleteToEndOfLine,
  71        /// Yanks (copies) the selected text.
  72        Yank,
  73        /// Yanks the entire line.
  74        YankLine,
  75        /// Toggles the case of selected text.
  76        ChangeCase,
  77        /// Converts selected text to uppercase.
  78        ConvertToUpperCase,
  79        /// Converts selected text to lowercase.
  80        ConvertToLowerCase,
  81        /// Applies ROT13 cipher to selected text.
  82        ConvertToRot13,
  83        /// Applies ROT47 cipher to selected text.
  84        ConvertToRot47,
  85        /// Toggles comments for selected lines.
  86        ToggleComments,
  87        /// Shows the current location in the file.
  88        ShowLocation,
  89        /// Undoes the last change.
  90        Undo,
  91        /// Redoes the last undone change.
  92        Redo,
  93    ]
  94);
  95
  96pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  97    Vim::action(editor, cx, Vim::insert_after);
  98    Vim::action(editor, cx, Vim::insert_before);
  99    Vim::action(editor, cx, Vim::insert_first_non_whitespace);
 100    Vim::action(editor, cx, Vim::insert_end_of_line);
 101    Vim::action(editor, cx, Vim::insert_line_above);
 102    Vim::action(editor, cx, Vim::insert_line_below);
 103    Vim::action(editor, cx, Vim::insert_empty_line_above);
 104    Vim::action(editor, cx, Vim::insert_empty_line_below);
 105    Vim::action(editor, cx, Vim::insert_at_previous);
 106    Vim::action(editor, cx, Vim::change_case);
 107    Vim::action(editor, cx, Vim::convert_to_upper_case);
 108    Vim::action(editor, cx, Vim::convert_to_lower_case);
 109    Vim::action(editor, cx, Vim::convert_to_rot13);
 110    Vim::action(editor, cx, Vim::convert_to_rot47);
 111    Vim::action(editor, cx, Vim::yank_line);
 112    Vim::action(editor, cx, Vim::toggle_comments);
 113    Vim::action(editor, cx, Vim::paste);
 114    Vim::action(editor, cx, Vim::show_location);
 115
 116    Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
 117        vim.record_current_action(cx);
 118        let times = Vim::take_count(cx);
 119        let forced_motion = Vim::take_forced_motion(cx);
 120        vim.delete_motion(Motion::Left, times, forced_motion, window, cx);
 121    });
 122    Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
 123        vim.record_current_action(cx);
 124        let times = Vim::take_count(cx);
 125        let forced_motion = Vim::take_forced_motion(cx);
 126        vim.delete_motion(Motion::Right, times, forced_motion, window, cx);
 127    });
 128
 129    Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
 130        vim.record_current_action(cx);
 131        vim.update_editor(window, cx, |_, editor, window, cx| {
 132            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 133                s.move_with(|map, selection| {
 134                    if selection.is_empty() {
 135                        selection.end = movement::right(map, selection.end)
 136                    }
 137                })
 138            })
 139        });
 140        vim.visual_delete(false, window, cx);
 141    });
 142
 143    Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
 144        vim.start_recording(cx);
 145        let times = Vim::take_count(cx);
 146        let forced_motion = Vim::take_forced_motion(cx);
 147        vim.change_motion(
 148            Motion::EndOfLine {
 149                display_lines: false,
 150            },
 151            times,
 152            forced_motion,
 153            window,
 154            cx,
 155        );
 156    });
 157    Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
 158        vim.record_current_action(cx);
 159        let times = Vim::take_count(cx);
 160        let forced_motion = Vim::take_forced_motion(cx);
 161        vim.delete_motion(
 162            Motion::EndOfLine {
 163                display_lines: false,
 164            },
 165            times,
 166            forced_motion,
 167            window,
 168            cx,
 169        );
 170    });
 171    Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| {
 172        vim.join_lines_impl(true, window, cx);
 173    });
 174
 175    Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| {
 176        vim.join_lines_impl(false, window, cx);
 177    });
 178
 179    Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
 180        let times = Vim::take_count(cx);
 181        Vim::take_forced_motion(cx);
 182        vim.update_editor(window, cx, |_, editor, window, cx| {
 183            for _ in 0..times.unwrap_or(1) {
 184                editor.undo(&editor::actions::Undo, window, cx);
 185            }
 186        });
 187    });
 188    Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
 189        let times = Vim::take_count(cx);
 190        Vim::take_forced_motion(cx);
 191        vim.update_editor(window, cx, |_, editor, window, cx| {
 192            for _ in 0..times.unwrap_or(1) {
 193                editor.redo(&editor::actions::Redo, window, cx);
 194            }
 195        });
 196    });
 197
 198    repeat::register(editor, cx);
 199    scroll::register(editor, cx);
 200    search::register(editor, cx);
 201    substitute::register(editor, cx);
 202    increment::register(editor, cx);
 203}
 204
 205impl Vim {
 206    pub fn normal_motion(
 207        &mut self,
 208        motion: Motion,
 209        operator: Option<Operator>,
 210        times: Option<usize>,
 211        forced_motion: bool,
 212        window: &mut Window,
 213        cx: &mut Context<Self>,
 214    ) {
 215        match operator {
 216            None => self.move_cursor(motion, times, window, cx),
 217            Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx),
 218            Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx),
 219            Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx),
 220            Some(Operator::AddSurrounds { target: None }) => {}
 221            Some(Operator::Indent) => self.indent_motion(
 222                motion,
 223                times,
 224                forced_motion,
 225                IndentDirection::In,
 226                window,
 227                cx,
 228            ),
 229            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx),
 230            Some(Operator::Outdent) => self.indent_motion(
 231                motion,
 232                times,
 233                forced_motion,
 234                IndentDirection::Out,
 235                window,
 236                cx,
 237            ),
 238            Some(Operator::AutoIndent) => self.indent_motion(
 239                motion,
 240                times,
 241                forced_motion,
 242                IndentDirection::Auto,
 243                window,
 244                cx,
 245            ),
 246            Some(Operator::ShellCommand) => {
 247                self.shell_command_motion(motion, times, forced_motion, window, cx)
 248            }
 249            Some(Operator::Lowercase) => self.convert_motion(
 250                motion,
 251                times,
 252                forced_motion,
 253                ConvertTarget::LowerCase,
 254                window,
 255                cx,
 256            ),
 257            Some(Operator::Uppercase) => self.convert_motion(
 258                motion,
 259                times,
 260                forced_motion,
 261                ConvertTarget::UpperCase,
 262                window,
 263                cx,
 264            ),
 265            Some(Operator::OppositeCase) => self.convert_motion(
 266                motion,
 267                times,
 268                forced_motion,
 269                ConvertTarget::OppositeCase,
 270                window,
 271                cx,
 272            ),
 273            Some(Operator::Rot13) => self.convert_motion(
 274                motion,
 275                times,
 276                forced_motion,
 277                ConvertTarget::Rot13,
 278                window,
 279                cx,
 280            ),
 281            Some(Operator::Rot47) => self.convert_motion(
 282                motion,
 283                times,
 284                forced_motion,
 285                ConvertTarget::Rot47,
 286                window,
 287                cx,
 288            ),
 289            Some(Operator::ToggleComments) => {
 290                self.toggle_comments_motion(motion, times, forced_motion, window, cx)
 291            }
 292            Some(Operator::ReplaceWithRegister) => {
 293                self.replace_with_register_motion(motion, times, forced_motion, window, cx)
 294            }
 295            Some(Operator::Exchange) => {
 296                self.exchange_motion(motion, times, forced_motion, window, cx)
 297            }
 298            Some(operator) => {
 299                // Can't do anything for text objects, Ignoring
 300                error!("Unexpected normal mode motion operator: {:?}", operator)
 301            }
 302        }
 303        // Exit temporary normal mode (if active).
 304        self.exit_temporary_normal(window, cx);
 305    }
 306
 307    pub fn normal_object(
 308        &mut self,
 309        object: Object,
 310        times: Option<usize>,
 311        window: &mut Window,
 312        cx: &mut Context<Self>,
 313    ) {
 314        let mut waiting_operator: Option<Operator> = None;
 315        match self.maybe_pop_operator() {
 316            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
 317                Some(Operator::Change) => self.change_object(object, around, times, window, cx),
 318                Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
 319                Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
 320                Some(Operator::Indent) => {
 321                    self.indent_object(object, around, IndentDirection::In, times, window, cx)
 322                }
 323                Some(Operator::Outdent) => {
 324                    self.indent_object(object, around, IndentDirection::Out, times, window, cx)
 325                }
 326                Some(Operator::AutoIndent) => {
 327                    self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
 328                }
 329                Some(Operator::ShellCommand) => {
 330                    self.shell_command_object(object, around, window, cx);
 331                }
 332                Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
 333                Some(Operator::Lowercase) => {
 334                    self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
 335                }
 336                Some(Operator::Uppercase) => {
 337                    self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
 338                }
 339                Some(Operator::OppositeCase) => self.convert_object(
 340                    object,
 341                    around,
 342                    ConvertTarget::OppositeCase,
 343                    times,
 344                    window,
 345                    cx,
 346                ),
 347                Some(Operator::Rot13) => {
 348                    self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
 349                }
 350                Some(Operator::Rot47) => {
 351                    self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
 352                }
 353                Some(Operator::AddSurrounds { target: None }) => {
 354                    waiting_operator = Some(Operator::AddSurrounds {
 355                        target: Some(SurroundsType::Object(object, around)),
 356                    });
 357                }
 358                Some(Operator::ToggleComments) => {
 359                    self.toggle_comments_object(object, around, times, window, cx)
 360                }
 361                Some(Operator::ReplaceWithRegister) => {
 362                    self.replace_with_register_object(object, around, window, cx)
 363                }
 364                Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
 365                _ => {
 366                    // Can't do anything for namespace operators. Ignoring
 367                }
 368            },
 369            Some(Operator::DeleteSurrounds) => {
 370                waiting_operator = Some(Operator::DeleteSurrounds);
 371            }
 372            Some(Operator::ChangeSurrounds { target: None }) => {
 373                if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
 374                    waiting_operator = Some(Operator::ChangeSurrounds {
 375                        target: Some(object),
 376                    });
 377                }
 378            }
 379            _ => {
 380                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 381            }
 382        }
 383        self.clear_operator(window, cx);
 384        if let Some(operator) = waiting_operator {
 385            self.push_operator(operator, window, cx);
 386        }
 387    }
 388
 389    pub(crate) fn move_cursor(
 390        &mut self,
 391        motion: Motion,
 392        times: Option<usize>,
 393        window: &mut Window,
 394        cx: &mut Context<Self>,
 395    ) {
 396        self.update_editor(window, cx, |_, editor, window, cx| {
 397            let text_layout_details = editor.text_layout_details(window);
 398            editor.change_selections(
 399                SelectionEffects::default().nav_history(motion.push_to_jump_list()),
 400                window,
 401                cx,
 402                |s| {
 403                    s.move_cursors_with(|map, cursor, goal| {
 404                        motion
 405                            .move_point(map, cursor, goal, times, &text_layout_details)
 406                            .unwrap_or((cursor, goal))
 407                    })
 408                },
 409            )
 410        });
 411    }
 412
 413    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 414        self.start_recording(cx);
 415        self.switch_mode(Mode::Insert, false, window, cx);
 416        self.update_editor(window, cx, |_, editor, window, cx| {
 417            editor.change_selections(Default::default(), window, cx, |s| {
 418                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 419            });
 420        });
 421    }
 422
 423    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 424        self.start_recording(cx);
 425        if self.mode.is_visual() {
 426            let current_mode = self.mode;
 427            self.update_editor(window, cx, |_, editor, window, cx| {
 428                editor.change_selections(Default::default(), window, cx, |s| {
 429                    s.move_with(|map, selection| {
 430                        if current_mode == Mode::VisualLine {
 431                            let start_of_line = motion::start_of_line(map, false, selection.start);
 432                            selection.collapse_to(start_of_line, SelectionGoal::None)
 433                        } else {
 434                            selection.collapse_to(selection.start, SelectionGoal::None)
 435                        }
 436                    });
 437                });
 438            });
 439        }
 440        self.switch_mode(Mode::Insert, false, window, cx);
 441    }
 442
 443    fn insert_first_non_whitespace(
 444        &mut self,
 445        _: &InsertFirstNonWhitespace,
 446        window: &mut Window,
 447        cx: &mut Context<Self>,
 448    ) {
 449        self.start_recording(cx);
 450        self.switch_mode(Mode::Insert, false, window, cx);
 451        self.update_editor(window, cx, |_, editor, window, cx| {
 452            editor.change_selections(Default::default(), window, cx, |s| {
 453                s.move_cursors_with(|map, cursor, _| {
 454                    (
 455                        first_non_whitespace(map, false, cursor),
 456                        SelectionGoal::None,
 457                    )
 458                });
 459            });
 460        });
 461    }
 462
 463    fn insert_end_of_line(
 464        &mut self,
 465        _: &InsertEndOfLine,
 466        window: &mut Window,
 467        cx: &mut Context<Self>,
 468    ) {
 469        self.start_recording(cx);
 470        self.switch_mode(Mode::Insert, false, window, cx);
 471        self.update_editor(window, cx, |_, editor, window, cx| {
 472            editor.change_selections(Default::default(), window, cx, |s| {
 473                s.move_cursors_with(|map, cursor, _| {
 474                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 475                });
 476            });
 477        });
 478    }
 479
 480    fn insert_at_previous(
 481        &mut self,
 482        _: &InsertAtPrevious,
 483        window: &mut Window,
 484        cx: &mut Context<Self>,
 485    ) {
 486        self.start_recording(cx);
 487        self.switch_mode(Mode::Insert, false, window, cx);
 488        self.update_editor(window, cx, |vim, editor, window, cx| {
 489            let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
 490                return;
 491            };
 492
 493            editor.change_selections(Default::default(), window, cx, |s| {
 494                s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 495            });
 496        });
 497    }
 498
 499    fn insert_line_above(
 500        &mut self,
 501        _: &InsertLineAbove,
 502        window: &mut Window,
 503        cx: &mut Context<Self>,
 504    ) {
 505        self.start_recording(cx);
 506        self.switch_mode(Mode::Insert, false, window, cx);
 507        self.update_editor(window, cx, |_, editor, window, cx| {
 508            editor.transact(window, cx, |editor, window, cx| {
 509                let selections = editor.selections.all::<Point>(cx);
 510                let snapshot = editor.buffer().read(cx).snapshot(cx);
 511
 512                let selection_start_rows: BTreeSet<u32> = selections
 513                    .into_iter()
 514                    .map(|selection| selection.start.row)
 515                    .collect();
 516                let edits = selection_start_rows
 517                    .into_iter()
 518                    .map(|row| {
 519                        let indent = snapshot
 520                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 521                            .chars()
 522                            .collect::<String>();
 523
 524                        let start_of_line = Point::new(row, 0);
 525                        (start_of_line..start_of_line, indent + "\n")
 526                    })
 527                    .collect::<Vec<_>>();
 528                editor.edit_with_autoindent(edits, cx);
 529                editor.change_selections(Default::default(), window, cx, |s| {
 530                    s.move_cursors_with(|map, cursor, _| {
 531                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 532                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 533                        (insert_point, SelectionGoal::None)
 534                    });
 535                });
 536            });
 537        });
 538    }
 539
 540    fn insert_line_below(
 541        &mut self,
 542        _: &InsertLineBelow,
 543        window: &mut Window,
 544        cx: &mut Context<Self>,
 545    ) {
 546        self.start_recording(cx);
 547        self.switch_mode(Mode::Insert, false, window, cx);
 548        self.update_editor(window, cx, |_, editor, window, cx| {
 549            let text_layout_details = editor.text_layout_details(window);
 550            editor.transact(window, cx, |editor, window, cx| {
 551                let selections = editor.selections.all::<Point>(cx);
 552                let snapshot = editor.buffer().read(cx).snapshot(cx);
 553
 554                let selection_end_rows: BTreeSet<u32> = selections
 555                    .into_iter()
 556                    .map(|selection| selection.end.row)
 557                    .collect();
 558                let edits = selection_end_rows
 559                    .into_iter()
 560                    .map(|row| {
 561                        let indent = snapshot
 562                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 563                            .chars()
 564                            .collect::<String>();
 565
 566                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 567                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 568                    })
 569                    .collect::<Vec<_>>();
 570                editor.change_selections(Default::default(), window, cx, |s| {
 571                    s.maybe_move_cursors_with(|map, cursor, goal| {
 572                        Motion::CurrentLine.move_point(
 573                            map,
 574                            cursor,
 575                            goal,
 576                            None,
 577                            &text_layout_details,
 578                        )
 579                    });
 580                });
 581                editor.edit_with_autoindent(edits, cx);
 582            });
 583        });
 584    }
 585
 586    fn insert_empty_line_above(
 587        &mut self,
 588        _: &InsertEmptyLineAbove,
 589        window: &mut Window,
 590        cx: &mut Context<Self>,
 591    ) {
 592        self.record_current_action(cx);
 593        let count = Vim::take_count(cx).unwrap_or(1);
 594        Vim::take_forced_motion(cx);
 595        self.update_editor(window, cx, |_, editor, window, cx| {
 596            editor.transact(window, cx, |editor, _, cx| {
 597                let selections = editor.selections.all::<Point>(cx);
 598
 599                let selection_start_rows: BTreeSet<u32> = selections
 600                    .into_iter()
 601                    .map(|selection| selection.start.row)
 602                    .collect();
 603                let edits = selection_start_rows
 604                    .into_iter()
 605                    .map(|row| {
 606                        let start_of_line = Point::new(row, 0);
 607                        (start_of_line..start_of_line, "\n".repeat(count))
 608                    })
 609                    .collect::<Vec<_>>();
 610                editor.edit(edits, cx);
 611            });
 612        });
 613    }
 614
 615    fn insert_empty_line_below(
 616        &mut self,
 617        _: &InsertEmptyLineBelow,
 618        window: &mut Window,
 619        cx: &mut Context<Self>,
 620    ) {
 621        self.record_current_action(cx);
 622        let count = Vim::take_count(cx).unwrap_or(1);
 623        Vim::take_forced_motion(cx);
 624        self.update_editor(window, cx, |_, editor, window, cx| {
 625            editor.transact(window, cx, |editor, window, cx| {
 626                let selections = editor.selections.all::<Point>(cx);
 627                let snapshot = editor.buffer().read(cx).snapshot(cx);
 628                let (_map, display_selections) = editor.selections.all_display(cx);
 629                let original_positions = display_selections
 630                    .iter()
 631                    .map(|s| (s.id, s.head()))
 632                    .collect::<HashMap<_, _>>();
 633
 634                let selection_end_rows: BTreeSet<u32> = selections
 635                    .into_iter()
 636                    .map(|selection| selection.end.row)
 637                    .collect();
 638                let edits = selection_end_rows
 639                    .into_iter()
 640                    .map(|row| {
 641                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 642                        (end_of_line..end_of_line, "\n".repeat(count))
 643                    })
 644                    .collect::<Vec<_>>();
 645                editor.edit(edits, cx);
 646
 647                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 648                    s.move_with(|_, selection| {
 649                        if let Some(position) = original_positions.get(&selection.id) {
 650                            selection.collapse_to(*position, SelectionGoal::None);
 651                        }
 652                    });
 653                });
 654            });
 655        });
 656    }
 657
 658    fn join_lines_impl(
 659        &mut self,
 660        insert_whitespace: bool,
 661        window: &mut Window,
 662        cx: &mut Context<Self>,
 663    ) {
 664        self.record_current_action(cx);
 665        let mut times = Vim::take_count(cx).unwrap_or(1);
 666        Vim::take_forced_motion(cx);
 667        if self.mode.is_visual() {
 668            times = 1;
 669        } else if times > 1 {
 670            // 2J joins two lines together (same as J or 1J)
 671            times -= 1;
 672        }
 673
 674        self.update_editor(window, cx, |_, editor, window, cx| {
 675            editor.transact(window, cx, |editor, window, cx| {
 676                for _ in 0..times {
 677                    editor.join_lines_impl(insert_whitespace, window, cx)
 678                }
 679            })
 680        });
 681        if self.mode.is_visual() {
 682            self.switch_mode(Mode::Normal, true, window, cx)
 683        }
 684    }
 685
 686    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 687        let count = Vim::take_count(cx);
 688        let forced_motion = Vim::take_forced_motion(cx);
 689        self.yank_motion(
 690            motion::Motion::CurrentLine,
 691            count,
 692            forced_motion,
 693            window,
 694            cx,
 695        )
 696    }
 697
 698    fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
 699        let count = Vim::take_count(cx);
 700        Vim::take_forced_motion(cx);
 701        self.update_editor(window, cx, |vim, editor, _window, cx| {
 702            let selection = editor.selections.newest_anchor();
 703            let Some((buffer, point, _)) = editor
 704                .buffer()
 705                .read(cx)
 706                .point_to_buffer_point(selection.head(), cx)
 707            else {
 708                return;
 709            };
 710            let filename = if let Some(file) = buffer.read(cx).file() {
 711                if count.is_some() {
 712                    if let Some(local) = file.as_local() {
 713                        local.abs_path(cx).to_string_lossy().to_string()
 714                    } else {
 715                        file.full_path(cx).to_string_lossy().to_string()
 716                    }
 717                } else {
 718                    file.path().to_string_lossy().to_string()
 719                }
 720            } else {
 721                "[No Name]".into()
 722            };
 723            let buffer = buffer.read(cx);
 724            let lines = buffer.max_point().row + 1;
 725            let current_line = point.row;
 726            let percentage = current_line as f32 / lines as f32;
 727            let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 728            vim.status_label = Some(
 729                format!(
 730                    "{}{} {} lines --{:.0}%--",
 731                    filename,
 732                    modified,
 733                    lines,
 734                    percentage * 100.0,
 735                )
 736                .into(),
 737            );
 738            cx.notify();
 739        });
 740    }
 741
 742    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 743        self.record_current_action(cx);
 744        self.store_visual_marks(window, cx);
 745        self.update_editor(window, cx, |vim, editor, window, cx| {
 746            editor.transact(window, cx, |editor, window, cx| {
 747                let original_positions = vim.save_selection_starts(editor, cx);
 748                editor.toggle_comments(&Default::default(), window, cx);
 749                vim.restore_selection_cursors(editor, window, cx, original_positions);
 750            });
 751        });
 752        if self.mode.is_visual() {
 753            self.switch_mode(Mode::Normal, true, window, cx)
 754        }
 755    }
 756
 757    pub(crate) fn normal_replace(
 758        &mut self,
 759        text: Arc<str>,
 760        window: &mut Window,
 761        cx: &mut Context<Self>,
 762    ) {
 763        let is_return_char = text == "\n".into() || text == "\r".into();
 764        let count = Vim::take_count(cx).unwrap_or(1);
 765        Vim::take_forced_motion(cx);
 766        self.stop_recording(cx);
 767        self.update_editor(window, cx, |_, editor, window, cx| {
 768            editor.transact(window, cx, |editor, window, cx| {
 769                editor.set_clip_at_line_ends(false, cx);
 770                let (map, display_selections) = editor.selections.all_display(cx);
 771
 772                let mut edits = Vec::new();
 773                for selection in &display_selections {
 774                    let mut range = selection.range();
 775                    for _ in 0..count {
 776                        let new_point = movement::saturating_right(&map, range.end);
 777                        if range.end == new_point {
 778                            return;
 779                        }
 780                        range.end = new_point;
 781                    }
 782
 783                    edits.push((
 784                        range.start.to_offset(&map, Bias::Left)
 785                            ..range.end.to_offset(&map, Bias::Left),
 786                        text.repeat(if is_return_char { 0 } else { count }),
 787                    ));
 788                }
 789
 790                editor.edit(edits, cx);
 791                if is_return_char {
 792                    editor.newline(&editor::actions::Newline, window, cx);
 793                }
 794                editor.set_clip_at_line_ends(true, cx);
 795                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 796                    s.move_with(|map, selection| {
 797                        let point = movement::saturating_left(map, selection.head());
 798                        selection.collapse_to(point, SelectionGoal::None)
 799                    });
 800                });
 801            });
 802        });
 803        self.pop_operator(window, cx);
 804    }
 805
 806    pub fn save_selection_starts(
 807        &self,
 808        editor: &Editor,
 809
 810        cx: &mut Context<Editor>,
 811    ) -> HashMap<usize, Anchor> {
 812        let (map, selections) = editor.selections.all_display(cx);
 813        selections
 814            .iter()
 815            .map(|selection| {
 816                (
 817                    selection.id,
 818                    map.display_point_to_anchor(selection.start, Bias::Right),
 819                )
 820            })
 821            .collect::<HashMap<_, _>>()
 822    }
 823
 824    pub fn restore_selection_cursors(
 825        &self,
 826        editor: &mut Editor,
 827        window: &mut Window,
 828        cx: &mut Context<Editor>,
 829        mut positions: HashMap<usize, Anchor>,
 830    ) {
 831        editor.change_selections(Default::default(), window, cx, |s| {
 832            s.move_with(|map, selection| {
 833                if let Some(anchor) = positions.remove(&selection.id) {
 834                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 835                }
 836            });
 837        });
 838    }
 839
 840    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 841        if self.temp_mode {
 842            self.switch_mode(Mode::Insert, true, window, cx);
 843        }
 844    }
 845}
 846#[cfg(test)]
 847mod test {
 848    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 849    use indoc::indoc;
 850    use language::language_settings::AllLanguageSettings;
 851    use settings::SettingsStore;
 852
 853    use crate::{
 854        VimSettings, motion,
 855        state::Mode::{self},
 856        test::{NeovimBackedTestContext, VimTestContext},
 857    };
 858
 859    #[gpui::test]
 860    async fn test_h(cx: &mut gpui::TestAppContext) {
 861        let mut cx = NeovimBackedTestContext::new(cx).await;
 862        cx.simulate_at_each_offset(
 863            "h",
 864            indoc! {"
 865            ˇThe qˇuick
 866            ˇbrown"
 867            },
 868        )
 869        .await
 870        .assert_matches();
 871    }
 872
 873    #[gpui::test]
 874    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 875        let mut cx = NeovimBackedTestContext::new(cx).await;
 876        cx.simulate_at_each_offset(
 877            "backspace",
 878            indoc! {"
 879            ˇThe qˇuick
 880            ˇbrown"
 881            },
 882        )
 883        .await
 884        .assert_matches();
 885    }
 886
 887    #[gpui::test]
 888    async fn test_j(cx: &mut gpui::TestAppContext) {
 889        let mut cx = NeovimBackedTestContext::new(cx).await;
 890
 891        cx.set_shared_state(indoc! {"
 892            aaˇaa
 893            😃😃"
 894        })
 895        .await;
 896        cx.simulate_shared_keystrokes("j").await;
 897        cx.shared_state().await.assert_eq(indoc! {"
 898            aaaa
 899            😃ˇ😃"
 900        });
 901
 902        cx.simulate_at_each_offset(
 903            "j",
 904            indoc! {"
 905                ˇThe qˇuick broˇwn
 906                ˇfox jumps"
 907            },
 908        )
 909        .await
 910        .assert_matches();
 911    }
 912
 913    #[gpui::test]
 914    async fn test_enter(cx: &mut gpui::TestAppContext) {
 915        let mut cx = NeovimBackedTestContext::new(cx).await;
 916        cx.simulate_at_each_offset(
 917            "enter",
 918            indoc! {"
 919            ˇThe qˇuick broˇwn
 920            ˇfox jumps"
 921            },
 922        )
 923        .await
 924        .assert_matches();
 925    }
 926
 927    #[gpui::test]
 928    async fn test_k(cx: &mut gpui::TestAppContext) {
 929        let mut cx = NeovimBackedTestContext::new(cx).await;
 930        cx.simulate_at_each_offset(
 931            "k",
 932            indoc! {"
 933            ˇThe qˇuick
 934            ˇbrown fˇox jumˇps"
 935            },
 936        )
 937        .await
 938        .assert_matches();
 939    }
 940
 941    #[gpui::test]
 942    async fn test_l(cx: &mut gpui::TestAppContext) {
 943        let mut cx = NeovimBackedTestContext::new(cx).await;
 944        cx.simulate_at_each_offset(
 945            "l",
 946            indoc! {"
 947            ˇThe qˇuicˇk
 948            ˇbrowˇn"},
 949        )
 950        .await
 951        .assert_matches();
 952    }
 953
 954    #[gpui::test]
 955    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 956        let mut cx = NeovimBackedTestContext::new(cx).await;
 957        cx.simulate_at_each_offset(
 958            "$",
 959            indoc! {"
 960            ˇThe qˇuicˇk
 961            ˇbrowˇn"},
 962        )
 963        .await
 964        .assert_matches();
 965        cx.simulate_at_each_offset(
 966            "0",
 967            indoc! {"
 968                ˇThe qˇuicˇk
 969                ˇbrowˇn"},
 970        )
 971        .await
 972        .assert_matches();
 973    }
 974
 975    #[gpui::test]
 976    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 977        let mut cx = NeovimBackedTestContext::new(cx).await;
 978
 979        cx.simulate_at_each_offset(
 980            "shift-g",
 981            indoc! {"
 982                The ˇquick
 983
 984                brown fox jumps
 985                overˇ the lazy doˇg"},
 986        )
 987        .await
 988        .assert_matches();
 989        cx.simulate(
 990            "shift-g",
 991            indoc! {"
 992            The quiˇck
 993
 994            brown"},
 995        )
 996        .await
 997        .assert_matches();
 998        cx.simulate(
 999            "shift-g",
1000            indoc! {"
1001            The quiˇck
1002
1003            "},
1004        )
1005        .await
1006        .assert_matches();
1007    }
1008
1009    #[gpui::test]
1010    async fn test_w(cx: &mut gpui::TestAppContext) {
1011        let mut cx = NeovimBackedTestContext::new(cx).await;
1012        cx.simulate_at_each_offset(
1013            "w",
1014            indoc! {"
1015            The ˇquickˇ-ˇbrown
1016            ˇ
1017            ˇ
1018            ˇfox_jumps ˇover
1019            ˇthˇe"},
1020        )
1021        .await
1022        .assert_matches();
1023        cx.simulate_at_each_offset(
1024            "shift-w",
1025            indoc! {"
1026            The ˇquickˇ-ˇbrown
1027            ˇ
1028            ˇ
1029            ˇfox_jumps ˇover
1030            ˇthˇe"},
1031        )
1032        .await
1033        .assert_matches();
1034    }
1035
1036    #[gpui::test]
1037    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1038        let mut cx = NeovimBackedTestContext::new(cx).await;
1039        cx.simulate_at_each_offset(
1040            "e",
1041            indoc! {"
1042            Thˇe quicˇkˇ-browˇn
1043
1044
1045            fox_jumpˇs oveˇr
1046            thˇe"},
1047        )
1048        .await
1049        .assert_matches();
1050        cx.simulate_at_each_offset(
1051            "shift-e",
1052            indoc! {"
1053            Thˇe quicˇkˇ-browˇn
1054
1055
1056            fox_jumpˇs oveˇr
1057            thˇe"},
1058        )
1059        .await
1060        .assert_matches();
1061    }
1062
1063    #[gpui::test]
1064    async fn test_b(cx: &mut gpui::TestAppContext) {
1065        let mut cx = NeovimBackedTestContext::new(cx).await;
1066        cx.simulate_at_each_offset(
1067            "b",
1068            indoc! {"
1069            ˇThe ˇquickˇ-ˇbrown
1070            ˇ
1071            ˇ
1072            ˇfox_jumps ˇover
1073            ˇthe"},
1074        )
1075        .await
1076        .assert_matches();
1077        cx.simulate_at_each_offset(
1078            "shift-b",
1079            indoc! {"
1080            ˇThe ˇquickˇ-ˇbrown
1081            ˇ
1082            ˇ
1083            ˇfox_jumps ˇover
1084            ˇthe"},
1085        )
1086        .await
1087        .assert_matches();
1088    }
1089
1090    #[gpui::test]
1091    async fn test_gg(cx: &mut gpui::TestAppContext) {
1092        let mut cx = NeovimBackedTestContext::new(cx).await;
1093        cx.simulate_at_each_offset(
1094            "g g",
1095            indoc! {"
1096                The qˇuick
1097
1098                brown fox jumps
1099                over ˇthe laˇzy dog"},
1100        )
1101        .await
1102        .assert_matches();
1103        cx.simulate(
1104            "g g",
1105            indoc! {"
1106
1107
1108                brown fox jumps
1109                over the laˇzy dog"},
1110        )
1111        .await
1112        .assert_matches();
1113        cx.simulate(
1114            "2 g g",
1115            indoc! {"
1116                ˇ
1117
1118                brown fox jumps
1119                over the lazydog"},
1120        )
1121        .await
1122        .assert_matches();
1123    }
1124
1125    #[gpui::test]
1126    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1127        let mut cx = NeovimBackedTestContext::new(cx).await;
1128        cx.simulate_at_each_offset(
1129            "shift-g",
1130            indoc! {"
1131                The qˇuick
1132
1133                brown fox jumps
1134                over ˇthe laˇzy dog"},
1135        )
1136        .await
1137        .assert_matches();
1138        cx.simulate(
1139            "shift-g",
1140            indoc! {"
1141
1142
1143                brown fox jumps
1144                over the laˇzy dog"},
1145        )
1146        .await
1147        .assert_matches();
1148        cx.simulate(
1149            "2 shift-g",
1150            indoc! {"
1151                ˇ
1152
1153                brown fox jumps
1154                over the lazydog"},
1155        )
1156        .await
1157        .assert_matches();
1158    }
1159
1160    #[gpui::test]
1161    async fn test_a(cx: &mut gpui::TestAppContext) {
1162        let mut cx = NeovimBackedTestContext::new(cx).await;
1163        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1164            .await
1165            .assert_matches();
1166    }
1167
1168    #[gpui::test]
1169    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1170        let mut cx = NeovimBackedTestContext::new(cx).await;
1171        cx.simulate_at_each_offset(
1172            "shift-a",
1173            indoc! {"
1174            ˇ
1175            The qˇuick
1176            brown ˇfox "},
1177        )
1178        .await
1179        .assert_matches();
1180    }
1181
1182    #[gpui::test]
1183    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1184        let mut cx = NeovimBackedTestContext::new(cx).await;
1185        cx.simulate("^", "The qˇuick").await.assert_matches();
1186        cx.simulate("^", " The qˇuick").await.assert_matches();
1187        cx.simulate("^", "ˇ").await.assert_matches();
1188        cx.simulate(
1189            "^",
1190            indoc! {"
1191                The qˇuick
1192                brown fox"},
1193        )
1194        .await
1195        .assert_matches();
1196        cx.simulate(
1197            "^",
1198            indoc! {"
1199                ˇ
1200                The quick"},
1201        )
1202        .await
1203        .assert_matches();
1204        // Indoc disallows trailing whitespace.
1205        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1206    }
1207
1208    #[gpui::test]
1209    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1210        let mut cx = NeovimBackedTestContext::new(cx).await;
1211        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1212        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1213        cx.simulate("shift-i", "ˇ").await.assert_matches();
1214        cx.simulate(
1215            "shift-i",
1216            indoc! {"
1217                The qˇuick
1218                brown fox"},
1219        )
1220        .await
1221        .assert_matches();
1222        cx.simulate(
1223            "shift-i",
1224            indoc! {"
1225                ˇ
1226                The quick"},
1227        )
1228        .await
1229        .assert_matches();
1230    }
1231
1232    #[gpui::test]
1233    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1234        let mut cx = NeovimBackedTestContext::new(cx).await;
1235        cx.simulate(
1236            "shift-d",
1237            indoc! {"
1238                The qˇuick
1239                brown fox"},
1240        )
1241        .await
1242        .assert_matches();
1243        cx.simulate(
1244            "shift-d",
1245            indoc! {"
1246                The quick
1247                ˇ
1248                brown fox"},
1249        )
1250        .await
1251        .assert_matches();
1252    }
1253
1254    #[gpui::test]
1255    async fn test_x(cx: &mut gpui::TestAppContext) {
1256        let mut cx = NeovimBackedTestContext::new(cx).await;
1257        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1258            .await
1259            .assert_matches();
1260        cx.simulate(
1261            "x",
1262            indoc! {"
1263                Tesˇt
1264                test"},
1265        )
1266        .await
1267        .assert_matches();
1268    }
1269
1270    #[gpui::test]
1271    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1272        let mut cx = NeovimBackedTestContext::new(cx).await;
1273        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1274            .await
1275            .assert_matches();
1276        cx.simulate(
1277            "shift-x",
1278            indoc! {"
1279                Test
1280                ˇtest"},
1281        )
1282        .await
1283        .assert_matches();
1284    }
1285
1286    #[gpui::test]
1287    async fn test_o(cx: &mut gpui::TestAppContext) {
1288        let mut cx = NeovimBackedTestContext::new(cx).await;
1289        cx.simulate("o", "ˇ").await.assert_matches();
1290        cx.simulate("o", "The ˇquick").await.assert_matches();
1291        cx.simulate_at_each_offset(
1292            "o",
1293            indoc! {"
1294                The qˇuick
1295                brown ˇfox
1296                jumps ˇover"},
1297        )
1298        .await
1299        .assert_matches();
1300        cx.simulate(
1301            "o",
1302            indoc! {"
1303                The quick
1304                ˇ
1305                brown fox"},
1306        )
1307        .await
1308        .assert_matches();
1309
1310        cx.assert_binding(
1311            "o",
1312            indoc! {"
1313                fn test() {
1314                    println!(ˇ);
1315                }"},
1316            Mode::Normal,
1317            indoc! {"
1318                fn test() {
1319                    println!();
1320                    ˇ
1321                }"},
1322            Mode::Insert,
1323        );
1324
1325        cx.assert_binding(
1326            "o",
1327            indoc! {"
1328                fn test(ˇ) {
1329                    println!();
1330                }"},
1331            Mode::Normal,
1332            indoc! {"
1333                fn test() {
1334                    ˇ
1335                    println!();
1336                }"},
1337            Mode::Insert,
1338        );
1339    }
1340
1341    #[gpui::test]
1342    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1343        let mut cx = NeovimBackedTestContext::new(cx).await;
1344        cx.simulate("shift-o", "ˇ").await.assert_matches();
1345        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1346        cx.simulate_at_each_offset(
1347            "shift-o",
1348            indoc! {"
1349            The qˇuick
1350            brown ˇfox
1351            jumps ˇover"},
1352        )
1353        .await
1354        .assert_matches();
1355        cx.simulate(
1356            "shift-o",
1357            indoc! {"
1358            The quick
1359            ˇ
1360            brown fox"},
1361        )
1362        .await
1363        .assert_matches();
1364
1365        // Our indentation is smarter than vims. So we don't match here
1366        cx.assert_binding(
1367            "shift-o",
1368            indoc! {"
1369                fn test() {
1370                    println!(ˇ);
1371                }"},
1372            Mode::Normal,
1373            indoc! {"
1374                fn test() {
1375                    ˇ
1376                    println!();
1377                }"},
1378            Mode::Insert,
1379        );
1380        cx.assert_binding(
1381            "shift-o",
1382            indoc! {"
1383                fn test(ˇ) {
1384                    println!();
1385                }"},
1386            Mode::Normal,
1387            indoc! {"
1388                ˇ
1389                fn test() {
1390                    println!();
1391                }"},
1392            Mode::Insert,
1393        );
1394    }
1395
1396    #[gpui::test]
1397    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1398        let mut cx = NeovimBackedTestContext::new(cx).await;
1399        cx.simulate("[ space", "ˇ").await.assert_matches();
1400        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1401        cx.simulate_at_each_offset(
1402            "3 [ space",
1403            indoc! {"
1404            The qˇuick
1405            brown ˇfox
1406            jumps ˇover"},
1407        )
1408        .await
1409        .assert_matches();
1410        cx.simulate_at_each_offset(
1411            "[ space",
1412            indoc! {"
1413            The qˇuick
1414            brown ˇfox
1415            jumps ˇover"},
1416        )
1417        .await
1418        .assert_matches();
1419        cx.simulate(
1420            "[ space",
1421            indoc! {"
1422            The quick
1423            ˇ
1424            brown fox"},
1425        )
1426        .await
1427        .assert_matches();
1428
1429        cx.simulate("] space", "ˇ").await.assert_matches();
1430        cx.simulate("] space", "The ˇquick").await.assert_matches();
1431        cx.simulate_at_each_offset(
1432            "3 ] space",
1433            indoc! {"
1434            The qˇuick
1435            brown ˇfox
1436            jumps ˇover"},
1437        )
1438        .await
1439        .assert_matches();
1440        cx.simulate_at_each_offset(
1441            "] space",
1442            indoc! {"
1443            The qˇuick
1444            brown ˇfox
1445            jumps ˇover"},
1446        )
1447        .await
1448        .assert_matches();
1449        cx.simulate(
1450            "] space",
1451            indoc! {"
1452            The quick
1453            ˇ
1454            brown fox"},
1455        )
1456        .await
1457        .assert_matches();
1458    }
1459
1460    #[gpui::test]
1461    async fn test_dd(cx: &mut gpui::TestAppContext) {
1462        let mut cx = NeovimBackedTestContext::new(cx).await;
1463        cx.simulate("d d", "ˇ").await.assert_matches();
1464        cx.simulate("d d", "The ˇquick").await.assert_matches();
1465        cx.simulate_at_each_offset(
1466            "d d",
1467            indoc! {"
1468            The qˇuick
1469            brown ˇfox
1470            jumps ˇover"},
1471        )
1472        .await
1473        .assert_matches();
1474        cx.simulate(
1475            "d d",
1476            indoc! {"
1477                The quick
1478                ˇ
1479                brown fox"},
1480        )
1481        .await
1482        .assert_matches();
1483    }
1484
1485    #[gpui::test]
1486    async fn test_cc(cx: &mut gpui::TestAppContext) {
1487        let mut cx = NeovimBackedTestContext::new(cx).await;
1488        cx.simulate("c c", "ˇ").await.assert_matches();
1489        cx.simulate("c c", "The ˇquick").await.assert_matches();
1490        cx.simulate_at_each_offset(
1491            "c c",
1492            indoc! {"
1493                The quˇick
1494                brown ˇfox
1495                jumps ˇover"},
1496        )
1497        .await
1498        .assert_matches();
1499        cx.simulate(
1500            "c c",
1501            indoc! {"
1502                The quick
1503                ˇ
1504                brown fox"},
1505        )
1506        .await
1507        .assert_matches();
1508    }
1509
1510    #[gpui::test]
1511    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1512        let mut cx = NeovimBackedTestContext::new(cx).await;
1513
1514        for count in 1..=5 {
1515            cx.simulate_at_each_offset(
1516                &format!("{count} w"),
1517                indoc! {"
1518                    ˇThe quˇickˇ browˇn
1519                    ˇ
1520                    ˇfox ˇjumpsˇ-ˇoˇver
1521                    ˇthe lazy dog
1522                "},
1523            )
1524            .await
1525            .assert_matches();
1526        }
1527    }
1528
1529    #[gpui::test]
1530    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1531        let mut cx = NeovimBackedTestContext::new(cx).await;
1532        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1533            .await
1534            .assert_matches();
1535    }
1536
1537    #[gpui::test]
1538    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1539        let mut cx = NeovimBackedTestContext::new(cx).await;
1540
1541        for count in 1..=3 {
1542            let test_case = indoc! {"
1543                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1544                ˇ    ˇbˇaaˇa ˇbˇbˇb
1545                ˇ
1546                ˇb
1547            "};
1548
1549            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1550                .await
1551                .assert_matches();
1552
1553            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1554                .await
1555                .assert_matches();
1556        }
1557    }
1558
1559    #[gpui::test]
1560    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1561        let mut cx = NeovimBackedTestContext::new(cx).await;
1562        let test_case = indoc! {"
1563            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1564            ˇ    ˇbˇaaˇa ˇbˇbˇb
1565            ˇ•••
1566            ˇb
1567            "
1568        };
1569
1570        for count in 1..=3 {
1571            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1572                .await
1573                .assert_matches();
1574
1575            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1576                .await
1577                .assert_matches();
1578        }
1579    }
1580
1581    #[gpui::test]
1582    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1583        let mut cx = VimTestContext::new(cx, true).await;
1584        cx.update_global(|store: &mut SettingsStore, cx| {
1585            store.update_user_settings::<VimSettings>(cx, |s| {
1586                s.use_smartcase_find = Some(true);
1587            });
1588        });
1589
1590        cx.assert_binding(
1591            "f p",
1592            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1593            Mode::Normal,
1594            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1595            Mode::Normal,
1596        );
1597
1598        cx.assert_binding(
1599            "shift-f p",
1600            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1601            Mode::Normal,
1602            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1603            Mode::Normal,
1604        );
1605
1606        cx.assert_binding(
1607            "t p",
1608            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1609            Mode::Normal,
1610            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1611            Mode::Normal,
1612        );
1613
1614        cx.assert_binding(
1615            "shift-t p",
1616            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1617            Mode::Normal,
1618            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1619            Mode::Normal,
1620        );
1621    }
1622
1623    #[gpui::test]
1624    async fn test_percent(cx: &mut TestAppContext) {
1625        let mut cx = NeovimBackedTestContext::new(cx).await;
1626        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1627            .await
1628            .assert_matches();
1629        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1630            .await
1631            .assert_matches();
1632        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1633            .await
1634            .assert_matches();
1635    }
1636
1637    #[gpui::test]
1638    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1639        let mut cx = NeovimBackedTestContext::new(cx).await;
1640
1641        // goes to current line end
1642        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1643        cx.simulate_shared_keystrokes("$").await;
1644        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1645
1646        // goes to next line end
1647        cx.simulate_shared_keystrokes("2 $").await;
1648        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1649
1650        // try to exceed the final line.
1651        cx.simulate_shared_keystrokes("4 $").await;
1652        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1653    }
1654
1655    #[gpui::test]
1656    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1657        let mut cx = VimTestContext::new(cx, true).await;
1658        cx.update(|_, cx| {
1659            cx.bind_keys(vec![
1660                KeyBinding::new(
1661                    "w",
1662                    motion::NextSubwordStart {
1663                        ignore_punctuation: false,
1664                    },
1665                    Some("Editor && VimControl && !VimWaiting && !menu"),
1666                ),
1667                KeyBinding::new(
1668                    "b",
1669                    motion::PreviousSubwordStart {
1670                        ignore_punctuation: false,
1671                    },
1672                    Some("Editor && VimControl && !VimWaiting && !menu"),
1673                ),
1674                KeyBinding::new(
1675                    "e",
1676                    motion::NextSubwordEnd {
1677                        ignore_punctuation: false,
1678                    },
1679                    Some("Editor && VimControl && !VimWaiting && !menu"),
1680                ),
1681                KeyBinding::new(
1682                    "g e",
1683                    motion::PreviousSubwordEnd {
1684                        ignore_punctuation: false,
1685                    },
1686                    Some("Editor && VimControl && !VimWaiting && !menu"),
1687                ),
1688            ]);
1689        });
1690
1691        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1692        // Special case: In 'cw', 'w' acts like 'e'
1693        cx.assert_binding(
1694            "c w",
1695            indoc! {"ˇassert_binding"},
1696            Mode::Normal,
1697            indoc! {"ˇ_binding"},
1698            Mode::Insert,
1699        );
1700
1701        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1702
1703        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1704
1705        cx.assert_binding_normal(
1706            "g e",
1707            indoc! {"assert_bindinˇg"},
1708            indoc! {"asserˇt_binding"},
1709        );
1710    }
1711
1712    #[gpui::test]
1713    async fn test_r(cx: &mut gpui::TestAppContext) {
1714        let mut cx = NeovimBackedTestContext::new(cx).await;
1715
1716        cx.set_shared_state("ˇhello\n").await;
1717        cx.simulate_shared_keystrokes("r -").await;
1718        cx.shared_state().await.assert_eq("ˇ-ello\n");
1719
1720        cx.set_shared_state("ˇhello\n").await;
1721        cx.simulate_shared_keystrokes("3 r -").await;
1722        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1723
1724        cx.set_shared_state("ˇhello\n").await;
1725        cx.simulate_shared_keystrokes("r - 2 l .").await;
1726        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1727
1728        cx.set_shared_state("ˇhello world\n").await;
1729        cx.simulate_shared_keystrokes("2 r - f w .").await;
1730        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1731
1732        cx.set_shared_state("ˇhello world\n").await;
1733        cx.simulate_shared_keystrokes("2 0 r - ").await;
1734        cx.shared_state().await.assert_eq("ˇhello world\n");
1735
1736        cx.set_shared_state("  helloˇ world\n").await;
1737        cx.simulate_shared_keystrokes("r enter").await;
1738        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
1739
1740        cx.set_shared_state("  helloˇ world\n").await;
1741        cx.simulate_shared_keystrokes("2 r enter").await;
1742        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
1743    }
1744
1745    #[gpui::test]
1746    async fn test_gq(cx: &mut gpui::TestAppContext) {
1747        let mut cx = NeovimBackedTestContext::new(cx).await;
1748        cx.set_neovim_option("textwidth=5").await;
1749
1750        cx.update(|_, cx| {
1751            SettingsStore::update_global(cx, |settings, cx| {
1752                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1753                    settings.defaults.preferred_line_length = Some(5);
1754                });
1755            })
1756        });
1757
1758        cx.set_shared_state("ˇth th th th th th\n").await;
1759        cx.simulate_shared_keystrokes("g q q").await;
1760        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1761
1762        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1763            .await;
1764        cx.simulate_shared_keystrokes("v j g q").await;
1765        cx.shared_state()
1766            .await
1767            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1768    }
1769
1770    #[gpui::test]
1771    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1772        let mut cx = NeovimBackedTestContext::new(cx).await;
1773        cx.set_neovim_option("filetype=rust").await;
1774
1775        cx.set_shared_state("// helloˇ\n").await;
1776        cx.simulate_shared_keystrokes("o").await;
1777        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1778        cx.simulate_shared_keystrokes("x escape shift-o").await;
1779        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1780    }
1781
1782    #[gpui::test]
1783    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1784        let mut cx = NeovimBackedTestContext::new(cx).await;
1785        cx.set_shared_state("heˇllo\n").await;
1786        cx.simulate_shared_keystrokes("y y p").await;
1787        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1788    }
1789
1790    #[gpui::test]
1791    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1792        let mut cx = NeovimBackedTestContext::new(cx).await;
1793        cx.set_shared_state("heˇllo").await;
1794        cx.simulate_shared_keystrokes("y y p").await;
1795        cx.shared_state().await.assert_eq("hello\nˇhello");
1796    }
1797
1798    #[gpui::test]
1799    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1800        let mut cx = NeovimBackedTestContext::new(cx).await;
1801        cx.set_shared_state("heˇllo\nhello").await;
1802        cx.simulate_shared_keystrokes("2 y y p").await;
1803        cx.shared_state()
1804            .await
1805            .assert_eq("hello\nˇhello\nhello\nhello");
1806    }
1807
1808    #[gpui::test]
1809    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1810        let mut cx = NeovimBackedTestContext::new(cx).await;
1811        cx.set_shared_state("heˇllo").await;
1812        cx.simulate_shared_keystrokes("d d").await;
1813        cx.shared_state().await.assert_eq("ˇ");
1814        cx.simulate_shared_keystrokes("p p").await;
1815        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1816    }
1817
1818    #[gpui::test]
1819    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1820        let mut cx = NeovimBackedTestContext::new(cx).await;
1821
1822        cx.set_shared_state("heˇllo").await;
1823        cx.simulate_shared_keystrokes("v i w shift-i").await;
1824        cx.shared_state().await.assert_eq("ˇhello");
1825
1826        cx.set_shared_state(indoc! {"
1827            The quick brown
1828            fox ˇjumps over
1829            the lazy dog"})
1830            .await;
1831        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1832        cx.shared_state().await.assert_eq(indoc! {"
1833            The quick brown
1834            ˇfox jumps over
1835            the lazy dog"});
1836
1837        cx.set_shared_state(indoc! {"
1838            The quick brown
1839            fox ˇjumps over
1840            the lazy dog"})
1841            .await;
1842        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1843        cx.shared_state().await.assert_eq(indoc! {"
1844            The quick brown
1845            fox jˇumps over
1846            the lazy dog"});
1847    }
1848
1849    #[gpui::test]
1850    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
1851        let mut cx = NeovimBackedTestContext::new(cx).await;
1852
1853        cx.set_shared_state(indoc! {"
1854            ˇfn a() { }
1855
1856
1857
1858
1859
1860            fn b() { }
1861
1862
1863
1864
1865
1866            fn b() { }"})
1867            .await;
1868        cx.simulate_shared_keystrokes("3 }").await;
1869        cx.shared_state().await.assert_matches();
1870        cx.simulate_shared_keystrokes("ctrl-o").await;
1871        cx.shared_state().await.assert_matches();
1872        cx.simulate_shared_keystrokes("ctrl-i").await;
1873        cx.shared_state().await.assert_matches();
1874        cx.simulate_shared_keystrokes("1 1 k").await;
1875        cx.shared_state().await.assert_matches();
1876        cx.simulate_shared_keystrokes("ctrl-o").await;
1877        cx.shared_state().await.assert_matches();
1878    }
1879}