normal.rs

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