normal.rs

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