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