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