normal.rs

   1mod change;
   2mod convert;
   3mod delete;
   4mod increment;
   5pub(crate) mod mark;
   6mod paste;
   7pub(crate) mod repeat;
   8mod scroll;
   9pub(crate) mod search;
  10pub mod substitute;
  11mod toggle_comments;
  12pub(crate) mod yank;
  13
  14use std::collections::HashMap;
  15use std::sync::Arc;
  16
  17use crate::{
  18    Vim,
  19    indent::IndentDirection,
  20    motion::{self, Motion, first_non_whitespace, next_line_end, right},
  21    object::Object,
  22    state::{Mark, Mode, Operator},
  23    surrounds::SurroundsType,
  24};
  25use collections::BTreeSet;
  26use convert::ConvertTarget;
  27use editor::Anchor;
  28use editor::Bias;
  29use editor::Editor;
  30use editor::scroll::Autoscroll;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{Context, Window, actions};
  33use language::{Point, SelectionGoal, ToPoint};
  34use log::error;
  35use multi_buffer::MultiBufferRow;
  36
  37actions!(
  38    vim,
  39    [
  40        InsertAfter,
  41        InsertBefore,
  42        InsertFirstNonWhitespace,
  43        InsertEndOfLine,
  44        InsertLineAbove,
  45        InsertLineBelow,
  46        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(Some(Autoscroll::fit()), window, cx, |s| {
 362                s.move_cursors_with(|map, cursor, goal| {
 363                    motion
 364                        .move_point(map, cursor, goal, times, &text_layout_details)
 365                        .unwrap_or((cursor, goal))
 366                })
 367            })
 368        });
 369    }
 370
 371    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 372        self.start_recording(cx);
 373        self.switch_mode(Mode::Insert, false, window, cx);
 374        self.update_editor(window, cx, |_, editor, window, cx| {
 375            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 376                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 377            });
 378        });
 379    }
 380
 381    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 382        self.start_recording(cx);
 383        if self.mode.is_visual() {
 384            let current_mode = self.mode;
 385            self.update_editor(window, cx, |_, editor, window, cx| {
 386                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 387                    s.move_with(|map, selection| {
 388                        if current_mode == Mode::VisualLine {
 389                            let start_of_line = motion::start_of_line(map, false, selection.start);
 390                            selection.collapse_to(start_of_line, SelectionGoal::None)
 391                        } else {
 392                            selection.collapse_to(selection.start, SelectionGoal::None)
 393                        }
 394                    });
 395                });
 396            });
 397        }
 398        self.switch_mode(Mode::Insert, false, window, cx);
 399    }
 400
 401    fn insert_first_non_whitespace(
 402        &mut self,
 403        _: &InsertFirstNonWhitespace,
 404        window: &mut Window,
 405        cx: &mut Context<Self>,
 406    ) {
 407        self.start_recording(cx);
 408        self.switch_mode(Mode::Insert, false, window, cx);
 409        self.update_editor(window, cx, |_, editor, window, cx| {
 410            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 411                s.move_cursors_with(|map, cursor, _| {
 412                    (
 413                        first_non_whitespace(map, false, cursor),
 414                        SelectionGoal::None,
 415                    )
 416                });
 417            });
 418        });
 419    }
 420
 421    fn insert_end_of_line(
 422        &mut self,
 423        _: &InsertEndOfLine,
 424        window: &mut Window,
 425        cx: &mut Context<Self>,
 426    ) {
 427        self.start_recording(cx);
 428        self.switch_mode(Mode::Insert, false, window, cx);
 429        self.update_editor(window, cx, |_, editor, window, cx| {
 430            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 431                s.move_cursors_with(|map, cursor, _| {
 432                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 433                });
 434            });
 435        });
 436    }
 437
 438    fn insert_at_previous(
 439        &mut self,
 440        _: &InsertAtPrevious,
 441        window: &mut Window,
 442        cx: &mut Context<Self>,
 443    ) {
 444        self.start_recording(cx);
 445        self.switch_mode(Mode::Insert, false, window, cx);
 446        self.update_editor(window, cx, |vim, editor, window, cx| {
 447            let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
 448                return;
 449            };
 450
 451            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 452                s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 453            });
 454        });
 455    }
 456
 457    fn insert_line_above(
 458        &mut self,
 459        _: &InsertLineAbove,
 460        window: &mut Window,
 461        cx: &mut Context<Self>,
 462    ) {
 463        self.start_recording(cx);
 464        self.switch_mode(Mode::Insert, false, window, cx);
 465        self.update_editor(window, cx, |_, editor, window, cx| {
 466            editor.transact(window, cx, |editor, window, cx| {
 467                let selections = editor.selections.all::<Point>(cx);
 468                let snapshot = editor.buffer().read(cx).snapshot(cx);
 469
 470                let selection_start_rows: BTreeSet<u32> = selections
 471                    .into_iter()
 472                    .map(|selection| selection.start.row)
 473                    .collect();
 474                let edits = selection_start_rows
 475                    .into_iter()
 476                    .map(|row| {
 477                        let indent = snapshot
 478                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 479                            .chars()
 480                            .collect::<String>();
 481
 482                        let start_of_line = Point::new(row, 0);
 483                        (start_of_line..start_of_line, indent + "\n")
 484                    })
 485                    .collect::<Vec<_>>();
 486                editor.edit_with_autoindent(edits, cx);
 487                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 488                    s.move_cursors_with(|map, cursor, _| {
 489                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 490                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 491                        (insert_point, SelectionGoal::None)
 492                    });
 493                });
 494            });
 495        });
 496    }
 497
 498    fn insert_line_below(
 499        &mut self,
 500        _: &InsertLineBelow,
 501        window: &mut Window,
 502        cx: &mut Context<Self>,
 503    ) {
 504        self.start_recording(cx);
 505        self.switch_mode(Mode::Insert, false, window, cx);
 506        self.update_editor(window, cx, |_, editor, window, cx| {
 507            let text_layout_details = editor.text_layout_details(window);
 508            editor.transact(window, cx, |editor, window, cx| {
 509                let selections = editor.selections.all::<Point>(cx);
 510                let snapshot = editor.buffer().read(cx).snapshot(cx);
 511
 512                let selection_end_rows: BTreeSet<u32> = selections
 513                    .into_iter()
 514                    .map(|selection| selection.end.row)
 515                    .collect();
 516                let edits = selection_end_rows
 517                    .into_iter()
 518                    .map(|row| {
 519                        let indent = snapshot
 520                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 521                            .chars()
 522                            .collect::<String>();
 523
 524                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 525                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 526                    })
 527                    .collect::<Vec<_>>();
 528                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 529                    s.maybe_move_cursors_with(|map, cursor, goal| {
 530                        Motion::CurrentLine.move_point(
 531                            map,
 532                            cursor,
 533                            goal,
 534                            None,
 535                            &text_layout_details,
 536                        )
 537                    });
 538                });
 539                editor.edit_with_autoindent(edits, cx);
 540            });
 541        });
 542    }
 543
 544    fn insert_empty_line_above(
 545        &mut self,
 546        _: &InsertEmptyLineAbove,
 547        window: &mut Window,
 548        cx: &mut Context<Self>,
 549    ) {
 550        self.record_current_action(cx);
 551        let count = Vim::take_count(cx).unwrap_or(1);
 552        Vim::take_forced_motion(cx);
 553        self.update_editor(window, cx, |_, editor, window, cx| {
 554            editor.transact(window, cx, |editor, _, cx| {
 555                let selections = editor.selections.all::<Point>(cx);
 556
 557                let selection_start_rows: BTreeSet<u32> = selections
 558                    .into_iter()
 559                    .map(|selection| selection.start.row)
 560                    .collect();
 561                let edits = selection_start_rows
 562                    .into_iter()
 563                    .map(|row| {
 564                        let start_of_line = Point::new(row, 0);
 565                        (start_of_line..start_of_line, "\n".repeat(count))
 566                    })
 567                    .collect::<Vec<_>>();
 568                editor.edit(edits, cx);
 569            });
 570        });
 571    }
 572
 573    fn insert_empty_line_below(
 574        &mut self,
 575        _: &InsertEmptyLineBelow,
 576        window: &mut Window,
 577        cx: &mut Context<Self>,
 578    ) {
 579        self.record_current_action(cx);
 580        let count = Vim::take_count(cx).unwrap_or(1);
 581        Vim::take_forced_motion(cx);
 582        self.update_editor(window, cx, |_, editor, window, cx| {
 583            editor.transact(window, cx, |editor, window, cx| {
 584                let selections = editor.selections.all::<Point>(cx);
 585                let snapshot = editor.buffer().read(cx).snapshot(cx);
 586                let (_map, display_selections) = editor.selections.all_display(cx);
 587                let original_positions = display_selections
 588                    .iter()
 589                    .map(|s| (s.id, s.head()))
 590                    .collect::<HashMap<_, _>>();
 591
 592                let selection_end_rows: BTreeSet<u32> = selections
 593                    .into_iter()
 594                    .map(|selection| selection.end.row)
 595                    .collect();
 596                let edits = selection_end_rows
 597                    .into_iter()
 598                    .map(|row| {
 599                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 600                        (end_of_line..end_of_line, "\n".repeat(count))
 601                    })
 602                    .collect::<Vec<_>>();
 603                editor.edit(edits, cx);
 604
 605                editor.change_selections(None, window, cx, |s| {
 606                    s.move_with(|_, selection| {
 607                        if let Some(position) = original_positions.get(&selection.id) {
 608                            selection.collapse_to(*position, SelectionGoal::None);
 609                        }
 610                    });
 611                });
 612            });
 613        });
 614    }
 615
 616    fn join_lines_impl(
 617        &mut self,
 618        insert_whitespace: bool,
 619        window: &mut Window,
 620        cx: &mut Context<Self>,
 621    ) {
 622        self.record_current_action(cx);
 623        let mut times = Vim::take_count(cx).unwrap_or(1);
 624        Vim::take_forced_motion(cx);
 625        if self.mode.is_visual() {
 626            times = 1;
 627        } else if times > 1 {
 628            // 2J joins two lines together (same as J or 1J)
 629            times -= 1;
 630        }
 631
 632        self.update_editor(window, cx, |_, editor, window, cx| {
 633            editor.transact(window, cx, |editor, window, cx| {
 634                for _ in 0..times {
 635                    editor.join_lines_impl(insert_whitespace, window, cx)
 636                }
 637            })
 638        });
 639        if self.mode.is_visual() {
 640            self.switch_mode(Mode::Normal, true, window, cx)
 641        }
 642    }
 643
 644    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 645        let count = Vim::take_count(cx);
 646        let forced_motion = Vim::take_forced_motion(cx);
 647        self.yank_motion(
 648            motion::Motion::CurrentLine,
 649            count,
 650            forced_motion,
 651            window,
 652            cx,
 653        )
 654    }
 655
 656    fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
 657        let count = Vim::take_count(cx);
 658        Vim::take_forced_motion(cx);
 659        self.update_editor(window, cx, |vim, editor, _window, cx| {
 660            let selection = editor.selections.newest_anchor();
 661            if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
 662                let filename = if let Some(file) = buffer.read(cx).file() {
 663                    if count.is_some() {
 664                        if let Some(local) = file.as_local() {
 665                            local.abs_path(cx).to_string_lossy().to_string()
 666                        } else {
 667                            file.full_path(cx).to_string_lossy().to_string()
 668                        }
 669                    } else {
 670                        file.path().to_string_lossy().to_string()
 671                    }
 672                } else {
 673                    "[No Name]".into()
 674                };
 675                let buffer = buffer.read(cx);
 676                let snapshot = buffer.snapshot();
 677                let lines = buffer.max_point().row + 1;
 678                let current_line = selection.head().text_anchor.to_point(&snapshot).row;
 679                let percentage = current_line as f32 / lines as f32;
 680                let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 681                vim.status_label = Some(
 682                    format!(
 683                        "{}{} {} lines --{:.0}%--",
 684                        filename,
 685                        modified,
 686                        lines,
 687                        percentage * 100.0,
 688                    )
 689                    .into(),
 690                );
 691                cx.notify();
 692            }
 693        });
 694    }
 695
 696    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 697        self.record_current_action(cx);
 698        self.store_visual_marks(window, cx);
 699        self.update_editor(window, cx, |vim, editor, window, cx| {
 700            editor.transact(window, cx, |editor, window, cx| {
 701                let original_positions = vim.save_selection_starts(editor, cx);
 702                editor.toggle_comments(&Default::default(), window, cx);
 703                vim.restore_selection_cursors(editor, window, cx, original_positions);
 704            });
 705        });
 706        if self.mode.is_visual() {
 707            self.switch_mode(Mode::Normal, true, window, cx)
 708        }
 709    }
 710
 711    pub(crate) fn normal_replace(
 712        &mut self,
 713        text: Arc<str>,
 714        window: &mut Window,
 715        cx: &mut Context<Self>,
 716    ) {
 717        let is_return_char = text == "\n".into() || text == "\r".into();
 718        let count = Vim::take_count(cx).unwrap_or(1);
 719        Vim::take_forced_motion(cx);
 720        self.stop_recording(cx);
 721        self.update_editor(window, cx, |_, editor, window, cx| {
 722            editor.transact(window, cx, |editor, window, cx| {
 723                editor.set_clip_at_line_ends(false, cx);
 724                let (map, display_selections) = editor.selections.all_display(cx);
 725
 726                let mut edits = Vec::new();
 727                for selection in &display_selections {
 728                    let mut range = selection.range();
 729                    for _ in 0..count {
 730                        let new_point = movement::saturating_right(&map, range.end);
 731                        if range.end == new_point {
 732                            return;
 733                        }
 734                        range.end = new_point;
 735                    }
 736
 737                    edits.push((
 738                        range.start.to_offset(&map, Bias::Left)
 739                            ..range.end.to_offset(&map, Bias::Left),
 740                        text.repeat(if is_return_char { 0 } else { count }),
 741                    ));
 742                }
 743
 744                editor.edit(edits, cx);
 745                if is_return_char {
 746                    editor.newline(&editor::actions::Newline, window, cx);
 747                }
 748                editor.set_clip_at_line_ends(true, cx);
 749                editor.change_selections(None, window, cx, |s| {
 750                    s.move_with(|map, selection| {
 751                        let point = movement::saturating_left(map, selection.head());
 752                        selection.collapse_to(point, SelectionGoal::None)
 753                    });
 754                });
 755            });
 756        });
 757        self.pop_operator(window, cx);
 758    }
 759
 760    pub fn save_selection_starts(
 761        &self,
 762        editor: &Editor,
 763
 764        cx: &mut Context<Editor>,
 765    ) -> HashMap<usize, Anchor> {
 766        let (map, selections) = editor.selections.all_display(cx);
 767        selections
 768            .iter()
 769            .map(|selection| {
 770                (
 771                    selection.id,
 772                    map.display_point_to_anchor(selection.start, Bias::Right),
 773                )
 774            })
 775            .collect::<HashMap<_, _>>()
 776    }
 777
 778    pub fn restore_selection_cursors(
 779        &self,
 780        editor: &mut Editor,
 781        window: &mut Window,
 782        cx: &mut Context<Editor>,
 783        mut positions: HashMap<usize, Anchor>,
 784    ) {
 785        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 786            s.move_with(|map, selection| {
 787                if let Some(anchor) = positions.remove(&selection.id) {
 788                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 789                }
 790            });
 791        });
 792    }
 793
 794    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 795        if self.temp_mode {
 796            self.switch_mode(Mode::Insert, true, window, cx);
 797        }
 798    }
 799}
 800#[cfg(test)]
 801mod test {
 802    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 803    use indoc::indoc;
 804    use language::language_settings::AllLanguageSettings;
 805    use settings::SettingsStore;
 806
 807    use crate::{
 808        VimSettings, motion,
 809        state::Mode::{self},
 810        test::{NeovimBackedTestContext, VimTestContext},
 811    };
 812
 813    #[gpui::test]
 814    async fn test_h(cx: &mut gpui::TestAppContext) {
 815        let mut cx = NeovimBackedTestContext::new(cx).await;
 816        cx.simulate_at_each_offset(
 817            "h",
 818            indoc! {"
 819            ˇThe qˇuick
 820            ˇbrown"
 821            },
 822        )
 823        .await
 824        .assert_matches();
 825    }
 826
 827    #[gpui::test]
 828    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 829        let mut cx = NeovimBackedTestContext::new(cx).await;
 830        cx.simulate_at_each_offset(
 831            "backspace",
 832            indoc! {"
 833            ˇThe qˇuick
 834            ˇbrown"
 835            },
 836        )
 837        .await
 838        .assert_matches();
 839    }
 840
 841    #[gpui::test]
 842    async fn test_j(cx: &mut gpui::TestAppContext) {
 843        let mut cx = NeovimBackedTestContext::new(cx).await;
 844
 845        cx.set_shared_state(indoc! {"
 846            aaˇaa
 847            😃😃"
 848        })
 849        .await;
 850        cx.simulate_shared_keystrokes("j").await;
 851        cx.shared_state().await.assert_eq(indoc! {"
 852            aaaa
 853            😃ˇ😃"
 854        });
 855
 856        cx.simulate_at_each_offset(
 857            "j",
 858            indoc! {"
 859                ˇThe qˇuick broˇwn
 860                ˇfox jumps"
 861            },
 862        )
 863        .await
 864        .assert_matches();
 865    }
 866
 867    #[gpui::test]
 868    async fn test_enter(cx: &mut gpui::TestAppContext) {
 869        let mut cx = NeovimBackedTestContext::new(cx).await;
 870        cx.simulate_at_each_offset(
 871            "enter",
 872            indoc! {"
 873            ˇThe qˇuick broˇwn
 874            ˇfox jumps"
 875            },
 876        )
 877        .await
 878        .assert_matches();
 879    }
 880
 881    #[gpui::test]
 882    async fn test_k(cx: &mut gpui::TestAppContext) {
 883        let mut cx = NeovimBackedTestContext::new(cx).await;
 884        cx.simulate_at_each_offset(
 885            "k",
 886            indoc! {"
 887            ˇThe qˇuick
 888            ˇbrown fˇox jumˇps"
 889            },
 890        )
 891        .await
 892        .assert_matches();
 893    }
 894
 895    #[gpui::test]
 896    async fn test_l(cx: &mut gpui::TestAppContext) {
 897        let mut cx = NeovimBackedTestContext::new(cx).await;
 898        cx.simulate_at_each_offset(
 899            "l",
 900            indoc! {"
 901            ˇThe qˇuicˇk
 902            ˇbrowˇn"},
 903        )
 904        .await
 905        .assert_matches();
 906    }
 907
 908    #[gpui::test]
 909    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 910        let mut cx = NeovimBackedTestContext::new(cx).await;
 911        cx.simulate_at_each_offset(
 912            "$",
 913            indoc! {"
 914            ˇThe qˇuicˇk
 915            ˇbrowˇn"},
 916        )
 917        .await
 918        .assert_matches();
 919        cx.simulate_at_each_offset(
 920            "0",
 921            indoc! {"
 922                ˇThe qˇuicˇk
 923                ˇbrowˇn"},
 924        )
 925        .await
 926        .assert_matches();
 927    }
 928
 929    #[gpui::test]
 930    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 931        let mut cx = NeovimBackedTestContext::new(cx).await;
 932
 933        cx.simulate_at_each_offset(
 934            "shift-g",
 935            indoc! {"
 936                The ˇquick
 937
 938                brown fox jumps
 939                overˇ the lazy doˇg"},
 940        )
 941        .await
 942        .assert_matches();
 943        cx.simulate(
 944            "shift-g",
 945            indoc! {"
 946            The quiˇck
 947
 948            brown"},
 949        )
 950        .await
 951        .assert_matches();
 952        cx.simulate(
 953            "shift-g",
 954            indoc! {"
 955            The quiˇck
 956
 957            "},
 958        )
 959        .await
 960        .assert_matches();
 961    }
 962
 963    #[gpui::test]
 964    async fn test_w(cx: &mut gpui::TestAppContext) {
 965        let mut cx = NeovimBackedTestContext::new(cx).await;
 966        cx.simulate_at_each_offset(
 967            "w",
 968            indoc! {"
 969            The ˇquickˇ-ˇbrown
 970            ˇ
 971            ˇ
 972            ˇfox_jumps ˇover
 973            ˇthˇe"},
 974        )
 975        .await
 976        .assert_matches();
 977        cx.simulate_at_each_offset(
 978            "shift-w",
 979            indoc! {"
 980            The ˇquickˇ-ˇbrown
 981            ˇ
 982            ˇ
 983            ˇfox_jumps ˇover
 984            ˇthˇe"},
 985        )
 986        .await
 987        .assert_matches();
 988    }
 989
 990    #[gpui::test]
 991    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 992        let mut cx = NeovimBackedTestContext::new(cx).await;
 993        cx.simulate_at_each_offset(
 994            "e",
 995            indoc! {"
 996            Thˇe quicˇkˇ-browˇn
 997
 998
 999            fox_jumpˇs oveˇr
1000            thˇe"},
1001        )
1002        .await
1003        .assert_matches();
1004        cx.simulate_at_each_offset(
1005            "shift-e",
1006            indoc! {"
1007            Thˇe quicˇkˇ-browˇn
1008
1009
1010            fox_jumpˇs oveˇr
1011            thˇe"},
1012        )
1013        .await
1014        .assert_matches();
1015    }
1016
1017    #[gpui::test]
1018    async fn test_b(cx: &mut gpui::TestAppContext) {
1019        let mut cx = NeovimBackedTestContext::new(cx).await;
1020        cx.simulate_at_each_offset(
1021            "b",
1022            indoc! {"
1023            ˇThe ˇquickˇ-ˇbrown
1024            ˇ
1025            ˇ
1026            ˇfox_jumps ˇover
1027            ˇthe"},
1028        )
1029        .await
1030        .assert_matches();
1031        cx.simulate_at_each_offset(
1032            "shift-b",
1033            indoc! {"
1034            ˇThe ˇquickˇ-ˇbrown
1035            ˇ
1036            ˇ
1037            ˇfox_jumps ˇover
1038            ˇthe"},
1039        )
1040        .await
1041        .assert_matches();
1042    }
1043
1044    #[gpui::test]
1045    async fn test_gg(cx: &mut gpui::TestAppContext) {
1046        let mut cx = NeovimBackedTestContext::new(cx).await;
1047        cx.simulate_at_each_offset(
1048            "g g",
1049            indoc! {"
1050                The qˇuick
1051
1052                brown fox jumps
1053                over ˇthe laˇzy dog"},
1054        )
1055        .await
1056        .assert_matches();
1057        cx.simulate(
1058            "g g",
1059            indoc! {"
1060
1061
1062                brown fox jumps
1063                over the laˇzy dog"},
1064        )
1065        .await
1066        .assert_matches();
1067        cx.simulate(
1068            "2 g g",
1069            indoc! {"
1070                ˇ
1071
1072                brown fox jumps
1073                over the lazydog"},
1074        )
1075        .await
1076        .assert_matches();
1077    }
1078
1079    #[gpui::test]
1080    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1081        let mut cx = NeovimBackedTestContext::new(cx).await;
1082        cx.simulate_at_each_offset(
1083            "shift-g",
1084            indoc! {"
1085                The qˇuick
1086
1087                brown fox jumps
1088                over ˇthe laˇzy dog"},
1089        )
1090        .await
1091        .assert_matches();
1092        cx.simulate(
1093            "shift-g",
1094            indoc! {"
1095
1096
1097                brown fox jumps
1098                over the laˇzy dog"},
1099        )
1100        .await
1101        .assert_matches();
1102        cx.simulate(
1103            "2 shift-g",
1104            indoc! {"
1105                ˇ
1106
1107                brown fox jumps
1108                over the lazydog"},
1109        )
1110        .await
1111        .assert_matches();
1112    }
1113
1114    #[gpui::test]
1115    async fn test_a(cx: &mut gpui::TestAppContext) {
1116        let mut cx = NeovimBackedTestContext::new(cx).await;
1117        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1118            .await
1119            .assert_matches();
1120    }
1121
1122    #[gpui::test]
1123    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1124        let mut cx = NeovimBackedTestContext::new(cx).await;
1125        cx.simulate_at_each_offset(
1126            "shift-a",
1127            indoc! {"
1128            ˇ
1129            The qˇuick
1130            brown ˇfox "},
1131        )
1132        .await
1133        .assert_matches();
1134    }
1135
1136    #[gpui::test]
1137    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1138        let mut cx = NeovimBackedTestContext::new(cx).await;
1139        cx.simulate("^", "The qˇuick").await.assert_matches();
1140        cx.simulate("^", " The qˇuick").await.assert_matches();
1141        cx.simulate("^", "ˇ").await.assert_matches();
1142        cx.simulate(
1143            "^",
1144            indoc! {"
1145                The qˇuick
1146                brown fox"},
1147        )
1148        .await
1149        .assert_matches();
1150        cx.simulate(
1151            "^",
1152            indoc! {"
1153                ˇ
1154                The quick"},
1155        )
1156        .await
1157        .assert_matches();
1158        // Indoc disallows trailing whitespace.
1159        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1160    }
1161
1162    #[gpui::test]
1163    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1164        let mut cx = NeovimBackedTestContext::new(cx).await;
1165        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1166        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1167        cx.simulate("shift-i", "ˇ").await.assert_matches();
1168        cx.simulate(
1169            "shift-i",
1170            indoc! {"
1171                The qˇuick
1172                brown fox"},
1173        )
1174        .await
1175        .assert_matches();
1176        cx.simulate(
1177            "shift-i",
1178            indoc! {"
1179                ˇ
1180                The quick"},
1181        )
1182        .await
1183        .assert_matches();
1184    }
1185
1186    #[gpui::test]
1187    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1188        let mut cx = NeovimBackedTestContext::new(cx).await;
1189        cx.simulate(
1190            "shift-d",
1191            indoc! {"
1192                The qˇuick
1193                brown fox"},
1194        )
1195        .await
1196        .assert_matches();
1197        cx.simulate(
1198            "shift-d",
1199            indoc! {"
1200                The quick
1201                ˇ
1202                brown fox"},
1203        )
1204        .await
1205        .assert_matches();
1206    }
1207
1208    #[gpui::test]
1209    async fn test_x(cx: &mut gpui::TestAppContext) {
1210        let mut cx = NeovimBackedTestContext::new(cx).await;
1211        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1212            .await
1213            .assert_matches();
1214        cx.simulate(
1215            "x",
1216            indoc! {"
1217                Tesˇt
1218                test"},
1219        )
1220        .await
1221        .assert_matches();
1222    }
1223
1224    #[gpui::test]
1225    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1226        let mut cx = NeovimBackedTestContext::new(cx).await;
1227        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1228            .await
1229            .assert_matches();
1230        cx.simulate(
1231            "shift-x",
1232            indoc! {"
1233                Test
1234                ˇtest"},
1235        )
1236        .await
1237        .assert_matches();
1238    }
1239
1240    #[gpui::test]
1241    async fn test_o(cx: &mut gpui::TestAppContext) {
1242        let mut cx = NeovimBackedTestContext::new(cx).await;
1243        cx.simulate("o", "ˇ").await.assert_matches();
1244        cx.simulate("o", "The ˇquick").await.assert_matches();
1245        cx.simulate_at_each_offset(
1246            "o",
1247            indoc! {"
1248                The qˇuick
1249                brown ˇfox
1250                jumps ˇover"},
1251        )
1252        .await
1253        .assert_matches();
1254        cx.simulate(
1255            "o",
1256            indoc! {"
1257                The quick
1258                ˇ
1259                brown fox"},
1260        )
1261        .await
1262        .assert_matches();
1263
1264        cx.assert_binding(
1265            "o",
1266            indoc! {"
1267                fn test() {
1268                    println!(ˇ);
1269                }"},
1270            Mode::Normal,
1271            indoc! {"
1272                fn test() {
1273                    println!();
1274                    ˇ
1275                }"},
1276            Mode::Insert,
1277        );
1278
1279        cx.assert_binding(
1280            "o",
1281            indoc! {"
1282                fn test(ˇ) {
1283                    println!();
1284                }"},
1285            Mode::Normal,
1286            indoc! {"
1287                fn test() {
1288                    ˇ
1289                    println!();
1290                }"},
1291            Mode::Insert,
1292        );
1293    }
1294
1295    #[gpui::test]
1296    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1297        let mut cx = NeovimBackedTestContext::new(cx).await;
1298        cx.simulate("shift-o", "ˇ").await.assert_matches();
1299        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1300        cx.simulate_at_each_offset(
1301            "shift-o",
1302            indoc! {"
1303            The qˇuick
1304            brown ˇfox
1305            jumps ˇover"},
1306        )
1307        .await
1308        .assert_matches();
1309        cx.simulate(
1310            "shift-o",
1311            indoc! {"
1312            The quick
1313            ˇ
1314            brown fox"},
1315        )
1316        .await
1317        .assert_matches();
1318
1319        // Our indentation is smarter than vims. So we don't match here
1320        cx.assert_binding(
1321            "shift-o",
1322            indoc! {"
1323                fn test() {
1324                    println!(ˇ);
1325                }"},
1326            Mode::Normal,
1327            indoc! {"
1328                fn test() {
1329                    ˇ
1330                    println!();
1331                }"},
1332            Mode::Insert,
1333        );
1334        cx.assert_binding(
1335            "shift-o",
1336            indoc! {"
1337                fn test(ˇ) {
1338                    println!();
1339                }"},
1340            Mode::Normal,
1341            indoc! {"
1342                ˇ
1343                fn test() {
1344                    println!();
1345                }"},
1346            Mode::Insert,
1347        );
1348    }
1349
1350    #[gpui::test]
1351    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1352        let mut cx = NeovimBackedTestContext::new(cx).await;
1353        cx.simulate("[ space", "ˇ").await.assert_matches();
1354        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1355        cx.simulate_at_each_offset(
1356            "3 [ space",
1357            indoc! {"
1358            The qˇuick
1359            brown ˇfox
1360            jumps ˇover"},
1361        )
1362        .await
1363        .assert_matches();
1364        cx.simulate_at_each_offset(
1365            "[ space",
1366            indoc! {"
1367            The qˇuick
1368            brown ˇfox
1369            jumps ˇover"},
1370        )
1371        .await
1372        .assert_matches();
1373        cx.simulate(
1374            "[ space",
1375            indoc! {"
1376            The quick
1377            ˇ
1378            brown fox"},
1379        )
1380        .await
1381        .assert_matches();
1382
1383        cx.simulate("] space", "ˇ").await.assert_matches();
1384        cx.simulate("] space", "The ˇquick").await.assert_matches();
1385        cx.simulate_at_each_offset(
1386            "3 ] space",
1387            indoc! {"
1388            The qˇuick
1389            brown ˇfox
1390            jumps ˇover"},
1391        )
1392        .await
1393        .assert_matches();
1394        cx.simulate_at_each_offset(
1395            "] space",
1396            indoc! {"
1397            The qˇuick
1398            brown ˇfox
1399            jumps ˇover"},
1400        )
1401        .await
1402        .assert_matches();
1403        cx.simulate(
1404            "] space",
1405            indoc! {"
1406            The quick
1407            ˇ
1408            brown fox"},
1409        )
1410        .await
1411        .assert_matches();
1412    }
1413
1414    #[gpui::test]
1415    async fn test_dd(cx: &mut gpui::TestAppContext) {
1416        let mut cx = NeovimBackedTestContext::new(cx).await;
1417        cx.simulate("d d", "ˇ").await.assert_matches();
1418        cx.simulate("d d", "The ˇquick").await.assert_matches();
1419        cx.simulate_at_each_offset(
1420            "d d",
1421            indoc! {"
1422            The qˇuick
1423            brown ˇfox
1424            jumps ˇover"},
1425        )
1426        .await
1427        .assert_matches();
1428        cx.simulate(
1429            "d d",
1430            indoc! {"
1431                The quick
1432                ˇ
1433                brown fox"},
1434        )
1435        .await
1436        .assert_matches();
1437    }
1438
1439    #[gpui::test]
1440    async fn test_cc(cx: &mut gpui::TestAppContext) {
1441        let mut cx = NeovimBackedTestContext::new(cx).await;
1442        cx.simulate("c c", "ˇ").await.assert_matches();
1443        cx.simulate("c c", "The ˇquick").await.assert_matches();
1444        cx.simulate_at_each_offset(
1445            "c c",
1446            indoc! {"
1447                The quˇick
1448                brown ˇfox
1449                jumps ˇover"},
1450        )
1451        .await
1452        .assert_matches();
1453        cx.simulate(
1454            "c c",
1455            indoc! {"
1456                The quick
1457                ˇ
1458                brown fox"},
1459        )
1460        .await
1461        .assert_matches();
1462    }
1463
1464    #[gpui::test]
1465    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1466        let mut cx = NeovimBackedTestContext::new(cx).await;
1467
1468        for count in 1..=5 {
1469            cx.simulate_at_each_offset(
1470                &format!("{count} w"),
1471                indoc! {"
1472                    ˇThe quˇickˇ browˇn
1473                    ˇ
1474                    ˇfox ˇjumpsˇ-ˇoˇver
1475                    ˇthe lazy dog
1476                "},
1477            )
1478            .await
1479            .assert_matches();
1480        }
1481    }
1482
1483    #[gpui::test]
1484    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1485        let mut cx = NeovimBackedTestContext::new(cx).await;
1486        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1487            .await
1488            .assert_matches();
1489    }
1490
1491    #[gpui::test]
1492    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1493        let mut cx = NeovimBackedTestContext::new(cx).await;
1494
1495        for count in 1..=3 {
1496            let test_case = indoc! {"
1497                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1498                ˇ    ˇbˇaaˇa ˇbˇbˇb
1499                ˇ
1500                ˇb
1501            "};
1502
1503            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1504                .await
1505                .assert_matches();
1506
1507            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1508                .await
1509                .assert_matches();
1510        }
1511    }
1512
1513    #[gpui::test]
1514    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1515        let mut cx = NeovimBackedTestContext::new(cx).await;
1516        let test_case = indoc! {"
1517            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1518            ˇ    ˇbˇaaˇa ˇbˇbˇb
1519            ˇ•••
1520            ˇb
1521            "
1522        };
1523
1524        for count in 1..=3 {
1525            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1526                .await
1527                .assert_matches();
1528
1529            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1530                .await
1531                .assert_matches();
1532        }
1533    }
1534
1535    #[gpui::test]
1536    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1537        let mut cx = VimTestContext::new(cx, true).await;
1538        cx.update_global(|store: &mut SettingsStore, cx| {
1539            store.update_user_settings::<VimSettings>(cx, |s| {
1540                s.use_multiline_find = Some(true);
1541            });
1542        });
1543
1544        cx.assert_binding(
1545            "f l",
1546            indoc! {"
1547            ˇfunction print() {
1548                console.log('ok')
1549            }
1550            "},
1551            Mode::Normal,
1552            indoc! {"
1553            function print() {
1554                consoˇle.log('ok')
1555            }
1556            "},
1557            Mode::Normal,
1558        );
1559
1560        cx.assert_binding(
1561            "t l",
1562            indoc! {"
1563            ˇfunction print() {
1564                console.log('ok')
1565            }
1566            "},
1567            Mode::Normal,
1568            indoc! {"
1569            function print() {
1570                consˇole.log('ok')
1571            }
1572            "},
1573            Mode::Normal,
1574        );
1575    }
1576
1577    #[gpui::test]
1578    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1579        let mut cx = VimTestContext::new(cx, true).await;
1580        cx.update_global(|store: &mut SettingsStore, cx| {
1581            store.update_user_settings::<VimSettings>(cx, |s| {
1582                s.use_multiline_find = Some(true);
1583            });
1584        });
1585
1586        cx.assert_binding(
1587            "shift-f p",
1588            indoc! {"
1589            function print() {
1590                console.ˇlog('ok')
1591            }
1592            "},
1593            Mode::Normal,
1594            indoc! {"
1595            function ˇprint() {
1596                console.log('ok')
1597            }
1598            "},
1599            Mode::Normal,
1600        );
1601
1602        cx.assert_binding(
1603            "shift-t p",
1604            indoc! {"
1605            function print() {
1606                console.ˇlog('ok')
1607            }
1608            "},
1609            Mode::Normal,
1610            indoc! {"
1611            function pˇrint() {
1612                console.log('ok')
1613            }
1614            "},
1615            Mode::Normal,
1616        );
1617    }
1618
1619    #[gpui::test]
1620    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1621        let mut cx = VimTestContext::new(cx, true).await;
1622        cx.update_global(|store: &mut SettingsStore, cx| {
1623            store.update_user_settings::<VimSettings>(cx, |s| {
1624                s.use_smartcase_find = Some(true);
1625            });
1626        });
1627
1628        cx.assert_binding(
1629            "f p",
1630            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1631            Mode::Normal,
1632            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1633            Mode::Normal,
1634        );
1635
1636        cx.assert_binding(
1637            "shift-f p",
1638            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1639            Mode::Normal,
1640            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1641            Mode::Normal,
1642        );
1643
1644        cx.assert_binding(
1645            "t p",
1646            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1647            Mode::Normal,
1648            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1649            Mode::Normal,
1650        );
1651
1652        cx.assert_binding(
1653            "shift-t p",
1654            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1655            Mode::Normal,
1656            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1657            Mode::Normal,
1658        );
1659    }
1660
1661    #[gpui::test]
1662    async fn test_percent(cx: &mut TestAppContext) {
1663        let mut cx = NeovimBackedTestContext::new(cx).await;
1664        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1665            .await
1666            .assert_matches();
1667        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1668            .await
1669            .assert_matches();
1670        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1671            .await
1672            .assert_matches();
1673    }
1674
1675    #[gpui::test]
1676    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1677        let mut cx = NeovimBackedTestContext::new(cx).await;
1678
1679        // goes to current line end
1680        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1681        cx.simulate_shared_keystrokes("$").await;
1682        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1683
1684        // goes to next line end
1685        cx.simulate_shared_keystrokes("2 $").await;
1686        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1687
1688        // try to exceed the final line.
1689        cx.simulate_shared_keystrokes("4 $").await;
1690        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1691    }
1692
1693    #[gpui::test]
1694    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1695        let mut cx = VimTestContext::new(cx, true).await;
1696        cx.update(|_, cx| {
1697            cx.bind_keys(vec![
1698                KeyBinding::new(
1699                    "w",
1700                    motion::NextSubwordStart {
1701                        ignore_punctuation: false,
1702                    },
1703                    Some("Editor && VimControl && !VimWaiting && !menu"),
1704                ),
1705                KeyBinding::new(
1706                    "b",
1707                    motion::PreviousSubwordStart {
1708                        ignore_punctuation: false,
1709                    },
1710                    Some("Editor && VimControl && !VimWaiting && !menu"),
1711                ),
1712                KeyBinding::new(
1713                    "e",
1714                    motion::NextSubwordEnd {
1715                        ignore_punctuation: false,
1716                    },
1717                    Some("Editor && VimControl && !VimWaiting && !menu"),
1718                ),
1719                KeyBinding::new(
1720                    "g e",
1721                    motion::PreviousSubwordEnd {
1722                        ignore_punctuation: false,
1723                    },
1724                    Some("Editor && VimControl && !VimWaiting && !menu"),
1725                ),
1726            ]);
1727        });
1728
1729        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1730        // Special case: In 'cw', 'w' acts like 'e'
1731        cx.assert_binding(
1732            "c w",
1733            indoc! {"ˇassert_binding"},
1734            Mode::Normal,
1735            indoc! {"ˇ_binding"},
1736            Mode::Insert,
1737        );
1738
1739        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1740
1741        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1742
1743        cx.assert_binding_normal(
1744            "g e",
1745            indoc! {"assert_bindinˇg"},
1746            indoc! {"asserˇt_binding"},
1747        );
1748    }
1749
1750    #[gpui::test]
1751    async fn test_r(cx: &mut gpui::TestAppContext) {
1752        let mut cx = NeovimBackedTestContext::new(cx).await;
1753
1754        cx.set_shared_state("ˇhello\n").await;
1755        cx.simulate_shared_keystrokes("r -").await;
1756        cx.shared_state().await.assert_eq("ˇ-ello\n");
1757
1758        cx.set_shared_state("ˇhello\n").await;
1759        cx.simulate_shared_keystrokes("3 r -").await;
1760        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1761
1762        cx.set_shared_state("ˇhello\n").await;
1763        cx.simulate_shared_keystrokes("r - 2 l .").await;
1764        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1765
1766        cx.set_shared_state("ˇhello world\n").await;
1767        cx.simulate_shared_keystrokes("2 r - f w .").await;
1768        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1769
1770        cx.set_shared_state("ˇhello world\n").await;
1771        cx.simulate_shared_keystrokes("2 0 r - ").await;
1772        cx.shared_state().await.assert_eq("ˇhello world\n");
1773
1774        cx.set_shared_state("  helloˇ world\n").await;
1775        cx.simulate_shared_keystrokes("r enter").await;
1776        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
1777
1778        cx.set_shared_state("  helloˇ world\n").await;
1779        cx.simulate_shared_keystrokes("2 r enter").await;
1780        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
1781    }
1782
1783    #[gpui::test]
1784    async fn test_gq(cx: &mut gpui::TestAppContext) {
1785        let mut cx = NeovimBackedTestContext::new(cx).await;
1786        cx.set_neovim_option("textwidth=5").await;
1787
1788        cx.update(|_, cx| {
1789            SettingsStore::update_global(cx, |settings, cx| {
1790                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1791                    settings.defaults.preferred_line_length = Some(5);
1792                });
1793            })
1794        });
1795
1796        cx.set_shared_state("ˇth th th th th th\n").await;
1797        cx.simulate_shared_keystrokes("g q q").await;
1798        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1799
1800        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1801            .await;
1802        cx.simulate_shared_keystrokes("v j g q").await;
1803        cx.shared_state()
1804            .await
1805            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1806    }
1807
1808    #[gpui::test]
1809    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1810        let mut cx = NeovimBackedTestContext::new(cx).await;
1811        cx.set_neovim_option("filetype=rust").await;
1812
1813        cx.set_shared_state("// helloˇ\n").await;
1814        cx.simulate_shared_keystrokes("o").await;
1815        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1816        cx.simulate_shared_keystrokes("x escape shift-o").await;
1817        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1818    }
1819
1820    #[gpui::test]
1821    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1822        let mut cx = NeovimBackedTestContext::new(cx).await;
1823        cx.set_shared_state("heˇllo\n").await;
1824        cx.simulate_shared_keystrokes("y y p").await;
1825        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1826    }
1827
1828    #[gpui::test]
1829    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1830        let mut cx = NeovimBackedTestContext::new(cx).await;
1831        cx.set_shared_state("heˇllo").await;
1832        cx.simulate_shared_keystrokes("y y p").await;
1833        cx.shared_state().await.assert_eq("hello\nˇhello");
1834    }
1835
1836    #[gpui::test]
1837    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1838        let mut cx = NeovimBackedTestContext::new(cx).await;
1839        cx.set_shared_state("heˇllo\nhello").await;
1840        cx.simulate_shared_keystrokes("2 y y p").await;
1841        cx.shared_state()
1842            .await
1843            .assert_eq("hello\nˇhello\nhello\nhello");
1844    }
1845
1846    #[gpui::test]
1847    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1848        let mut cx = NeovimBackedTestContext::new(cx).await;
1849        cx.set_shared_state("heˇllo").await;
1850        cx.simulate_shared_keystrokes("d d").await;
1851        cx.shared_state().await.assert_eq("ˇ");
1852        cx.simulate_shared_keystrokes("p p").await;
1853        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1854    }
1855
1856    #[gpui::test]
1857    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1858        let mut cx = NeovimBackedTestContext::new(cx).await;
1859
1860        cx.set_shared_state("heˇllo").await;
1861        cx.simulate_shared_keystrokes("v i w shift-i").await;
1862        cx.shared_state().await.assert_eq("ˇhello");
1863
1864        cx.set_shared_state(indoc! {"
1865            The quick brown
1866            fox ˇjumps over
1867            the lazy dog"})
1868            .await;
1869        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1870        cx.shared_state().await.assert_eq(indoc! {"
1871            The quick brown
1872            ˇfox jumps over
1873            the lazy dog"});
1874
1875        cx.set_shared_state(indoc! {"
1876            The quick brown
1877            fox ˇjumps over
1878            the lazy dog"})
1879            .await;
1880        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1881        cx.shared_state().await.assert_eq(indoc! {"
1882            The quick brown
1883            fox jˇumps over
1884            the lazy dog"});
1885    }
1886}