normal.rs

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