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