normal.rs

   1mod case;
   2mod change;
   3mod delete;
   4mod scroll;
   5mod search;
   6mod substitute;
   7mod yank;
   8
   9use std::{borrow::Cow, sync::Arc};
  10
  11use crate::{
  12    motion::Motion,
  13    object::Object,
  14    state::{Mode, Operator},
  15    Vim,
  16};
  17use collections::{HashMap, HashSet};
  18use editor::{
  19    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
  20    DisplayPoint,
  21};
  22use gpui::{actions, AppContext, ViewContext, WindowContext};
  23use language::{AutoindentMode, Point, SelectionGoal};
  24use log::error;
  25use workspace::Workspace;
  26
  27use self::{
  28    case::change_case,
  29    change::{change_motion, change_object},
  30    delete::{delete_motion, delete_object},
  31    substitute::substitute,
  32    yank::{yank_motion, yank_object},
  33};
  34
  35actions!(
  36    vim,
  37    [
  38        InsertAfter,
  39        InsertFirstNonWhitespace,
  40        InsertEndOfLine,
  41        InsertLineAbove,
  42        InsertLineBelow,
  43        DeleteLeft,
  44        DeleteRight,
  45        ChangeToEndOfLine,
  46        DeleteToEndOfLine,
  47        Paste,
  48        Yank,
  49        Substitute,
  50        ChangeCase,
  51    ]
  52);
  53
  54pub fn init(cx: &mut AppContext) {
  55    cx.add_action(insert_after);
  56    cx.add_action(insert_first_non_whitespace);
  57    cx.add_action(insert_end_of_line);
  58    cx.add_action(insert_line_above);
  59    cx.add_action(insert_line_below);
  60    cx.add_action(change_case);
  61    search::init(cx);
  62    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
  63        Vim::update(cx, |vim, cx| {
  64            let times = vim.pop_number_operator(cx);
  65            substitute(vim, times, cx);
  66        })
  67    });
  68    cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
  69        Vim::update(cx, |vim, cx| {
  70            let times = vim.pop_number_operator(cx);
  71            delete_motion(vim, Motion::Left, times, cx);
  72        })
  73    });
  74    cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
  75        Vim::update(cx, |vim, cx| {
  76            let times = vim.pop_number_operator(cx);
  77            delete_motion(vim, Motion::Right, times, cx);
  78        })
  79    });
  80    cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
  81        Vim::update(cx, |vim, cx| {
  82            let times = vim.pop_number_operator(cx);
  83            change_motion(vim, Motion::EndOfLine, times, cx);
  84        })
  85    });
  86    cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
  87        Vim::update(cx, |vim, cx| {
  88            let times = vim.pop_number_operator(cx);
  89            delete_motion(vim, Motion::EndOfLine, times, cx);
  90        })
  91    });
  92    cx.add_action(paste);
  93
  94    scroll::init(cx);
  95}
  96
  97pub fn normal_motion(
  98    motion: Motion,
  99    operator: Option<Operator>,
 100    times: Option<usize>,
 101    cx: &mut WindowContext,
 102) {
 103    Vim::update(cx, |vim, cx| {
 104        match operator {
 105            None => move_cursor(vim, motion, times, cx),
 106            Some(Operator::Change) => change_motion(vim, motion, times, cx),
 107            Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
 108            Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
 109            Some(operator) => {
 110                // Can't do anything for text objects or namespace operators. Ignoring
 111                error!("Unexpected normal mode motion operator: {:?}", operator)
 112            }
 113        }
 114    });
 115}
 116
 117pub fn normal_object(object: Object, cx: &mut WindowContext) {
 118    Vim::update(cx, |vim, cx| {
 119        match vim.state.operator_stack.pop() {
 120            Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
 121                Some(Operator::Change) => change_object(vim, object, around, cx),
 122                Some(Operator::Delete) => delete_object(vim, object, around, cx),
 123                Some(Operator::Yank) => yank_object(vim, object, around, cx),
 124                _ => {
 125                    // Can't do anything for namespace operators. Ignoring
 126                }
 127            },
 128            _ => {
 129                // Can't do anything with change/delete/yank and text objects. Ignoring
 130            }
 131        }
 132        vim.clear_operator(cx);
 133    })
 134}
 135
 136fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
 137    vim.update_active_editor(cx, |editor, cx| {
 138        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 139            s.move_cursors_with(|map, cursor, goal| {
 140                motion
 141                    .move_point(map, cursor, goal, times)
 142                    .unwrap_or((cursor, goal))
 143            })
 144        })
 145    });
 146}
 147
 148fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
 149    Vim::update(cx, |vim, cx| {
 150        vim.switch_mode(Mode::Insert, false, cx);
 151        vim.update_active_editor(cx, |editor, cx| {
 152            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 153                s.maybe_move_cursors_with(|map, cursor, goal| {
 154                    Motion::Right.move_point(map, cursor, goal, None)
 155                });
 156            });
 157        });
 158    });
 159}
 160
 161fn insert_first_non_whitespace(
 162    _: &mut Workspace,
 163    _: &InsertFirstNonWhitespace,
 164    cx: &mut ViewContext<Workspace>,
 165) {
 166    Vim::update(cx, |vim, cx| {
 167        vim.switch_mode(Mode::Insert, false, cx);
 168        vim.update_active_editor(cx, |editor, cx| {
 169            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 170                s.maybe_move_cursors_with(|map, cursor, goal| {
 171                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
 172                });
 173            });
 174        });
 175    });
 176}
 177
 178fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
 179    Vim::update(cx, |vim, cx| {
 180        vim.switch_mode(Mode::Insert, false, cx);
 181        vim.update_active_editor(cx, |editor, cx| {
 182            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 183                s.maybe_move_cursors_with(|map, cursor, goal| {
 184                    Motion::EndOfLine.move_point(map, cursor, goal, None)
 185                });
 186            });
 187        });
 188    });
 189}
 190
 191fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
 192    Vim::update(cx, |vim, cx| {
 193        vim.switch_mode(Mode::Insert, false, cx);
 194        vim.update_active_editor(cx, |editor, cx| {
 195            editor.transact(cx, |editor, cx| {
 196                let (map, old_selections) = editor.selections.all_display(cx);
 197                let selection_start_rows: HashSet<u32> = old_selections
 198                    .into_iter()
 199                    .map(|selection| selection.start.row())
 200                    .collect();
 201                let edits = selection_start_rows.into_iter().map(|row| {
 202                    let (indent, _) = map.line_indent(row);
 203                    let start_of_line = map
 204                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
 205                        .to_point(&map);
 206                    let mut new_text = " ".repeat(indent as usize);
 207                    new_text.push('\n');
 208                    (start_of_line..start_of_line, new_text)
 209                });
 210                editor.edit_with_autoindent(edits, cx);
 211                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 212                    s.move_cursors_with(|map, mut cursor, _| {
 213                        *cursor.row_mut() -= 1;
 214                        *cursor.column_mut() = map.line_len(cursor.row());
 215                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
 216                    });
 217                });
 218            });
 219        });
 220    });
 221}
 222
 223fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
 224    Vim::update(cx, |vim, cx| {
 225        vim.switch_mode(Mode::Insert, false, cx);
 226        vim.update_active_editor(cx, |editor, cx| {
 227            editor.transact(cx, |editor, cx| {
 228                let (map, old_selections) = editor.selections.all_display(cx);
 229                let selection_end_rows: HashSet<u32> = old_selections
 230                    .into_iter()
 231                    .map(|selection| selection.end.row())
 232                    .collect();
 233                let edits = selection_end_rows.into_iter().map(|row| {
 234                    let (indent, _) = map.line_indent(row);
 235                    let end_of_line = map
 236                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
 237                        .to_point(&map);
 238                    let mut new_text = "\n".to_string();
 239                    new_text.push_str(&" ".repeat(indent as usize));
 240                    (end_of_line..end_of_line, new_text)
 241                });
 242                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 243                    s.maybe_move_cursors_with(|map, cursor, goal| {
 244                        Motion::EndOfLine.move_point(map, cursor, goal, None)
 245                    });
 246                });
 247                editor.edit_with_autoindent(edits, cx);
 248            });
 249        });
 250    });
 251}
 252
 253fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
 254    Vim::update(cx, |vim, cx| {
 255        vim.update_active_editor(cx, |editor, cx| {
 256            editor.transact(cx, |editor, cx| {
 257                editor.set_clip_at_line_ends(false, cx);
 258                if let Some(item) = cx.read_from_clipboard() {
 259                    let mut clipboard_text = Cow::Borrowed(item.text());
 260                    if let Some(mut clipboard_selections) =
 261                        item.metadata::<Vec<ClipboardSelection>>()
 262                    {
 263                        let (display_map, selections) = editor.selections.all_display(cx);
 264                        let all_selections_were_entire_line =
 265                            clipboard_selections.iter().all(|s| s.is_entire_line);
 266                        if clipboard_selections.len() != selections.len() {
 267                            let mut newline_separated_text = String::new();
 268                            let mut clipboard_selections =
 269                                clipboard_selections.drain(..).peekable();
 270                            let mut ix = 0;
 271                            while let Some(clipboard_selection) = clipboard_selections.next() {
 272                                newline_separated_text
 273                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
 274                                ix += clipboard_selection.len;
 275                                if clipboard_selections.peek().is_some() {
 276                                    newline_separated_text.push('\n');
 277                                }
 278                            }
 279                            clipboard_text = Cow::Owned(newline_separated_text);
 280                        }
 281
 282                        // If the pasted text is a single line, the cursor should be placed after
 283                        // the newly pasted text. This is easiest done with an anchor after the
 284                        // insertion, and then with a fixup to move the selection back one position.
 285                        // However if the pasted text is linewise, the cursor should be placed at the start
 286                        // of the new text on the following line. This is easiest done with a manually adjusted
 287                        // point.
 288                        // This enum lets us represent both cases
 289                        enum NewPosition {
 290                            Inside(Point),
 291                            After(Anchor),
 292                        }
 293                        let mut new_selections: HashMap<usize, NewPosition> = Default::default();
 294                        editor.buffer().update(cx, |buffer, cx| {
 295                            let snapshot = buffer.snapshot(cx);
 296                            let mut start_offset = 0;
 297                            let mut edits = Vec::new();
 298                            for (ix, selection) in selections.iter().enumerate() {
 299                                let to_insert;
 300                                let linewise;
 301                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
 302                                    let end_offset = start_offset + clipboard_selection.len;
 303                                    to_insert = &clipboard_text[start_offset..end_offset];
 304                                    linewise = clipboard_selection.is_entire_line;
 305                                    start_offset = end_offset;
 306                                } else {
 307                                    to_insert = clipboard_text.as_str();
 308                                    linewise = all_selections_were_entire_line;
 309                                }
 310
 311                                // If the clipboard text was copied linewise, and the current selection
 312                                // is empty, then paste the text after this line and move the selection
 313                                // to the start of the pasted text
 314                                let insert_at = if linewise {
 315                                    let (point, _) = display_map
 316                                        .next_line_boundary(selection.start.to_point(&display_map));
 317
 318                                    if !to_insert.starts_with('\n') {
 319                                        // Add newline before pasted text so that it shows up
 320                                        edits.push((point..point, "\n"));
 321                                    }
 322                                    // Drop selection at the start of the next line
 323                                    new_selections.insert(
 324                                        selection.id,
 325                                        NewPosition::Inside(Point::new(point.row + 1, 0)),
 326                                    );
 327                                    point
 328                                } else {
 329                                    let mut point = selection.end;
 330                                    // Paste the text after the current selection
 331                                    *point.column_mut() = point.column() + 1;
 332                                    let point = display_map
 333                                        .clip_point(point, Bias::Right)
 334                                        .to_point(&display_map);
 335
 336                                    new_selections.insert(
 337                                        selection.id,
 338                                        if to_insert.contains('\n') {
 339                                            NewPosition::Inside(point)
 340                                        } else {
 341                                            NewPosition::After(snapshot.anchor_after(point))
 342                                        },
 343                                    );
 344                                    point
 345                                };
 346
 347                                if linewise && to_insert.ends_with('\n') {
 348                                    edits.push((
 349                                        insert_at..insert_at,
 350                                        &to_insert[0..to_insert.len().saturating_sub(1)],
 351                                    ))
 352                                } else {
 353                                    edits.push((insert_at..insert_at, to_insert));
 354                                }
 355                            }
 356                            drop(snapshot);
 357                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
 358                        });
 359
 360                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 361                            s.move_with(|map, selection| {
 362                                if let Some(new_position) = new_selections.get(&selection.id) {
 363                                    match new_position {
 364                                        NewPosition::Inside(new_point) => {
 365                                            selection.collapse_to(
 366                                                new_point.to_display_point(map),
 367                                                SelectionGoal::None,
 368                                            );
 369                                        }
 370                                        NewPosition::After(after_point) => {
 371                                            let mut new_point = after_point.to_display_point(map);
 372                                            *new_point.column_mut() =
 373                                                new_point.column().saturating_sub(1);
 374                                            new_point = map.clip_point(new_point, Bias::Left);
 375                                            selection.collapse_to(new_point, SelectionGoal::None);
 376                                        }
 377                                    }
 378                                }
 379                            });
 380                        });
 381                    } else {
 382                        editor.insert(&clipboard_text, cx);
 383                    }
 384                }
 385                editor.set_clip_at_line_ends(true, cx);
 386            });
 387        });
 388    });
 389}
 390
 391pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
 392    Vim::update(cx, |vim, cx| {
 393        vim.update_active_editor(cx, |editor, cx| {
 394            editor.transact(cx, |editor, cx| {
 395                editor.set_clip_at_line_ends(false, cx);
 396                let (map, display_selections) = editor.selections.all_display(cx);
 397                // Selections are biased right at the start. So we need to store
 398                // anchors that are biased left so that we can restore the selections
 399                // after the change
 400                let stable_anchors = editor
 401                    .selections
 402                    .disjoint_anchors()
 403                    .into_iter()
 404                    .map(|selection| {
 405                        let start = selection.start.bias_left(&map.buffer_snapshot);
 406                        start..start
 407                    })
 408                    .collect::<Vec<_>>();
 409
 410                let edits = display_selections
 411                    .into_iter()
 412                    .map(|selection| {
 413                        let mut range = selection.range();
 414                        *range.end.column_mut() += 1;
 415                        range.end = map.clip_point(range.end, Bias::Right);
 416
 417                        (
 418                            range.start.to_offset(&map, Bias::Left)
 419                                ..range.end.to_offset(&map, Bias::Left),
 420                            text.clone(),
 421                        )
 422                    })
 423                    .collect::<Vec<_>>();
 424
 425                editor.buffer().update(cx, |buffer, cx| {
 426                    buffer.edit(edits, None, cx);
 427                });
 428                editor.set_clip_at_line_ends(true, cx);
 429                editor.change_selections(None, cx, |s| {
 430                    s.select_anchor_ranges(stable_anchors);
 431                });
 432            });
 433        });
 434        vim.pop_operator(cx)
 435    });
 436}
 437
 438#[cfg(test)]
 439mod test {
 440    use gpui::TestAppContext;
 441    use indoc::indoc;
 442
 443    use crate::{
 444        state::{
 445            Mode::{self, *},
 446            Namespace, Operator,
 447        },
 448        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
 449    };
 450
 451    #[gpui::test]
 452    async fn test_h(cx: &mut gpui::TestAppContext) {
 453        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 454        cx.assert_all(indoc! {"
 455            ˇThe qˇuick
 456            ˇbrown"
 457        })
 458        .await;
 459    }
 460
 461    #[gpui::test]
 462    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 463        let mut cx = NeovimBackedTestContext::new(cx)
 464            .await
 465            .binding(["backspace"]);
 466        cx.assert_all(indoc! {"
 467            ˇThe qˇuick
 468            ˇbrown"
 469        })
 470        .await;
 471    }
 472
 473    #[gpui::test]
 474    async fn test_j(cx: &mut gpui::TestAppContext) {
 475        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
 476        cx.assert_all(indoc! {"
 477            ˇThe qˇuick broˇwn
 478            ˇfox jumps"
 479        })
 480        .await;
 481    }
 482
 483    #[gpui::test]
 484    async fn test_enter(cx: &mut gpui::TestAppContext) {
 485        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
 486        cx.assert_all(indoc! {"
 487            ˇThe qˇuick broˇwn
 488            ˇfox jumps"
 489        })
 490        .await;
 491    }
 492
 493    #[gpui::test]
 494    async fn test_k(cx: &mut gpui::TestAppContext) {
 495        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
 496        cx.assert_all(indoc! {"
 497            ˇThe qˇuick
 498            ˇbrown fˇox jumˇps"
 499        })
 500        .await;
 501    }
 502
 503    #[gpui::test]
 504    async fn test_l(cx: &mut gpui::TestAppContext) {
 505        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
 506        cx.assert_all(indoc! {"
 507            ˇThe qˇuicˇk
 508            ˇbrowˇn"})
 509            .await;
 510    }
 511
 512    #[gpui::test]
 513    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 514        let mut cx = NeovimBackedTestContext::new(cx).await;
 515        cx.assert_binding_matches_all(
 516            ["$"],
 517            indoc! {"
 518            ˇThe qˇuicˇk
 519            ˇbrowˇn"},
 520        )
 521        .await;
 522        cx.assert_binding_matches_all(
 523            ["0"],
 524            indoc! {"
 525                ˇThe qˇuicˇk
 526                ˇbrowˇn"},
 527        )
 528        .await;
 529    }
 530
 531    #[gpui::test]
 532    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 533        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
 534
 535        cx.assert_all(indoc! {"
 536                The ˇquick
 537
 538                brown fox jumps
 539                overˇ the lazy doˇg"})
 540            .await;
 541        cx.assert(indoc! {"
 542            The quiˇck
 543
 544            brown"})
 545            .await;
 546        cx.assert(indoc! {"
 547            The quiˇck
 548
 549            "})
 550            .await;
 551    }
 552
 553    #[gpui::test]
 554    async fn test_w(cx: &mut gpui::TestAppContext) {
 555        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
 556        cx.assert_all(indoc! {"
 557            The ˇquickˇ-ˇbrown
 558            ˇ
 559            ˇ
 560            ˇfox_jumps ˇover
 561            ˇthˇe"})
 562            .await;
 563        let mut cx = cx.binding(["shift-w"]);
 564        cx.assert_all(indoc! {"
 565            The ˇquickˇ-ˇbrown
 566            ˇ
 567            ˇ
 568            ˇfox_jumps ˇover
 569            ˇthˇe"})
 570            .await;
 571    }
 572
 573    #[gpui::test]
 574    async fn test_e(cx: &mut gpui::TestAppContext) {
 575        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
 576        cx.assert_all(indoc! {"
 577            Thˇe quicˇkˇ-browˇn
 578
 579
 580            fox_jumpˇs oveˇr
 581            thˇe"})
 582            .await;
 583        let mut cx = cx.binding(["shift-e"]);
 584        cx.assert_all(indoc! {"
 585            Thˇe quicˇkˇ-browˇn
 586
 587
 588            fox_jumpˇs oveˇr
 589            thˇe"})
 590            .await;
 591    }
 592
 593    #[gpui::test]
 594    async fn test_b(cx: &mut gpui::TestAppContext) {
 595        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
 596        cx.assert_all(indoc! {"
 597            ˇThe ˇquickˇ-ˇbrown
 598            ˇ
 599            ˇ
 600            ˇfox_jumps ˇover
 601            ˇthe"})
 602            .await;
 603        let mut cx = cx.binding(["shift-b"]);
 604        cx.assert_all(indoc! {"
 605            ˇThe ˇquickˇ-ˇbrown
 606            ˇ
 607            ˇ
 608            ˇfox_jumps ˇover
 609            ˇthe"})
 610            .await;
 611    }
 612
 613    #[gpui::test]
 614    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
 615        let mut cx = VimTestContext::new(cx, true).await;
 616
 617        // Can abort with escape to get back to normal mode
 618        cx.simulate_keystroke("g");
 619        assert_eq!(cx.mode(), Normal);
 620        assert_eq!(
 621            cx.active_operator(),
 622            Some(Operator::Namespace(Namespace::G))
 623        );
 624        cx.simulate_keystroke("escape");
 625        assert_eq!(cx.mode(), Normal);
 626        assert_eq!(cx.active_operator(), None);
 627    }
 628
 629    #[gpui::test]
 630    async fn test_gg(cx: &mut gpui::TestAppContext) {
 631        let mut cx = NeovimBackedTestContext::new(cx).await;
 632        cx.assert_binding_matches_all(
 633            ["g", "g"],
 634            indoc! {"
 635                The qˇuick
 636
 637                brown fox jumps
 638                over ˇthe laˇzy dog"},
 639        )
 640        .await;
 641        cx.assert_binding_matches(
 642            ["g", "g"],
 643            indoc! {"
 644
 645
 646                brown fox jumps
 647                over the laˇzy dog"},
 648        )
 649        .await;
 650        cx.assert_binding_matches(
 651            ["2", "g", "g"],
 652            indoc! {"
 653                ˇ
 654
 655                brown fox jumps
 656                over the lazydog"},
 657        )
 658        .await;
 659    }
 660
 661    #[gpui::test]
 662    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 663        let mut cx = NeovimBackedTestContext::new(cx).await;
 664        cx.assert_binding_matches_all(
 665            ["shift-g"],
 666            indoc! {"
 667                The qˇuick
 668
 669                brown fox jumps
 670                over ˇthe laˇzy dog"},
 671        )
 672        .await;
 673        cx.assert_binding_matches(
 674            ["shift-g"],
 675            indoc! {"
 676
 677
 678                brown fox jumps
 679                over the laˇzy dog"},
 680        )
 681        .await;
 682        cx.assert_binding_matches(
 683            ["2", "shift-g"],
 684            indoc! {"
 685                ˇ
 686
 687                brown fox jumps
 688                over the lazydog"},
 689        )
 690        .await;
 691    }
 692
 693    #[gpui::test]
 694    async fn test_a(cx: &mut gpui::TestAppContext) {
 695        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
 696        cx.assert_all("The qˇuicˇk").await;
 697    }
 698
 699    #[gpui::test]
 700    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 701        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
 702        cx.assert_all(indoc! {"
 703            ˇ
 704            The qˇuick
 705            brown ˇfox "})
 706            .await;
 707    }
 708
 709    #[gpui::test]
 710    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 711        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
 712        cx.assert("The qˇuick").await;
 713        cx.assert(" The qˇuick").await;
 714        cx.assert("ˇ").await;
 715        cx.assert(indoc! {"
 716                The qˇuick
 717                brown fox"})
 718            .await;
 719        cx.assert(indoc! {"
 720                ˇ
 721                The quick"})
 722            .await;
 723        // Indoc disallows trailing whitespace.
 724        cx.assert("   ˇ \nThe quick").await;
 725    }
 726
 727    #[gpui::test]
 728    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 729        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
 730        cx.assert("The qˇuick").await;
 731        cx.assert(" The qˇuick").await;
 732        cx.assert("ˇ").await;
 733        cx.assert(indoc! {"
 734                The qˇuick
 735                brown fox"})
 736            .await;
 737        cx.assert(indoc! {"
 738                ˇ
 739                The quick"})
 740            .await;
 741    }
 742
 743    #[gpui::test]
 744    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 745        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
 746        cx.assert(indoc! {"
 747                The qˇuick
 748                brown fox"})
 749            .await;
 750        cx.assert(indoc! {"
 751                The quick
 752                ˇ
 753                brown fox"})
 754            .await;
 755    }
 756
 757    #[gpui::test]
 758    async fn test_x(cx: &mut gpui::TestAppContext) {
 759        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
 760        cx.assert_all("ˇTeˇsˇt").await;
 761        cx.assert(indoc! {"
 762                Tesˇt
 763                test"})
 764            .await;
 765    }
 766
 767    #[gpui::test]
 768    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 769        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
 770        cx.assert_all("ˇTˇeˇsˇt").await;
 771        cx.assert(indoc! {"
 772                Test
 773                ˇtest"})
 774            .await;
 775    }
 776
 777    #[gpui::test]
 778    async fn test_o(cx: &mut gpui::TestAppContext) {
 779        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
 780        cx.assert("ˇ").await;
 781        cx.assert("The ˇquick").await;
 782        cx.assert_all(indoc! {"
 783                The qˇuick
 784                brown ˇfox
 785                jumps ˇover"})
 786            .await;
 787        cx.assert(indoc! {"
 788                The quick
 789                ˇ
 790                brown fox"})
 791            .await;
 792
 793        cx.assert_manual(
 794            indoc! {"
 795                fn test() {
 796                    println!(ˇ);
 797                }"},
 798            Mode::Normal,
 799            indoc! {"
 800                fn test() {
 801                    println!();
 802                    ˇ
 803                }"},
 804            Mode::Insert,
 805        );
 806
 807        cx.assert_manual(
 808            indoc! {"
 809                fn test(ˇ) {
 810                    println!();
 811                }"},
 812            Mode::Normal,
 813            indoc! {"
 814                fn test() {
 815                    ˇ
 816                    println!();
 817                }"},
 818            Mode::Insert,
 819        );
 820    }
 821
 822    #[gpui::test]
 823    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
 824        let cx = NeovimBackedTestContext::new(cx).await;
 825        let mut cx = cx.binding(["shift-o"]);
 826        cx.assert("ˇ").await;
 827        cx.assert("The ˇquick").await;
 828        cx.assert_all(indoc! {"
 829            The qˇuick
 830            brown ˇfox
 831            jumps ˇover"})
 832            .await;
 833        cx.assert(indoc! {"
 834            The quick
 835            ˇ
 836            brown fox"})
 837            .await;
 838
 839        // Our indentation is smarter than vims. So we don't match here
 840        cx.assert_manual(
 841            indoc! {"
 842                fn test() {
 843                    println!(ˇ);
 844                }"},
 845            Mode::Normal,
 846            indoc! {"
 847                fn test() {
 848                    ˇ
 849                    println!();
 850                }"},
 851            Mode::Insert,
 852        );
 853        cx.assert_manual(
 854            indoc! {"
 855                fn test(ˇ) {
 856                    println!();
 857                }"},
 858            Mode::Normal,
 859            indoc! {"
 860                ˇ
 861                fn test() {
 862                    println!();
 863                }"},
 864            Mode::Insert,
 865        );
 866    }
 867
 868    #[gpui::test]
 869    async fn test_dd(cx: &mut gpui::TestAppContext) {
 870        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
 871        cx.assert("ˇ").await;
 872        cx.assert("The ˇquick").await;
 873        cx.assert_all(indoc! {"
 874                The qˇuick
 875                brown ˇfox
 876                jumps ˇover"})
 877            .await;
 878        cx.assert_exempted(
 879            indoc! {"
 880                The quick
 881                ˇ
 882                brown fox"},
 883            ExemptionFeatures::DeletionOnEmptyLine,
 884        )
 885        .await;
 886    }
 887
 888    #[gpui::test]
 889    async fn test_cc(cx: &mut gpui::TestAppContext) {
 890        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
 891        cx.assert("ˇ").await;
 892        cx.assert("The ˇquick").await;
 893        cx.assert_all(indoc! {"
 894                The quˇick
 895                brown ˇfox
 896                jumps ˇover"})
 897            .await;
 898        cx.assert(indoc! {"
 899                The quick
 900                ˇ
 901                brown fox"})
 902            .await;
 903    }
 904
 905    #[gpui::test]
 906    async fn test_p(cx: &mut gpui::TestAppContext) {
 907        let mut cx = NeovimBackedTestContext::new(cx).await;
 908        cx.set_shared_state(indoc! {"
 909                The quick brown
 910                fox juˇmps over
 911                the lazy dog"})
 912            .await;
 913
 914        cx.simulate_shared_keystrokes(["d", "d"]).await;
 915        cx.assert_state_matches().await;
 916
 917        cx.simulate_shared_keystroke("p").await;
 918        cx.assert_state_matches().await;
 919
 920        cx.set_shared_state(indoc! {"
 921                The quick brown
 922                fox ˇjumps over
 923                the lazy dog"})
 924            .await;
 925        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
 926        cx.set_shared_state(indoc! {"
 927                The quick brown
 928                fox jumps oveˇr
 929                the lazy dog"})
 930            .await;
 931        cx.simulate_shared_keystroke("p").await;
 932        cx.assert_state_matches().await;
 933    }
 934
 935    #[gpui::test]
 936    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
 937        let mut cx = NeovimBackedTestContext::new(cx).await;
 938
 939        for count in 1..=5 {
 940            cx.assert_binding_matches_all(
 941                [&count.to_string(), "w"],
 942                indoc! {"
 943                    ˇThe quˇickˇ browˇn
 944                    ˇ
 945                    ˇfox ˇjumpsˇ-ˇoˇver
 946                    ˇthe lazy dog
 947                "},
 948            )
 949            .await;
 950        }
 951    }
 952
 953    #[gpui::test]
 954    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
 955        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 956        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
 957    }
 958
 959    #[gpui::test]
 960    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
 961        let mut cx = NeovimBackedTestContext::new(cx).await;
 962        for count in 1..=3 {
 963            let test_case = indoc! {"
 964                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 965                ˇ    ˇbˇaaˇa ˇbˇbˇb
 966                ˇ
 967                ˇb
 968            "};
 969
 970            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
 971                .await;
 972
 973            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
 974                .await;
 975        }
 976    }
 977
 978    #[gpui::test]
 979    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
 980        let mut cx = NeovimBackedTestContext::new(cx).await;
 981        let test_case = indoc! {"
 982            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
 983            ˇ    ˇbˇaaˇa ˇbˇbˇb
 984            ˇ•••
 985            ˇb
 986            "
 987        };
 988
 989        for count in 1..=3 {
 990            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
 991                .await;
 992
 993            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
 994                .await;
 995        }
 996    }
 997
 998    #[gpui::test]
 999    async fn test_percent(cx: &mut TestAppContext) {
1000        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1001        cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1002        cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1003            .await;
1004        cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1005    }
1006}