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