normal.rs

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