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