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