normal.rs

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