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