normal.rs

   1mod case;
   2mod change;
   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    indent::IndentDirection,
  19    motion::{self, first_non_whitespace, next_line_end, right, Motion},
  20    object::Object,
  21    state::{Mode, Operator},
  22    surrounds::SurroundsType,
  23    Vim,
  24};
  25use case::CaseTarget;
  26use collections::BTreeSet;
  27use editor::scroll::Autoscroll;
  28use editor::Anchor;
  29use editor::Bias;
  30use editor::Editor;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{actions, ViewContext};
  33use language::{Point, SelectionGoal};
  34use log::error;
  35use multi_buffer::MultiBufferRow;
  36
  37actions!(
  38    vim,
  39    [
  40        InsertAfter,
  41        InsertBefore,
  42        InsertFirstNonWhitespace,
  43        InsertEndOfLine,
  44        InsertLineAbove,
  45        InsertLineBelow,
  46        InsertAtPrevious,
  47        DeleteLeft,
  48        DeleteRight,
  49        ChangeToEndOfLine,
  50        DeleteToEndOfLine,
  51        Yank,
  52        YankLine,
  53        ChangeCase,
  54        ConvertToUpperCase,
  55        ConvertToLowerCase,
  56        JoinLines,
  57        ToggleComments,
  58        Undo,
  59        Redo,
  60    ]
  61);
  62
  63pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
  64    Vim::action(editor, cx, Vim::insert_after);
  65    Vim::action(editor, cx, Vim::insert_before);
  66    Vim::action(editor, cx, Vim::insert_first_non_whitespace);
  67    Vim::action(editor, cx, Vim::insert_end_of_line);
  68    Vim::action(editor, cx, Vim::insert_line_above);
  69    Vim::action(editor, cx, Vim::insert_line_below);
  70    Vim::action(editor, cx, Vim::insert_at_previous);
  71    Vim::action(editor, cx, Vim::change_case);
  72    Vim::action(editor, cx, Vim::convert_to_upper_case);
  73    Vim::action(editor, cx, Vim::convert_to_lower_case);
  74    Vim::action(editor, cx, Vim::yank_line);
  75    Vim::action(editor, cx, Vim::toggle_comments);
  76    Vim::action(editor, cx, Vim::paste);
  77
  78    Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| {
  79        vim.record_current_action(cx);
  80        let times = vim.take_count(cx);
  81        vim.delete_motion(Motion::Left, times, cx);
  82    });
  83    Vim::action(editor, cx, |vim, _: &DeleteRight, cx| {
  84        vim.record_current_action(cx);
  85        let times = vim.take_count(cx);
  86        vim.delete_motion(Motion::Right, times, cx);
  87    });
  88    Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| {
  89        vim.start_recording(cx);
  90        let times = vim.take_count(cx);
  91        vim.change_motion(
  92            Motion::EndOfLine {
  93                display_lines: false,
  94            },
  95            times,
  96            cx,
  97        );
  98    });
  99    Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| {
 100        vim.record_current_action(cx);
 101        let times = vim.take_count(cx);
 102        vim.delete_motion(
 103            Motion::EndOfLine {
 104                display_lines: false,
 105            },
 106            times,
 107            cx,
 108        );
 109    });
 110    Vim::action(editor, cx, |vim, _: &JoinLines, cx| {
 111        vim.record_current_action(cx);
 112        let mut times = vim.take_count(cx).unwrap_or(1);
 113        if vim.mode.is_visual() {
 114            times = 1;
 115        } else if times > 1 {
 116            // 2J joins two lines together (same as J or 1J)
 117            times -= 1;
 118        }
 119
 120        vim.update_editor(cx, |_, editor, cx| {
 121            editor.transact(cx, |editor, cx| {
 122                for _ in 0..times {
 123                    editor.join_lines(&Default::default(), cx)
 124                }
 125            })
 126        });
 127        if vim.mode.is_visual() {
 128            vim.switch_mode(Mode::Normal, true, cx)
 129        }
 130    });
 131
 132    Vim::action(editor, cx, |vim, _: &Undo, cx| {
 133        let times = vim.take_count(cx);
 134        vim.update_editor(cx, |_, editor, cx| {
 135            for _ in 0..times.unwrap_or(1) {
 136                editor.undo(&editor::actions::Undo, cx);
 137            }
 138        });
 139    });
 140    Vim::action(editor, cx, |vim, _: &Redo, cx| {
 141        let times = vim.take_count(cx);
 142        vim.update_editor(cx, |_, editor, cx| {
 143            for _ in 0..times.unwrap_or(1) {
 144                editor.redo(&editor::actions::Redo, cx);
 145            }
 146        });
 147    });
 148
 149    repeat::register(editor, cx);
 150    scroll::register(editor, cx);
 151    search::register(editor, cx);
 152    substitute::register(editor, cx);
 153    increment::register(editor, cx);
 154}
 155
 156impl Vim {
 157    pub fn normal_motion(
 158        &mut self,
 159        motion: Motion,
 160        operator: Option<Operator>,
 161        times: Option<usize>,
 162        cx: &mut ViewContext<Self>,
 163    ) {
 164        match operator {
 165            None => self.move_cursor(motion, times, cx),
 166            Some(Operator::Change) => self.change_motion(motion, times, cx),
 167            Some(Operator::Delete) => self.delete_motion(motion, times, cx),
 168            Some(Operator::Yank) => self.yank_motion(motion, times, cx),
 169            Some(Operator::AddSurrounds { target: None }) => {}
 170            Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx),
 171            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx),
 172            Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx),
 173            Some(Operator::Lowercase) => {
 174                self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
 175            }
 176            Some(Operator::Uppercase) => {
 177                self.change_case_motion(motion, times, CaseTarget::Uppercase, cx)
 178            }
 179            Some(Operator::OppositeCase) => {
 180                self.change_case_motion(motion, times, CaseTarget::OppositeCase, cx)
 181            }
 182            Some(Operator::ToggleComments) => self.toggle_comments_motion(motion, times, cx),
 183            Some(operator) => {
 184                // Can't do anything for text objects, Ignoring
 185                error!("Unexpected normal mode motion operator: {:?}", operator)
 186            }
 187        }
 188        // Exit temporary normal mode (if active).
 189        self.exit_temporary_normal(cx);
 190    }
 191
 192    pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
 193        let mut waiting_operator: Option<Operator> = None;
 194        match self.maybe_pop_operator() {
 195            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
 196                Some(Operator::Change) => self.change_object(object, around, cx),
 197                Some(Operator::Delete) => self.delete_object(object, around, cx),
 198                Some(Operator::Yank) => self.yank_object(object, around, cx),
 199                Some(Operator::Indent) => {
 200                    self.indent_object(object, around, IndentDirection::In, cx)
 201                }
 202                Some(Operator::Outdent) => {
 203                    self.indent_object(object, around, IndentDirection::Out, cx)
 204                }
 205                Some(Operator::Rewrap) => self.rewrap_object(object, around, cx),
 206                Some(Operator::Lowercase) => {
 207                    self.change_case_object(object, around, CaseTarget::Lowercase, cx)
 208                }
 209                Some(Operator::Uppercase) => {
 210                    self.change_case_object(object, around, CaseTarget::Uppercase, cx)
 211                }
 212                Some(Operator::OppositeCase) => {
 213                    self.change_case_object(object, around, CaseTarget::OppositeCase, cx)
 214                }
 215                Some(Operator::AddSurrounds { target: None }) => {
 216                    waiting_operator = Some(Operator::AddSurrounds {
 217                        target: Some(SurroundsType::Object(object, around)),
 218                    });
 219                }
 220                Some(Operator::ToggleComments) => self.toggle_comments_object(object, around, cx),
 221                _ => {
 222                    // Can't do anything for namespace operators. Ignoring
 223                }
 224            },
 225            Some(Operator::DeleteSurrounds) => {
 226                waiting_operator = Some(Operator::DeleteSurrounds);
 227            }
 228            Some(Operator::ChangeSurrounds { target: None }) => {
 229                if self.check_and_move_to_valid_bracket_pair(object, cx) {
 230                    waiting_operator = Some(Operator::ChangeSurrounds {
 231                        target: Some(object),
 232                    });
 233                }
 234            }
 235            _ => {
 236                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 237            }
 238        }
 239        self.clear_operator(cx);
 240        if let Some(operator) = waiting_operator {
 241            self.push_operator(operator, cx);
 242        }
 243    }
 244
 245    pub(crate) fn move_cursor(
 246        &mut self,
 247        motion: Motion,
 248        times: Option<usize>,
 249        cx: &mut ViewContext<Self>,
 250    ) {
 251        self.update_editor(cx, |_, editor, cx| {
 252            let text_layout_details = editor.text_layout_details(cx);
 253            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 254                s.move_cursors_with(|map, cursor, goal| {
 255                    motion
 256                        .move_point(map, cursor, goal, times, &text_layout_details)
 257                        .unwrap_or((cursor, goal))
 258                })
 259            })
 260        });
 261    }
 262
 263    fn insert_after(&mut self, _: &InsertAfter, cx: &mut ViewContext<Self>) {
 264        self.start_recording(cx);
 265        self.switch_mode(Mode::Insert, false, cx);
 266        self.update_editor(cx, |_, editor, cx| {
 267            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 268                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 269            });
 270        });
 271    }
 272
 273    fn insert_before(&mut self, _: &InsertBefore, cx: &mut ViewContext<Self>) {
 274        self.start_recording(cx);
 275        self.switch_mode(Mode::Insert, false, cx);
 276    }
 277
 278    fn insert_first_non_whitespace(
 279        &mut self,
 280        _: &InsertFirstNonWhitespace,
 281        cx: &mut ViewContext<Self>,
 282    ) {
 283        self.start_recording(cx);
 284        self.switch_mode(Mode::Insert, false, cx);
 285        self.update_editor(cx, |_, editor, cx| {
 286            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 287                s.move_cursors_with(|map, cursor, _| {
 288                    (
 289                        first_non_whitespace(map, false, cursor),
 290                        SelectionGoal::None,
 291                    )
 292                });
 293            });
 294        });
 295    }
 296
 297    fn insert_end_of_line(&mut self, _: &InsertEndOfLine, cx: &mut ViewContext<Self>) {
 298        self.start_recording(cx);
 299        self.switch_mode(Mode::Insert, false, cx);
 300        self.update_editor(cx, |_, editor, cx| {
 301            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 302                s.move_cursors_with(|map, cursor, _| {
 303                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 304                });
 305            });
 306        });
 307    }
 308
 309    fn insert_at_previous(&mut self, _: &InsertAtPrevious, cx: &mut ViewContext<Self>) {
 310        self.start_recording(cx);
 311        self.switch_mode(Mode::Insert, false, cx);
 312        self.update_editor(cx, |vim, editor, cx| {
 313            if let Some(marks) = vim.marks.get("^") {
 314                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 315                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 316                });
 317            }
 318        });
 319    }
 320
 321    fn insert_line_above(&mut self, _: &InsertLineAbove, cx: &mut ViewContext<Self>) {
 322        self.start_recording(cx);
 323        self.switch_mode(Mode::Insert, false, cx);
 324        self.update_editor(cx, |_, editor, cx| {
 325            editor.transact(cx, |editor, cx| {
 326                let selections = editor.selections.all::<Point>(cx);
 327                let snapshot = editor.buffer().read(cx).snapshot(cx);
 328
 329                let selection_start_rows: BTreeSet<u32> = selections
 330                    .into_iter()
 331                    .map(|selection| selection.start.row)
 332                    .collect();
 333                let edits = selection_start_rows
 334                    .into_iter()
 335                    .map(|row| {
 336                        let indent = snapshot
 337                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 338                            .chars()
 339                            .collect::<String>();
 340
 341                        let start_of_line = Point::new(row, 0);
 342                        (start_of_line..start_of_line, indent + "\n")
 343                    })
 344                    .collect::<Vec<_>>();
 345                editor.edit_with_autoindent(edits, cx);
 346                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 347                    s.move_cursors_with(|map, cursor, _| {
 348                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 349                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 350                        (insert_point, SelectionGoal::None)
 351                    });
 352                });
 353            });
 354        });
 355    }
 356
 357    fn insert_line_below(&mut self, _: &InsertLineBelow, cx: &mut ViewContext<Self>) {
 358        self.start_recording(cx);
 359        self.switch_mode(Mode::Insert, false, cx);
 360        self.update_editor(cx, |_, editor, cx| {
 361            let text_layout_details = editor.text_layout_details(cx);
 362            editor.transact(cx, |editor, cx| {
 363                let selections = editor.selections.all::<Point>(cx);
 364                let snapshot = editor.buffer().read(cx).snapshot(cx);
 365
 366                let selection_end_rows: BTreeSet<u32> = selections
 367                    .into_iter()
 368                    .map(|selection| selection.end.row)
 369                    .collect();
 370                let edits = selection_end_rows
 371                    .into_iter()
 372                    .map(|row| {
 373                        let indent = snapshot
 374                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 375                            .chars()
 376                            .collect::<String>();
 377
 378                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 379                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 380                    })
 381                    .collect::<Vec<_>>();
 382                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 383                    s.maybe_move_cursors_with(|map, cursor, goal| {
 384                        Motion::CurrentLine.move_point(
 385                            map,
 386                            cursor,
 387                            goal,
 388                            None,
 389                            &text_layout_details,
 390                        )
 391                    });
 392                });
 393                editor.edit_with_autoindent(edits, cx);
 394            });
 395        });
 396    }
 397
 398    fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext<Self>) {
 399        let count = self.take_count(cx);
 400        self.yank_motion(motion::Motion::CurrentLine, count, cx)
 401    }
 402
 403    fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
 404        self.record_current_action(cx);
 405        self.store_visual_marks(cx);
 406        self.update_editor(cx, |vim, editor, cx| {
 407            editor.transact(cx, |editor, cx| {
 408                let original_positions = vim.save_selection_starts(editor, cx);
 409                editor.toggle_comments(&Default::default(), cx);
 410                vim.restore_selection_cursors(editor, cx, original_positions);
 411            });
 412        });
 413        if self.mode.is_visual() {
 414            self.switch_mode(Mode::Normal, true, cx)
 415        }
 416    }
 417
 418    pub(crate) fn normal_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
 419        let count = self.take_count(cx).unwrap_or(1);
 420        self.stop_recording(cx);
 421        self.update_editor(cx, |_, editor, cx| {
 422            editor.transact(cx, |editor, cx| {
 423                editor.set_clip_at_line_ends(false, cx);
 424                let (map, display_selections) = editor.selections.all_display(cx);
 425
 426                let mut edits = Vec::new();
 427                for selection in display_selections {
 428                    let mut range = selection.range();
 429                    for _ in 0..count {
 430                        let new_point = movement::saturating_right(&map, range.end);
 431                        if range.end == new_point {
 432                            return;
 433                        }
 434                        range.end = new_point;
 435                    }
 436
 437                    edits.push((
 438                        range.start.to_offset(&map, Bias::Left)
 439                            ..range.end.to_offset(&map, Bias::Left),
 440                        text.repeat(count),
 441                    ))
 442                }
 443
 444                editor.edit(edits, cx);
 445                editor.set_clip_at_line_ends(true, cx);
 446                editor.change_selections(None, cx, |s| {
 447                    s.move_with(|map, selection| {
 448                        let point = movement::saturating_left(map, selection.head());
 449                        selection.collapse_to(point, SelectionGoal::None)
 450                    });
 451                });
 452            });
 453        });
 454        self.pop_operator(cx);
 455    }
 456
 457    pub fn save_selection_starts(
 458        &self,
 459        editor: &Editor,
 460        cx: &mut ViewContext<Editor>,
 461    ) -> HashMap<usize, Anchor> {
 462        let (map, selections) = editor.selections.all_display(cx);
 463        selections
 464            .iter()
 465            .map(|selection| {
 466                (
 467                    selection.id,
 468                    map.display_point_to_anchor(selection.start, Bias::Right),
 469                )
 470            })
 471            .collect::<HashMap<_, _>>()
 472    }
 473
 474    pub fn restore_selection_cursors(
 475        &self,
 476        editor: &mut Editor,
 477        cx: &mut ViewContext<Editor>,
 478        mut positions: HashMap<usize, Anchor>,
 479    ) {
 480        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 481            s.move_with(|map, selection| {
 482                if let Some(anchor) = positions.remove(&selection.id) {
 483                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 484                }
 485            });
 486        });
 487    }
 488
 489    fn exit_temporary_normal(&mut self, cx: &mut ViewContext<Self>) {
 490        if self.temp_mode {
 491            self.switch_mode(Mode::Insert, true, cx);
 492        }
 493    }
 494}
 495#[cfg(test)]
 496mod test {
 497    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 498    use indoc::indoc;
 499    use language::language_settings::AllLanguageSettings;
 500    use settings::SettingsStore;
 501
 502    use crate::{
 503        motion,
 504        state::Mode::{self},
 505        test::{NeovimBackedTestContext, VimTestContext},
 506        VimSettings,
 507    };
 508
 509    #[gpui::test]
 510    async fn test_h(cx: &mut gpui::TestAppContext) {
 511        let mut cx = NeovimBackedTestContext::new(cx).await;
 512        cx.simulate_at_each_offset(
 513            "h",
 514            indoc! {"
 515            ˇThe qˇuick
 516            ˇbrown"
 517            },
 518        )
 519        .await
 520        .assert_matches();
 521    }
 522
 523    #[gpui::test]
 524    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 525        let mut cx = NeovimBackedTestContext::new(cx).await;
 526        cx.simulate_at_each_offset(
 527            "backspace",
 528            indoc! {"
 529            ˇThe qˇuick
 530            ˇbrown"
 531            },
 532        )
 533        .await
 534        .assert_matches();
 535    }
 536
 537    #[gpui::test]
 538    async fn test_j(cx: &mut gpui::TestAppContext) {
 539        let mut cx = NeovimBackedTestContext::new(cx).await;
 540
 541        cx.set_shared_state(indoc! {"
 542            aaˇaa
 543            😃😃"
 544        })
 545        .await;
 546        cx.simulate_shared_keystrokes("j").await;
 547        cx.shared_state().await.assert_eq(indoc! {"
 548            aaaa
 549            😃ˇ😃"
 550        });
 551
 552        cx.simulate_at_each_offset(
 553            "j",
 554            indoc! {"
 555                ˇThe qˇuick broˇwn
 556                ˇfox jumps"
 557            },
 558        )
 559        .await
 560        .assert_matches();
 561    }
 562
 563    #[gpui::test]
 564    async fn test_enter(cx: &mut gpui::TestAppContext) {
 565        let mut cx = NeovimBackedTestContext::new(cx).await;
 566        cx.simulate_at_each_offset(
 567            "enter",
 568            indoc! {"
 569            ˇThe qˇuick broˇwn
 570            ˇfox jumps"
 571            },
 572        )
 573        .await
 574        .assert_matches();
 575    }
 576
 577    #[gpui::test]
 578    async fn test_k(cx: &mut gpui::TestAppContext) {
 579        let mut cx = NeovimBackedTestContext::new(cx).await;
 580        cx.simulate_at_each_offset(
 581            "k",
 582            indoc! {"
 583            ˇThe qˇuick
 584            ˇbrown fˇox jumˇps"
 585            },
 586        )
 587        .await
 588        .assert_matches();
 589    }
 590
 591    #[gpui::test]
 592    async fn test_l(cx: &mut gpui::TestAppContext) {
 593        let mut cx = NeovimBackedTestContext::new(cx).await;
 594        cx.simulate_at_each_offset(
 595            "l",
 596            indoc! {"
 597            ˇThe qˇuicˇk
 598            ˇbrowˇn"},
 599        )
 600        .await
 601        .assert_matches();
 602    }
 603
 604    #[gpui::test]
 605    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 606        let mut cx = NeovimBackedTestContext::new(cx).await;
 607        cx.simulate_at_each_offset(
 608            "$",
 609            indoc! {"
 610            ˇThe qˇuicˇk
 611            ˇbrowˇn"},
 612        )
 613        .await
 614        .assert_matches();
 615        cx.simulate_at_each_offset(
 616            "0",
 617            indoc! {"
 618                ˇThe qˇuicˇk
 619                ˇbrowˇn"},
 620        )
 621        .await
 622        .assert_matches();
 623    }
 624
 625    #[gpui::test]
 626    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 627        let mut cx = NeovimBackedTestContext::new(cx).await;
 628
 629        cx.simulate_at_each_offset(
 630            "shift-g",
 631            indoc! {"
 632                The ˇquick
 633
 634                brown fox jumps
 635                overˇ the lazy doˇg"},
 636        )
 637        .await
 638        .assert_matches();
 639        cx.simulate(
 640            "shift-g",
 641            indoc! {"
 642            The quiˇck
 643
 644            brown"},
 645        )
 646        .await
 647        .assert_matches();
 648        cx.simulate(
 649            "shift-g",
 650            indoc! {"
 651            The quiˇck
 652
 653            "},
 654        )
 655        .await
 656        .assert_matches();
 657    }
 658
 659    #[gpui::test]
 660    async fn test_w(cx: &mut gpui::TestAppContext) {
 661        let mut cx = NeovimBackedTestContext::new(cx).await;
 662        cx.simulate_at_each_offset(
 663            "w",
 664            indoc! {"
 665            The ˇquickˇ-ˇbrown
 666            ˇ
 667            ˇ
 668            ˇfox_jumps ˇover
 669            ˇthˇe"},
 670        )
 671        .await
 672        .assert_matches();
 673        cx.simulate_at_each_offset(
 674            "shift-w",
 675            indoc! {"
 676            The ˇquickˇ-ˇbrown
 677            ˇ
 678            ˇ
 679            ˇfox_jumps ˇover
 680            ˇthˇe"},
 681        )
 682        .await
 683        .assert_matches();
 684    }
 685
 686    #[gpui::test]
 687    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 688        let mut cx = NeovimBackedTestContext::new(cx).await;
 689        cx.simulate_at_each_offset(
 690            "e",
 691            indoc! {"
 692            Thˇe quicˇkˇ-browˇn
 693
 694
 695            fox_jumpˇs oveˇr
 696            thˇe"},
 697        )
 698        .await
 699        .assert_matches();
 700        cx.simulate_at_each_offset(
 701            "shift-e",
 702            indoc! {"
 703            Thˇe quicˇkˇ-browˇn
 704
 705
 706            fox_jumpˇs oveˇr
 707            thˇe"},
 708        )
 709        .await
 710        .assert_matches();
 711    }
 712
 713    #[gpui::test]
 714    async fn test_b(cx: &mut gpui::TestAppContext) {
 715        let mut cx = NeovimBackedTestContext::new(cx).await;
 716        cx.simulate_at_each_offset(
 717            "b",
 718            indoc! {"
 719            ˇThe ˇquickˇ-ˇbrown
 720            ˇ
 721            ˇ
 722            ˇfox_jumps ˇover
 723            ˇthe"},
 724        )
 725        .await
 726        .assert_matches();
 727        cx.simulate_at_each_offset(
 728            "shift-b",
 729            indoc! {"
 730            ˇThe ˇquickˇ-ˇbrown
 731            ˇ
 732            ˇ
 733            ˇfox_jumps ˇover
 734            ˇthe"},
 735        )
 736        .await
 737        .assert_matches();
 738    }
 739
 740    #[gpui::test]
 741    async fn test_gg(cx: &mut gpui::TestAppContext) {
 742        let mut cx = NeovimBackedTestContext::new(cx).await;
 743        cx.simulate_at_each_offset(
 744            "g g",
 745            indoc! {"
 746                The qˇuick
 747
 748                brown fox jumps
 749                over ˇthe laˇzy dog"},
 750        )
 751        .await
 752        .assert_matches();
 753        cx.simulate(
 754            "g g",
 755            indoc! {"
 756
 757
 758                brown fox jumps
 759                over the laˇzy dog"},
 760        )
 761        .await
 762        .assert_matches();
 763        cx.simulate(
 764            "2 g g",
 765            indoc! {"
 766                ˇ
 767
 768                brown fox jumps
 769                over the lazydog"},
 770        )
 771        .await
 772        .assert_matches();
 773    }
 774
 775    #[gpui::test]
 776    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 777        let mut cx = NeovimBackedTestContext::new(cx).await;
 778        cx.simulate_at_each_offset(
 779            "shift-g",
 780            indoc! {"
 781                The qˇuick
 782
 783                brown fox jumps
 784                over ˇthe laˇzy dog"},
 785        )
 786        .await
 787        .assert_matches();
 788        cx.simulate(
 789            "shift-g",
 790            indoc! {"
 791
 792
 793                brown fox jumps
 794                over the laˇzy dog"},
 795        )
 796        .await
 797        .assert_matches();
 798        cx.simulate(
 799            "2 shift-g",
 800            indoc! {"
 801                ˇ
 802
 803                brown fox jumps
 804                over the lazydog"},
 805        )
 806        .await
 807        .assert_matches();
 808    }
 809
 810    #[gpui::test]
 811    async fn test_a(cx: &mut gpui::TestAppContext) {
 812        let mut cx = NeovimBackedTestContext::new(cx).await;
 813        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 814            .await
 815            .assert_matches();
 816    }
 817
 818    #[gpui::test]
 819    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 820        let mut cx = NeovimBackedTestContext::new(cx).await;
 821        cx.simulate_at_each_offset(
 822            "shift-a",
 823            indoc! {"
 824            ˇ
 825            The qˇuick
 826            brown ˇfox "},
 827        )
 828        .await
 829        .assert_matches();
 830    }
 831
 832    #[gpui::test]
 833    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 834        let mut cx = NeovimBackedTestContext::new(cx).await;
 835        cx.simulate("^", "The qˇuick").await.assert_matches();
 836        cx.simulate("^", " The qˇuick").await.assert_matches();
 837        cx.simulate("^", "ˇ").await.assert_matches();
 838        cx.simulate(
 839            "^",
 840            indoc! {"
 841                The qˇuick
 842                brown fox"},
 843        )
 844        .await
 845        .assert_matches();
 846        cx.simulate(
 847            "^",
 848            indoc! {"
 849                ˇ
 850                The quick"},
 851        )
 852        .await
 853        .assert_matches();
 854        // Indoc disallows trailing whitespace.
 855        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
 856    }
 857
 858    #[gpui::test]
 859    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 860        let mut cx = NeovimBackedTestContext::new(cx).await;
 861        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
 862        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
 863        cx.simulate("shift-i", "ˇ").await.assert_matches();
 864        cx.simulate(
 865            "shift-i",
 866            indoc! {"
 867                The qˇuick
 868                brown fox"},
 869        )
 870        .await
 871        .assert_matches();
 872        cx.simulate(
 873            "shift-i",
 874            indoc! {"
 875                ˇ
 876                The quick"},
 877        )
 878        .await
 879        .assert_matches();
 880    }
 881
 882    #[gpui::test]
 883    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 884        let mut cx = NeovimBackedTestContext::new(cx).await;
 885        cx.simulate(
 886            "shift-d",
 887            indoc! {"
 888                The qˇuick
 889                brown fox"},
 890        )
 891        .await
 892        .assert_matches();
 893        cx.simulate(
 894            "shift-d",
 895            indoc! {"
 896                The quick
 897                ˇ
 898                brown fox"},
 899        )
 900        .await
 901        .assert_matches();
 902    }
 903
 904    #[gpui::test]
 905    async fn test_x(cx: &mut gpui::TestAppContext) {
 906        let mut cx = NeovimBackedTestContext::new(cx).await;
 907        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
 908            .await
 909            .assert_matches();
 910        cx.simulate(
 911            "x",
 912            indoc! {"
 913                Tesˇt
 914                test"},
 915        )
 916        .await
 917        .assert_matches();
 918    }
 919
 920    #[gpui::test]
 921    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 922        let mut cx = NeovimBackedTestContext::new(cx).await;
 923        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
 924            .await
 925            .assert_matches();
 926        cx.simulate(
 927            "shift-x",
 928            indoc! {"
 929                Test
 930                ˇtest"},
 931        )
 932        .await
 933        .assert_matches();
 934    }
 935
 936    #[gpui::test]
 937    async fn test_o(cx: &mut gpui::TestAppContext) {
 938        let mut cx = NeovimBackedTestContext::new(cx).await;
 939        cx.simulate("o", "ˇ").await.assert_matches();
 940        cx.simulate("o", "The ˇquick").await.assert_matches();
 941        cx.simulate_at_each_offset(
 942            "o",
 943            indoc! {"
 944                The qˇuick
 945                brown ˇfox
 946                jumps ˇover"},
 947        )
 948        .await
 949        .assert_matches();
 950        cx.simulate(
 951            "o",
 952            indoc! {"
 953                The quick
 954                ˇ
 955                brown fox"},
 956        )
 957        .await
 958        .assert_matches();
 959
 960        cx.assert_binding(
 961            "o",
 962            indoc! {"
 963                fn test() {
 964                    println!(ˇ);
 965                }"},
 966            Mode::Normal,
 967            indoc! {"
 968                fn test() {
 969                    println!();
 970                    ˇ
 971                }"},
 972            Mode::Insert,
 973        );
 974
 975        cx.assert_binding(
 976            "o",
 977            indoc! {"
 978                fn test(ˇ) {
 979                    println!();
 980                }"},
 981            Mode::Normal,
 982            indoc! {"
 983                fn test() {
 984                    ˇ
 985                    println!();
 986                }"},
 987            Mode::Insert,
 988        );
 989    }
 990
 991    #[gpui::test]
 992    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
 993        let mut cx = NeovimBackedTestContext::new(cx).await;
 994        cx.simulate("shift-o", "ˇ").await.assert_matches();
 995        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
 996        cx.simulate_at_each_offset(
 997            "shift-o",
 998            indoc! {"
 999            The qˇuick
1000            brown ˇfox
1001            jumps ˇover"},
1002        )
1003        .await
1004        .assert_matches();
1005        cx.simulate(
1006            "shift-o",
1007            indoc! {"
1008            The quick
1009            ˇ
1010            brown fox"},
1011        )
1012        .await
1013        .assert_matches();
1014
1015        // Our indentation is smarter than vims. So we don't match here
1016        cx.assert_binding(
1017            "shift-o",
1018            indoc! {"
1019                fn test() {
1020                    println!(ˇ);
1021                }"},
1022            Mode::Normal,
1023            indoc! {"
1024                fn test() {
1025                    ˇ
1026                    println!();
1027                }"},
1028            Mode::Insert,
1029        );
1030        cx.assert_binding(
1031            "shift-o",
1032            indoc! {"
1033                fn test(ˇ) {
1034                    println!();
1035                }"},
1036            Mode::Normal,
1037            indoc! {"
1038                ˇ
1039                fn test() {
1040                    println!();
1041                }"},
1042            Mode::Insert,
1043        );
1044    }
1045
1046    #[gpui::test]
1047    async fn test_dd(cx: &mut gpui::TestAppContext) {
1048        let mut cx = NeovimBackedTestContext::new(cx).await;
1049        cx.simulate("d d", "ˇ").await.assert_matches();
1050        cx.simulate("d d", "The ˇquick").await.assert_matches();
1051        cx.simulate_at_each_offset(
1052            "d d",
1053            indoc! {"
1054            The qˇuick
1055            brown ˇfox
1056            jumps ˇover"},
1057        )
1058        .await
1059        .assert_matches();
1060        cx.simulate(
1061            "d d",
1062            indoc! {"
1063                The quick
1064                ˇ
1065                brown fox"},
1066        )
1067        .await
1068        .assert_matches();
1069    }
1070
1071    #[gpui::test]
1072    async fn test_cc(cx: &mut gpui::TestAppContext) {
1073        let mut cx = NeovimBackedTestContext::new(cx).await;
1074        cx.simulate("c c", "ˇ").await.assert_matches();
1075        cx.simulate("c c", "The ˇquick").await.assert_matches();
1076        cx.simulate_at_each_offset(
1077            "c c",
1078            indoc! {"
1079                The quˇick
1080                brown ˇfox
1081                jumps ˇover"},
1082        )
1083        .await
1084        .assert_matches();
1085        cx.simulate(
1086            "c c",
1087            indoc! {"
1088                The quick
1089                ˇ
1090                brown fox"},
1091        )
1092        .await
1093        .assert_matches();
1094    }
1095
1096    #[gpui::test]
1097    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1098        let mut cx = NeovimBackedTestContext::new(cx).await;
1099
1100        for count in 1..=5 {
1101            cx.simulate_at_each_offset(
1102                &format!("{count} w"),
1103                indoc! {"
1104                    ˇThe quˇickˇ browˇn
1105                    ˇ
1106                    ˇfox ˇjumpsˇ-ˇoˇver
1107                    ˇthe lazy dog
1108                "},
1109            )
1110            .await
1111            .assert_matches();
1112        }
1113    }
1114
1115    #[gpui::test]
1116    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1117        let mut cx = NeovimBackedTestContext::new(cx).await;
1118        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1119            .await
1120            .assert_matches();
1121    }
1122
1123    #[gpui::test]
1124    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1125        let mut cx = NeovimBackedTestContext::new(cx).await;
1126
1127        for count in 1..=3 {
1128            let test_case = indoc! {"
1129                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1130                ˇ    ˇbˇaaˇa ˇbˇbˇb
1131                ˇ
1132                ˇb
1133            "};
1134
1135            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1136                .await
1137                .assert_matches();
1138
1139            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1140                .await
1141                .assert_matches();
1142        }
1143    }
1144
1145    #[gpui::test]
1146    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1147        let mut cx = NeovimBackedTestContext::new(cx).await;
1148        let test_case = indoc! {"
1149            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1150            ˇ    ˇbˇaaˇa ˇbˇbˇb
1151            ˇ•••
1152            ˇb
1153            "
1154        };
1155
1156        for count in 1..=3 {
1157            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1158                .await
1159                .assert_matches();
1160
1161            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1162                .await
1163                .assert_matches();
1164        }
1165    }
1166
1167    #[gpui::test]
1168    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1169        let mut cx = VimTestContext::new(cx, true).await;
1170        cx.update_global(|store: &mut SettingsStore, cx| {
1171            store.update_user_settings::<VimSettings>(cx, |s| {
1172                s.use_multiline_find = Some(true);
1173            });
1174        });
1175
1176        cx.assert_binding(
1177            "f l",
1178            indoc! {"
1179            ˇfunction print() {
1180                console.log('ok')
1181            }
1182            "},
1183            Mode::Normal,
1184            indoc! {"
1185            function print() {
1186                consoˇle.log('ok')
1187            }
1188            "},
1189            Mode::Normal,
1190        );
1191
1192        cx.assert_binding(
1193            "t l",
1194            indoc! {"
1195            ˇfunction print() {
1196                console.log('ok')
1197            }
1198            "},
1199            Mode::Normal,
1200            indoc! {"
1201            function print() {
1202                consˇole.log('ok')
1203            }
1204            "},
1205            Mode::Normal,
1206        );
1207    }
1208
1209    #[gpui::test]
1210    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1211        let mut cx = VimTestContext::new(cx, true).await;
1212        cx.update_global(|store: &mut SettingsStore, cx| {
1213            store.update_user_settings::<VimSettings>(cx, |s| {
1214                s.use_multiline_find = Some(true);
1215            });
1216        });
1217
1218        cx.assert_binding(
1219            "shift-f p",
1220            indoc! {"
1221            function print() {
1222                console.ˇlog('ok')
1223            }
1224            "},
1225            Mode::Normal,
1226            indoc! {"
1227            function ˇprint() {
1228                console.log('ok')
1229            }
1230            "},
1231            Mode::Normal,
1232        );
1233
1234        cx.assert_binding(
1235            "shift-t p",
1236            indoc! {"
1237            function print() {
1238                console.ˇlog('ok')
1239            }
1240            "},
1241            Mode::Normal,
1242            indoc! {"
1243            function pˇrint() {
1244                console.log('ok')
1245            }
1246            "},
1247            Mode::Normal,
1248        );
1249    }
1250
1251    #[gpui::test]
1252    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1253        let mut cx = VimTestContext::new(cx, true).await;
1254        cx.update_global(|store: &mut SettingsStore, cx| {
1255            store.update_user_settings::<VimSettings>(cx, |s| {
1256                s.use_smartcase_find = Some(true);
1257            });
1258        });
1259
1260        cx.assert_binding(
1261            "f p",
1262            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1263            Mode::Normal,
1264            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1265            Mode::Normal,
1266        );
1267
1268        cx.assert_binding(
1269            "shift-f p",
1270            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1271            Mode::Normal,
1272            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1273            Mode::Normal,
1274        );
1275
1276        cx.assert_binding(
1277            "t p",
1278            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1279            Mode::Normal,
1280            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1281            Mode::Normal,
1282        );
1283
1284        cx.assert_binding(
1285            "shift-t p",
1286            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1287            Mode::Normal,
1288            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1289            Mode::Normal,
1290        );
1291    }
1292
1293    #[gpui::test]
1294    async fn test_percent(cx: &mut TestAppContext) {
1295        let mut cx = NeovimBackedTestContext::new(cx).await;
1296        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1297            .await
1298            .assert_matches();
1299        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1300            .await
1301            .assert_matches();
1302        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1303            .await
1304            .assert_matches();
1305    }
1306
1307    #[gpui::test]
1308    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1309        let mut cx = NeovimBackedTestContext::new(cx).await;
1310
1311        // goes to current line end
1312        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1313        cx.simulate_shared_keystrokes("$").await;
1314        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1315
1316        // goes to next line end
1317        cx.simulate_shared_keystrokes("2 $").await;
1318        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1319
1320        // try to exceed the final line.
1321        cx.simulate_shared_keystrokes("4 $").await;
1322        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1323    }
1324
1325    #[gpui::test]
1326    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1327        let mut cx = VimTestContext::new(cx, true).await;
1328        cx.update(|cx| {
1329            cx.bind_keys(vec![
1330                KeyBinding::new(
1331                    "w",
1332                    motion::NextSubwordStart {
1333                        ignore_punctuation: false,
1334                    },
1335                    Some("Editor && VimControl && !VimWaiting && !menu"),
1336                ),
1337                KeyBinding::new(
1338                    "b",
1339                    motion::PreviousSubwordStart {
1340                        ignore_punctuation: false,
1341                    },
1342                    Some("Editor && VimControl && !VimWaiting && !menu"),
1343                ),
1344                KeyBinding::new(
1345                    "e",
1346                    motion::NextSubwordEnd {
1347                        ignore_punctuation: false,
1348                    },
1349                    Some("Editor && VimControl && !VimWaiting && !menu"),
1350                ),
1351                KeyBinding::new(
1352                    "g e",
1353                    motion::PreviousSubwordEnd {
1354                        ignore_punctuation: false,
1355                    },
1356                    Some("Editor && VimControl && !VimWaiting && !menu"),
1357                ),
1358            ]);
1359        });
1360
1361        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1362        // Special case: In 'cw', 'w' acts like 'e'
1363        cx.assert_binding(
1364            "c w",
1365            indoc! {"ˇassert_binding"},
1366            Mode::Normal,
1367            indoc! {"ˇ_binding"},
1368            Mode::Insert,
1369        );
1370
1371        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1372
1373        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1374
1375        cx.assert_binding_normal(
1376            "g e",
1377            indoc! {"assert_bindinˇg"},
1378            indoc! {"asserˇt_binding"},
1379        );
1380    }
1381
1382    #[gpui::test]
1383    async fn test_r(cx: &mut gpui::TestAppContext) {
1384        let mut cx = NeovimBackedTestContext::new(cx).await;
1385
1386        cx.set_shared_state("ˇhello\n").await;
1387        cx.simulate_shared_keystrokes("r -").await;
1388        cx.shared_state().await.assert_eq("ˇ-ello\n");
1389
1390        cx.set_shared_state("ˇhello\n").await;
1391        cx.simulate_shared_keystrokes("3 r -").await;
1392        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1393
1394        cx.set_shared_state("ˇhello\n").await;
1395        cx.simulate_shared_keystrokes("r - 2 l .").await;
1396        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1397
1398        cx.set_shared_state("ˇhello world\n").await;
1399        cx.simulate_shared_keystrokes("2 r - f w .").await;
1400        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1401
1402        cx.set_shared_state("ˇhello world\n").await;
1403        cx.simulate_shared_keystrokes("2 0 r - ").await;
1404        cx.shared_state().await.assert_eq("ˇhello world\n");
1405    }
1406
1407    #[gpui::test]
1408    async fn test_gq(cx: &mut gpui::TestAppContext) {
1409        let mut cx = NeovimBackedTestContext::new(cx).await;
1410        cx.set_neovim_option("textwidth=5").await;
1411
1412        cx.update(|cx| {
1413            SettingsStore::update_global(cx, |settings, cx| {
1414                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1415                    settings.defaults.preferred_line_length = Some(5);
1416                });
1417            })
1418        });
1419
1420        cx.set_shared_state("ˇth th th th th th\n").await;
1421        cx.simulate_shared_keystrokes("g q q").await;
1422        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1423
1424        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1425            .await;
1426        cx.simulate_shared_keystrokes("v j g q").await;
1427        cx.shared_state()
1428            .await
1429            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1430    }
1431
1432    #[gpui::test]
1433    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1434        let mut cx = NeovimBackedTestContext::new(cx).await;
1435        cx.set_neovim_option("filetype=rust").await;
1436
1437        cx.set_shared_state("// helloˇ\n").await;
1438        cx.simulate_shared_keystrokes("o").await;
1439        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1440        cx.simulate_shared_keystrokes("x escape shift-o").await;
1441        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1442    }
1443}