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, 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: BTreeSet<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, 1);
 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                let selection_end_rows: BTreeSet<u32> = old_selections
 322                    .into_iter()
 323                    .map(|selection| selection.end.row())
 324                    .collect();
 325                let edits = selection_end_rows.into_iter().map(|row| {
 326                    let (indent, _) = map.line_indent(row);
 327                    let end_of_line =
 328                        motion::end_of_line(&map, false, DisplayPoint::new(row, 0), 1)
 329                            .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::{KeyBinding, TestAppContext};
 410    use indoc::indoc;
 411    use settings::SettingsStore;
 412
 413    use crate::{
 414        motion,
 415        state::Mode::{self},
 416        test::{NeovimBackedTestContext, VimTestContext},
 417        VimSettings,
 418    };
 419
 420    #[gpui::test]
 421    async fn test_h(cx: &mut gpui::TestAppContext) {
 422        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 423        cx.assert_all(indoc! {"
 424            ˇThe qˇuick
 425            ˇbrown"
 426        })
 427        .await;
 428    }
 429
 430    #[gpui::test]
 431    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 432        let mut cx = NeovimBackedTestContext::new(cx)
 433            .await
 434            .binding(["backspace"]);
 435        cx.assert_all(indoc! {"
 436            ˇThe qˇuick
 437            ˇbrown"
 438        })
 439        .await;
 440    }
 441
 442    #[gpui::test]
 443    async fn test_j(cx: &mut gpui::TestAppContext) {
 444        let mut cx = NeovimBackedTestContext::new(cx).await;
 445
 446        cx.set_shared_state(indoc! {"
 447                    aaˇaa
 448                    😃😃"
 449        })
 450        .await;
 451        cx.simulate_shared_keystrokes(["j"]).await;
 452        cx.assert_shared_state(indoc! {"
 453                    aaaa
 454                    😃ˇ😃"
 455        })
 456        .await;
 457
 458        for marked_position in cx.each_marked_position(indoc! {"
 459                    ˇThe qˇuick broˇwn
 460                    ˇfox jumps"
 461        }) {
 462            cx.assert_neovim_compatible(&marked_position, ["j"]).await;
 463        }
 464    }
 465
 466    #[gpui::test]
 467    async fn test_enter(cx: &mut gpui::TestAppContext) {
 468        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
 469        cx.assert_all(indoc! {"
 470            ˇThe qˇuick broˇwn
 471            ˇfox jumps"
 472        })
 473        .await;
 474    }
 475
 476    #[gpui::test]
 477    async fn test_k(cx: &mut gpui::TestAppContext) {
 478        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
 479        cx.assert_all(indoc! {"
 480            ˇThe qˇuick
 481            ˇbrown fˇox jumˇps"
 482        })
 483        .await;
 484    }
 485
 486    #[gpui::test]
 487    async fn test_l(cx: &mut gpui::TestAppContext) {
 488        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
 489        cx.assert_all(indoc! {"
 490            ˇThe qˇuicˇk
 491            ˇbrowˇn"})
 492            .await;
 493    }
 494
 495    #[gpui::test]
 496    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 497        let mut cx = NeovimBackedTestContext::new(cx).await;
 498        cx.assert_binding_matches_all(
 499            ["$"],
 500            indoc! {"
 501            ˇThe qˇuicˇk
 502            ˇbrowˇn"},
 503        )
 504        .await;
 505        cx.assert_binding_matches_all(
 506            ["0"],
 507            indoc! {"
 508                ˇThe qˇuicˇk
 509                ˇbrowˇn"},
 510        )
 511        .await;
 512    }
 513
 514    #[gpui::test]
 515    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 516        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
 517
 518        cx.assert_all(indoc! {"
 519                The ˇquick
 520
 521                brown fox jumps
 522                overˇ the lazy doˇg"})
 523            .await;
 524        cx.assert(indoc! {"
 525            The quiˇck
 526
 527            brown"})
 528            .await;
 529        cx.assert(indoc! {"
 530            The quiˇck
 531
 532            "})
 533            .await;
 534    }
 535
 536    #[gpui::test]
 537    async fn test_w(cx: &mut gpui::TestAppContext) {
 538        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
 539        cx.assert_all(indoc! {"
 540            The ˇquickˇ-ˇbrown
 541            ˇ
 542            ˇ
 543            ˇfox_jumps ˇover
 544            ˇthˇe"})
 545            .await;
 546        let mut cx = cx.binding(["shift-w"]);
 547        cx.assert_all(indoc! {"
 548            The ˇquickˇ-ˇbrown
 549            ˇ
 550            ˇ
 551            ˇfox_jumps ˇover
 552            ˇthˇe"})
 553            .await;
 554    }
 555
 556    #[gpui::test]
 557    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 558        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
 559        cx.assert_all(indoc! {"
 560            Thˇe quicˇkˇ-browˇn
 561
 562
 563            fox_jumpˇs oveˇr
 564            thˇe"})
 565            .await;
 566        let mut cx = cx.binding(["shift-e"]);
 567        cx.assert_all(indoc! {"
 568            Thˇe quicˇkˇ-browˇn
 569
 570
 571            fox_jumpˇs oveˇr
 572            thˇe"})
 573            .await;
 574    }
 575
 576    #[gpui::test]
 577    async fn test_b(cx: &mut gpui::TestAppContext) {
 578        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
 579        cx.assert_all(indoc! {"
 580            ˇThe ˇquickˇ-ˇbrown
 581            ˇ
 582            ˇ
 583            ˇfox_jumps ˇover
 584            ˇthe"})
 585            .await;
 586        let mut cx = cx.binding(["shift-b"]);
 587        cx.assert_all(indoc! {"
 588            ˇThe ˇquickˇ-ˇbrown
 589            ˇ
 590            ˇ
 591            ˇfox_jumps ˇover
 592            ˇthe"})
 593            .await;
 594    }
 595
 596    #[gpui::test]
 597    async fn test_gg(cx: &mut gpui::TestAppContext) {
 598        let mut cx = NeovimBackedTestContext::new(cx).await;
 599        cx.assert_binding_matches_all(
 600            ["g", "g"],
 601            indoc! {"
 602                The qˇuick
 603
 604                brown fox jumps
 605                over ˇthe laˇzy dog"},
 606        )
 607        .await;
 608        cx.assert_binding_matches(
 609            ["g", "g"],
 610            indoc! {"
 611
 612
 613                brown fox jumps
 614                over the laˇzy dog"},
 615        )
 616        .await;
 617        cx.assert_binding_matches(
 618            ["2", "g", "g"],
 619            indoc! {"
 620                ˇ
 621
 622                brown fox jumps
 623                over the lazydog"},
 624        )
 625        .await;
 626    }
 627
 628    #[gpui::test]
 629    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 630        let mut cx = NeovimBackedTestContext::new(cx).await;
 631        cx.assert_binding_matches_all(
 632            ["shift-g"],
 633            indoc! {"
 634                The qˇuick
 635
 636                brown fox jumps
 637                over ˇthe laˇzy dog"},
 638        )
 639        .await;
 640        cx.assert_binding_matches(
 641            ["shift-g"],
 642            indoc! {"
 643
 644
 645                brown fox jumps
 646                over the laˇzy dog"},
 647        )
 648        .await;
 649        cx.assert_binding_matches(
 650            ["2", "shift-g"],
 651            indoc! {"
 652                ˇ
 653
 654                brown fox jumps
 655                over the lazydog"},
 656        )
 657        .await;
 658    }
 659
 660    #[gpui::test]
 661    async fn test_a(cx: &mut gpui::TestAppContext) {
 662        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
 663        cx.assert_all("The qˇuicˇk").await;
 664    }
 665
 666    #[gpui::test]
 667    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 668        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
 669        cx.assert_all(indoc! {"
 670            ˇ
 671            The qˇuick
 672            brown ˇfox "})
 673            .await;
 674    }
 675
 676    #[gpui::test]
 677    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 678        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
 679        cx.assert("The qˇuick").await;
 680        cx.assert(" The qˇuick").await;
 681        cx.assert("ˇ").await;
 682        cx.assert(indoc! {"
 683                The qˇuick
 684                brown fox"})
 685            .await;
 686        cx.assert(indoc! {"
 687                ˇ
 688                The quick"})
 689            .await;
 690        // Indoc disallows trailing whitespace.
 691        cx.assert("   ˇ \nThe quick").await;
 692    }
 693
 694    #[gpui::test]
 695    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 696        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
 697        cx.assert("The qˇuick").await;
 698        cx.assert(" The qˇuick").await;
 699        cx.assert("ˇ").await;
 700        cx.assert(indoc! {"
 701                The qˇuick
 702                brown fox"})
 703            .await;
 704        cx.assert(indoc! {"
 705                ˇ
 706                The quick"})
 707            .await;
 708    }
 709
 710    #[gpui::test]
 711    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 712        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
 713        cx.assert(indoc! {"
 714                The qˇuick
 715                brown fox"})
 716            .await;
 717        cx.assert(indoc! {"
 718                The quick
 719                ˇ
 720                brown fox"})
 721            .await;
 722    }
 723
 724    #[gpui::test]
 725    async fn test_x(cx: &mut gpui::TestAppContext) {
 726        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
 727        cx.assert_all("ˇTeˇsˇt").await;
 728        cx.assert(indoc! {"
 729                Tesˇt
 730                test"})
 731            .await;
 732    }
 733
 734    #[gpui::test]
 735    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 736        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
 737        cx.assert_all("ˇTˇeˇsˇt").await;
 738        cx.assert(indoc! {"
 739                Test
 740                ˇtest"})
 741            .await;
 742    }
 743
 744    #[gpui::test]
 745    async fn test_o(cx: &mut gpui::TestAppContext) {
 746        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
 747        cx.assert("ˇ").await;
 748        cx.assert("The ˇquick").await;
 749        cx.assert_all(indoc! {"
 750                The qˇuick
 751                brown ˇfox
 752                jumps ˇover"})
 753            .await;
 754        cx.assert(indoc! {"
 755                The quick
 756                ˇ
 757                brown fox"})
 758            .await;
 759
 760        cx.assert_manual(
 761            indoc! {"
 762                fn test() {
 763                    println!(ˇ);
 764                }"},
 765            Mode::Normal,
 766            indoc! {"
 767                fn test() {
 768                    println!();
 769                    ˇ
 770                }"},
 771            Mode::Insert,
 772        );
 773
 774        cx.assert_manual(
 775            indoc! {"
 776                fn test(ˇ) {
 777                    println!();
 778                }"},
 779            Mode::Normal,
 780            indoc! {"
 781                fn test() {
 782                    ˇ
 783                    println!();
 784                }"},
 785            Mode::Insert,
 786        );
 787    }
 788
 789    #[gpui::test]
 790    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
 791        let cx = NeovimBackedTestContext::new(cx).await;
 792        let mut cx = cx.binding(["shift-o"]);
 793        cx.assert("ˇ").await;
 794        cx.assert("The ˇquick").await;
 795        cx.assert_all(indoc! {"
 796            The qˇuick
 797            brown ˇfox
 798            jumps ˇover"})
 799            .await;
 800        cx.assert(indoc! {"
 801            The quick
 802            ˇ
 803            brown fox"})
 804            .await;
 805
 806        // Our indentation is smarter than vims. So we don't match here
 807        cx.assert_manual(
 808            indoc! {"
 809                fn test() {
 810                    println!(ˇ);
 811                }"},
 812            Mode::Normal,
 813            indoc! {"
 814                fn test() {
 815                    ˇ
 816                    println!();
 817                }"},
 818            Mode::Insert,
 819        );
 820        cx.assert_manual(
 821            indoc! {"
 822                fn test(ˇ) {
 823                    println!();
 824                }"},
 825            Mode::Normal,
 826            indoc! {"
 827                ˇ
 828                fn test() {
 829                    println!();
 830                }"},
 831            Mode::Insert,
 832        );
 833    }
 834
 835    #[gpui::test]
 836    async fn test_dd(cx: &mut gpui::TestAppContext) {
 837        let mut cx = NeovimBackedTestContext::new(cx).await;
 838        cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
 839        cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
 840        for marked_text in cx.each_marked_position(indoc! {"
 841            The qˇuick
 842            brown ˇfox
 843            jumps ˇover"})
 844        {
 845            cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
 846        }
 847        cx.assert_neovim_compatible(
 848            indoc! {"
 849                The quick
 850                ˇ
 851                brown fox"},
 852            ["d", "d"],
 853        )
 854        .await;
 855    }
 856
 857    #[gpui::test]
 858    async fn test_cc(cx: &mut gpui::TestAppContext) {
 859        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
 860        cx.assert("ˇ").await;
 861        cx.assert("The ˇquick").await;
 862        cx.assert_all(indoc! {"
 863                The quˇick
 864                brown ˇfox
 865                jumps ˇover"})
 866            .await;
 867        cx.assert(indoc! {"
 868                The quick
 869                ˇ
 870                brown fox"})
 871            .await;
 872    }
 873
 874    #[gpui::test]
 875    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
 876        let mut cx = NeovimBackedTestContext::new(cx).await;
 877
 878        for count in 1..=5 {
 879            cx.assert_binding_matches_all(
 880                [&count.to_string(), "w"],
 881                indoc! {"
 882                    ˇThe quˇickˇ browˇn
 883                    ˇ
 884                    ˇfox ˇjumpsˇ-ˇoˇver
 885                    ˇthe lazy dog
 886                "},
 887            )
 888            .await;
 889        }
 890    }
 891
 892    #[gpui::test]
 893    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
 894        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 895        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
 896    }
 897
 898    #[gpui::test]
 899    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
 900        let mut cx = NeovimBackedTestContext::new(cx).await;
 901
 902        for count in 1..=3 {
 903            let test_case = indoc! {"
 904                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 905                ˇ    ˇbˇaaˇa ˇbˇbˇb
 906                ˇ
 907                ˇb
 908            "};
 909
 910            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
 911                .await;
 912
 913            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
 914                .await;
 915        }
 916    }
 917
 918    #[gpui::test]
 919    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
 920        let mut cx = NeovimBackedTestContext::new(cx).await;
 921        let test_case = indoc! {"
 922            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 923            ˇ    ˇbˇaaˇa ˇbˇbˇb
 924            ˇ•••
 925            ˇb
 926            "
 927        };
 928
 929        for count in 1..=3 {
 930            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
 931                .await;
 932
 933            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
 934                .await;
 935        }
 936    }
 937
 938    #[gpui::test]
 939    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
 940        let mut cx = VimTestContext::new(cx, true).await;
 941        cx.update_global(|store: &mut SettingsStore, cx| {
 942            store.update_user_settings::<VimSettings>(cx, |s| {
 943                s.use_multiline_find = Some(true);
 944            });
 945        });
 946
 947        cx.assert_binding(
 948            ["f", "l"],
 949            indoc! {"
 950            ˇfunction print() {
 951                console.log('ok')
 952            }
 953            "},
 954            Mode::Normal,
 955            indoc! {"
 956            function print() {
 957                consoˇle.log('ok')
 958            }
 959            "},
 960            Mode::Normal,
 961        );
 962
 963        cx.assert_binding(
 964            ["t", "l"],
 965            indoc! {"
 966            ˇfunction print() {
 967                console.log('ok')
 968            }
 969            "},
 970            Mode::Normal,
 971            indoc! {"
 972            function print() {
 973                consˇole.log('ok')
 974            }
 975            "},
 976            Mode::Normal,
 977        );
 978    }
 979
 980    #[gpui::test]
 981    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
 982        let mut cx = VimTestContext::new(cx, true).await;
 983        cx.update_global(|store: &mut SettingsStore, cx| {
 984            store.update_user_settings::<VimSettings>(cx, |s| {
 985                s.use_multiline_find = Some(true);
 986            });
 987        });
 988
 989        cx.assert_binding(
 990            ["shift-f", "p"],
 991            indoc! {"
 992            function print() {
 993                console.ˇlog('ok')
 994            }
 995            "},
 996            Mode::Normal,
 997            indoc! {"
 998            function ˇprint() {
 999                console.log('ok')
1000            }
1001            "},
1002            Mode::Normal,
1003        );
1004
1005        cx.assert_binding(
1006            ["shift-t", "p"],
1007            indoc! {"
1008            function print() {
1009                console.ˇlog('ok')
1010            }
1011            "},
1012            Mode::Normal,
1013            indoc! {"
1014            function pˇrint() {
1015                console.log('ok')
1016            }
1017            "},
1018            Mode::Normal,
1019        );
1020    }
1021
1022    #[gpui::test]
1023    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1024        let mut cx = VimTestContext::new(cx, true).await;
1025        cx.update_global(|store: &mut SettingsStore, cx| {
1026            store.update_user_settings::<VimSettings>(cx, |s| {
1027                s.use_smartcase_find = Some(true);
1028            });
1029        });
1030
1031        cx.assert_binding(
1032            ["f", "p"],
1033            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1034            Mode::Normal,
1035            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1036            Mode::Normal,
1037        );
1038
1039        cx.assert_binding(
1040            ["shift-f", "p"],
1041            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1042            Mode::Normal,
1043            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1044            Mode::Normal,
1045        );
1046
1047        cx.assert_binding(
1048            ["t", "p"],
1049            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1050            Mode::Normal,
1051            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1052            Mode::Normal,
1053        );
1054
1055        cx.assert_binding(
1056            ["shift-t", "p"],
1057            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1058            Mode::Normal,
1059            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1060            Mode::Normal,
1061        );
1062    }
1063
1064    #[gpui::test]
1065    async fn test_percent(cx: &mut TestAppContext) {
1066        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1067        cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1068        cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1069            .await;
1070        cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1071    }
1072
1073    #[gpui::test]
1074    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1075        let mut cx = NeovimBackedTestContext::new(cx).await;
1076
1077        // goes to current line end
1078        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1079        cx.simulate_shared_keystrokes(["$"]).await;
1080        cx.assert_shared_state(indoc! {"aˇa\nbb\ncc"}).await;
1081
1082        // goes to next line end
1083        cx.simulate_shared_keystrokes(["2", "$"]).await;
1084        cx.assert_shared_state("aa\nbˇb\ncc").await;
1085
1086        // try to exceed the final line.
1087        cx.simulate_shared_keystrokes(["4", "$"]).await;
1088        cx.assert_shared_state("aa\nbb\ncˇc").await;
1089    }
1090
1091    #[gpui::test]
1092    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1093        let mut cx = VimTestContext::new(cx, true).await;
1094        cx.update(|cx| {
1095            cx.bind_keys(vec![
1096                KeyBinding::new(
1097                    "w",
1098                    motion::NextSubwordStart {
1099                        ignore_punctuation: false,
1100                    },
1101                    Some("Editor && VimControl && !VimWaiting && !menu"),
1102                ),
1103                KeyBinding::new(
1104                    "b",
1105                    motion::PreviousSubwordStart {
1106                        ignore_punctuation: false,
1107                    },
1108                    Some("Editor && VimControl && !VimWaiting && !menu"),
1109                ),
1110                KeyBinding::new(
1111                    "e",
1112                    motion::NextSubwordEnd {
1113                        ignore_punctuation: false,
1114                    },
1115                    Some("Editor && VimControl && !VimWaiting && !menu"),
1116                ),
1117                KeyBinding::new(
1118                    "g e",
1119                    motion::PreviousSubwordEnd {
1120                        ignore_punctuation: false,
1121                    },
1122                    Some("Editor && VimControl && !VimWaiting && !menu"),
1123                ),
1124            ]);
1125        });
1126
1127        cx.assert_binding_normal(
1128            ["w"],
1129            indoc! {"ˇassert_binding"},
1130            indoc! {"assert_ˇbinding"},
1131        );
1132        // Special case: In 'cw', 'w' acts like 'e'
1133        cx.assert_binding(
1134            ["c", "w"],
1135            indoc! {"ˇassert_binding"},
1136            Mode::Normal,
1137            indoc! {"ˇ_binding"},
1138            Mode::Insert,
1139        );
1140
1141        cx.assert_binding_normal(
1142            ["e"],
1143            indoc! {"ˇassert_binding"},
1144            indoc! {"asserˇt_binding"},
1145        );
1146
1147        cx.assert_binding_normal(
1148            ["b"],
1149            indoc! {"assert_ˇbinding"},
1150            indoc! {"ˇassert_binding"},
1151        );
1152
1153        cx.assert_binding_normal(
1154            ["g", "e"],
1155            indoc! {"assert_bindinˇg"},
1156            indoc! {"asserˇt_binding"},
1157        );
1158    }
1159}