normal.rs

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