normal.rs

   1mod case;
   2mod change;
   3mod delete;
   4mod increment;
   5pub(crate) mod mark;
   6mod paste;
   7pub(crate) mod repeat;
   8mod scroll;
   9pub(crate) mod search;
  10pub mod substitute;
  11mod toggle_comments;
  12pub(crate) mod yank;
  13
  14use std::collections::HashMap;
  15use std::sync::Arc;
  16
  17use crate::{
  18    indent::IndentDirection,
  19    motion::{self, first_non_whitespace, next_line_end, right, Motion},
  20    object::Object,
  21    state::{Mode, Operator},
  22    surrounds::SurroundsType,
  23    Vim,
  24};
  25use case::CaseTarget;
  26use collections::BTreeSet;
  27use editor::scroll::Autoscroll;
  28use editor::Anchor;
  29use editor::Bias;
  30use editor::Editor;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{actions, Context, Window};
  33use language::{Point, SelectionGoal, ToPoint};
  34use log::error;
  35use multi_buffer::MultiBufferRow;
  36
  37actions!(
  38    vim,
  39    [
  40        InsertAfter,
  41        InsertBefore,
  42        InsertFirstNonWhitespace,
  43        InsertEndOfLine,
  44        InsertLineAbove,
  45        InsertLineBelow,
  46        InsertAtPrevious,
  47        JoinLines,
  48        JoinLinesNoWhitespace,
  49        DeleteLeft,
  50        DeleteRight,
  51        ChangeToEndOfLine,
  52        DeleteToEndOfLine,
  53        Yank,
  54        YankLine,
  55        ChangeCase,
  56        ConvertToUpperCase,
  57        ConvertToLowerCase,
  58        ToggleComments,
  59        ShowLocation,
  60        Undo,
  61        Redo,
  62    ]
  63);
  64
  65pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  66    Vim::action(editor, cx, Vim::insert_after);
  67    Vim::action(editor, cx, Vim::insert_before);
  68    Vim::action(editor, cx, Vim::insert_first_non_whitespace);
  69    Vim::action(editor, cx, Vim::insert_end_of_line);
  70    Vim::action(editor, cx, Vim::insert_line_above);
  71    Vim::action(editor, cx, Vim::insert_line_below);
  72    Vim::action(editor, cx, Vim::insert_at_previous);
  73    Vim::action(editor, cx, Vim::change_case);
  74    Vim::action(editor, cx, Vim::convert_to_upper_case);
  75    Vim::action(editor, cx, Vim::convert_to_lower_case);
  76    Vim::action(editor, cx, Vim::yank_line);
  77    Vim::action(editor, cx, Vim::toggle_comments);
  78    Vim::action(editor, cx, Vim::paste);
  79    Vim::action(editor, cx, Vim::show_location);
  80
  81    Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
  82        vim.record_current_action(cx);
  83        let times = Vim::take_count(cx);
  84        vim.delete_motion(Motion::Left, times, window, cx);
  85    });
  86    Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
  87        vim.record_current_action(cx);
  88        let times = Vim::take_count(cx);
  89        vim.delete_motion(Motion::Right, times, window, cx);
  90    });
  91    Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
  92        vim.start_recording(cx);
  93        let times = Vim::take_count(cx);
  94        vim.change_motion(
  95            Motion::EndOfLine {
  96                display_lines: false,
  97            },
  98            times,
  99            window,
 100            cx,
 101        );
 102    });
 103    Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
 104        vim.record_current_action(cx);
 105        let times = Vim::take_count(cx);
 106        vim.delete_motion(
 107            Motion::EndOfLine {
 108                display_lines: false,
 109            },
 110            times,
 111            window,
 112            cx,
 113        );
 114    });
 115    Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| {
 116        vim.join_lines_impl(true, window, cx);
 117    });
 118
 119    Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| {
 120        vim.join_lines_impl(false, window, cx);
 121    });
 122
 123    Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
 124        let times = Vim::take_count(cx);
 125        vim.update_editor(window, cx, |_, editor, window, cx| {
 126            for _ in 0..times.unwrap_or(1) {
 127                editor.undo(&editor::actions::Undo, window, cx);
 128            }
 129        });
 130    });
 131    Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
 132        let times = Vim::take_count(cx);
 133        vim.update_editor(window, cx, |_, editor, window, cx| {
 134            for _ in 0..times.unwrap_or(1) {
 135                editor.redo(&editor::actions::Redo, window, cx);
 136            }
 137        });
 138    });
 139
 140    repeat::register(editor, cx);
 141    scroll::register(editor, cx);
 142    search::register(editor, cx);
 143    substitute::register(editor, cx);
 144    increment::register(editor, cx);
 145}
 146
 147impl Vim {
 148    pub fn normal_motion(
 149        &mut self,
 150        motion: Motion,
 151        operator: Option<Operator>,
 152        times: Option<usize>,
 153        window: &mut Window,
 154        cx: &mut Context<Self>,
 155    ) {
 156        match operator {
 157            None => self.move_cursor(motion, times, window, cx),
 158            Some(Operator::Change) => self.change_motion(motion, times, window, cx),
 159            Some(Operator::Delete) => self.delete_motion(motion, times, window, cx),
 160            Some(Operator::Yank) => self.yank_motion(motion, times, window, cx),
 161            Some(Operator::AddSurrounds { target: None }) => {}
 162            Some(Operator::Indent) => {
 163                self.indent_motion(motion, times, IndentDirection::In, window, cx)
 164            }
 165            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, window, cx),
 166            Some(Operator::Outdent) => {
 167                self.indent_motion(motion, times, IndentDirection::Out, window, cx)
 168            }
 169            Some(Operator::AutoIndent) => {
 170                self.indent_motion(motion, times, IndentDirection::Auto, window, cx)
 171            }
 172            Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, window, cx),
 173            Some(Operator::Lowercase) => {
 174                self.change_case_motion(motion, times, CaseTarget::Lowercase, window, cx)
 175            }
 176            Some(Operator::Uppercase) => {
 177                self.change_case_motion(motion, times, CaseTarget::Uppercase, window, cx)
 178            }
 179            Some(Operator::OppositeCase) => {
 180                self.change_case_motion(motion, times, CaseTarget::OppositeCase, window, cx)
 181            }
 182            Some(Operator::ToggleComments) => {
 183                self.toggle_comments_motion(motion, times, window, cx)
 184            }
 185            Some(Operator::ReplaceWithRegister) => {
 186                self.replace_with_register_motion(motion, times, window, cx)
 187            }
 188            Some(Operator::Exchange) => self.exchange_motion(motion, times, window, cx),
 189            Some(operator) => {
 190                // Can't do anything for text objects, Ignoring
 191                error!("Unexpected normal mode motion operator: {:?}", operator)
 192            }
 193        }
 194        // Exit temporary normal mode (if active).
 195        self.exit_temporary_normal(window, cx);
 196    }
 197
 198    pub fn normal_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 199        let mut waiting_operator: Option<Operator> = None;
 200        match self.maybe_pop_operator() {
 201            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
 202                Some(Operator::Change) => self.change_object(object, around, window, cx),
 203                Some(Operator::Delete) => self.delete_object(object, around, window, cx),
 204                Some(Operator::Yank) => self.yank_object(object, around, window, cx),
 205                Some(Operator::Indent) => {
 206                    self.indent_object(object, around, IndentDirection::In, window, cx)
 207                }
 208                Some(Operator::Outdent) => {
 209                    self.indent_object(object, around, IndentDirection::Out, window, cx)
 210                }
 211                Some(Operator::AutoIndent) => {
 212                    self.indent_object(object, around, IndentDirection::Auto, window, cx)
 213                }
 214                Some(Operator::ShellCommand) => {
 215                    self.shell_command_object(object, around, window, cx);
 216                }
 217                Some(Operator::Rewrap) => self.rewrap_object(object, around, window, cx),
 218                Some(Operator::Lowercase) => {
 219                    self.change_case_object(object, around, CaseTarget::Lowercase, window, cx)
 220                }
 221                Some(Operator::Uppercase) => {
 222                    self.change_case_object(object, around, CaseTarget::Uppercase, window, cx)
 223                }
 224                Some(Operator::OppositeCase) => {
 225                    self.change_case_object(object, around, CaseTarget::OppositeCase, window, cx)
 226                }
 227                Some(Operator::AddSurrounds { target: None }) => {
 228                    waiting_operator = Some(Operator::AddSurrounds {
 229                        target: Some(SurroundsType::Object(object, around)),
 230                    });
 231                }
 232                Some(Operator::ToggleComments) => {
 233                    self.toggle_comments_object(object, around, window, cx)
 234                }
 235                Some(Operator::ReplaceWithRegister) => {
 236                    self.replace_with_register_object(object, around, window, cx)
 237                }
 238                Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
 239                _ => {
 240                    // Can't do anything for namespace operators. Ignoring
 241                }
 242            },
 243            Some(Operator::DeleteSurrounds) => {
 244                waiting_operator = Some(Operator::DeleteSurrounds);
 245            }
 246            Some(Operator::ChangeSurrounds { target: None }) => {
 247                if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
 248                    waiting_operator = Some(Operator::ChangeSurrounds {
 249                        target: Some(object),
 250                    });
 251                }
 252            }
 253            _ => {
 254                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 255            }
 256        }
 257        self.clear_operator(window, cx);
 258        if let Some(operator) = waiting_operator {
 259            self.push_operator(operator, window, cx);
 260        }
 261    }
 262
 263    pub(crate) fn move_cursor(
 264        &mut self,
 265        motion: Motion,
 266        times: Option<usize>,
 267        window: &mut Window,
 268        cx: &mut Context<Self>,
 269    ) {
 270        self.update_editor(window, cx, |_, editor, window, cx| {
 271            let text_layout_details = editor.text_layout_details(window);
 272            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 273                s.move_cursors_with(|map, cursor, goal| {
 274                    motion
 275                        .move_point(map, cursor, goal, times, &text_layout_details)
 276                        .unwrap_or((cursor, goal))
 277                })
 278            })
 279        });
 280    }
 281
 282    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 283        self.start_recording(cx);
 284        self.switch_mode(Mode::Insert, false, window, cx);
 285        self.update_editor(window, cx, |_, editor, window, cx| {
 286            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 287                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 288            });
 289        });
 290    }
 291
 292    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 293        self.start_recording(cx);
 294        self.switch_mode(Mode::Insert, false, window, cx);
 295    }
 296
 297    fn insert_first_non_whitespace(
 298        &mut self,
 299        _: &InsertFirstNonWhitespace,
 300        window: &mut Window,
 301        cx: &mut Context<Self>,
 302    ) {
 303        self.start_recording(cx);
 304        self.switch_mode(Mode::Insert, false, window, cx);
 305        self.update_editor(window, cx, |_, editor, window, cx| {
 306            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 307                s.move_cursors_with(|map, cursor, _| {
 308                    (
 309                        first_non_whitespace(map, false, cursor),
 310                        SelectionGoal::None,
 311                    )
 312                });
 313            });
 314        });
 315    }
 316
 317    fn insert_end_of_line(
 318        &mut self,
 319        _: &InsertEndOfLine,
 320        window: &mut Window,
 321        cx: &mut Context<Self>,
 322    ) {
 323        self.start_recording(cx);
 324        self.switch_mode(Mode::Insert, false, window, cx);
 325        self.update_editor(window, cx, |_, editor, window, cx| {
 326            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 327                s.move_cursors_with(|map, cursor, _| {
 328                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 329                });
 330            });
 331        });
 332    }
 333
 334    fn insert_at_previous(
 335        &mut self,
 336        _: &InsertAtPrevious,
 337        window: &mut Window,
 338        cx: &mut Context<Self>,
 339    ) {
 340        self.start_recording(cx);
 341        self.switch_mode(Mode::Insert, false, window, cx);
 342        self.update_editor(window, cx, |vim, editor, window, cx| {
 343            if let Some(marks) = vim.marks.get("^") {
 344                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 345                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 346                });
 347            }
 348        });
 349    }
 350
 351    fn insert_line_above(
 352        &mut self,
 353        _: &InsertLineAbove,
 354        window: &mut Window,
 355        cx: &mut Context<Self>,
 356    ) {
 357        self.start_recording(cx);
 358        self.switch_mode(Mode::Insert, false, window, cx);
 359        self.update_editor(window, cx, |_, editor, window, cx| {
 360            editor.transact(window, cx, |editor, window, cx| {
 361                let selections = editor.selections.all::<Point>(cx);
 362                let snapshot = editor.buffer().read(cx).snapshot(cx);
 363
 364                let selection_start_rows: BTreeSet<u32> = selections
 365                    .into_iter()
 366                    .map(|selection| selection.start.row)
 367                    .collect();
 368                let edits = selection_start_rows
 369                    .into_iter()
 370                    .map(|row| {
 371                        let indent = snapshot
 372                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 373                            .chars()
 374                            .collect::<String>();
 375
 376                        let start_of_line = Point::new(row, 0);
 377                        (start_of_line..start_of_line, indent + "\n")
 378                    })
 379                    .collect::<Vec<_>>();
 380                editor.edit_with_autoindent(edits, cx);
 381                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 382                    s.move_cursors_with(|map, cursor, _| {
 383                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 384                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 385                        (insert_point, SelectionGoal::None)
 386                    });
 387                });
 388            });
 389        });
 390    }
 391
 392    fn insert_line_below(
 393        &mut self,
 394        _: &InsertLineBelow,
 395        window: &mut Window,
 396        cx: &mut Context<Self>,
 397    ) {
 398        self.start_recording(cx);
 399        self.switch_mode(Mode::Insert, false, window, cx);
 400        self.update_editor(window, cx, |_, editor, window, cx| {
 401            let text_layout_details = editor.text_layout_details(window);
 402            editor.transact(window, cx, |editor, window, cx| {
 403                let selections = editor.selections.all::<Point>(cx);
 404                let snapshot = editor.buffer().read(cx).snapshot(cx);
 405
 406                let selection_end_rows: BTreeSet<u32> = selections
 407                    .into_iter()
 408                    .map(|selection| selection.end.row)
 409                    .collect();
 410                let edits = selection_end_rows
 411                    .into_iter()
 412                    .map(|row| {
 413                        let indent = snapshot
 414                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 415                            .chars()
 416                            .collect::<String>();
 417
 418                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 419                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 420                    })
 421                    .collect::<Vec<_>>();
 422                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 423                    s.maybe_move_cursors_with(|map, cursor, goal| {
 424                        Motion::CurrentLine.move_point(
 425                            map,
 426                            cursor,
 427                            goal,
 428                            None,
 429                            &text_layout_details,
 430                        )
 431                    });
 432                });
 433                editor.edit_with_autoindent(edits, cx);
 434            });
 435        });
 436    }
 437
 438    fn join_lines_impl(
 439        &mut self,
 440        insert_whitespace: bool,
 441        window: &mut Window,
 442        cx: &mut Context<Self>,
 443    ) {
 444        self.record_current_action(cx);
 445        let mut times = Vim::take_count(cx).unwrap_or(1);
 446        if self.mode.is_visual() {
 447            times = 1;
 448        } else if times > 1 {
 449            // 2J joins two lines together (same as J or 1J)
 450            times -= 1;
 451        }
 452
 453        self.update_editor(window, cx, |_, editor, window, cx| {
 454            editor.transact(window, cx, |editor, window, cx| {
 455                for _ in 0..times {
 456                    editor.join_lines_impl(insert_whitespace, window, cx)
 457                }
 458            })
 459        });
 460        if self.mode.is_visual() {
 461            self.switch_mode(Mode::Normal, true, window, cx)
 462        }
 463    }
 464
 465    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 466        let count = Vim::take_count(cx);
 467        self.yank_motion(motion::Motion::CurrentLine, count, window, cx)
 468    }
 469
 470    fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
 471        let count = Vim::take_count(cx);
 472        self.update_editor(window, cx, |vim, editor, _window, cx| {
 473            let selection = editor.selections.newest_anchor();
 474            if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
 475                let filename = if let Some(file) = buffer.read(cx).file() {
 476                    if count.is_some() {
 477                        if let Some(local) = file.as_local() {
 478                            local.abs_path(cx).to_string_lossy().to_string()
 479                        } else {
 480                            file.full_path(cx).to_string_lossy().to_string()
 481                        }
 482                    } else {
 483                        file.path().to_string_lossy().to_string()
 484                    }
 485                } else {
 486                    "[No Name]".into()
 487                };
 488                let buffer = buffer.read(cx);
 489                let snapshot = buffer.snapshot();
 490                let lines = buffer.max_point().row + 1;
 491                let current_line = selection.head().text_anchor.to_point(&snapshot).row;
 492                let percentage = current_line as f32 / lines as f32;
 493                let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 494                vim.status_label = Some(
 495                    format!(
 496                        "{}{} {} lines --{:.0}%--",
 497                        filename,
 498                        modified,
 499                        lines,
 500                        percentage * 100.0,
 501                    )
 502                    .into(),
 503                );
 504                cx.notify();
 505            }
 506        });
 507    }
 508
 509    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 510        self.record_current_action(cx);
 511        self.store_visual_marks(window, cx);
 512        self.update_editor(window, cx, |vim, editor, window, cx| {
 513            editor.transact(window, cx, |editor, window, cx| {
 514                let original_positions = vim.save_selection_starts(editor, cx);
 515                editor.toggle_comments(&Default::default(), window, cx);
 516                vim.restore_selection_cursors(editor, window, cx, original_positions);
 517            });
 518        });
 519        if self.mode.is_visual() {
 520            self.switch_mode(Mode::Normal, true, window, cx)
 521        }
 522    }
 523
 524    pub(crate) fn normal_replace(
 525        &mut self,
 526        text: Arc<str>,
 527        window: &mut Window,
 528        cx: &mut Context<Self>,
 529    ) {
 530        let count = Vim::take_count(cx).unwrap_or(1);
 531        self.stop_recording(cx);
 532        self.update_editor(window, cx, |_, editor, window, cx| {
 533            editor.transact(window, cx, |editor, window, cx| {
 534                editor.set_clip_at_line_ends(false, cx);
 535                let (map, display_selections) = editor.selections.all_display(cx);
 536
 537                let mut edits = Vec::new();
 538                for selection in display_selections {
 539                    let mut range = selection.range();
 540                    for _ in 0..count {
 541                        let new_point = movement::saturating_right(&map, range.end);
 542                        if range.end == new_point {
 543                            return;
 544                        }
 545                        range.end = new_point;
 546                    }
 547
 548                    edits.push((
 549                        range.start.to_offset(&map, Bias::Left)
 550                            ..range.end.to_offset(&map, Bias::Left),
 551                        text.repeat(count),
 552                    ))
 553                }
 554
 555                editor.edit(edits, cx);
 556                editor.set_clip_at_line_ends(true, cx);
 557                editor.change_selections(None, window, cx, |s| {
 558                    s.move_with(|map, selection| {
 559                        let point = movement::saturating_left(map, selection.head());
 560                        selection.collapse_to(point, SelectionGoal::None)
 561                    });
 562                });
 563            });
 564        });
 565        self.pop_operator(window, cx);
 566    }
 567
 568    pub fn save_selection_starts(
 569        &self,
 570        editor: &Editor,
 571
 572        cx: &mut Context<Editor>,
 573    ) -> HashMap<usize, Anchor> {
 574        let (map, selections) = editor.selections.all_display(cx);
 575        selections
 576            .iter()
 577            .map(|selection| {
 578                (
 579                    selection.id,
 580                    map.display_point_to_anchor(selection.start, Bias::Right),
 581                )
 582            })
 583            .collect::<HashMap<_, _>>()
 584    }
 585
 586    pub fn restore_selection_cursors(
 587        &self,
 588        editor: &mut Editor,
 589        window: &mut Window,
 590        cx: &mut Context<Editor>,
 591        mut positions: HashMap<usize, Anchor>,
 592    ) {
 593        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 594            s.move_with(|map, selection| {
 595                if let Some(anchor) = positions.remove(&selection.id) {
 596                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 597                }
 598            });
 599        });
 600    }
 601
 602    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 603        if self.temp_mode {
 604            self.switch_mode(Mode::Insert, true, window, cx);
 605        }
 606    }
 607}
 608#[cfg(test)]
 609mod test {
 610    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 611    use indoc::indoc;
 612    use language::language_settings::AllLanguageSettings;
 613    use settings::SettingsStore;
 614
 615    use crate::{
 616        motion,
 617        state::Mode::{self},
 618        test::{NeovimBackedTestContext, VimTestContext},
 619        VimSettings,
 620    };
 621
 622    #[gpui::test]
 623    async fn test_h(cx: &mut gpui::TestAppContext) {
 624        let mut cx = NeovimBackedTestContext::new(cx).await;
 625        cx.simulate_at_each_offset(
 626            "h",
 627            indoc! {"
 628            ˇThe qˇuick
 629            ˇbrown"
 630            },
 631        )
 632        .await
 633        .assert_matches();
 634    }
 635
 636    #[gpui::test]
 637    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 638        let mut cx = NeovimBackedTestContext::new(cx).await;
 639        cx.simulate_at_each_offset(
 640            "backspace",
 641            indoc! {"
 642            ˇThe qˇuick
 643            ˇbrown"
 644            },
 645        )
 646        .await
 647        .assert_matches();
 648    }
 649
 650    #[gpui::test]
 651    async fn test_j(cx: &mut gpui::TestAppContext) {
 652        let mut cx = NeovimBackedTestContext::new(cx).await;
 653
 654        cx.set_shared_state(indoc! {"
 655            aaˇaa
 656            😃😃"
 657        })
 658        .await;
 659        cx.simulate_shared_keystrokes("j").await;
 660        cx.shared_state().await.assert_eq(indoc! {"
 661            aaaa
 662            😃ˇ😃"
 663        });
 664
 665        cx.simulate_at_each_offset(
 666            "j",
 667            indoc! {"
 668                ˇThe qˇuick broˇwn
 669                ˇfox jumps"
 670            },
 671        )
 672        .await
 673        .assert_matches();
 674    }
 675
 676    #[gpui::test]
 677    async fn test_enter(cx: &mut gpui::TestAppContext) {
 678        let mut cx = NeovimBackedTestContext::new(cx).await;
 679        cx.simulate_at_each_offset(
 680            "enter",
 681            indoc! {"
 682            ˇThe qˇuick broˇwn
 683            ˇfox jumps"
 684            },
 685        )
 686        .await
 687        .assert_matches();
 688    }
 689
 690    #[gpui::test]
 691    async fn test_k(cx: &mut gpui::TestAppContext) {
 692        let mut cx = NeovimBackedTestContext::new(cx).await;
 693        cx.simulate_at_each_offset(
 694            "k",
 695            indoc! {"
 696            ˇThe qˇuick
 697            ˇbrown fˇox jumˇps"
 698            },
 699        )
 700        .await
 701        .assert_matches();
 702    }
 703
 704    #[gpui::test]
 705    async fn test_l(cx: &mut gpui::TestAppContext) {
 706        let mut cx = NeovimBackedTestContext::new(cx).await;
 707        cx.simulate_at_each_offset(
 708            "l",
 709            indoc! {"
 710            ˇThe qˇuicˇk
 711            ˇbrowˇn"},
 712        )
 713        .await
 714        .assert_matches();
 715    }
 716
 717    #[gpui::test]
 718    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 719        let mut cx = NeovimBackedTestContext::new(cx).await;
 720        cx.simulate_at_each_offset(
 721            "$",
 722            indoc! {"
 723            ˇThe qˇuicˇk
 724            ˇbrowˇn"},
 725        )
 726        .await
 727        .assert_matches();
 728        cx.simulate_at_each_offset(
 729            "0",
 730            indoc! {"
 731                ˇThe qˇuicˇk
 732                ˇbrowˇn"},
 733        )
 734        .await
 735        .assert_matches();
 736    }
 737
 738    #[gpui::test]
 739    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 740        let mut cx = NeovimBackedTestContext::new(cx).await;
 741
 742        cx.simulate_at_each_offset(
 743            "shift-g",
 744            indoc! {"
 745                The ˇquick
 746
 747                brown fox jumps
 748                overˇ the lazy doˇg"},
 749        )
 750        .await
 751        .assert_matches();
 752        cx.simulate(
 753            "shift-g",
 754            indoc! {"
 755            The quiˇck
 756
 757            brown"},
 758        )
 759        .await
 760        .assert_matches();
 761        cx.simulate(
 762            "shift-g",
 763            indoc! {"
 764            The quiˇck
 765
 766            "},
 767        )
 768        .await
 769        .assert_matches();
 770    }
 771
 772    #[gpui::test]
 773    async fn test_w(cx: &mut gpui::TestAppContext) {
 774        let mut cx = NeovimBackedTestContext::new(cx).await;
 775        cx.simulate_at_each_offset(
 776            "w",
 777            indoc! {"
 778            The ˇquickˇ-ˇbrown
 779            ˇ
 780            ˇ
 781            ˇfox_jumps ˇover
 782            ˇthˇe"},
 783        )
 784        .await
 785        .assert_matches();
 786        cx.simulate_at_each_offset(
 787            "shift-w",
 788            indoc! {"
 789            The ˇquickˇ-ˇbrown
 790            ˇ
 791            ˇ
 792            ˇfox_jumps ˇover
 793            ˇthˇe"},
 794        )
 795        .await
 796        .assert_matches();
 797    }
 798
 799    #[gpui::test]
 800    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 801        let mut cx = NeovimBackedTestContext::new(cx).await;
 802        cx.simulate_at_each_offset(
 803            "e",
 804            indoc! {"
 805            Thˇe quicˇkˇ-browˇn
 806
 807
 808            fox_jumpˇs oveˇr
 809            thˇe"},
 810        )
 811        .await
 812        .assert_matches();
 813        cx.simulate_at_each_offset(
 814            "shift-e",
 815            indoc! {"
 816            Thˇe quicˇkˇ-browˇn
 817
 818
 819            fox_jumpˇs oveˇr
 820            thˇe"},
 821        )
 822        .await
 823        .assert_matches();
 824    }
 825
 826    #[gpui::test]
 827    async fn test_b(cx: &mut gpui::TestAppContext) {
 828        let mut cx = NeovimBackedTestContext::new(cx).await;
 829        cx.simulate_at_each_offset(
 830            "b",
 831            indoc! {"
 832            ˇThe ˇquickˇ-ˇbrown
 833            ˇ
 834            ˇ
 835            ˇfox_jumps ˇover
 836            ˇthe"},
 837        )
 838        .await
 839        .assert_matches();
 840        cx.simulate_at_each_offset(
 841            "shift-b",
 842            indoc! {"
 843            ˇThe ˇquickˇ-ˇbrown
 844            ˇ
 845            ˇ
 846            ˇfox_jumps ˇover
 847            ˇthe"},
 848        )
 849        .await
 850        .assert_matches();
 851    }
 852
 853    #[gpui::test]
 854    async fn test_gg(cx: &mut gpui::TestAppContext) {
 855        let mut cx = NeovimBackedTestContext::new(cx).await;
 856        cx.simulate_at_each_offset(
 857            "g g",
 858            indoc! {"
 859                The qˇuick
 860
 861                brown fox jumps
 862                over ˇthe laˇzy dog"},
 863        )
 864        .await
 865        .assert_matches();
 866        cx.simulate(
 867            "g g",
 868            indoc! {"
 869
 870
 871                brown fox jumps
 872                over the laˇzy dog"},
 873        )
 874        .await
 875        .assert_matches();
 876        cx.simulate(
 877            "2 g g",
 878            indoc! {"
 879                ˇ
 880
 881                brown fox jumps
 882                over the lazydog"},
 883        )
 884        .await
 885        .assert_matches();
 886    }
 887
 888    #[gpui::test]
 889    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 890        let mut cx = NeovimBackedTestContext::new(cx).await;
 891        cx.simulate_at_each_offset(
 892            "shift-g",
 893            indoc! {"
 894                The qˇuick
 895
 896                brown fox jumps
 897                over ˇthe laˇzy dog"},
 898        )
 899        .await
 900        .assert_matches();
 901        cx.simulate(
 902            "shift-g",
 903            indoc! {"
 904
 905
 906                brown fox jumps
 907                over the laˇzy dog"},
 908        )
 909        .await
 910        .assert_matches();
 911        cx.simulate(
 912            "2 shift-g",
 913            indoc! {"
 914                ˇ
 915
 916                brown fox jumps
 917                over the lazydog"},
 918        )
 919        .await
 920        .assert_matches();
 921    }
 922
 923    #[gpui::test]
 924    async fn test_a(cx: &mut gpui::TestAppContext) {
 925        let mut cx = NeovimBackedTestContext::new(cx).await;
 926        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 927            .await
 928            .assert_matches();
 929    }
 930
 931    #[gpui::test]
 932    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 933        let mut cx = NeovimBackedTestContext::new(cx).await;
 934        cx.simulate_at_each_offset(
 935            "shift-a",
 936            indoc! {"
 937            ˇ
 938            The qˇuick
 939            brown ˇfox "},
 940        )
 941        .await
 942        .assert_matches();
 943    }
 944
 945    #[gpui::test]
 946    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 947        let mut cx = NeovimBackedTestContext::new(cx).await;
 948        cx.simulate("^", "The qˇuick").await.assert_matches();
 949        cx.simulate("^", " The qˇuick").await.assert_matches();
 950        cx.simulate("^", "ˇ").await.assert_matches();
 951        cx.simulate(
 952            "^",
 953            indoc! {"
 954                The qˇuick
 955                brown fox"},
 956        )
 957        .await
 958        .assert_matches();
 959        cx.simulate(
 960            "^",
 961            indoc! {"
 962                ˇ
 963                The quick"},
 964        )
 965        .await
 966        .assert_matches();
 967        // Indoc disallows trailing whitespace.
 968        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
 969    }
 970
 971    #[gpui::test]
 972    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 973        let mut cx = NeovimBackedTestContext::new(cx).await;
 974        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
 975        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
 976        cx.simulate("shift-i", "ˇ").await.assert_matches();
 977        cx.simulate(
 978            "shift-i",
 979            indoc! {"
 980                The qˇuick
 981                brown fox"},
 982        )
 983        .await
 984        .assert_matches();
 985        cx.simulate(
 986            "shift-i",
 987            indoc! {"
 988                ˇ
 989                The quick"},
 990        )
 991        .await
 992        .assert_matches();
 993    }
 994
 995    #[gpui::test]
 996    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
 997        let mut cx = NeovimBackedTestContext::new(cx).await;
 998        cx.simulate(
 999            "shift-d",
1000            indoc! {"
1001                The qˇuick
1002                brown fox"},
1003        )
1004        .await
1005        .assert_matches();
1006        cx.simulate(
1007            "shift-d",
1008            indoc! {"
1009                The quick
1010                ˇ
1011                brown fox"},
1012        )
1013        .await
1014        .assert_matches();
1015    }
1016
1017    #[gpui::test]
1018    async fn test_x(cx: &mut gpui::TestAppContext) {
1019        let mut cx = NeovimBackedTestContext::new(cx).await;
1020        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1021            .await
1022            .assert_matches();
1023        cx.simulate(
1024            "x",
1025            indoc! {"
1026                Tesˇt
1027                test"},
1028        )
1029        .await
1030        .assert_matches();
1031    }
1032
1033    #[gpui::test]
1034    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1035        let mut cx = NeovimBackedTestContext::new(cx).await;
1036        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1037            .await
1038            .assert_matches();
1039        cx.simulate(
1040            "shift-x",
1041            indoc! {"
1042                Test
1043                ˇtest"},
1044        )
1045        .await
1046        .assert_matches();
1047    }
1048
1049    #[gpui::test]
1050    async fn test_o(cx: &mut gpui::TestAppContext) {
1051        let mut cx = NeovimBackedTestContext::new(cx).await;
1052        cx.simulate("o", "ˇ").await.assert_matches();
1053        cx.simulate("o", "The ˇquick").await.assert_matches();
1054        cx.simulate_at_each_offset(
1055            "o",
1056            indoc! {"
1057                The qˇuick
1058                brown ˇfox
1059                jumps ˇover"},
1060        )
1061        .await
1062        .assert_matches();
1063        cx.simulate(
1064            "o",
1065            indoc! {"
1066                The quick
1067                ˇ
1068                brown fox"},
1069        )
1070        .await
1071        .assert_matches();
1072
1073        cx.assert_binding(
1074            "o",
1075            indoc! {"
1076                fn test() {
1077                    println!(ˇ);
1078                }"},
1079            Mode::Normal,
1080            indoc! {"
1081                fn test() {
1082                    println!();
1083                    ˇ
1084                }"},
1085            Mode::Insert,
1086        );
1087
1088        cx.assert_binding(
1089            "o",
1090            indoc! {"
1091                fn test(ˇ) {
1092                    println!();
1093                }"},
1094            Mode::Normal,
1095            indoc! {"
1096                fn test() {
1097                    ˇ
1098                    println!();
1099                }"},
1100            Mode::Insert,
1101        );
1102    }
1103
1104    #[gpui::test]
1105    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1106        let mut cx = NeovimBackedTestContext::new(cx).await;
1107        cx.simulate("shift-o", "ˇ").await.assert_matches();
1108        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1109        cx.simulate_at_each_offset(
1110            "shift-o",
1111            indoc! {"
1112            The qˇuick
1113            brown ˇfox
1114            jumps ˇover"},
1115        )
1116        .await
1117        .assert_matches();
1118        cx.simulate(
1119            "shift-o",
1120            indoc! {"
1121            The quick
1122            ˇ
1123            brown fox"},
1124        )
1125        .await
1126        .assert_matches();
1127
1128        // Our indentation is smarter than vims. So we don't match here
1129        cx.assert_binding(
1130            "shift-o",
1131            indoc! {"
1132                fn test() {
1133                    println!(ˇ);
1134                }"},
1135            Mode::Normal,
1136            indoc! {"
1137                fn test() {
1138                    ˇ
1139                    println!();
1140                }"},
1141            Mode::Insert,
1142        );
1143        cx.assert_binding(
1144            "shift-o",
1145            indoc! {"
1146                fn test(ˇ) {
1147                    println!();
1148                }"},
1149            Mode::Normal,
1150            indoc! {"
1151                ˇ
1152                fn test() {
1153                    println!();
1154                }"},
1155            Mode::Insert,
1156        );
1157    }
1158
1159    #[gpui::test]
1160    async fn test_dd(cx: &mut gpui::TestAppContext) {
1161        let mut cx = NeovimBackedTestContext::new(cx).await;
1162        cx.simulate("d d", "ˇ").await.assert_matches();
1163        cx.simulate("d d", "The ˇquick").await.assert_matches();
1164        cx.simulate_at_each_offset(
1165            "d d",
1166            indoc! {"
1167            The qˇuick
1168            brown ˇfox
1169            jumps ˇover"},
1170        )
1171        .await
1172        .assert_matches();
1173        cx.simulate(
1174            "d d",
1175            indoc! {"
1176                The quick
1177                ˇ
1178                brown fox"},
1179        )
1180        .await
1181        .assert_matches();
1182    }
1183
1184    #[gpui::test]
1185    async fn test_cc(cx: &mut gpui::TestAppContext) {
1186        let mut cx = NeovimBackedTestContext::new(cx).await;
1187        cx.simulate("c c", "ˇ").await.assert_matches();
1188        cx.simulate("c c", "The ˇquick").await.assert_matches();
1189        cx.simulate_at_each_offset(
1190            "c c",
1191            indoc! {"
1192                The quˇick
1193                brown ˇfox
1194                jumps ˇover"},
1195        )
1196        .await
1197        .assert_matches();
1198        cx.simulate(
1199            "c c",
1200            indoc! {"
1201                The quick
1202                ˇ
1203                brown fox"},
1204        )
1205        .await
1206        .assert_matches();
1207    }
1208
1209    #[gpui::test]
1210    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1211        let mut cx = NeovimBackedTestContext::new(cx).await;
1212
1213        for count in 1..=5 {
1214            cx.simulate_at_each_offset(
1215                &format!("{count} w"),
1216                indoc! {"
1217                    ˇThe quˇickˇ browˇn
1218                    ˇ
1219                    ˇfox ˇjumpsˇ-ˇoˇver
1220                    ˇthe lazy dog
1221                "},
1222            )
1223            .await
1224            .assert_matches();
1225        }
1226    }
1227
1228    #[gpui::test]
1229    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1230        let mut cx = NeovimBackedTestContext::new(cx).await;
1231        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1232            .await
1233            .assert_matches();
1234    }
1235
1236    #[gpui::test]
1237    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1238        let mut cx = NeovimBackedTestContext::new(cx).await;
1239
1240        for count in 1..=3 {
1241            let test_case = indoc! {"
1242                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1243                ˇ    ˇbˇaaˇa ˇbˇbˇb
1244                ˇ
1245                ˇb
1246            "};
1247
1248            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1249                .await
1250                .assert_matches();
1251
1252            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1253                .await
1254                .assert_matches();
1255        }
1256    }
1257
1258    #[gpui::test]
1259    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1260        let mut cx = NeovimBackedTestContext::new(cx).await;
1261        let test_case = indoc! {"
1262            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1263            ˇ    ˇbˇaaˇa ˇbˇbˇb
1264            ˇ•••
1265            ˇb
1266            "
1267        };
1268
1269        for count in 1..=3 {
1270            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1271                .await
1272                .assert_matches();
1273
1274            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1275                .await
1276                .assert_matches();
1277        }
1278    }
1279
1280    #[gpui::test]
1281    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1282        let mut cx = VimTestContext::new(cx, true).await;
1283        cx.update_global(|store: &mut SettingsStore, cx| {
1284            store.update_user_settings::<VimSettings>(cx, |s| {
1285                s.use_multiline_find = Some(true);
1286            });
1287        });
1288
1289        cx.assert_binding(
1290            "f l",
1291            indoc! {"
1292            ˇfunction print() {
1293                console.log('ok')
1294            }
1295            "},
1296            Mode::Normal,
1297            indoc! {"
1298            function print() {
1299                consoˇle.log('ok')
1300            }
1301            "},
1302            Mode::Normal,
1303        );
1304
1305        cx.assert_binding(
1306            "t l",
1307            indoc! {"
1308            ˇfunction print() {
1309                console.log('ok')
1310            }
1311            "},
1312            Mode::Normal,
1313            indoc! {"
1314            function print() {
1315                consˇole.log('ok')
1316            }
1317            "},
1318            Mode::Normal,
1319        );
1320    }
1321
1322    #[gpui::test]
1323    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1324        let mut cx = VimTestContext::new(cx, true).await;
1325        cx.update_global(|store: &mut SettingsStore, cx| {
1326            store.update_user_settings::<VimSettings>(cx, |s| {
1327                s.use_multiline_find = Some(true);
1328            });
1329        });
1330
1331        cx.assert_binding(
1332            "shift-f p",
1333            indoc! {"
1334            function print() {
1335                console.ˇlog('ok')
1336            }
1337            "},
1338            Mode::Normal,
1339            indoc! {"
1340            function ˇprint() {
1341                console.log('ok')
1342            }
1343            "},
1344            Mode::Normal,
1345        );
1346
1347        cx.assert_binding(
1348            "shift-t p",
1349            indoc! {"
1350            function print() {
1351                console.ˇlog('ok')
1352            }
1353            "},
1354            Mode::Normal,
1355            indoc! {"
1356            function pˇrint() {
1357                console.log('ok')
1358            }
1359            "},
1360            Mode::Normal,
1361        );
1362    }
1363
1364    #[gpui::test]
1365    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1366        let mut cx = VimTestContext::new(cx, true).await;
1367        cx.update_global(|store: &mut SettingsStore, cx| {
1368            store.update_user_settings::<VimSettings>(cx, |s| {
1369                s.use_smartcase_find = Some(true);
1370            });
1371        });
1372
1373        cx.assert_binding(
1374            "f p",
1375            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1376            Mode::Normal,
1377            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1378            Mode::Normal,
1379        );
1380
1381        cx.assert_binding(
1382            "shift-f p",
1383            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1384            Mode::Normal,
1385            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1386            Mode::Normal,
1387        );
1388
1389        cx.assert_binding(
1390            "t p",
1391            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1392            Mode::Normal,
1393            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1394            Mode::Normal,
1395        );
1396
1397        cx.assert_binding(
1398            "shift-t p",
1399            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1400            Mode::Normal,
1401            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1402            Mode::Normal,
1403        );
1404    }
1405
1406    #[gpui::test]
1407    async fn test_percent(cx: &mut TestAppContext) {
1408        let mut cx = NeovimBackedTestContext::new(cx).await;
1409        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1410            .await
1411            .assert_matches();
1412        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1413            .await
1414            .assert_matches();
1415        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1416            .await
1417            .assert_matches();
1418    }
1419
1420    #[gpui::test]
1421    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1422        let mut cx = NeovimBackedTestContext::new(cx).await;
1423
1424        // goes to current line end
1425        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1426        cx.simulate_shared_keystrokes("$").await;
1427        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1428
1429        // goes to next line end
1430        cx.simulate_shared_keystrokes("2 $").await;
1431        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1432
1433        // try to exceed the final line.
1434        cx.simulate_shared_keystrokes("4 $").await;
1435        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1436    }
1437
1438    #[gpui::test]
1439    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1440        let mut cx = VimTestContext::new(cx, true).await;
1441        cx.update(|_, cx| {
1442            cx.bind_keys(vec![
1443                KeyBinding::new(
1444                    "w",
1445                    motion::NextSubwordStart {
1446                        ignore_punctuation: false,
1447                    },
1448                    Some("Editor && VimControl && !VimWaiting && !menu"),
1449                ),
1450                KeyBinding::new(
1451                    "b",
1452                    motion::PreviousSubwordStart {
1453                        ignore_punctuation: false,
1454                    },
1455                    Some("Editor && VimControl && !VimWaiting && !menu"),
1456                ),
1457                KeyBinding::new(
1458                    "e",
1459                    motion::NextSubwordEnd {
1460                        ignore_punctuation: false,
1461                    },
1462                    Some("Editor && VimControl && !VimWaiting && !menu"),
1463                ),
1464                KeyBinding::new(
1465                    "g e",
1466                    motion::PreviousSubwordEnd {
1467                        ignore_punctuation: false,
1468                    },
1469                    Some("Editor && VimControl && !VimWaiting && !menu"),
1470                ),
1471            ]);
1472        });
1473
1474        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1475        // Special case: In 'cw', 'w' acts like 'e'
1476        cx.assert_binding(
1477            "c w",
1478            indoc! {"ˇassert_binding"},
1479            Mode::Normal,
1480            indoc! {"ˇ_binding"},
1481            Mode::Insert,
1482        );
1483
1484        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1485
1486        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1487
1488        cx.assert_binding_normal(
1489            "g e",
1490            indoc! {"assert_bindinˇg"},
1491            indoc! {"asserˇt_binding"},
1492        );
1493    }
1494
1495    #[gpui::test]
1496    async fn test_r(cx: &mut gpui::TestAppContext) {
1497        let mut cx = NeovimBackedTestContext::new(cx).await;
1498
1499        cx.set_shared_state("ˇhello\n").await;
1500        cx.simulate_shared_keystrokes("r -").await;
1501        cx.shared_state().await.assert_eq("ˇ-ello\n");
1502
1503        cx.set_shared_state("ˇhello\n").await;
1504        cx.simulate_shared_keystrokes("3 r -").await;
1505        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1506
1507        cx.set_shared_state("ˇhello\n").await;
1508        cx.simulate_shared_keystrokes("r - 2 l .").await;
1509        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1510
1511        cx.set_shared_state("ˇhello world\n").await;
1512        cx.simulate_shared_keystrokes("2 r - f w .").await;
1513        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1514
1515        cx.set_shared_state("ˇhello world\n").await;
1516        cx.simulate_shared_keystrokes("2 0 r - ").await;
1517        cx.shared_state().await.assert_eq("ˇhello world\n");
1518    }
1519
1520    #[gpui::test]
1521    async fn test_gq(cx: &mut gpui::TestAppContext) {
1522        let mut cx = NeovimBackedTestContext::new(cx).await;
1523        cx.set_neovim_option("textwidth=5").await;
1524
1525        cx.update(|_, cx| {
1526            SettingsStore::update_global(cx, |settings, cx| {
1527                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1528                    settings.defaults.preferred_line_length = Some(5);
1529                });
1530            })
1531        });
1532
1533        cx.set_shared_state("ˇth th th th th th\n").await;
1534        cx.simulate_shared_keystrokes("g q q").await;
1535        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1536
1537        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1538            .await;
1539        cx.simulate_shared_keystrokes("v j g q").await;
1540        cx.shared_state()
1541            .await
1542            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1543    }
1544
1545    #[gpui::test]
1546    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1547        let mut cx = NeovimBackedTestContext::new(cx).await;
1548        cx.set_neovim_option("filetype=rust").await;
1549
1550        cx.set_shared_state("// helloˇ\n").await;
1551        cx.simulate_shared_keystrokes("o").await;
1552        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1553        cx.simulate_shared_keystrokes("x escape shift-o").await;
1554        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1555    }
1556
1557    #[gpui::test]
1558    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1559        let mut cx = NeovimBackedTestContext::new(cx).await;
1560        cx.set_shared_state("heˇllo\n").await;
1561        cx.simulate_shared_keystrokes("y y p").await;
1562        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1563    }
1564
1565    #[gpui::test]
1566    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1567        let mut cx = NeovimBackedTestContext::new(cx).await;
1568        cx.set_shared_state("heˇllo").await;
1569        cx.simulate_shared_keystrokes("y y p").await;
1570        cx.shared_state().await.assert_eq("hello\nˇhello");
1571    }
1572
1573    #[gpui::test]
1574    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1575        let mut cx = NeovimBackedTestContext::new(cx).await;
1576        cx.set_shared_state("heˇllo\nhello").await;
1577        cx.simulate_shared_keystrokes("2 y y p").await;
1578        cx.shared_state()
1579            .await
1580            .assert_eq("hello\nˇhello\nhello\nhello");
1581    }
1582
1583    #[gpui::test]
1584    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1585        let mut cx = NeovimBackedTestContext::new(cx).await;
1586        cx.set_shared_state("heˇllo").await;
1587        cx.simulate_shared_keystrokes("d d").await;
1588        cx.shared_state().await.assert_eq("ˇ");
1589        cx.simulate_shared_keystrokes("p p").await;
1590        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1591    }
1592}