normal.rs

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