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::HashSet;
  21use editor::scroll::Autoscroll;
  22use editor::{Bias, DisplayPoint};
  23use gpui::{actions, ViewContext, WindowContext};
  24use language::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 (map, old_selections) = editor.selections.all_display(cx);
 287                let selection_start_rows: HashSet<u32> = old_selections
 288                    .into_iter()
 289                    .map(|selection| selection.start.row())
 290                    .collect();
 291                let edits = selection_start_rows.into_iter().map(|row| {
 292                    let (indent, _) = map.line_indent(row);
 293                    let start_of_line =
 294                        motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
 295                            .to_point(&map);
 296                    let mut new_text = " ".repeat(indent as usize);
 297                    new_text.push('\n');
 298                    (start_of_line..start_of_line, new_text)
 299                });
 300                editor.edit_with_autoindent(edits, cx);
 301                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 302                    s.move_cursors_with(|map, cursor, _| {
 303                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 304                        let insert_point = motion::end_of_line(map, false, previous_line);
 305                        (insert_point, SelectionGoal::None)
 306                    });
 307                });
 308            });
 309        });
 310    });
 311}
 312
 313fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, 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            let text_layout_details = editor.text_layout_details(cx);
 319            editor.transact(cx, |editor, cx| {
 320                let (map, old_selections) = editor.selections.all_display(cx);
 321
 322                let selection_end_rows: HashSet<u32> = old_selections
 323                    .into_iter()
 324                    .map(|selection| selection.end.row())
 325                    .collect();
 326                let edits = selection_end_rows.into_iter().map(|row| {
 327                    let (indent, _) = map.line_indent(row);
 328                    let end_of_line =
 329                        motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
 330
 331                    let mut new_text = "\n".to_string();
 332                    new_text.push_str(&" ".repeat(indent as usize));
 333                    (end_of_line..end_of_line, new_text)
 334                });
 335                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 336                    s.maybe_move_cursors_with(|map, cursor, goal| {
 337                        Motion::CurrentLine.move_point(
 338                            map,
 339                            cursor,
 340                            goal,
 341                            None,
 342                            &text_layout_details,
 343                        )
 344                    });
 345                });
 346                editor.edit_with_autoindent(edits, cx);
 347            });
 348        });
 349    });
 350}
 351
 352fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
 353    Vim::update(cx, |vim, cx| {
 354        let count = vim.take_count(cx);
 355        yank_motion(vim, motion::Motion::CurrentLine, count, cx)
 356    })
 357}
 358
 359pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
 360    Vim::update(cx, |vim, cx| {
 361        vim.stop_recording();
 362        vim.update_active_editor(cx, |_, editor, cx| {
 363            editor.transact(cx, |editor, cx| {
 364                editor.set_clip_at_line_ends(false, cx);
 365                let (map, display_selections) = editor.selections.all_display(cx);
 366                // Selections are biased right at the start. So we need to store
 367                // anchors that are biased left so that we can restore the selections
 368                // after the change
 369                let stable_anchors = editor
 370                    .selections
 371                    .disjoint_anchors()
 372                    .into_iter()
 373                    .map(|selection| {
 374                        let start = selection.start.bias_left(&map.buffer_snapshot);
 375                        start..start
 376                    })
 377                    .collect::<Vec<_>>();
 378
 379                let edits = display_selections
 380                    .into_iter()
 381                    .map(|selection| {
 382                        let mut range = selection.range();
 383                        *range.end.column_mut() += 1;
 384                        range.end = map.clip_point(range.end, Bias::Right);
 385
 386                        (
 387                            range.start.to_offset(&map, Bias::Left)
 388                                ..range.end.to_offset(&map, Bias::Left),
 389                            text.clone(),
 390                        )
 391                    })
 392                    .collect::<Vec<_>>();
 393
 394                editor.buffer().update(cx, |buffer, cx| {
 395                    buffer.edit(edits, None, cx);
 396                });
 397                editor.set_clip_at_line_ends(true, cx);
 398                editor.change_selections(None, cx, |s| {
 399                    s.select_anchor_ranges(stable_anchors);
 400                });
 401            });
 402        });
 403        vim.pop_operator(cx)
 404    });
 405}
 406
 407#[cfg(test)]
 408mod test {
 409    use gpui::TestAppContext;
 410    use indoc::indoc;
 411    use settings::SettingsStore;
 412
 413    use crate::{
 414        state::Mode::{self},
 415        test::{NeovimBackedTestContext, VimTestContext},
 416        VimSettings,
 417    };
 418
 419    #[gpui::test]
 420    async fn test_h(cx: &mut gpui::TestAppContext) {
 421        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 422        cx.assert_all(indoc! {"
 423            ˇThe qˇuick
 424            ˇbrown"
 425        })
 426        .await;
 427    }
 428
 429    #[gpui::test]
 430    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 431        let mut cx = NeovimBackedTestContext::new(cx)
 432            .await
 433            .binding(["backspace"]);
 434        cx.assert_all(indoc! {"
 435            ˇThe qˇuick
 436            ˇbrown"
 437        })
 438        .await;
 439    }
 440
 441    #[gpui::test]
 442    async fn test_j(cx: &mut gpui::TestAppContext) {
 443        let mut cx = NeovimBackedTestContext::new(cx).await;
 444
 445        cx.set_shared_state(indoc! {"
 446                    aaˇaa
 447                    😃😃"
 448        })
 449        .await;
 450        cx.simulate_shared_keystrokes(["j"]).await;
 451        cx.assert_shared_state(indoc! {"
 452                    aaaa
 453                    😃ˇ😃"
 454        })
 455        .await;
 456
 457        for marked_position in cx.each_marked_position(indoc! {"
 458                    ˇThe qˇuick broˇwn
 459                    ˇfox jumps"
 460        }) {
 461            cx.assert_neovim_compatible(&marked_position, ["j"]).await;
 462        }
 463    }
 464
 465    #[gpui::test]
 466    async fn test_enter(cx: &mut gpui::TestAppContext) {
 467        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
 468        cx.assert_all(indoc! {"
 469            ˇThe qˇuick broˇwn
 470            ˇfox jumps"
 471        })
 472        .await;
 473    }
 474
 475    #[gpui::test]
 476    async fn test_k(cx: &mut gpui::TestAppContext) {
 477        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
 478        cx.assert_all(indoc! {"
 479            ˇThe qˇuick
 480            ˇbrown fˇox jumˇps"
 481        })
 482        .await;
 483    }
 484
 485    #[gpui::test]
 486    async fn test_l(cx: &mut gpui::TestAppContext) {
 487        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
 488        cx.assert_all(indoc! {"
 489            ˇThe qˇuicˇk
 490            ˇbrowˇn"})
 491            .await;
 492    }
 493
 494    #[gpui::test]
 495    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 496        let mut cx = NeovimBackedTestContext::new(cx).await;
 497        cx.assert_binding_matches_all(
 498            ["$"],
 499            indoc! {"
 500            ˇThe qˇuicˇk
 501            ˇbrowˇn"},
 502        )
 503        .await;
 504        cx.assert_binding_matches_all(
 505            ["0"],
 506            indoc! {"
 507                ˇThe qˇuicˇk
 508                ˇbrowˇn"},
 509        )
 510        .await;
 511    }
 512
 513    #[gpui::test]
 514    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 515        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
 516
 517        cx.assert_all(indoc! {"
 518                The ˇquick
 519
 520                brown fox jumps
 521                overˇ the lazy doˇg"})
 522            .await;
 523        cx.assert(indoc! {"
 524            The quiˇck
 525
 526            brown"})
 527            .await;
 528        cx.assert(indoc! {"
 529            The quiˇck
 530
 531            "})
 532            .await;
 533    }
 534
 535    #[gpui::test]
 536    async fn test_w(cx: &mut gpui::TestAppContext) {
 537        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
 538        cx.assert_all(indoc! {"
 539            The ˇquickˇ-ˇbrown
 540            ˇ
 541            ˇ
 542            ˇfox_jumps ˇover
 543            ˇthˇe"})
 544            .await;
 545        let mut cx = cx.binding(["shift-w"]);
 546        cx.assert_all(indoc! {"
 547            The ˇquickˇ-ˇbrown
 548            ˇ
 549            ˇ
 550            ˇfox_jumps ˇover
 551            ˇthˇe"})
 552            .await;
 553    }
 554
 555    #[gpui::test]
 556    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 557        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
 558        cx.assert_all(indoc! {"
 559            Thˇe quicˇkˇ-browˇn
 560
 561
 562            fox_jumpˇs oveˇr
 563            thˇe"})
 564            .await;
 565        let mut cx = cx.binding(["shift-e"]);
 566        cx.assert_all(indoc! {"
 567            Thˇe quicˇkˇ-browˇn
 568
 569
 570            fox_jumpˇs oveˇr
 571            thˇe"})
 572            .await;
 573    }
 574
 575    #[gpui::test]
 576    async fn test_b(cx: &mut gpui::TestAppContext) {
 577        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
 578        cx.assert_all(indoc! {"
 579            ˇThe ˇquickˇ-ˇbrown
 580            ˇ
 581            ˇ
 582            ˇfox_jumps ˇover
 583            ˇthe"})
 584            .await;
 585        let mut cx = cx.binding(["shift-b"]);
 586        cx.assert_all(indoc! {"
 587            ˇThe ˇquickˇ-ˇbrown
 588            ˇ
 589            ˇ
 590            ˇfox_jumps ˇover
 591            ˇthe"})
 592            .await;
 593    }
 594
 595    #[gpui::test]
 596    async fn test_gg(cx: &mut gpui::TestAppContext) {
 597        let mut cx = NeovimBackedTestContext::new(cx).await;
 598        cx.assert_binding_matches_all(
 599            ["g", "g"],
 600            indoc! {"
 601                The qˇuick
 602
 603                brown fox jumps
 604                over ˇthe laˇzy dog"},
 605        )
 606        .await;
 607        cx.assert_binding_matches(
 608            ["g", "g"],
 609            indoc! {"
 610
 611
 612                brown fox jumps
 613                over the laˇzy dog"},
 614        )
 615        .await;
 616        cx.assert_binding_matches(
 617            ["2", "g", "g"],
 618            indoc! {"
 619                ˇ
 620
 621                brown fox jumps
 622                over the lazydog"},
 623        )
 624        .await;
 625    }
 626
 627    #[gpui::test]
 628    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 629        let mut cx = NeovimBackedTestContext::new(cx).await;
 630        cx.assert_binding_matches_all(
 631            ["shift-g"],
 632            indoc! {"
 633                The qˇuick
 634
 635                brown fox jumps
 636                over ˇthe laˇzy dog"},
 637        )
 638        .await;
 639        cx.assert_binding_matches(
 640            ["shift-g"],
 641            indoc! {"
 642
 643
 644                brown fox jumps
 645                over the laˇzy dog"},
 646        )
 647        .await;
 648        cx.assert_binding_matches(
 649            ["2", "shift-g"],
 650            indoc! {"
 651                ˇ
 652
 653                brown fox jumps
 654                over the lazydog"},
 655        )
 656        .await;
 657    }
 658
 659    #[gpui::test]
 660    async fn test_a(cx: &mut gpui::TestAppContext) {
 661        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
 662        cx.assert_all("The qˇuicˇk").await;
 663    }
 664
 665    #[gpui::test]
 666    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 667        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
 668        cx.assert_all(indoc! {"
 669            ˇ
 670            The qˇuick
 671            brown ˇfox "})
 672            .await;
 673    }
 674
 675    #[gpui::test]
 676    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 677        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
 678        cx.assert("The qˇuick").await;
 679        cx.assert(" The qˇuick").await;
 680        cx.assert("ˇ").await;
 681        cx.assert(indoc! {"
 682                The qˇuick
 683                brown fox"})
 684            .await;
 685        cx.assert(indoc! {"
 686                ˇ
 687                The quick"})
 688            .await;
 689        // Indoc disallows trailing whitespace.
 690        cx.assert("   ˇ \nThe quick").await;
 691    }
 692
 693    #[gpui::test]
 694    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 695        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
 696        cx.assert("The qˇuick").await;
 697        cx.assert(" The qˇuick").await;
 698        cx.assert("ˇ").await;
 699        cx.assert(indoc! {"
 700                The qˇuick
 701                brown fox"})
 702            .await;
 703        cx.assert(indoc! {"
 704                ˇ
 705                The quick"})
 706            .await;
 707    }
 708
 709    #[gpui::test]
 710    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 711        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
 712        cx.assert(indoc! {"
 713                The qˇuick
 714                brown fox"})
 715            .await;
 716        cx.assert(indoc! {"
 717                The quick
 718                ˇ
 719                brown fox"})
 720            .await;
 721    }
 722
 723    #[gpui::test]
 724    async fn test_x(cx: &mut gpui::TestAppContext) {
 725        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
 726        cx.assert_all("ˇTeˇsˇt").await;
 727        cx.assert(indoc! {"
 728                Tesˇt
 729                test"})
 730            .await;
 731    }
 732
 733    #[gpui::test]
 734    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 735        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
 736        cx.assert_all("ˇTˇeˇsˇt").await;
 737        cx.assert(indoc! {"
 738                Test
 739                ˇtest"})
 740            .await;
 741    }
 742
 743    #[gpui::test]
 744    async fn test_o(cx: &mut gpui::TestAppContext) {
 745        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
 746        cx.assert("ˇ").await;
 747        cx.assert("The ˇquick").await;
 748        cx.assert_all(indoc! {"
 749                The qˇuick
 750                brown ˇfox
 751                jumps ˇover"})
 752            .await;
 753        cx.assert(indoc! {"
 754                The quick
 755                ˇ
 756                brown fox"})
 757            .await;
 758
 759        cx.assert_manual(
 760            indoc! {"
 761                fn test() {
 762                    println!(ˇ);
 763                }"},
 764            Mode::Normal,
 765            indoc! {"
 766                fn test() {
 767                    println!();
 768                    ˇ
 769                }"},
 770            Mode::Insert,
 771        );
 772
 773        cx.assert_manual(
 774            indoc! {"
 775                fn test(ˇ) {
 776                    println!();
 777                }"},
 778            Mode::Normal,
 779            indoc! {"
 780                fn test() {
 781                    ˇ
 782                    println!();
 783                }"},
 784            Mode::Insert,
 785        );
 786    }
 787
 788    #[gpui::test]
 789    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
 790        let cx = NeovimBackedTestContext::new(cx).await;
 791        let mut cx = cx.binding(["shift-o"]);
 792        cx.assert("ˇ").await;
 793        cx.assert("The ˇquick").await;
 794        cx.assert_all(indoc! {"
 795            The qˇuick
 796            brown ˇfox
 797            jumps ˇover"})
 798            .await;
 799        cx.assert(indoc! {"
 800            The quick
 801            ˇ
 802            brown fox"})
 803            .await;
 804
 805        // Our indentation is smarter than vims. So we don't match here
 806        cx.assert_manual(
 807            indoc! {"
 808                fn test() {
 809                    println!(ˇ);
 810                }"},
 811            Mode::Normal,
 812            indoc! {"
 813                fn test() {
 814                    ˇ
 815                    println!();
 816                }"},
 817            Mode::Insert,
 818        );
 819        cx.assert_manual(
 820            indoc! {"
 821                fn test(ˇ) {
 822                    println!();
 823                }"},
 824            Mode::Normal,
 825            indoc! {"
 826                ˇ
 827                fn test() {
 828                    println!();
 829                }"},
 830            Mode::Insert,
 831        );
 832    }
 833
 834    #[gpui::test]
 835    async fn test_dd(cx: &mut gpui::TestAppContext) {
 836        let mut cx = NeovimBackedTestContext::new(cx).await;
 837        cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
 838        cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
 839        for marked_text in cx.each_marked_position(indoc! {"
 840            The qˇuick
 841            brown ˇfox
 842            jumps ˇover"})
 843        {
 844            cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
 845        }
 846        cx.assert_neovim_compatible(
 847            indoc! {"
 848                The quick
 849                ˇ
 850                brown fox"},
 851            ["d", "d"],
 852        )
 853        .await;
 854    }
 855
 856    #[gpui::test]
 857    async fn test_cc(cx: &mut gpui::TestAppContext) {
 858        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
 859        cx.assert("ˇ").await;
 860        cx.assert("The ˇquick").await;
 861        cx.assert_all(indoc! {"
 862                The quˇick
 863                brown ˇfox
 864                jumps ˇover"})
 865            .await;
 866        cx.assert(indoc! {"
 867                The quick
 868                ˇ
 869                brown fox"})
 870            .await;
 871    }
 872
 873    #[gpui::test]
 874    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
 875        let mut cx = NeovimBackedTestContext::new(cx).await;
 876
 877        for count in 1..=5 {
 878            cx.assert_binding_matches_all(
 879                [&count.to_string(), "w"],
 880                indoc! {"
 881                    ˇThe quˇickˇ browˇn
 882                    ˇ
 883                    ˇfox ˇjumpsˇ-ˇoˇver
 884                    ˇthe lazy dog
 885                "},
 886            )
 887            .await;
 888        }
 889    }
 890
 891    #[gpui::test]
 892    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
 893        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 894        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
 895    }
 896
 897    #[gpui::test]
 898    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
 899        let mut cx = NeovimBackedTestContext::new(cx).await;
 900
 901        for count in 1..=3 {
 902            let test_case = indoc! {"
 903                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 904                ˇ    ˇbˇaaˇa ˇbˇbˇb
 905                ˇ
 906                ˇb
 907            "};
 908
 909            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
 910                .await;
 911
 912            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
 913                .await;
 914        }
 915    }
 916
 917    #[gpui::test]
 918    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
 919        let mut cx = NeovimBackedTestContext::new(cx).await;
 920        let test_case = indoc! {"
 921            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 922            ˇ    ˇbˇaaˇa ˇbˇbˇb
 923            ˇ•••
 924            ˇb
 925            "
 926        };
 927
 928        for count in 1..=3 {
 929            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
 930                .await;
 931
 932            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
 933                .await;
 934        }
 935    }
 936
 937    #[gpui::test]
 938    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
 939        let mut cx = VimTestContext::new(cx, true).await;
 940        cx.update_global(|store: &mut SettingsStore, cx| {
 941            store.update_user_settings::<VimSettings>(cx, |s| {
 942                s.use_multiline_find = Some(true);
 943            });
 944        });
 945
 946        cx.assert_binding(
 947            ["f", "l"],
 948            indoc! {"
 949            ˇfunction print() {
 950                console.log('ok')
 951            }
 952            "},
 953            Mode::Normal,
 954            indoc! {"
 955            function print() {
 956                consoˇle.log('ok')
 957            }
 958            "},
 959            Mode::Normal,
 960        );
 961
 962        cx.assert_binding(
 963            ["t", "l"],
 964            indoc! {"
 965            ˇfunction print() {
 966                console.log('ok')
 967            }
 968            "},
 969            Mode::Normal,
 970            indoc! {"
 971            function print() {
 972                consˇole.log('ok')
 973            }
 974            "},
 975            Mode::Normal,
 976        );
 977    }
 978
 979    #[gpui::test]
 980    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
 981        let mut cx = VimTestContext::new(cx, true).await;
 982        cx.update_global(|store: &mut SettingsStore, cx| {
 983            store.update_user_settings::<VimSettings>(cx, |s| {
 984                s.use_multiline_find = Some(true);
 985            });
 986        });
 987
 988        cx.assert_binding(
 989            ["shift-f", "p"],
 990            indoc! {"
 991            function print() {
 992                console.ˇlog('ok')
 993            }
 994            "},
 995            Mode::Normal,
 996            indoc! {"
 997            function ˇprint() {
 998                console.log('ok')
 999            }
1000            "},
1001            Mode::Normal,
1002        );
1003
1004        cx.assert_binding(
1005            ["shift-t", "p"],
1006            indoc! {"
1007            function print() {
1008                console.ˇlog('ok')
1009            }
1010            "},
1011            Mode::Normal,
1012            indoc! {"
1013            function pˇrint() {
1014                console.log('ok')
1015            }
1016            "},
1017            Mode::Normal,
1018        );
1019    }
1020
1021    #[gpui::test]
1022    async fn test_percent(cx: &mut TestAppContext) {
1023        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1024        cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1025        cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1026            .await;
1027        cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1028    }
1029}