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