normal.rs

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