normal.rs

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