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