normal.rs

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