normal.rs

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