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