normal.rs

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