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