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    ]
  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
 494                let mut edits = Vec::new();
 495                for selection in display_selections {
 496                    let mut range = selection.range();
 497                    for _ in 0..count {
 498                        let new_point = movement::saturating_right(&map, range.end);
 499                        if range.end == new_point {
 500                            return;
 501                        }
 502                        range.end = new_point;
 503                    }
 504
 505                    edits.push((
 506                        range.start.to_offset(&map, Bias::Left)
 507                            ..range.end.to_offset(&map, Bias::Left),
 508                        text.repeat(count),
 509                    ))
 510                }
 511
 512                editor.buffer().update(cx, |buffer, cx| {
 513                    buffer.edit(edits, None, cx);
 514                });
 515                editor.set_clip_at_line_ends(true, cx);
 516                editor.change_selections(None, cx, |s| {
 517                    s.move_with(|map, selection| {
 518                        let point = movement::saturating_left(map, selection.head());
 519                        selection.collapse_to(point, SelectionGoal::None)
 520                    });
 521                });
 522            });
 523        });
 524        vim.pop_operator(cx)
 525    });
 526}
 527
 528#[cfg(test)]
 529mod test {
 530    use gpui::{KeyBinding, TestAppContext};
 531    use indoc::indoc;
 532    use settings::SettingsStore;
 533
 534    use crate::{
 535        motion,
 536        state::Mode::{self},
 537        test::{NeovimBackedTestContext, VimTestContext},
 538        VimSettings,
 539    };
 540
 541    #[gpui::test]
 542    async fn test_h(cx: &mut gpui::TestAppContext) {
 543        let mut cx = NeovimBackedTestContext::new(cx).await;
 544        cx.simulate_at_each_offset(
 545            "h",
 546            indoc! {"
 547            ˇThe qˇuick
 548            ˇbrown"
 549            },
 550        )
 551        .await
 552        .assert_matches();
 553    }
 554
 555    #[gpui::test]
 556    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 557        let mut cx = NeovimBackedTestContext::new(cx).await;
 558        cx.simulate_at_each_offset(
 559            "backspace",
 560            indoc! {"
 561            ˇThe qˇuick
 562            ˇbrown"
 563            },
 564        )
 565        .await
 566        .assert_matches();
 567    }
 568
 569    #[gpui::test]
 570    async fn test_j(cx: &mut gpui::TestAppContext) {
 571        let mut cx = NeovimBackedTestContext::new(cx).await;
 572
 573        cx.set_shared_state(indoc! {"
 574            aaˇaa
 575            😃😃"
 576        })
 577        .await;
 578        cx.simulate_shared_keystrokes("j").await;
 579        cx.shared_state().await.assert_eq(indoc! {"
 580            aaaa
 581            😃ˇ😃"
 582        });
 583
 584        cx.simulate_at_each_offset(
 585            "j",
 586            indoc! {"
 587                ˇThe qˇuick broˇwn
 588                ˇfox jumps"
 589            },
 590        )
 591        .await
 592        .assert_matches();
 593    }
 594
 595    #[gpui::test]
 596    async fn test_enter(cx: &mut gpui::TestAppContext) {
 597        let mut cx = NeovimBackedTestContext::new(cx).await;
 598        cx.simulate_at_each_offset(
 599            "enter",
 600            indoc! {"
 601            ˇThe qˇuick broˇwn
 602            ˇfox jumps"
 603            },
 604        )
 605        .await
 606        .assert_matches();
 607    }
 608
 609    #[gpui::test]
 610    async fn test_k(cx: &mut gpui::TestAppContext) {
 611        let mut cx = NeovimBackedTestContext::new(cx).await;
 612        cx.simulate_at_each_offset(
 613            "k",
 614            indoc! {"
 615            ˇThe qˇuick
 616            ˇbrown fˇox jumˇps"
 617            },
 618        )
 619        .await
 620        .assert_matches();
 621    }
 622
 623    #[gpui::test]
 624    async fn test_l(cx: &mut gpui::TestAppContext) {
 625        let mut cx = NeovimBackedTestContext::new(cx).await;
 626        cx.simulate_at_each_offset(
 627            "l",
 628            indoc! {"
 629            ˇThe qˇuicˇk
 630            ˇbrowˇn"},
 631        )
 632        .await
 633        .assert_matches();
 634    }
 635
 636    #[gpui::test]
 637    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 638        let mut cx = NeovimBackedTestContext::new(cx).await;
 639        cx.simulate_at_each_offset(
 640            "$",
 641            indoc! {"
 642            ˇThe qˇuicˇk
 643            ˇbrowˇn"},
 644        )
 645        .await
 646        .assert_matches();
 647        cx.simulate_at_each_offset(
 648            "0",
 649            indoc! {"
 650                ˇThe qˇuicˇk
 651                ˇbrowˇn"},
 652        )
 653        .await
 654        .assert_matches();
 655    }
 656
 657    #[gpui::test]
 658    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 659        let mut cx = NeovimBackedTestContext::new(cx).await;
 660
 661        cx.simulate_at_each_offset(
 662            "shift-g",
 663            indoc! {"
 664                The ˇquick
 665
 666                brown fox jumps
 667                overˇ the lazy doˇg"},
 668        )
 669        .await
 670        .assert_matches();
 671        cx.simulate(
 672            "shift-g",
 673            indoc! {"
 674            The quiˇck
 675
 676            brown"},
 677        )
 678        .await
 679        .assert_matches();
 680        cx.simulate(
 681            "shift-g",
 682            indoc! {"
 683            The quiˇck
 684
 685            "},
 686        )
 687        .await
 688        .assert_matches();
 689    }
 690
 691    #[gpui::test]
 692    async fn test_w(cx: &mut gpui::TestAppContext) {
 693        let mut cx = NeovimBackedTestContext::new(cx).await;
 694        cx.simulate_at_each_offset(
 695            "w",
 696            indoc! {"
 697            The ˇquickˇ-ˇbrown
 698            ˇ
 699            ˇ
 700            ˇfox_jumps ˇover
 701            ˇthˇe"},
 702        )
 703        .await
 704        .assert_matches();
 705        cx.simulate_at_each_offset(
 706            "shift-w",
 707            indoc! {"
 708            The ˇquickˇ-ˇbrown
 709            ˇ
 710            ˇ
 711            ˇfox_jumps ˇover
 712            ˇthˇe"},
 713        )
 714        .await
 715        .assert_matches();
 716    }
 717
 718    #[gpui::test]
 719    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 720        let mut cx = NeovimBackedTestContext::new(cx).await;
 721        cx.simulate_at_each_offset(
 722            "e",
 723            indoc! {"
 724            Thˇe quicˇkˇ-browˇn
 725
 726
 727            fox_jumpˇs oveˇr
 728            thˇe"},
 729        )
 730        .await
 731        .assert_matches();
 732        cx.simulate_at_each_offset(
 733            "shift-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    }
 744
 745    #[gpui::test]
 746    async fn test_b(cx: &mut gpui::TestAppContext) {
 747        let mut cx = NeovimBackedTestContext::new(cx).await;
 748        cx.simulate_at_each_offset(
 749            "b",
 750            indoc! {"
 751            ˇThe ˇquickˇ-ˇbrown
 752            ˇ
 753            ˇ
 754            ˇfox_jumps ˇover
 755            ˇthe"},
 756        )
 757        .await
 758        .assert_matches();
 759        cx.simulate_at_each_offset(
 760            "shift-b",
 761            indoc! {"
 762            ˇThe ˇquickˇ-ˇbrown
 763            ˇ
 764            ˇ
 765            ˇfox_jumps ˇover
 766            ˇthe"},
 767        )
 768        .await
 769        .assert_matches();
 770    }
 771
 772    #[gpui::test]
 773    async fn test_gg(cx: &mut gpui::TestAppContext) {
 774        let mut cx = NeovimBackedTestContext::new(cx).await;
 775        cx.simulate_at_each_offset(
 776            "g g",
 777            indoc! {"
 778                The qˇuick
 779
 780                brown fox jumps
 781                over ˇthe laˇzy dog"},
 782        )
 783        .await
 784        .assert_matches();
 785        cx.simulate(
 786            "g g",
 787            indoc! {"
 788
 789
 790                brown fox jumps
 791                over the laˇzy dog"},
 792        )
 793        .await
 794        .assert_matches();
 795        cx.simulate(
 796            "2 g g",
 797            indoc! {"
 798                ˇ
 799
 800                brown fox jumps
 801                over the lazydog"},
 802        )
 803        .await
 804        .assert_matches();
 805    }
 806
 807    #[gpui::test]
 808    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 809        let mut cx = NeovimBackedTestContext::new(cx).await;
 810        cx.simulate_at_each_offset(
 811            "shift-g",
 812            indoc! {"
 813                The qˇuick
 814
 815                brown fox jumps
 816                over ˇthe laˇzy dog"},
 817        )
 818        .await
 819        .assert_matches();
 820        cx.simulate(
 821            "shift-g",
 822            indoc! {"
 823
 824
 825                brown fox jumps
 826                over the laˇzy dog"},
 827        )
 828        .await
 829        .assert_matches();
 830        cx.simulate(
 831            "2 shift-g",
 832            indoc! {"
 833                ˇ
 834
 835                brown fox jumps
 836                over the lazydog"},
 837        )
 838        .await
 839        .assert_matches();
 840    }
 841
 842    #[gpui::test]
 843    async fn test_a(cx: &mut gpui::TestAppContext) {
 844        let mut cx = NeovimBackedTestContext::new(cx).await;
 845        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 846            .await
 847            .assert_matches();
 848    }
 849
 850    #[gpui::test]
 851    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 852        let mut cx = NeovimBackedTestContext::new(cx).await;
 853        cx.simulate_at_each_offset(
 854            "shift-a",
 855            indoc! {"
 856            ˇ
 857            The qˇuick
 858            brown ˇfox "},
 859        )
 860        .await
 861        .assert_matches();
 862    }
 863
 864    #[gpui::test]
 865    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 866        let mut cx = NeovimBackedTestContext::new(cx).await;
 867        cx.simulate("^", "The qˇuick").await.assert_matches();
 868        cx.simulate("^", " The qˇuick").await.assert_matches();
 869        cx.simulate("^", "ˇ").await.assert_matches();
 870        cx.simulate(
 871            "^",
 872            indoc! {"
 873                The qˇuick
 874                brown fox"},
 875        )
 876        .await
 877        .assert_matches();
 878        cx.simulate(
 879            "^",
 880            indoc! {"
 881                ˇ
 882                The quick"},
 883        )
 884        .await
 885        .assert_matches();
 886        // Indoc disallows trailing whitespace.
 887        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
 888    }
 889
 890    #[gpui::test]
 891    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 892        let mut cx = NeovimBackedTestContext::new(cx).await;
 893        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
 894        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
 895        cx.simulate("shift-i", "ˇ").await.assert_matches();
 896        cx.simulate(
 897            "shift-i",
 898            indoc! {"
 899                The qˇuick
 900                brown fox"},
 901        )
 902        .await
 903        .assert_matches();
 904        cx.simulate(
 905            "shift-i",
 906            indoc! {"
 907                ˇ
 908                The quick"},
 909        )
 910        .await
 911        .assert_matches();
 912    }
 913
 914    #[gpui::test]
 915    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 916        let mut cx = NeovimBackedTestContext::new(cx).await;
 917        cx.simulate(
 918            "shift-d",
 919            indoc! {"
 920                The qˇuick
 921                brown fox"},
 922        )
 923        .await
 924        .assert_matches();
 925        cx.simulate(
 926            "shift-d",
 927            indoc! {"
 928                The quick
 929                ˇ
 930                brown fox"},
 931        )
 932        .await
 933        .assert_matches();
 934    }
 935
 936    #[gpui::test]
 937    async fn test_x(cx: &mut gpui::TestAppContext) {
 938        let mut cx = NeovimBackedTestContext::new(cx).await;
 939        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
 940            .await
 941            .assert_matches();
 942        cx.simulate(
 943            "x",
 944            indoc! {"
 945                Tesˇt
 946                test"},
 947        )
 948        .await
 949        .assert_matches();
 950    }
 951
 952    #[gpui::test]
 953    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 954        let mut cx = NeovimBackedTestContext::new(cx).await;
 955        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
 956            .await
 957            .assert_matches();
 958        cx.simulate(
 959            "shift-x",
 960            indoc! {"
 961                Test
 962                ˇtest"},
 963        )
 964        .await
 965        .assert_matches();
 966    }
 967
 968    #[gpui::test]
 969    async fn test_o(cx: &mut gpui::TestAppContext) {
 970        let mut cx = NeovimBackedTestContext::new(cx).await;
 971        cx.simulate("o", "ˇ").await.assert_matches();
 972        cx.simulate("o", "The ˇquick").await.assert_matches();
 973        cx.simulate_at_each_offset(
 974            "o",
 975            indoc! {"
 976                The qˇuick
 977                brown ˇfox
 978                jumps ˇover"},
 979        )
 980        .await
 981        .assert_matches();
 982        cx.simulate(
 983            "o",
 984            indoc! {"
 985                The quick
 986                ˇ
 987                brown fox"},
 988        )
 989        .await
 990        .assert_matches();
 991
 992        cx.assert_binding(
 993            "o",
 994            indoc! {"
 995                fn test() {
 996                    println!(ˇ);
 997                }"},
 998            Mode::Normal,
 999            indoc! {"
1000                fn test() {
1001                    println!();
1002                    ˇ
1003                }"},
1004            Mode::Insert,
1005        );
1006
1007        cx.assert_binding(
1008            "o",
1009            indoc! {"
1010                fn test(ˇ) {
1011                    println!();
1012                }"},
1013            Mode::Normal,
1014            indoc! {"
1015                fn test() {
1016                    ˇ
1017                    println!();
1018                }"},
1019            Mode::Insert,
1020        );
1021    }
1022
1023    #[gpui::test]
1024    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1025        let mut cx = NeovimBackedTestContext::new(cx).await;
1026        cx.simulate("shift-o", "ˇ").await.assert_matches();
1027        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1028        cx.simulate_at_each_offset(
1029            "shift-o",
1030            indoc! {"
1031            The qˇuick
1032            brown ˇfox
1033            jumps ˇover"},
1034        )
1035        .await
1036        .assert_matches();
1037        cx.simulate(
1038            "shift-o",
1039            indoc! {"
1040            The quick
1041            ˇ
1042            brown fox"},
1043        )
1044        .await
1045        .assert_matches();
1046
1047        // Our indentation is smarter than vims. So we don't match here
1048        cx.assert_binding(
1049            "shift-o",
1050            indoc! {"
1051                fn test() {
1052                    println!(ˇ);
1053                }"},
1054            Mode::Normal,
1055            indoc! {"
1056                fn test() {
1057                    ˇ
1058                    println!();
1059                }"},
1060            Mode::Insert,
1061        );
1062        cx.assert_binding(
1063            "shift-o",
1064            indoc! {"
1065                fn test(ˇ) {
1066                    println!();
1067                }"},
1068            Mode::Normal,
1069            indoc! {"
1070                ˇ
1071                fn test() {
1072                    println!();
1073                }"},
1074            Mode::Insert,
1075        );
1076    }
1077
1078    #[gpui::test]
1079    async fn test_dd(cx: &mut gpui::TestAppContext) {
1080        let mut cx = NeovimBackedTestContext::new(cx).await;
1081        cx.simulate("d d", "ˇ").await.assert_matches();
1082        cx.simulate("d d", "The ˇquick").await.assert_matches();
1083        cx.simulate_at_each_offset(
1084            "d d",
1085            indoc! {"
1086            The qˇuick
1087            brown ˇfox
1088            jumps ˇover"},
1089        )
1090        .await
1091        .assert_matches();
1092        cx.simulate(
1093            "d d",
1094            indoc! {"
1095                The quick
1096                ˇ
1097                brown fox"},
1098        )
1099        .await
1100        .assert_matches();
1101    }
1102
1103    #[gpui::test]
1104    async fn test_cc(cx: &mut gpui::TestAppContext) {
1105        let mut cx = NeovimBackedTestContext::new(cx).await;
1106        cx.simulate("c c", "ˇ").await.assert_matches();
1107        cx.simulate("c c", "The ˇquick").await.assert_matches();
1108        cx.simulate_at_each_offset(
1109            "c c",
1110            indoc! {"
1111                The quˇick
1112                brown ˇfox
1113                jumps ˇover"},
1114        )
1115        .await
1116        .assert_matches();
1117        cx.simulate(
1118            "c c",
1119            indoc! {"
1120                The quick
1121                ˇ
1122                brown fox"},
1123        )
1124        .await
1125        .assert_matches();
1126    }
1127
1128    #[gpui::test]
1129    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1130        let mut cx = NeovimBackedTestContext::new(cx).await;
1131
1132        for count in 1..=5 {
1133            cx.simulate_at_each_offset(
1134                &format!("{count} w"),
1135                indoc! {"
1136                    ˇThe quˇickˇ browˇn
1137                    ˇ
1138                    ˇfox ˇjumpsˇ-ˇoˇver
1139                    ˇthe lazy dog
1140                "},
1141            )
1142            .await
1143            .assert_matches();
1144        }
1145    }
1146
1147    #[gpui::test]
1148    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1149        let mut cx = NeovimBackedTestContext::new(cx).await;
1150        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1151            .await
1152            .assert_matches();
1153    }
1154
1155    #[gpui::test]
1156    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1157        let mut cx = NeovimBackedTestContext::new(cx).await;
1158
1159        for count in 1..=3 {
1160            let test_case = indoc! {"
1161                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1162                ˇ    ˇbˇaaˇa ˇbˇbˇb
1163                ˇ
1164                ˇb
1165            "};
1166
1167            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1168                .await
1169                .assert_matches();
1170
1171            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1172                .await
1173                .assert_matches();
1174        }
1175    }
1176
1177    #[gpui::test]
1178    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1179        let mut cx = NeovimBackedTestContext::new(cx).await;
1180        let test_case = indoc! {"
1181            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1182            ˇ    ˇbˇaaˇa ˇbˇbˇb
1183            ˇ•••
1184            ˇb
1185            "
1186        };
1187
1188        for count in 1..=3 {
1189            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1190                .await
1191                .assert_matches();
1192
1193            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1194                .await
1195                .assert_matches();
1196        }
1197    }
1198
1199    #[gpui::test]
1200    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1201        let mut cx = VimTestContext::new(cx, true).await;
1202        cx.update_global(|store: &mut SettingsStore, cx| {
1203            store.update_user_settings::<VimSettings>(cx, |s| {
1204                s.use_multiline_find = Some(true);
1205            });
1206        });
1207
1208        cx.assert_binding(
1209            "f l",
1210            indoc! {"
1211            ˇfunction print() {
1212                console.log('ok')
1213            }
1214            "},
1215            Mode::Normal,
1216            indoc! {"
1217            function print() {
1218                consoˇle.log('ok')
1219            }
1220            "},
1221            Mode::Normal,
1222        );
1223
1224        cx.assert_binding(
1225            "t l",
1226            indoc! {"
1227            ˇfunction print() {
1228                console.log('ok')
1229            }
1230            "},
1231            Mode::Normal,
1232            indoc! {"
1233            function print() {
1234                consˇole.log('ok')
1235            }
1236            "},
1237            Mode::Normal,
1238        );
1239    }
1240
1241    #[gpui::test]
1242    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1243        let mut cx = VimTestContext::new(cx, true).await;
1244        cx.update_global(|store: &mut SettingsStore, cx| {
1245            store.update_user_settings::<VimSettings>(cx, |s| {
1246                s.use_multiline_find = Some(true);
1247            });
1248        });
1249
1250        cx.assert_binding(
1251            "shift-f p",
1252            indoc! {"
1253            function print() {
1254                console.ˇlog('ok')
1255            }
1256            "},
1257            Mode::Normal,
1258            indoc! {"
1259            function ˇprint() {
1260                console.log('ok')
1261            }
1262            "},
1263            Mode::Normal,
1264        );
1265
1266        cx.assert_binding(
1267            "shift-t p",
1268            indoc! {"
1269            function print() {
1270                console.ˇlog('ok')
1271            }
1272            "},
1273            Mode::Normal,
1274            indoc! {"
1275            function pˇrint() {
1276                console.log('ok')
1277            }
1278            "},
1279            Mode::Normal,
1280        );
1281    }
1282
1283    #[gpui::test]
1284    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1285        let mut cx = VimTestContext::new(cx, true).await;
1286        cx.update_global(|store: &mut SettingsStore, cx| {
1287            store.update_user_settings::<VimSettings>(cx, |s| {
1288                s.use_smartcase_find = Some(true);
1289            });
1290        });
1291
1292        cx.assert_binding(
1293            "f p",
1294            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1295            Mode::Normal,
1296            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1297            Mode::Normal,
1298        );
1299
1300        cx.assert_binding(
1301            "shift-f p",
1302            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1303            Mode::Normal,
1304            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1305            Mode::Normal,
1306        );
1307
1308        cx.assert_binding(
1309            "t p",
1310            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1311            Mode::Normal,
1312            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1313            Mode::Normal,
1314        );
1315
1316        cx.assert_binding(
1317            "shift-t p",
1318            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1319            Mode::Normal,
1320            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1321            Mode::Normal,
1322        );
1323    }
1324
1325    #[gpui::test]
1326    async fn test_percent(cx: &mut TestAppContext) {
1327        let mut cx = NeovimBackedTestContext::new(cx).await;
1328        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1329            .await
1330            .assert_matches();
1331        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1332            .await
1333            .assert_matches();
1334        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1335            .await
1336            .assert_matches();
1337    }
1338
1339    #[gpui::test]
1340    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1341        let mut cx = NeovimBackedTestContext::new(cx).await;
1342
1343        // goes to current line end
1344        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1345        cx.simulate_shared_keystrokes("$").await;
1346        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1347
1348        // goes to next line end
1349        cx.simulate_shared_keystrokes("2 $").await;
1350        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1351
1352        // try to exceed the final line.
1353        cx.simulate_shared_keystrokes("4 $").await;
1354        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1355    }
1356
1357    #[gpui::test]
1358    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1359        let mut cx = VimTestContext::new(cx, true).await;
1360        cx.update(|cx| {
1361            cx.bind_keys(vec![
1362                KeyBinding::new(
1363                    "w",
1364                    motion::NextSubwordStart {
1365                        ignore_punctuation: false,
1366                    },
1367                    Some("Editor && VimControl && !VimWaiting && !menu"),
1368                ),
1369                KeyBinding::new(
1370                    "b",
1371                    motion::PreviousSubwordStart {
1372                        ignore_punctuation: false,
1373                    },
1374                    Some("Editor && VimControl && !VimWaiting && !menu"),
1375                ),
1376                KeyBinding::new(
1377                    "e",
1378                    motion::NextSubwordEnd {
1379                        ignore_punctuation: false,
1380                    },
1381                    Some("Editor && VimControl && !VimWaiting && !menu"),
1382                ),
1383                KeyBinding::new(
1384                    "g e",
1385                    motion::PreviousSubwordEnd {
1386                        ignore_punctuation: false,
1387                    },
1388                    Some("Editor && VimControl && !VimWaiting && !menu"),
1389                ),
1390            ]);
1391        });
1392
1393        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1394        // Special case: In 'cw', 'w' acts like 'e'
1395        cx.assert_binding(
1396            "c w",
1397            indoc! {"ˇassert_binding"},
1398            Mode::Normal,
1399            indoc! {"ˇ_binding"},
1400            Mode::Insert,
1401        );
1402
1403        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1404
1405        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1406
1407        cx.assert_binding_normal(
1408            "g e",
1409            indoc! {"assert_bindinˇg"},
1410            indoc! {"asserˇt_binding"},
1411        );
1412    }
1413
1414    #[gpui::test]
1415    async fn test_r(cx: &mut gpui::TestAppContext) {
1416        let mut cx = NeovimBackedTestContext::new(cx).await;
1417
1418        cx.set_shared_state("ˇhello\n").await;
1419        cx.simulate_shared_keystrokes("r -").await;
1420        cx.shared_state().await.assert_eq("ˇ-ello\n");
1421
1422        cx.set_shared_state("ˇhello\n").await;
1423        cx.simulate_shared_keystrokes("3 r -").await;
1424        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1425
1426        cx.set_shared_state("ˇhello\n").await;
1427        cx.simulate_shared_keystrokes("r - 2 l .").await;
1428        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1429
1430        cx.set_shared_state("ˇhello world\n").await;
1431        cx.simulate_shared_keystrokes("2 r - f w .").await;
1432        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1433
1434        cx.set_shared_state("ˇhello world\n").await;
1435        cx.simulate_shared_keystrokes("2 0 r - ").await;
1436        cx.shared_state().await.assert_eq("ˇhello world\n");
1437    }
1438}