normal.rs

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