normal.rs

   1mod change;
   2mod delete;
   3mod yank;
   4
   5use std::{borrow::Cow, cmp::Ordering, sync::Arc};
   6
   7use crate::{
   8    motion::Motion,
   9    object::Object,
  10    state::{Mode, Operator},
  11    Vim,
  12};
  13use collections::{HashMap, HashSet};
  14use editor::{
  15    display_map::ToDisplayPoint,
  16    scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
  17    Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
  18};
  19use gpui::{actions, impl_actions, AppContext, ViewContext, WindowContext};
  20use language::{AutoindentMode, Point, SelectionGoal};
  21use log::error;
  22use serde::Deserialize;
  23use workspace::Workspace;
  24
  25use self::{
  26    change::{change_motion, change_object},
  27    delete::{delete_motion, delete_object},
  28    yank::{yank_motion, yank_object},
  29};
  30
  31#[derive(Clone, PartialEq, Deserialize)]
  32struct Scroll(ScrollAmount);
  33
  34actions!(
  35    vim,
  36    [
  37        InsertAfter,
  38        InsertFirstNonWhitespace,
  39        InsertEndOfLine,
  40        InsertLineAbove,
  41        InsertLineBelow,
  42        DeleteLeft,
  43        DeleteRight,
  44        ChangeToEndOfLine,
  45        DeleteToEndOfLine,
  46        Paste,
  47        Yank,
  48    ]
  49);
  50
  51impl_actions!(vim, [Scroll]);
  52
  53pub fn init(cx: &mut AppContext) {
  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 WindowContext,
  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 WindowContext) {
 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 WindowContext) {
 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.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: Arc<str>, cx: &mut WindowContext) {
 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.clone(),
 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 gpui::TestAppContext;
 477    use indoc::indoc;
 478
 479    use crate::{
 480        state::{
 481            Mode::{self, *},
 482            Namespace, Operator,
 483        },
 484        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
 485    };
 486
 487    #[gpui::test]
 488    async fn test_h(cx: &mut gpui::TestAppContext) {
 489        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 490        cx.assert_all(indoc! {"
 491            ˇThe qˇuick
 492            ˇbrown"
 493        })
 494        .await;
 495    }
 496
 497    #[gpui::test]
 498    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 499        let mut cx = NeovimBackedTestContext::new(cx)
 500            .await
 501            .binding(["backspace"]);
 502        cx.assert_all(indoc! {"
 503            ˇThe qˇuick
 504            ˇbrown"
 505        })
 506        .await;
 507    }
 508
 509    #[gpui::test]
 510    async fn test_j(cx: &mut gpui::TestAppContext) {
 511        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
 512        cx.assert_all(indoc! {"
 513            ˇThe qˇuick broˇwn
 514            ˇfox jumps"
 515        })
 516        .await;
 517    }
 518
 519    #[gpui::test]
 520    async fn test_enter(cx: &mut gpui::TestAppContext) {
 521        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
 522        cx.assert_all(indoc! {"
 523            ˇThe qˇuick broˇwn
 524            ˇfox jumps"
 525        })
 526        .await;
 527    }
 528
 529    #[gpui::test]
 530    async fn test_k(cx: &mut gpui::TestAppContext) {
 531        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
 532        cx.assert_all(indoc! {"
 533            ˇThe qˇuick
 534            ˇbrown fˇox jumˇps"
 535        })
 536        .await;
 537    }
 538
 539    #[gpui::test]
 540    async fn test_l(cx: &mut gpui::TestAppContext) {
 541        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
 542        cx.assert_all(indoc! {"
 543            ˇThe qˇuicˇk
 544            ˇbrowˇn"})
 545            .await;
 546    }
 547
 548    #[gpui::test]
 549    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 550        let mut cx = NeovimBackedTestContext::new(cx).await;
 551        cx.assert_binding_matches_all(
 552            ["$"],
 553            indoc! {"
 554            ˇThe qˇuicˇk
 555            ˇbrowˇn"},
 556        )
 557        .await;
 558        cx.assert_binding_matches_all(
 559            ["0"],
 560            indoc! {"
 561                ˇThe qˇuicˇk
 562                ˇbrowˇn"},
 563        )
 564        .await;
 565    }
 566
 567    #[gpui::test]
 568    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 569        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
 570
 571        cx.assert_all(indoc! {"
 572                The ˇquick
 573
 574                brown fox jumps
 575                overˇ the lazy doˇg"})
 576            .await;
 577        cx.assert(indoc! {"
 578            The quiˇck
 579
 580            brown"})
 581            .await;
 582        cx.assert(indoc! {"
 583            The quiˇck
 584
 585            "})
 586            .await;
 587    }
 588
 589    #[gpui::test]
 590    async fn test_w(cx: &mut gpui::TestAppContext) {
 591        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
 592        cx.assert_all(indoc! {"
 593            The ˇquickˇ-ˇbrown
 594            ˇ
 595            ˇ
 596            ˇfox_jumps ˇover
 597            ˇthˇe"})
 598            .await;
 599        let mut cx = cx.binding(["shift-w"]);
 600        cx.assert_all(indoc! {"
 601            The ˇquickˇ-ˇbrown
 602            ˇ
 603            ˇ
 604            ˇfox_jumps ˇover
 605            ˇthˇe"})
 606            .await;
 607    }
 608
 609    #[gpui::test]
 610    async fn test_e(cx: &mut gpui::TestAppContext) {
 611        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
 612        cx.assert_all(indoc! {"
 613            Thˇe quicˇkˇ-browˇn
 614
 615
 616            fox_jumpˇs oveˇr
 617            thˇe"})
 618            .await;
 619        let mut cx = cx.binding(["shift-e"]);
 620        cx.assert_all(indoc! {"
 621            Thˇe quicˇkˇ-browˇn
 622
 623
 624            fox_jumpˇs oveˇr
 625            thˇe"})
 626            .await;
 627    }
 628
 629    #[gpui::test]
 630    async fn test_b(cx: &mut gpui::TestAppContext) {
 631        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
 632        cx.assert_all(indoc! {"
 633            ˇThe ˇquickˇ-ˇbrown
 634            ˇ
 635            ˇ
 636            ˇfox_jumps ˇover
 637            ˇthe"})
 638            .await;
 639        let mut cx = cx.binding(["shift-b"]);
 640        cx.assert_all(indoc! {"
 641            ˇThe ˇquickˇ-ˇbrown
 642            ˇ
 643            ˇ
 644            ˇfox_jumps ˇover
 645            ˇthe"})
 646            .await;
 647    }
 648
 649    #[gpui::test]
 650    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
 651        let mut cx = VimTestContext::new(cx, true).await;
 652
 653        // Can abort with escape to get back to normal mode
 654        cx.simulate_keystroke("g");
 655        assert_eq!(cx.mode(), Normal);
 656        assert_eq!(
 657            cx.active_operator(),
 658            Some(Operator::Namespace(Namespace::G))
 659        );
 660        cx.simulate_keystroke("escape");
 661        assert_eq!(cx.mode(), Normal);
 662        assert_eq!(cx.active_operator(), None);
 663    }
 664
 665    #[gpui::test]
 666    async fn test_gg(cx: &mut gpui::TestAppContext) {
 667        let mut cx = NeovimBackedTestContext::new(cx).await;
 668        cx.assert_binding_matches_all(
 669            ["g", "g"],
 670            indoc! {"
 671                The qˇuick
 672
 673                brown fox jumps
 674                over ˇthe laˇzy dog"},
 675        )
 676        .await;
 677        cx.assert_binding_matches(
 678            ["g", "g"],
 679            indoc! {"
 680
 681
 682                brown fox jumps
 683                over the laˇzy dog"},
 684        )
 685        .await;
 686        cx.assert_binding_matches(
 687            ["2", "g", "g"],
 688            indoc! {"
 689                ˇ
 690
 691                brown fox jumps
 692                over the lazydog"},
 693        )
 694        .await;
 695    }
 696
 697    #[gpui::test]
 698    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 699        let mut cx = NeovimBackedTestContext::new(cx).await;
 700        cx.assert_binding_matches_all(
 701            ["shift-g"],
 702            indoc! {"
 703                The qˇuick
 704
 705                brown fox jumps
 706                over ˇthe laˇzy dog"},
 707        )
 708        .await;
 709        cx.assert_binding_matches(
 710            ["shift-g"],
 711            indoc! {"
 712
 713
 714                brown fox jumps
 715                over the laˇzy dog"},
 716        )
 717        .await;
 718        cx.assert_binding_matches(
 719            ["2", "shift-g"],
 720            indoc! {"
 721                ˇ
 722
 723                brown fox jumps
 724                over the lazydog"},
 725        )
 726        .await;
 727    }
 728
 729    #[gpui::test]
 730    async fn test_a(cx: &mut gpui::TestAppContext) {
 731        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
 732        cx.assert_all("The qˇuicˇk").await;
 733    }
 734
 735    #[gpui::test]
 736    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 737        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
 738        cx.assert_all(indoc! {"
 739            ˇ
 740            The qˇuick
 741            brown ˇfox "})
 742            .await;
 743    }
 744
 745    #[gpui::test]
 746    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 747        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
 748        cx.assert("The qˇuick").await;
 749        cx.assert(" The qˇuick").await;
 750        cx.assert("ˇ").await;
 751        cx.assert(indoc! {"
 752                The qˇuick
 753                brown fox"})
 754            .await;
 755        cx.assert(indoc! {"
 756                ˇ
 757                The quick"})
 758            .await;
 759        // Indoc disallows trailing whitespace.
 760        cx.assert("   ˇ \nThe quick").await;
 761    }
 762
 763    #[gpui::test]
 764    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 765        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
 766        cx.assert("The qˇuick").await;
 767        cx.assert(" The qˇuick").await;
 768        cx.assert("ˇ").await;
 769        cx.assert(indoc! {"
 770                The qˇuick
 771                brown fox"})
 772            .await;
 773        cx.assert(indoc! {"
 774                ˇ
 775                The quick"})
 776            .await;
 777    }
 778
 779    #[gpui::test]
 780    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 781        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
 782        cx.assert(indoc! {"
 783                The qˇuick
 784                brown fox"})
 785            .await;
 786        cx.assert(indoc! {"
 787                The quick
 788                ˇ
 789                brown fox"})
 790            .await;
 791    }
 792
 793    #[gpui::test]
 794    async fn test_x(cx: &mut gpui::TestAppContext) {
 795        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
 796        cx.assert_all("ˇTeˇsˇt").await;
 797        cx.assert(indoc! {"
 798                Tesˇt
 799                test"})
 800            .await;
 801    }
 802
 803    #[gpui::test]
 804    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
 805        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
 806        cx.assert_all("ˇTˇeˇsˇt").await;
 807        cx.assert(indoc! {"
 808                Test
 809                ˇtest"})
 810            .await;
 811    }
 812
 813    #[gpui::test]
 814    async fn test_o(cx: &mut gpui::TestAppContext) {
 815        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
 816        cx.assert("ˇ").await;
 817        cx.assert("The ˇquick").await;
 818        cx.assert_all(indoc! {"
 819                The qˇuick
 820                brown ˇfox
 821                jumps ˇover"})
 822            .await;
 823        cx.assert(indoc! {"
 824                The quick
 825                ˇ
 826                brown fox"})
 827            .await;
 828
 829        cx.assert_manual(
 830            indoc! {"
 831                fn test() {
 832                    println!(ˇ);
 833                }"},
 834            Mode::Normal,
 835            indoc! {"
 836                fn test() {
 837                    println!();
 838                    ˇ
 839                }"},
 840            Mode::Insert,
 841        );
 842
 843        cx.assert_manual(
 844            indoc! {"
 845                fn test(ˇ) {
 846                    println!();
 847                }"},
 848            Mode::Normal,
 849            indoc! {"
 850                fn test() {
 851                    ˇ
 852                    println!();
 853                }"},
 854            Mode::Insert,
 855        );
 856    }
 857
 858    #[gpui::test]
 859    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
 860        let cx = NeovimBackedTestContext::new(cx).await;
 861        let mut cx = cx.binding(["shift-o"]);
 862        cx.assert("ˇ").await;
 863        cx.assert("The ˇquick").await;
 864        cx.assert_all(indoc! {"
 865            The qˇuick
 866            brown ˇfox
 867            jumps ˇover"})
 868            .await;
 869        cx.assert(indoc! {"
 870            The quick
 871            ˇ
 872            brown fox"})
 873            .await;
 874
 875        // Our indentation is smarter than vims. So we don't match here
 876        cx.assert_manual(
 877            indoc! {"
 878                fn test() {
 879                    println!(ˇ);
 880                }"},
 881            Mode::Normal,
 882            indoc! {"
 883                fn test() {
 884                    ˇ
 885                    println!();
 886                }"},
 887            Mode::Insert,
 888        );
 889        cx.assert_manual(
 890            indoc! {"
 891                fn test(ˇ) {
 892                    println!();
 893                }"},
 894            Mode::Normal,
 895            indoc! {"
 896                ˇ
 897                fn test() {
 898                    println!();
 899                }"},
 900            Mode::Insert,
 901        );
 902    }
 903
 904    #[gpui::test]
 905    async fn test_dd(cx: &mut gpui::TestAppContext) {
 906        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
 907        cx.assert("ˇ").await;
 908        cx.assert("The ˇquick").await;
 909        cx.assert_all(indoc! {"
 910                The qˇuick
 911                brown ˇfox
 912                jumps ˇover"})
 913            .await;
 914        cx.assert_exempted(
 915            indoc! {"
 916                The quick
 917                ˇ
 918                brown fox"},
 919            ExemptionFeatures::DeletionOnEmptyLine,
 920        )
 921        .await;
 922    }
 923
 924    #[gpui::test]
 925    async fn test_cc(cx: &mut gpui::TestAppContext) {
 926        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
 927        cx.assert("ˇ").await;
 928        cx.assert("The ˇquick").await;
 929        cx.assert_all(indoc! {"
 930                The quˇick
 931                brown ˇfox
 932                jumps ˇover"})
 933            .await;
 934        cx.assert(indoc! {"
 935                The quick
 936                ˇ
 937                brown fox"})
 938            .await;
 939    }
 940
 941    #[gpui::test]
 942    async fn test_p(cx: &mut gpui::TestAppContext) {
 943        let mut cx = NeovimBackedTestContext::new(cx).await;
 944        cx.set_shared_state(indoc! {"
 945                The quick brown
 946                fox juˇmps over
 947                the lazy dog"})
 948            .await;
 949
 950        cx.simulate_shared_keystrokes(["d", "d"]).await;
 951        cx.assert_state_matches().await;
 952
 953        cx.simulate_shared_keystroke("p").await;
 954        cx.assert_state_matches().await;
 955
 956        cx.set_shared_state(indoc! {"
 957                The quick brown
 958                fox ˇjumps over
 959                the lazy dog"})
 960            .await;
 961        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
 962        cx.set_shared_state(indoc! {"
 963                The quick brown
 964                fox jumps oveˇr
 965                the lazy dog"})
 966            .await;
 967        cx.simulate_shared_keystroke("p").await;
 968        cx.assert_state_matches().await;
 969    }
 970
 971    #[gpui::test]
 972    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
 973        let mut cx = NeovimBackedTestContext::new(cx).await;
 974
 975        for count in 1..=5 {
 976            cx.assert_binding_matches_all(
 977                [&count.to_string(), "w"],
 978                indoc! {"
 979                    ˇThe quˇickˇ browˇn
 980                    ˇ
 981                    ˇfox ˇjumpsˇ-ˇoˇver
 982                    ˇthe lazy dog
 983                "},
 984            )
 985            .await;
 986        }
 987    }
 988
 989    #[gpui::test]
 990    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
 991        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
 992        cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
 993    }
 994
 995    #[gpui::test]
 996    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
 997        let mut cx = NeovimBackedTestContext::new(cx).await;
 998        for count in 1..=3 {
 999            let test_case = indoc! {"
1000                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1001                ˇ    ˇbˇaaˇa ˇbˇbˇb
1002                ˇ
1003                ˇb
1004            "};
1005
1006            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
1007                .await;
1008
1009            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
1010                .await;
1011        }
1012    }
1013
1014    #[gpui::test]
1015    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1016        let mut cx = NeovimBackedTestContext::new(cx).await;
1017        let test_case = indoc! {"
1018            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1019            ˇ    ˇbˇaaˇa ˇbˇbˇb
1020            ˇ•••
1021            ˇb
1022            "
1023        };
1024
1025        for count in 1..=3 {
1026            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
1027                .await;
1028
1029            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
1030                .await;
1031        }
1032    }
1033
1034    #[gpui::test]
1035    async fn test_percent(cx: &mut TestAppContext) {
1036        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1037        cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1038        cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1039            .await;
1040        cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1041    }
1042}