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