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};
  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            let Some((buffer, point, _)) = editor
 667                .buffer()
 668                .read(cx)
 669                .point_to_buffer_point(selection.head(), cx)
 670            else {
 671                return;
 672            };
 673            let filename = if let Some(file) = buffer.read(cx).file() {
 674                if count.is_some() {
 675                    if let Some(local) = file.as_local() {
 676                        local.abs_path(cx).to_string_lossy().to_string()
 677                    } else {
 678                        file.full_path(cx).to_string_lossy().to_string()
 679                    }
 680                } else {
 681                    file.path().to_string_lossy().to_string()
 682                }
 683            } else {
 684                "[No Name]".into()
 685            };
 686            let buffer = buffer.read(cx);
 687            let lines = buffer.max_point().row + 1;
 688            let current_line = point.row;
 689            let percentage = current_line as f32 / lines as f32;
 690            let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 691            vim.status_label = Some(
 692                format!(
 693                    "{}{} {} lines --{:.0}%--",
 694                    filename,
 695                    modified,
 696                    lines,
 697                    percentage * 100.0,
 698                )
 699                .into(),
 700            );
 701            cx.notify();
 702        });
 703    }
 704
 705    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 706        self.record_current_action(cx);
 707        self.store_visual_marks(window, cx);
 708        self.update_editor(window, cx, |vim, editor, window, cx| {
 709            editor.transact(window, cx, |editor, window, cx| {
 710                let original_positions = vim.save_selection_starts(editor, cx);
 711                editor.toggle_comments(&Default::default(), window, cx);
 712                vim.restore_selection_cursors(editor, window, cx, original_positions);
 713            });
 714        });
 715        if self.mode.is_visual() {
 716            self.switch_mode(Mode::Normal, true, window, cx)
 717        }
 718    }
 719
 720    pub(crate) fn normal_replace(
 721        &mut self,
 722        text: Arc<str>,
 723        window: &mut Window,
 724        cx: &mut Context<Self>,
 725    ) {
 726        let is_return_char = text == "\n".into() || text == "\r".into();
 727        let count = Vim::take_count(cx).unwrap_or(1);
 728        Vim::take_forced_motion(cx);
 729        self.stop_recording(cx);
 730        self.update_editor(window, cx, |_, editor, window, cx| {
 731            editor.transact(window, cx, |editor, window, cx| {
 732                editor.set_clip_at_line_ends(false, cx);
 733                let (map, display_selections) = editor.selections.all_display(cx);
 734
 735                let mut edits = Vec::new();
 736                for selection in &display_selections {
 737                    let mut range = selection.range();
 738                    for _ in 0..count {
 739                        let new_point = movement::saturating_right(&map, range.end);
 740                        if range.end == new_point {
 741                            return;
 742                        }
 743                        range.end = new_point;
 744                    }
 745
 746                    edits.push((
 747                        range.start.to_offset(&map, Bias::Left)
 748                            ..range.end.to_offset(&map, Bias::Left),
 749                        text.repeat(if is_return_char { 0 } else { count }),
 750                    ));
 751                }
 752
 753                editor.edit(edits, cx);
 754                if is_return_char {
 755                    editor.newline(&editor::actions::Newline, window, cx);
 756                }
 757                editor.set_clip_at_line_ends(true, cx);
 758                editor.change_selections(None, window, cx, |s| {
 759                    s.move_with(|map, selection| {
 760                        let point = movement::saturating_left(map, selection.head());
 761                        selection.collapse_to(point, SelectionGoal::None)
 762                    });
 763                });
 764            });
 765        });
 766        self.pop_operator(window, cx);
 767    }
 768
 769    pub fn save_selection_starts(
 770        &self,
 771        editor: &Editor,
 772
 773        cx: &mut Context<Editor>,
 774    ) -> HashMap<usize, Anchor> {
 775        let (map, selections) = editor.selections.all_display(cx);
 776        selections
 777            .iter()
 778            .map(|selection| {
 779                (
 780                    selection.id,
 781                    map.display_point_to_anchor(selection.start, Bias::Right),
 782                )
 783            })
 784            .collect::<HashMap<_, _>>()
 785    }
 786
 787    pub fn restore_selection_cursors(
 788        &self,
 789        editor: &mut Editor,
 790        window: &mut Window,
 791        cx: &mut Context<Editor>,
 792        mut positions: HashMap<usize, Anchor>,
 793    ) {
 794        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 795            s.move_with(|map, selection| {
 796                if let Some(anchor) = positions.remove(&selection.id) {
 797                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 798                }
 799            });
 800        });
 801    }
 802
 803    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 804        if self.temp_mode {
 805            self.switch_mode(Mode::Insert, true, window, cx);
 806        }
 807    }
 808}
 809#[cfg(test)]
 810mod test {
 811    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 812    use indoc::indoc;
 813    use language::language_settings::AllLanguageSettings;
 814    use settings::SettingsStore;
 815
 816    use crate::{
 817        VimSettings, motion,
 818        state::Mode::{self},
 819        test::{NeovimBackedTestContext, VimTestContext},
 820    };
 821
 822    #[gpui::test]
 823    async fn test_h(cx: &mut gpui::TestAppContext) {
 824        let mut cx = NeovimBackedTestContext::new(cx).await;
 825        cx.simulate_at_each_offset(
 826            "h",
 827            indoc! {"
 828            ˇThe qˇuick
 829            ˇbrown"
 830            },
 831        )
 832        .await
 833        .assert_matches();
 834    }
 835
 836    #[gpui::test]
 837    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 838        let mut cx = NeovimBackedTestContext::new(cx).await;
 839        cx.simulate_at_each_offset(
 840            "backspace",
 841            indoc! {"
 842            ˇThe qˇuick
 843            ˇbrown"
 844            },
 845        )
 846        .await
 847        .assert_matches();
 848    }
 849
 850    #[gpui::test]
 851    async fn test_j(cx: &mut gpui::TestAppContext) {
 852        let mut cx = NeovimBackedTestContext::new(cx).await;
 853
 854        cx.set_shared_state(indoc! {"
 855            aaˇaa
 856            😃😃"
 857        })
 858        .await;
 859        cx.simulate_shared_keystrokes("j").await;
 860        cx.shared_state().await.assert_eq(indoc! {"
 861            aaaa
 862            😃ˇ😃"
 863        });
 864
 865        cx.simulate_at_each_offset(
 866            "j",
 867            indoc! {"
 868                ˇThe qˇuick broˇwn
 869                ˇfox jumps"
 870            },
 871        )
 872        .await
 873        .assert_matches();
 874    }
 875
 876    #[gpui::test]
 877    async fn test_enter(cx: &mut gpui::TestAppContext) {
 878        let mut cx = NeovimBackedTestContext::new(cx).await;
 879        cx.simulate_at_each_offset(
 880            "enter",
 881            indoc! {"
 882            ˇThe qˇuick broˇwn
 883            ˇfox jumps"
 884            },
 885        )
 886        .await
 887        .assert_matches();
 888    }
 889
 890    #[gpui::test]
 891    async fn test_k(cx: &mut gpui::TestAppContext) {
 892        let mut cx = NeovimBackedTestContext::new(cx).await;
 893        cx.simulate_at_each_offset(
 894            "k",
 895            indoc! {"
 896            ˇThe qˇuick
 897            ˇbrown fˇox jumˇps"
 898            },
 899        )
 900        .await
 901        .assert_matches();
 902    }
 903
 904    #[gpui::test]
 905    async fn test_l(cx: &mut gpui::TestAppContext) {
 906        let mut cx = NeovimBackedTestContext::new(cx).await;
 907        cx.simulate_at_each_offset(
 908            "l",
 909            indoc! {"
 910            ˇThe qˇuicˇk
 911            ˇbrowˇn"},
 912        )
 913        .await
 914        .assert_matches();
 915    }
 916
 917    #[gpui::test]
 918    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 919        let mut cx = NeovimBackedTestContext::new(cx).await;
 920        cx.simulate_at_each_offset(
 921            "$",
 922            indoc! {"
 923            ˇThe qˇuicˇk
 924            ˇbrowˇn"},
 925        )
 926        .await
 927        .assert_matches();
 928        cx.simulate_at_each_offset(
 929            "0",
 930            indoc! {"
 931                ˇThe qˇuicˇk
 932                ˇbrowˇn"},
 933        )
 934        .await
 935        .assert_matches();
 936    }
 937
 938    #[gpui::test]
 939    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 940        let mut cx = NeovimBackedTestContext::new(cx).await;
 941
 942        cx.simulate_at_each_offset(
 943            "shift-g",
 944            indoc! {"
 945                The ˇquick
 946
 947                brown fox jumps
 948                overˇ the lazy doˇg"},
 949        )
 950        .await
 951        .assert_matches();
 952        cx.simulate(
 953            "shift-g",
 954            indoc! {"
 955            The quiˇck
 956
 957            brown"},
 958        )
 959        .await
 960        .assert_matches();
 961        cx.simulate(
 962            "shift-g",
 963            indoc! {"
 964            The quiˇck
 965
 966            "},
 967        )
 968        .await
 969        .assert_matches();
 970    }
 971
 972    #[gpui::test]
 973    async fn test_w(cx: &mut gpui::TestAppContext) {
 974        let mut cx = NeovimBackedTestContext::new(cx).await;
 975        cx.simulate_at_each_offset(
 976            "w",
 977            indoc! {"
 978            The ˇquickˇ-ˇbrown
 979            ˇ
 980            ˇ
 981            ˇfox_jumps ˇover
 982            ˇthˇe"},
 983        )
 984        .await
 985        .assert_matches();
 986        cx.simulate_at_each_offset(
 987            "shift-w",
 988            indoc! {"
 989            The ˇquickˇ-ˇbrown
 990            ˇ
 991            ˇ
 992            ˇfox_jumps ˇover
 993            ˇthˇe"},
 994        )
 995        .await
 996        .assert_matches();
 997    }
 998
 999    #[gpui::test]
1000    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1001        let mut cx = NeovimBackedTestContext::new(cx).await;
1002        cx.simulate_at_each_offset(
1003            "e",
1004            indoc! {"
1005            Thˇe quicˇkˇ-browˇn
1006
1007
1008            fox_jumpˇs oveˇr
1009            thˇe"},
1010        )
1011        .await
1012        .assert_matches();
1013        cx.simulate_at_each_offset(
1014            "shift-e",
1015            indoc! {"
1016            Thˇe quicˇkˇ-browˇn
1017
1018
1019            fox_jumpˇs oveˇr
1020            thˇe"},
1021        )
1022        .await
1023        .assert_matches();
1024    }
1025
1026    #[gpui::test]
1027    async fn test_b(cx: &mut gpui::TestAppContext) {
1028        let mut cx = NeovimBackedTestContext::new(cx).await;
1029        cx.simulate_at_each_offset(
1030            "b",
1031            indoc! {"
1032            ˇThe ˇquickˇ-ˇbrown
1033            ˇ
1034            ˇ
1035            ˇfox_jumps ˇover
1036            ˇthe"},
1037        )
1038        .await
1039        .assert_matches();
1040        cx.simulate_at_each_offset(
1041            "shift-b",
1042            indoc! {"
1043            ˇThe ˇquickˇ-ˇbrown
1044            ˇ
1045            ˇ
1046            ˇfox_jumps ˇover
1047            ˇthe"},
1048        )
1049        .await
1050        .assert_matches();
1051    }
1052
1053    #[gpui::test]
1054    async fn test_gg(cx: &mut gpui::TestAppContext) {
1055        let mut cx = NeovimBackedTestContext::new(cx).await;
1056        cx.simulate_at_each_offset(
1057            "g g",
1058            indoc! {"
1059                The qˇuick
1060
1061                brown fox jumps
1062                over ˇthe laˇzy dog"},
1063        )
1064        .await
1065        .assert_matches();
1066        cx.simulate(
1067            "g g",
1068            indoc! {"
1069
1070
1071                brown fox jumps
1072                over the laˇzy dog"},
1073        )
1074        .await
1075        .assert_matches();
1076        cx.simulate(
1077            "2 g g",
1078            indoc! {"
1079                ˇ
1080
1081                brown fox jumps
1082                over the lazydog"},
1083        )
1084        .await
1085        .assert_matches();
1086    }
1087
1088    #[gpui::test]
1089    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1090        let mut cx = NeovimBackedTestContext::new(cx).await;
1091        cx.simulate_at_each_offset(
1092            "shift-g",
1093            indoc! {"
1094                The qˇuick
1095
1096                brown fox jumps
1097                over ˇthe laˇzy dog"},
1098        )
1099        .await
1100        .assert_matches();
1101        cx.simulate(
1102            "shift-g",
1103            indoc! {"
1104
1105
1106                brown fox jumps
1107                over the laˇzy dog"},
1108        )
1109        .await
1110        .assert_matches();
1111        cx.simulate(
1112            "2 shift-g",
1113            indoc! {"
1114                ˇ
1115
1116                brown fox jumps
1117                over the lazydog"},
1118        )
1119        .await
1120        .assert_matches();
1121    }
1122
1123    #[gpui::test]
1124    async fn test_a(cx: &mut gpui::TestAppContext) {
1125        let mut cx = NeovimBackedTestContext::new(cx).await;
1126        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1127            .await
1128            .assert_matches();
1129    }
1130
1131    #[gpui::test]
1132    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1133        let mut cx = NeovimBackedTestContext::new(cx).await;
1134        cx.simulate_at_each_offset(
1135            "shift-a",
1136            indoc! {"
1137            ˇ
1138            The qˇuick
1139            brown ˇfox "},
1140        )
1141        .await
1142        .assert_matches();
1143    }
1144
1145    #[gpui::test]
1146    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1147        let mut cx = NeovimBackedTestContext::new(cx).await;
1148        cx.simulate("^", "The qˇuick").await.assert_matches();
1149        cx.simulate("^", " The qˇuick").await.assert_matches();
1150        cx.simulate("^", "ˇ").await.assert_matches();
1151        cx.simulate(
1152            "^",
1153            indoc! {"
1154                The qˇuick
1155                brown fox"},
1156        )
1157        .await
1158        .assert_matches();
1159        cx.simulate(
1160            "^",
1161            indoc! {"
1162                ˇ
1163                The quick"},
1164        )
1165        .await
1166        .assert_matches();
1167        // Indoc disallows trailing whitespace.
1168        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1169    }
1170
1171    #[gpui::test]
1172    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1173        let mut cx = NeovimBackedTestContext::new(cx).await;
1174        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1175        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1176        cx.simulate("shift-i", "ˇ").await.assert_matches();
1177        cx.simulate(
1178            "shift-i",
1179            indoc! {"
1180                The qˇuick
1181                brown fox"},
1182        )
1183        .await
1184        .assert_matches();
1185        cx.simulate(
1186            "shift-i",
1187            indoc! {"
1188                ˇ
1189                The quick"},
1190        )
1191        .await
1192        .assert_matches();
1193    }
1194
1195    #[gpui::test]
1196    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1197        let mut cx = NeovimBackedTestContext::new(cx).await;
1198        cx.simulate(
1199            "shift-d",
1200            indoc! {"
1201                The qˇuick
1202                brown fox"},
1203        )
1204        .await
1205        .assert_matches();
1206        cx.simulate(
1207            "shift-d",
1208            indoc! {"
1209                The quick
1210                ˇ
1211                brown fox"},
1212        )
1213        .await
1214        .assert_matches();
1215    }
1216
1217    #[gpui::test]
1218    async fn test_x(cx: &mut gpui::TestAppContext) {
1219        let mut cx = NeovimBackedTestContext::new(cx).await;
1220        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1221            .await
1222            .assert_matches();
1223        cx.simulate(
1224            "x",
1225            indoc! {"
1226                Tesˇt
1227                test"},
1228        )
1229        .await
1230        .assert_matches();
1231    }
1232
1233    #[gpui::test]
1234    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1235        let mut cx = NeovimBackedTestContext::new(cx).await;
1236        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1237            .await
1238            .assert_matches();
1239        cx.simulate(
1240            "shift-x",
1241            indoc! {"
1242                Test
1243                ˇtest"},
1244        )
1245        .await
1246        .assert_matches();
1247    }
1248
1249    #[gpui::test]
1250    async fn test_o(cx: &mut gpui::TestAppContext) {
1251        let mut cx = NeovimBackedTestContext::new(cx).await;
1252        cx.simulate("o", "ˇ").await.assert_matches();
1253        cx.simulate("o", "The ˇquick").await.assert_matches();
1254        cx.simulate_at_each_offset(
1255            "o",
1256            indoc! {"
1257                The qˇuick
1258                brown ˇfox
1259                jumps ˇover"},
1260        )
1261        .await
1262        .assert_matches();
1263        cx.simulate(
1264            "o",
1265            indoc! {"
1266                The quick
1267                ˇ
1268                brown fox"},
1269        )
1270        .await
1271        .assert_matches();
1272
1273        cx.assert_binding(
1274            "o",
1275            indoc! {"
1276                fn test() {
1277                    println!(ˇ);
1278                }"},
1279            Mode::Normal,
1280            indoc! {"
1281                fn test() {
1282                    println!();
1283                    ˇ
1284                }"},
1285            Mode::Insert,
1286        );
1287
1288        cx.assert_binding(
1289            "o",
1290            indoc! {"
1291                fn test(ˇ) {
1292                    println!();
1293                }"},
1294            Mode::Normal,
1295            indoc! {"
1296                fn test() {
1297                    ˇ
1298                    println!();
1299                }"},
1300            Mode::Insert,
1301        );
1302    }
1303
1304    #[gpui::test]
1305    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1306        let mut cx = NeovimBackedTestContext::new(cx).await;
1307        cx.simulate("shift-o", "ˇ").await.assert_matches();
1308        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1309        cx.simulate_at_each_offset(
1310            "shift-o",
1311            indoc! {"
1312            The qˇuick
1313            brown ˇfox
1314            jumps ˇover"},
1315        )
1316        .await
1317        .assert_matches();
1318        cx.simulate(
1319            "shift-o",
1320            indoc! {"
1321            The quick
1322            ˇ
1323            brown fox"},
1324        )
1325        .await
1326        .assert_matches();
1327
1328        // Our indentation is smarter than vims. So we don't match here
1329        cx.assert_binding(
1330            "shift-o",
1331            indoc! {"
1332                fn test() {
1333                    println!(ˇ);
1334                }"},
1335            Mode::Normal,
1336            indoc! {"
1337                fn test() {
1338                    ˇ
1339                    println!();
1340                }"},
1341            Mode::Insert,
1342        );
1343        cx.assert_binding(
1344            "shift-o",
1345            indoc! {"
1346                fn test(ˇ) {
1347                    println!();
1348                }"},
1349            Mode::Normal,
1350            indoc! {"
1351                ˇ
1352                fn test() {
1353                    println!();
1354                }"},
1355            Mode::Insert,
1356        );
1357    }
1358
1359    #[gpui::test]
1360    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1361        let mut cx = NeovimBackedTestContext::new(cx).await;
1362        cx.simulate("[ space", "ˇ").await.assert_matches();
1363        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1364        cx.simulate_at_each_offset(
1365            "3 [ space",
1366            indoc! {"
1367            The qˇuick
1368            brown ˇfox
1369            jumps ˇover"},
1370        )
1371        .await
1372        .assert_matches();
1373        cx.simulate_at_each_offset(
1374            "[ space",
1375            indoc! {"
1376            The qˇuick
1377            brown ˇfox
1378            jumps ˇover"},
1379        )
1380        .await
1381        .assert_matches();
1382        cx.simulate(
1383            "[ space",
1384            indoc! {"
1385            The quick
1386            ˇ
1387            brown fox"},
1388        )
1389        .await
1390        .assert_matches();
1391
1392        cx.simulate("] space", "ˇ").await.assert_matches();
1393        cx.simulate("] space", "The ˇquick").await.assert_matches();
1394        cx.simulate_at_each_offset(
1395            "3 ] space",
1396            indoc! {"
1397            The qˇuick
1398            brown ˇfox
1399            jumps ˇover"},
1400        )
1401        .await
1402        .assert_matches();
1403        cx.simulate_at_each_offset(
1404            "] space",
1405            indoc! {"
1406            The qˇuick
1407            brown ˇfox
1408            jumps ˇover"},
1409        )
1410        .await
1411        .assert_matches();
1412        cx.simulate(
1413            "] space",
1414            indoc! {"
1415            The quick
1416            ˇ
1417            brown fox"},
1418        )
1419        .await
1420        .assert_matches();
1421    }
1422
1423    #[gpui::test]
1424    async fn test_dd(cx: &mut gpui::TestAppContext) {
1425        let mut cx = NeovimBackedTestContext::new(cx).await;
1426        cx.simulate("d d", "ˇ").await.assert_matches();
1427        cx.simulate("d d", "The ˇquick").await.assert_matches();
1428        cx.simulate_at_each_offset(
1429            "d d",
1430            indoc! {"
1431            The qˇuick
1432            brown ˇfox
1433            jumps ˇover"},
1434        )
1435        .await
1436        .assert_matches();
1437        cx.simulate(
1438            "d d",
1439            indoc! {"
1440                The quick
1441                ˇ
1442                brown fox"},
1443        )
1444        .await
1445        .assert_matches();
1446    }
1447
1448    #[gpui::test]
1449    async fn test_cc(cx: &mut gpui::TestAppContext) {
1450        let mut cx = NeovimBackedTestContext::new(cx).await;
1451        cx.simulate("c c", "ˇ").await.assert_matches();
1452        cx.simulate("c c", "The ˇquick").await.assert_matches();
1453        cx.simulate_at_each_offset(
1454            "c c",
1455            indoc! {"
1456                The quˇick
1457                brown ˇfox
1458                jumps ˇover"},
1459        )
1460        .await
1461        .assert_matches();
1462        cx.simulate(
1463            "c c",
1464            indoc! {"
1465                The quick
1466                ˇ
1467                brown fox"},
1468        )
1469        .await
1470        .assert_matches();
1471    }
1472
1473    #[gpui::test]
1474    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1475        let mut cx = NeovimBackedTestContext::new(cx).await;
1476
1477        for count in 1..=5 {
1478            cx.simulate_at_each_offset(
1479                &format!("{count} w"),
1480                indoc! {"
1481                    ˇThe quˇickˇ browˇn
1482                    ˇ
1483                    ˇfox ˇjumpsˇ-ˇoˇver
1484                    ˇthe lazy dog
1485                "},
1486            )
1487            .await
1488            .assert_matches();
1489        }
1490    }
1491
1492    #[gpui::test]
1493    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1494        let mut cx = NeovimBackedTestContext::new(cx).await;
1495        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1496            .await
1497            .assert_matches();
1498    }
1499
1500    #[gpui::test]
1501    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1502        let mut cx = NeovimBackedTestContext::new(cx).await;
1503
1504        for count in 1..=3 {
1505            let test_case = indoc! {"
1506                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1507                ˇ    ˇbˇaaˇa ˇbˇbˇb
1508                ˇ
1509                ˇb
1510            "};
1511
1512            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1513                .await
1514                .assert_matches();
1515
1516            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1517                .await
1518                .assert_matches();
1519        }
1520    }
1521
1522    #[gpui::test]
1523    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1524        let mut cx = NeovimBackedTestContext::new(cx).await;
1525        let test_case = indoc! {"
1526            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1527            ˇ    ˇbˇaaˇa ˇbˇbˇb
1528            ˇ•••
1529            ˇb
1530            "
1531        };
1532
1533        for count in 1..=3 {
1534            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1535                .await
1536                .assert_matches();
1537
1538            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1539                .await
1540                .assert_matches();
1541        }
1542    }
1543
1544    #[gpui::test]
1545    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1546        let mut cx = VimTestContext::new(cx, true).await;
1547        cx.update_global(|store: &mut SettingsStore, cx| {
1548            store.update_user_settings::<VimSettings>(cx, |s| {
1549                s.use_smartcase_find = Some(true);
1550            });
1551        });
1552
1553        cx.assert_binding(
1554            "f p",
1555            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1556            Mode::Normal,
1557            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1558            Mode::Normal,
1559        );
1560
1561        cx.assert_binding(
1562            "shift-f p",
1563            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1564            Mode::Normal,
1565            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1566            Mode::Normal,
1567        );
1568
1569        cx.assert_binding(
1570            "t p",
1571            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1572            Mode::Normal,
1573            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1574            Mode::Normal,
1575        );
1576
1577        cx.assert_binding(
1578            "shift-t p",
1579            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1580            Mode::Normal,
1581            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1582            Mode::Normal,
1583        );
1584    }
1585
1586    #[gpui::test]
1587    async fn test_percent(cx: &mut TestAppContext) {
1588        let mut cx = NeovimBackedTestContext::new(cx).await;
1589        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1590            .await
1591            .assert_matches();
1592        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1593            .await
1594            .assert_matches();
1595        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1596            .await
1597            .assert_matches();
1598    }
1599
1600    #[gpui::test]
1601    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1602        let mut cx = NeovimBackedTestContext::new(cx).await;
1603
1604        // goes to current line end
1605        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1606        cx.simulate_shared_keystrokes("$").await;
1607        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1608
1609        // goes to next line end
1610        cx.simulate_shared_keystrokes("2 $").await;
1611        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1612
1613        // try to exceed the final line.
1614        cx.simulate_shared_keystrokes("4 $").await;
1615        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1616    }
1617
1618    #[gpui::test]
1619    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1620        let mut cx = VimTestContext::new(cx, true).await;
1621        cx.update(|_, cx| {
1622            cx.bind_keys(vec![
1623                KeyBinding::new(
1624                    "w",
1625                    motion::NextSubwordStart {
1626                        ignore_punctuation: false,
1627                    },
1628                    Some("Editor && VimControl && !VimWaiting && !menu"),
1629                ),
1630                KeyBinding::new(
1631                    "b",
1632                    motion::PreviousSubwordStart {
1633                        ignore_punctuation: false,
1634                    },
1635                    Some("Editor && VimControl && !VimWaiting && !menu"),
1636                ),
1637                KeyBinding::new(
1638                    "e",
1639                    motion::NextSubwordEnd {
1640                        ignore_punctuation: false,
1641                    },
1642                    Some("Editor && VimControl && !VimWaiting && !menu"),
1643                ),
1644                KeyBinding::new(
1645                    "g e",
1646                    motion::PreviousSubwordEnd {
1647                        ignore_punctuation: false,
1648                    },
1649                    Some("Editor && VimControl && !VimWaiting && !menu"),
1650                ),
1651            ]);
1652        });
1653
1654        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1655        // Special case: In 'cw', 'w' acts like 'e'
1656        cx.assert_binding(
1657            "c w",
1658            indoc! {"ˇassert_binding"},
1659            Mode::Normal,
1660            indoc! {"ˇ_binding"},
1661            Mode::Insert,
1662        );
1663
1664        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1665
1666        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1667
1668        cx.assert_binding_normal(
1669            "g e",
1670            indoc! {"assert_bindinˇg"},
1671            indoc! {"asserˇt_binding"},
1672        );
1673    }
1674
1675    #[gpui::test]
1676    async fn test_r(cx: &mut gpui::TestAppContext) {
1677        let mut cx = NeovimBackedTestContext::new(cx).await;
1678
1679        cx.set_shared_state("ˇhello\n").await;
1680        cx.simulate_shared_keystrokes("r -").await;
1681        cx.shared_state().await.assert_eq("ˇ-ello\n");
1682
1683        cx.set_shared_state("ˇhello\n").await;
1684        cx.simulate_shared_keystrokes("3 r -").await;
1685        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1686
1687        cx.set_shared_state("ˇhello\n").await;
1688        cx.simulate_shared_keystrokes("r - 2 l .").await;
1689        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1690
1691        cx.set_shared_state("ˇhello world\n").await;
1692        cx.simulate_shared_keystrokes("2 r - f w .").await;
1693        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1694
1695        cx.set_shared_state("ˇhello world\n").await;
1696        cx.simulate_shared_keystrokes("2 0 r - ").await;
1697        cx.shared_state().await.assert_eq("ˇhello world\n");
1698
1699        cx.set_shared_state("  helloˇ world\n").await;
1700        cx.simulate_shared_keystrokes("r enter").await;
1701        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
1702
1703        cx.set_shared_state("  helloˇ world\n").await;
1704        cx.simulate_shared_keystrokes("2 r enter").await;
1705        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
1706    }
1707
1708    #[gpui::test]
1709    async fn test_gq(cx: &mut gpui::TestAppContext) {
1710        let mut cx = NeovimBackedTestContext::new(cx).await;
1711        cx.set_neovim_option("textwidth=5").await;
1712
1713        cx.update(|_, cx| {
1714            SettingsStore::update_global(cx, |settings, cx| {
1715                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1716                    settings.defaults.preferred_line_length = Some(5);
1717                });
1718            })
1719        });
1720
1721        cx.set_shared_state("ˇth th th th th th\n").await;
1722        cx.simulate_shared_keystrokes("g q q").await;
1723        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1724
1725        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1726            .await;
1727        cx.simulate_shared_keystrokes("v j g q").await;
1728        cx.shared_state()
1729            .await
1730            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1731    }
1732
1733    #[gpui::test]
1734    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1735        let mut cx = NeovimBackedTestContext::new(cx).await;
1736        cx.set_neovim_option("filetype=rust").await;
1737
1738        cx.set_shared_state("// helloˇ\n").await;
1739        cx.simulate_shared_keystrokes("o").await;
1740        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1741        cx.simulate_shared_keystrokes("x escape shift-o").await;
1742        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1743    }
1744
1745    #[gpui::test]
1746    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1747        let mut cx = NeovimBackedTestContext::new(cx).await;
1748        cx.set_shared_state("heˇllo\n").await;
1749        cx.simulate_shared_keystrokes("y y p").await;
1750        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1751    }
1752
1753    #[gpui::test]
1754    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1755        let mut cx = NeovimBackedTestContext::new(cx).await;
1756        cx.set_shared_state("heˇllo").await;
1757        cx.simulate_shared_keystrokes("y y p").await;
1758        cx.shared_state().await.assert_eq("hello\nˇhello");
1759    }
1760
1761    #[gpui::test]
1762    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1763        let mut cx = NeovimBackedTestContext::new(cx).await;
1764        cx.set_shared_state("heˇllo\nhello").await;
1765        cx.simulate_shared_keystrokes("2 y y p").await;
1766        cx.shared_state()
1767            .await
1768            .assert_eq("hello\nˇhello\nhello\nhello");
1769    }
1770
1771    #[gpui::test]
1772    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1773        let mut cx = NeovimBackedTestContext::new(cx).await;
1774        cx.set_shared_state("heˇllo").await;
1775        cx.simulate_shared_keystrokes("d d").await;
1776        cx.shared_state().await.assert_eq("ˇ");
1777        cx.simulate_shared_keystrokes("p p").await;
1778        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1779    }
1780
1781    #[gpui::test]
1782    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1783        let mut cx = NeovimBackedTestContext::new(cx).await;
1784
1785        cx.set_shared_state("heˇllo").await;
1786        cx.simulate_shared_keystrokes("v i w shift-i").await;
1787        cx.shared_state().await.assert_eq("ˇhello");
1788
1789        cx.set_shared_state(indoc! {"
1790            The quick brown
1791            fox ˇjumps over
1792            the lazy dog"})
1793            .await;
1794        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1795        cx.shared_state().await.assert_eq(indoc! {"
1796            The quick brown
1797            ˇfox jumps over
1798            the lazy dog"});
1799
1800        cx.set_shared_state(indoc! {"
1801            The quick brown
1802            fox ˇjumps over
1803            the lazy dog"})
1804            .await;
1805        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1806        cx.shared_state().await.assert_eq(indoc! {"
1807            The quick brown
1808            fox jˇumps over
1809            the lazy dog"});
1810    }
1811
1812    #[gpui::test]
1813    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
1814        let mut cx = NeovimBackedTestContext::new(cx).await;
1815
1816        cx.set_shared_state(indoc! {"
1817            ˇfn a() { }
1818
1819
1820
1821
1822
1823            fn b() { }
1824
1825
1826
1827
1828
1829            fn b() { }"})
1830            .await;
1831        cx.simulate_shared_keystrokes("3 }").await;
1832        cx.shared_state().await.assert_matches();
1833        cx.simulate_shared_keystrokes("ctrl-o").await;
1834        cx.shared_state().await.assert_matches();
1835        cx.simulate_shared_keystrokes("ctrl-i").await;
1836        cx.shared_state().await.assert_matches();
1837        cx.simulate_shared_keystrokes("1 1 k").await;
1838        cx.shared_state().await.assert_matches();
1839        cx.simulate_shared_keystrokes("ctrl-o").await;
1840        cx.shared_state().await.assert_matches();
1841    }
1842}