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