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