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