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