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