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