normal.rs

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