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