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