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 settings::SettingsStore;
 993
 994    use crate::{
 995        motion,
 996        state::Mode::{self},
 997        test::{NeovimBackedTestContext, VimTestContext},
 998    };
 999
1000    #[gpui::test]
1001    async fn test_h(cx: &mut gpui::TestAppContext) {
1002        let mut cx = NeovimBackedTestContext::new(cx).await;
1003        cx.simulate_at_each_offset(
1004            "h",
1005            indoc! {"
1006            ˇThe qˇuick
1007            ˇbrown"
1008            },
1009        )
1010        .await
1011        .assert_matches();
1012    }
1013
1014    #[gpui::test]
1015    async fn test_backspace(cx: &mut gpui::TestAppContext) {
1016        let mut cx = NeovimBackedTestContext::new(cx).await;
1017        cx.simulate_at_each_offset(
1018            "backspace",
1019            indoc! {"
1020            ˇThe qˇuick
1021            ˇbrown"
1022            },
1023        )
1024        .await
1025        .assert_matches();
1026    }
1027
1028    #[gpui::test]
1029    async fn test_j(cx: &mut gpui::TestAppContext) {
1030        let mut cx = NeovimBackedTestContext::new(cx).await;
1031
1032        cx.set_shared_state(indoc! {"
1033            aaˇaa
1034            😃😃"
1035        })
1036        .await;
1037        cx.simulate_shared_keystrokes("j").await;
1038        cx.shared_state().await.assert_eq(indoc! {"
1039            aaaa
1040            😃ˇ😃"
1041        });
1042
1043        cx.simulate_at_each_offset(
1044            "j",
1045            indoc! {"
1046                ˇThe qˇuick broˇwn
1047                ˇfox jumps"
1048            },
1049        )
1050        .await
1051        .assert_matches();
1052    }
1053
1054    #[gpui::test]
1055    async fn test_enter(cx: &mut gpui::TestAppContext) {
1056        let mut cx = NeovimBackedTestContext::new(cx).await;
1057        cx.simulate_at_each_offset(
1058            "enter",
1059            indoc! {"
1060            ˇThe qˇuick broˇwn
1061            ˇfox jumps"
1062            },
1063        )
1064        .await
1065        .assert_matches();
1066    }
1067
1068    #[gpui::test]
1069    async fn test_k(cx: &mut gpui::TestAppContext) {
1070        let mut cx = NeovimBackedTestContext::new(cx).await;
1071        cx.simulate_at_each_offset(
1072            "k",
1073            indoc! {"
1074            ˇThe qˇuick
1075            ˇbrown fˇox jumˇps"
1076            },
1077        )
1078        .await
1079        .assert_matches();
1080    }
1081
1082    #[gpui::test]
1083    async fn test_l(cx: &mut gpui::TestAppContext) {
1084        let mut cx = NeovimBackedTestContext::new(cx).await;
1085        cx.simulate_at_each_offset(
1086            "l",
1087            indoc! {"
1088            ˇThe qˇuicˇk
1089            ˇbrowˇn"},
1090        )
1091        .await
1092        .assert_matches();
1093    }
1094
1095    #[gpui::test]
1096    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1097        let mut cx = NeovimBackedTestContext::new(cx).await;
1098        cx.simulate_at_each_offset(
1099            "$",
1100            indoc! {"
1101            ˇThe qˇuicˇk
1102            ˇbrowˇn"},
1103        )
1104        .await
1105        .assert_matches();
1106        cx.simulate_at_each_offset(
1107            "0",
1108            indoc! {"
1109                ˇThe qˇuicˇk
1110                ˇbrowˇn"},
1111        )
1112        .await
1113        .assert_matches();
1114    }
1115
1116    #[gpui::test]
1117    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1118        let mut cx = NeovimBackedTestContext::new(cx).await;
1119
1120        cx.simulate_at_each_offset(
1121            "shift-g",
1122            indoc! {"
1123                The ˇquick
1124
1125                brown fox jumps
1126                overˇ the lazy doˇg"},
1127        )
1128        .await
1129        .assert_matches();
1130        cx.simulate(
1131            "shift-g",
1132            indoc! {"
1133            The quiˇck
1134
1135            brown"},
1136        )
1137        .await
1138        .assert_matches();
1139        cx.simulate(
1140            "shift-g",
1141            indoc! {"
1142            The quiˇck
1143
1144            "},
1145        )
1146        .await
1147        .assert_matches();
1148    }
1149
1150    #[gpui::test]
1151    async fn test_w(cx: &mut gpui::TestAppContext) {
1152        let mut cx = NeovimBackedTestContext::new(cx).await;
1153        cx.simulate_at_each_offset(
1154            "w",
1155            indoc! {"
1156            The ˇquickˇ-ˇbrown
1157            ˇ
1158            ˇ
1159            ˇfox_jumps ˇover
1160            ˇthˇe"},
1161        )
1162        .await
1163        .assert_matches();
1164        cx.simulate_at_each_offset(
1165            "shift-w",
1166            indoc! {"
1167            The ˇquickˇ-ˇbrown
1168            ˇ
1169            ˇ
1170            ˇfox_jumps ˇover
1171            ˇthˇe"},
1172        )
1173        .await
1174        .assert_matches();
1175    }
1176
1177    #[gpui::test]
1178    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1179        let mut cx = NeovimBackedTestContext::new(cx).await;
1180        cx.simulate_at_each_offset(
1181            "e",
1182            indoc! {"
1183            Thˇe quicˇkˇ-browˇn
1184
1185
1186            fox_jumpˇs oveˇr
1187            thˇe"},
1188        )
1189        .await
1190        .assert_matches();
1191        cx.simulate_at_each_offset(
1192            "shift-e",
1193            indoc! {"
1194            Thˇe quicˇkˇ-browˇn
1195
1196
1197            fox_jumpˇs oveˇr
1198            thˇe"},
1199        )
1200        .await
1201        .assert_matches();
1202    }
1203
1204    #[gpui::test]
1205    async fn test_b(cx: &mut gpui::TestAppContext) {
1206        let mut cx = NeovimBackedTestContext::new(cx).await;
1207        cx.simulate_at_each_offset(
1208            "b",
1209            indoc! {"
1210            ˇThe ˇquickˇ-ˇbrown
1211            ˇ
1212            ˇ
1213            ˇfox_jumps ˇover
1214            ˇthe"},
1215        )
1216        .await
1217        .assert_matches();
1218        cx.simulate_at_each_offset(
1219            "shift-b",
1220            indoc! {"
1221            ˇThe ˇquickˇ-ˇbrown
1222            ˇ
1223            ˇ
1224            ˇfox_jumps ˇover
1225            ˇthe"},
1226        )
1227        .await
1228        .assert_matches();
1229    }
1230
1231    #[gpui::test]
1232    async fn test_gg(cx: &mut gpui::TestAppContext) {
1233        let mut cx = NeovimBackedTestContext::new(cx).await;
1234        cx.simulate_at_each_offset(
1235            "g g",
1236            indoc! {"
1237                The qˇuick
1238
1239                brown fox jumps
1240                over ˇthe laˇzy dog"},
1241        )
1242        .await
1243        .assert_matches();
1244        cx.simulate(
1245            "g g",
1246            indoc! {"
1247
1248
1249                brown fox jumps
1250                over the laˇzy dog"},
1251        )
1252        .await
1253        .assert_matches();
1254        cx.simulate(
1255            "2 g g",
1256            indoc! {"
1257                ˇ
1258
1259                brown fox jumps
1260                over the lazydog"},
1261        )
1262        .await
1263        .assert_matches();
1264    }
1265
1266    #[gpui::test]
1267    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1268        let mut cx = NeovimBackedTestContext::new(cx).await;
1269        cx.simulate_at_each_offset(
1270            "shift-g",
1271            indoc! {"
1272                The qˇuick
1273
1274                brown fox jumps
1275                over ˇthe laˇzy dog"},
1276        )
1277        .await
1278        .assert_matches();
1279        cx.simulate(
1280            "shift-g",
1281            indoc! {"
1282
1283
1284                brown fox jumps
1285                over the laˇzy dog"},
1286        )
1287        .await
1288        .assert_matches();
1289        cx.simulate(
1290            "2 shift-g",
1291            indoc! {"
1292                ˇ
1293
1294                brown fox jumps
1295                over the lazydog"},
1296        )
1297        .await
1298        .assert_matches();
1299    }
1300
1301    #[gpui::test]
1302    async fn test_a(cx: &mut gpui::TestAppContext) {
1303        let mut cx = NeovimBackedTestContext::new(cx).await;
1304        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1305            .await
1306            .assert_matches();
1307    }
1308
1309    #[gpui::test]
1310    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1311        let mut cx = NeovimBackedTestContext::new(cx).await;
1312        cx.simulate_at_each_offset(
1313            "shift-a",
1314            indoc! {"
1315            ˇ
1316            The qˇuick
1317            brown ˇfox "},
1318        )
1319        .await
1320        .assert_matches();
1321    }
1322
1323    #[gpui::test]
1324    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1325        let mut cx = NeovimBackedTestContext::new(cx).await;
1326        cx.simulate("^", "The qˇuick").await.assert_matches();
1327        cx.simulate("^", " The qˇuick").await.assert_matches();
1328        cx.simulate("^", "ˇ").await.assert_matches();
1329        cx.simulate(
1330            "^",
1331            indoc! {"
1332                The qˇuick
1333                brown fox"},
1334        )
1335        .await
1336        .assert_matches();
1337        cx.simulate(
1338            "^",
1339            indoc! {"
1340                ˇ
1341                The quick"},
1342        )
1343        .await
1344        .assert_matches();
1345        // Indoc disallows trailing whitespace.
1346        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1347    }
1348
1349    #[gpui::test]
1350    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1351        let mut cx = NeovimBackedTestContext::new(cx).await;
1352        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1353        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1354        cx.simulate("shift-i", "ˇ").await.assert_matches();
1355        cx.simulate(
1356            "shift-i",
1357            indoc! {"
1358                The qˇuick
1359                brown fox"},
1360        )
1361        .await
1362        .assert_matches();
1363        cx.simulate(
1364            "shift-i",
1365            indoc! {"
1366                ˇ
1367                The quick"},
1368        )
1369        .await
1370        .assert_matches();
1371    }
1372
1373    #[gpui::test]
1374    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1375        let mut cx = NeovimBackedTestContext::new(cx).await;
1376        cx.simulate(
1377            "shift-d",
1378            indoc! {"
1379                The qˇuick
1380                brown fox"},
1381        )
1382        .await
1383        .assert_matches();
1384        cx.simulate(
1385            "shift-d",
1386            indoc! {"
1387                The quick
1388                ˇ
1389                brown fox"},
1390        )
1391        .await
1392        .assert_matches();
1393    }
1394
1395    #[gpui::test]
1396    async fn test_x(cx: &mut gpui::TestAppContext) {
1397        let mut cx = NeovimBackedTestContext::new(cx).await;
1398        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1399            .await
1400            .assert_matches();
1401        cx.simulate(
1402            "x",
1403            indoc! {"
1404                Tesˇt
1405                test"},
1406        )
1407        .await
1408        .assert_matches();
1409    }
1410
1411    #[gpui::test]
1412    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1413        let mut cx = NeovimBackedTestContext::new(cx).await;
1414        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1415            .await
1416            .assert_matches();
1417        cx.simulate(
1418            "shift-x",
1419            indoc! {"
1420                Test
1421                ˇtest"},
1422        )
1423        .await
1424        .assert_matches();
1425    }
1426
1427    #[gpui::test]
1428    async fn test_o(cx: &mut gpui::TestAppContext) {
1429        let mut cx = NeovimBackedTestContext::new(cx).await;
1430        cx.simulate("o", "ˇ").await.assert_matches();
1431        cx.simulate("o", "The ˇquick").await.assert_matches();
1432        cx.simulate_at_each_offset(
1433            "o",
1434            indoc! {"
1435                The qˇuick
1436                brown ˇfox
1437                jumps ˇover"},
1438        )
1439        .await
1440        .assert_matches();
1441        cx.simulate(
1442            "o",
1443            indoc! {"
1444                The quick
1445                ˇ
1446                brown fox"},
1447        )
1448        .await
1449        .assert_matches();
1450
1451        cx.assert_binding(
1452            "o",
1453            indoc! {"
1454                fn test() {
1455                    println!(ˇ);
1456                }"},
1457            Mode::Normal,
1458            indoc! {"
1459                fn test() {
1460                    println!();
1461                    ˇ
1462                }"},
1463            Mode::Insert,
1464        );
1465
1466        cx.assert_binding(
1467            "o",
1468            indoc! {"
1469                fn test(ˇ) {
1470                    println!();
1471                }"},
1472            Mode::Normal,
1473            indoc! {"
1474                fn test() {
1475                    ˇ
1476                    println!();
1477                }"},
1478            Mode::Insert,
1479        );
1480    }
1481
1482    #[gpui::test]
1483    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1484        let mut cx = NeovimBackedTestContext::new(cx).await;
1485        cx.simulate("shift-o", "ˇ").await.assert_matches();
1486        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1487        cx.simulate_at_each_offset(
1488            "shift-o",
1489            indoc! {"
1490            The qˇuick
1491            brown ˇfox
1492            jumps ˇover"},
1493        )
1494        .await
1495        .assert_matches();
1496        cx.simulate(
1497            "shift-o",
1498            indoc! {"
1499            The quick
1500            ˇ
1501            brown fox"},
1502        )
1503        .await
1504        .assert_matches();
1505
1506        // Our indentation is smarter than vims. So we don't match here
1507        cx.assert_binding(
1508            "shift-o",
1509            indoc! {"
1510                fn test() {
1511                    println!(ˇ);
1512                }"},
1513            Mode::Normal,
1514            indoc! {"
1515                fn test() {
1516                    ˇ
1517                    println!();
1518                }"},
1519            Mode::Insert,
1520        );
1521        cx.assert_binding(
1522            "shift-o",
1523            indoc! {"
1524                fn test(ˇ) {
1525                    println!();
1526                }"},
1527            Mode::Normal,
1528            indoc! {"
1529                ˇ
1530                fn test() {
1531                    println!();
1532                }"},
1533            Mode::Insert,
1534        );
1535    }
1536
1537    #[gpui::test]
1538    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1539        let mut cx = NeovimBackedTestContext::new(cx).await;
1540        cx.simulate("[ space", "ˇ").await.assert_matches();
1541        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1542        cx.simulate_at_each_offset(
1543            "3 [ space",
1544            indoc! {"
1545            The qˇuick
1546            brown ˇfox
1547            jumps ˇover"},
1548        )
1549        .await
1550        .assert_matches();
1551        cx.simulate_at_each_offset(
1552            "[ space",
1553            indoc! {"
1554            The qˇuick
1555            brown ˇfox
1556            jumps ˇover"},
1557        )
1558        .await
1559        .assert_matches();
1560        cx.simulate(
1561            "[ space",
1562            indoc! {"
1563            The quick
1564            ˇ
1565            brown fox"},
1566        )
1567        .await
1568        .assert_matches();
1569
1570        cx.simulate("] space", "ˇ").await.assert_matches();
1571        cx.simulate("] space", "The ˇquick").await.assert_matches();
1572        cx.simulate_at_each_offset(
1573            "3 ] space",
1574            indoc! {"
1575            The qˇuick
1576            brown ˇfox
1577            jumps ˇover"},
1578        )
1579        .await
1580        .assert_matches();
1581        cx.simulate_at_each_offset(
1582            "] space",
1583            indoc! {"
1584            The qˇuick
1585            brown ˇfox
1586            jumps ˇover"},
1587        )
1588        .await
1589        .assert_matches();
1590        cx.simulate(
1591            "] space",
1592            indoc! {"
1593            The quick
1594            ˇ
1595            brown fox"},
1596        )
1597        .await
1598        .assert_matches();
1599    }
1600
1601    #[gpui::test]
1602    async fn test_dd(cx: &mut gpui::TestAppContext) {
1603        let mut cx = NeovimBackedTestContext::new(cx).await;
1604        cx.simulate("d d", "ˇ").await.assert_matches();
1605        cx.simulate("d d", "The ˇquick").await.assert_matches();
1606        cx.simulate_at_each_offset(
1607            "d d",
1608            indoc! {"
1609            The qˇuick
1610            brown ˇfox
1611            jumps ˇover"},
1612        )
1613        .await
1614        .assert_matches();
1615        cx.simulate(
1616            "d d",
1617            indoc! {"
1618                The quick
1619                ˇ
1620                brown fox"},
1621        )
1622        .await
1623        .assert_matches();
1624    }
1625
1626    #[gpui::test]
1627    async fn test_cc(cx: &mut gpui::TestAppContext) {
1628        let mut cx = NeovimBackedTestContext::new(cx).await;
1629        cx.simulate("c c", "ˇ").await.assert_matches();
1630        cx.simulate("c c", "The ˇquick").await.assert_matches();
1631        cx.simulate_at_each_offset(
1632            "c c",
1633            indoc! {"
1634                The quˇick
1635                brown ˇfox
1636                jumps ˇover"},
1637        )
1638        .await
1639        .assert_matches();
1640        cx.simulate(
1641            "c c",
1642            indoc! {"
1643                The quick
1644                ˇ
1645                brown fox"},
1646        )
1647        .await
1648        .assert_matches();
1649    }
1650
1651    #[gpui::test]
1652    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1653        let mut cx = NeovimBackedTestContext::new(cx).await;
1654
1655        for count in 1..=5 {
1656            cx.simulate_at_each_offset(
1657                &format!("{count} w"),
1658                indoc! {"
1659                    ˇThe quˇickˇ browˇn
1660                    ˇ
1661                    ˇfox ˇjumpsˇ-ˇoˇver
1662                    ˇthe lazy dog
1663                "},
1664            )
1665            .await
1666            .assert_matches();
1667        }
1668    }
1669
1670    #[gpui::test]
1671    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1672        let mut cx = NeovimBackedTestContext::new(cx).await;
1673        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1674            .await
1675            .assert_matches();
1676    }
1677
1678    #[gpui::test]
1679    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1680        let mut cx = NeovimBackedTestContext::new(cx).await;
1681
1682        for count in 1..=3 {
1683            let test_case = indoc! {"
1684                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1685                ˇ    ˇbˇaaˇa ˇbˇbˇb
1686                ˇ
1687                ˇb
1688            "};
1689
1690            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1691                .await
1692                .assert_matches();
1693
1694            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1695                .await
1696                .assert_matches();
1697        }
1698    }
1699
1700    #[gpui::test]
1701    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1702        let mut cx = NeovimBackedTestContext::new(cx).await;
1703        let test_case = indoc! {"
1704            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1705            ˇ    ˇbˇaaˇa ˇbˇbˇb
1706            ˇ•••
1707            ˇb
1708            "
1709        };
1710
1711        for count in 1..=3 {
1712            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1713                .await
1714                .assert_matches();
1715
1716            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1717                .await
1718                .assert_matches();
1719        }
1720    }
1721
1722    #[gpui::test]
1723    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1724        let mut cx = VimTestContext::new(cx, true).await;
1725        cx.update_global(|store: &mut SettingsStore, cx| {
1726            store.update_user_settings(cx, |s| {
1727                s.vim.get_or_insert_default().use_smartcase_find = Some(true);
1728            });
1729        });
1730
1731        cx.assert_binding(
1732            "f p",
1733            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1734            Mode::Normal,
1735            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1736            Mode::Normal,
1737        );
1738
1739        cx.assert_binding(
1740            "shift-f p",
1741            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1742            Mode::Normal,
1743            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1744            Mode::Normal,
1745        );
1746
1747        cx.assert_binding(
1748            "t p",
1749            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1750            Mode::Normal,
1751            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1752            Mode::Normal,
1753        );
1754
1755        cx.assert_binding(
1756            "shift-t p",
1757            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1758            Mode::Normal,
1759            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1760            Mode::Normal,
1761        );
1762    }
1763
1764    #[gpui::test]
1765    async fn test_percent(cx: &mut TestAppContext) {
1766        let mut cx = NeovimBackedTestContext::new(cx).await;
1767        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1768            .await
1769            .assert_matches();
1770        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1771            .await
1772            .assert_matches();
1773        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1774            .await
1775            .assert_matches();
1776    }
1777
1778    #[gpui::test]
1779    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1780        let mut cx = NeovimBackedTestContext::new(cx).await;
1781
1782        // goes to current line end
1783        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1784        cx.simulate_shared_keystrokes("$").await;
1785        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1786
1787        // goes to next line end
1788        cx.simulate_shared_keystrokes("2 $").await;
1789        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1790
1791        // try to exceed the final line.
1792        cx.simulate_shared_keystrokes("4 $").await;
1793        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1794    }
1795
1796    #[gpui::test]
1797    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1798        let mut cx = VimTestContext::new(cx, true).await;
1799        cx.update(|_, cx| {
1800            cx.bind_keys(vec![
1801                KeyBinding::new(
1802                    "w",
1803                    motion::NextSubwordStart {
1804                        ignore_punctuation: false,
1805                    },
1806                    Some("Editor && VimControl && !VimWaiting && !menu"),
1807                ),
1808                KeyBinding::new(
1809                    "b",
1810                    motion::PreviousSubwordStart {
1811                        ignore_punctuation: false,
1812                    },
1813                    Some("Editor && VimControl && !VimWaiting && !menu"),
1814                ),
1815                KeyBinding::new(
1816                    "e",
1817                    motion::NextSubwordEnd {
1818                        ignore_punctuation: false,
1819                    },
1820                    Some("Editor && VimControl && !VimWaiting && !menu"),
1821                ),
1822                KeyBinding::new(
1823                    "g e",
1824                    motion::PreviousSubwordEnd {
1825                        ignore_punctuation: false,
1826                    },
1827                    Some("Editor && VimControl && !VimWaiting && !menu"),
1828                ),
1829            ]);
1830        });
1831
1832        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1833        // Special case: In 'cw', 'w' acts like 'e'
1834        cx.assert_binding(
1835            "c w",
1836            indoc! {"ˇassert_binding"},
1837            Mode::Normal,
1838            indoc! {"ˇ_binding"},
1839            Mode::Insert,
1840        );
1841
1842        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1843
1844        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1845
1846        cx.assert_binding_normal(
1847            "g e",
1848            indoc! {"assert_bindinˇg"},
1849            indoc! {"asserˇt_binding"},
1850        );
1851    }
1852
1853    #[gpui::test]
1854    async fn test_r(cx: &mut gpui::TestAppContext) {
1855        let mut cx = NeovimBackedTestContext::new(cx).await;
1856
1857        cx.set_shared_state("ˇhello\n").await;
1858        cx.simulate_shared_keystrokes("r -").await;
1859        cx.shared_state().await.assert_eq("ˇ-ello\n");
1860
1861        cx.set_shared_state("ˇhello\n").await;
1862        cx.simulate_shared_keystrokes("3 r -").await;
1863        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1864
1865        cx.set_shared_state("ˇhello\n").await;
1866        cx.simulate_shared_keystrokes("r - 2 l .").await;
1867        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1868
1869        cx.set_shared_state("ˇhello world\n").await;
1870        cx.simulate_shared_keystrokes("2 r - f w .").await;
1871        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1872
1873        cx.set_shared_state("ˇhello world\n").await;
1874        cx.simulate_shared_keystrokes("2 0 r - ").await;
1875        cx.shared_state().await.assert_eq("ˇhello world\n");
1876
1877        cx.set_shared_state("  helloˇ world\n").await;
1878        cx.simulate_shared_keystrokes("r enter").await;
1879        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
1880
1881        cx.set_shared_state("  helloˇ world\n").await;
1882        cx.simulate_shared_keystrokes("2 r enter").await;
1883        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
1884    }
1885
1886    #[gpui::test]
1887    async fn test_gq(cx: &mut gpui::TestAppContext) {
1888        let mut cx = NeovimBackedTestContext::new(cx).await;
1889        cx.set_neovim_option("textwidth=5").await;
1890
1891        cx.update(|_, cx| {
1892            SettingsStore::update_global(cx, |settings, cx| {
1893                settings.update_user_settings(cx, |settings| {
1894                    settings
1895                        .project
1896                        .all_languages
1897                        .defaults
1898                        .preferred_line_length = Some(5);
1899                });
1900            })
1901        });
1902
1903        cx.set_shared_state("ˇth th th th th th\n").await;
1904        cx.simulate_shared_keystrokes("g q q").await;
1905        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1906
1907        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1908            .await;
1909        cx.simulate_shared_keystrokes("v j g q").await;
1910        cx.shared_state()
1911            .await
1912            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1913    }
1914
1915    #[gpui::test]
1916    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1917        let mut cx = NeovimBackedTestContext::new(cx).await;
1918        cx.set_neovim_option("filetype=rust").await;
1919
1920        cx.set_shared_state("// helloˇ\n").await;
1921        cx.simulate_shared_keystrokes("o").await;
1922        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1923        cx.simulate_shared_keystrokes("x escape shift-o").await;
1924        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1925    }
1926
1927    #[gpui::test]
1928    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1929        let mut cx = NeovimBackedTestContext::new(cx).await;
1930        cx.set_shared_state("heˇllo\n").await;
1931        cx.simulate_shared_keystrokes("y y p").await;
1932        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1933    }
1934
1935    #[gpui::test]
1936    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1937        let mut cx = NeovimBackedTestContext::new(cx).await;
1938        cx.set_shared_state("heˇllo").await;
1939        cx.simulate_shared_keystrokes("y y p").await;
1940        cx.shared_state().await.assert_eq("hello\nˇhello");
1941    }
1942
1943    #[gpui::test]
1944    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1945        let mut cx = NeovimBackedTestContext::new(cx).await;
1946        cx.set_shared_state("heˇllo\nhello").await;
1947        cx.simulate_shared_keystrokes("2 y y p").await;
1948        cx.shared_state()
1949            .await
1950            .assert_eq("hello\nˇhello\nhello\nhello");
1951    }
1952
1953    #[gpui::test]
1954    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1955        let mut cx = NeovimBackedTestContext::new(cx).await;
1956        cx.set_shared_state("heˇllo").await;
1957        cx.simulate_shared_keystrokes("d d").await;
1958        cx.shared_state().await.assert_eq("ˇ");
1959        cx.simulate_shared_keystrokes("p p").await;
1960        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1961    }
1962
1963    #[gpui::test]
1964    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1965        let mut cx = NeovimBackedTestContext::new(cx).await;
1966
1967        cx.set_shared_state("heˇllo").await;
1968        cx.simulate_shared_keystrokes("v i w shift-i").await;
1969        cx.shared_state().await.assert_eq("ˇhello");
1970
1971        cx.set_shared_state(indoc! {"
1972            The quick brown
1973            fox ˇjumps over
1974            the lazy dog"})
1975            .await;
1976        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1977        cx.shared_state().await.assert_eq(indoc! {"
1978            The quick brown
1979            ˇfox jumps over
1980            the lazy dog"});
1981
1982        cx.set_shared_state(indoc! {"
1983            The quick brown
1984            fox ˇjumps over
1985            the lazy dog"})
1986            .await;
1987        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1988        cx.shared_state().await.assert_eq(indoc! {"
1989            The quick brown
1990            fox jˇumps over
1991            the lazy dog"});
1992    }
1993
1994    #[gpui::test]
1995    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
1996        let mut cx = NeovimBackedTestContext::new(cx).await;
1997
1998        cx.set_shared_state(indoc! {"
1999            ˇfn a() { }
2000
2001
2002
2003
2004
2005            fn b() { }
2006
2007
2008
2009
2010
2011            fn b() { }"})
2012            .await;
2013        cx.simulate_shared_keystrokes("3 }").await;
2014        cx.shared_state().await.assert_matches();
2015        cx.simulate_shared_keystrokes("ctrl-o").await;
2016        cx.shared_state().await.assert_matches();
2017        cx.simulate_shared_keystrokes("ctrl-i").await;
2018        cx.shared_state().await.assert_matches();
2019        cx.simulate_shared_keystrokes("1 1 k").await;
2020        cx.shared_state().await.assert_matches();
2021        cx.simulate_shared_keystrokes("ctrl-o").await;
2022        cx.shared_state().await.assert_matches();
2023    }
2024
2025    #[gpui::test]
2026    async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2027        let mut cx = NeovimBackedTestContext::new(cx).await;
2028
2029        cx.set_shared_state(indoc! {"
2030            ˇfn a() { }
2031            fn a() { }
2032            fn a() { }
2033        "})
2034            .await;
2035        // do a jump to reset vim's undo grouping
2036        cx.simulate_shared_keystrokes("shift-g").await;
2037        cx.shared_state().await.assert_matches();
2038        cx.simulate_shared_keystrokes("r a").await;
2039        cx.shared_state().await.assert_matches();
2040        cx.simulate_shared_keystrokes("shift-u").await;
2041        cx.shared_state().await.assert_matches();
2042        cx.simulate_shared_keystrokes("shift-u").await;
2043        cx.shared_state().await.assert_matches();
2044        cx.simulate_shared_keystrokes("g g shift-u").await;
2045        cx.shared_state().await.assert_matches();
2046    }
2047
2048    #[gpui::test]
2049    async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2050        let mut cx = NeovimBackedTestContext::new(cx).await;
2051
2052        cx.set_shared_state(indoc! {"
2053            ˇfn a() { }
2054            fn a() { }
2055            fn a() { }
2056        "})
2057            .await;
2058        // do a jump to reset vim's undo grouping
2059        cx.simulate_shared_keystrokes("shift-g k").await;
2060        cx.shared_state().await.assert_matches();
2061        cx.simulate_shared_keystrokes("o h e l l o escape").await;
2062        cx.shared_state().await.assert_matches();
2063        cx.simulate_shared_keystrokes("shift-u").await;
2064        cx.shared_state().await.assert_matches();
2065        cx.simulate_shared_keystrokes("shift-u").await;
2066    }
2067
2068    #[gpui::test]
2069    async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2070        let mut cx = NeovimBackedTestContext::new(cx).await;
2071
2072        cx.set_shared_state(indoc! {"
2073            ˇfn a() { }
2074            fn a() { }
2075            fn a() { }
2076        "})
2077            .await;
2078        // do a jump to reset vim's undo grouping
2079        cx.simulate_shared_keystrokes("x shift-g k").await;
2080        cx.shared_state().await.assert_matches();
2081        cx.simulate_shared_keystrokes("x f a x f { x").await;
2082        cx.shared_state().await.assert_matches();
2083        cx.simulate_shared_keystrokes("shift-u").await;
2084        cx.shared_state().await.assert_matches();
2085        cx.simulate_shared_keystrokes("shift-u").await;
2086        cx.shared_state().await.assert_matches();
2087        cx.simulate_shared_keystrokes("shift-u").await;
2088        cx.shared_state().await.assert_matches();
2089        cx.simulate_shared_keystrokes("shift-u").await;
2090        cx.shared_state().await.assert_matches();
2091    }
2092
2093    #[gpui::test]
2094    async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2095        let mut cx = VimTestContext::new(cx, true).await;
2096
2097        cx.set_state(
2098            indoc! {"
2099            ˇone two ˇone
2100            two ˇone two
2101        "},
2102            Mode::Normal,
2103        );
2104        cx.simulate_keystrokes("3 r a");
2105        cx.assert_state(
2106            indoc! {"
2107            aaˇa two aaˇa
2108            two aaˇa two
2109        "},
2110            Mode::Normal,
2111        );
2112        cx.simulate_keystrokes("escape escape");
2113        cx.simulate_keystrokes("shift-u");
2114        cx.set_state(
2115            indoc! {"
2116            onˇe two onˇe
2117            two onˇe two
2118        "},
2119            Mode::Normal,
2120        );
2121    }
2122}