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