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                _ => {
 499                    // Can't do anything for namespace operators. Ignoring
 500                }
 501            },
 502            Some(Operator::DeleteSurrounds) => {
 503                waiting_operator = Some(Operator::DeleteSurrounds);
 504            }
 505            Some(Operator::ChangeSurrounds { target: None }) => {
 506                if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
 507                    waiting_operator = Some(Operator::ChangeSurrounds {
 508                        target: Some(object),
 509                    });
 510                }
 511            }
 512            _ => {
 513                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 514            }
 515        }
 516        self.clear_operator(window, cx);
 517        if let Some(operator) = waiting_operator {
 518            self.push_operator(operator, window, cx);
 519        }
 520    }
 521
 522    pub(crate) fn move_cursor(
 523        &mut self,
 524        motion: Motion,
 525        times: Option<usize>,
 526        window: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) {
 529        self.update_editor(cx, |_, editor, cx| {
 530            let text_layout_details = editor.text_layout_details(window);
 531            editor.change_selections(
 532                SelectionEffects::default().nav_history(motion.push_to_jump_list()),
 533                window,
 534                cx,
 535                |s| {
 536                    s.move_cursors_with(|map, cursor, goal| {
 537                        motion
 538                            .move_point(map, cursor, goal, times, &text_layout_details)
 539                            .unwrap_or((cursor, goal))
 540                    })
 541                },
 542            )
 543        });
 544    }
 545
 546    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 547        self.start_recording(cx);
 548        self.switch_mode(Mode::Insert, false, window, cx);
 549        self.update_editor(cx, |_, editor, cx| {
 550            editor.change_selections(Default::default(), window, cx, |s| {
 551                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 552            });
 553        });
 554    }
 555
 556    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 557        self.start_recording(cx);
 558        if self.mode.is_visual() {
 559            let current_mode = self.mode;
 560            self.update_editor(cx, |_, editor, cx| {
 561                editor.change_selections(Default::default(), window, cx, |s| {
 562                    s.move_with(|map, selection| {
 563                        if current_mode == Mode::VisualLine {
 564                            let start_of_line = motion::start_of_line(map, false, selection.start);
 565                            selection.collapse_to(start_of_line, SelectionGoal::None)
 566                        } else {
 567                            selection.collapse_to(selection.start, SelectionGoal::None)
 568                        }
 569                    });
 570                });
 571            });
 572        }
 573        self.switch_mode(Mode::Insert, false, window, cx);
 574    }
 575
 576    fn insert_first_non_whitespace(
 577        &mut self,
 578        _: &InsertFirstNonWhitespace,
 579        window: &mut Window,
 580        cx: &mut Context<Self>,
 581    ) {
 582        self.start_recording(cx);
 583        self.switch_mode(Mode::Insert, false, window, cx);
 584        self.update_editor(cx, |_, editor, cx| {
 585            editor.change_selections(Default::default(), window, cx, |s| {
 586                s.move_cursors_with(|map, cursor, _| {
 587                    (
 588                        first_non_whitespace(map, false, cursor),
 589                        SelectionGoal::None,
 590                    )
 591                });
 592            });
 593        });
 594    }
 595
 596    fn insert_end_of_line(
 597        &mut self,
 598        _: &InsertEndOfLine,
 599        window: &mut Window,
 600        cx: &mut Context<Self>,
 601    ) {
 602        self.start_recording(cx);
 603        self.switch_mode(Mode::Insert, false, window, cx);
 604        self.update_editor(cx, |_, editor, cx| {
 605            editor.change_selections(Default::default(), window, cx, |s| {
 606                s.move_cursors_with(|map, cursor, _| {
 607                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 608                });
 609            });
 610        });
 611    }
 612
 613    fn insert_at_previous(
 614        &mut self,
 615        _: &InsertAtPrevious,
 616        window: &mut Window,
 617        cx: &mut Context<Self>,
 618    ) {
 619        self.start_recording(cx);
 620        self.switch_mode(Mode::Insert, false, window, cx);
 621        self.update_editor(cx, |vim, editor, cx| {
 622            let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
 623                return;
 624            };
 625
 626            editor.change_selections(Default::default(), window, cx, |s| {
 627                s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 628            });
 629        });
 630    }
 631
 632    fn insert_line_above(
 633        &mut self,
 634        _: &InsertLineAbove,
 635        window: &mut Window,
 636        cx: &mut Context<Self>,
 637    ) {
 638        self.start_recording(cx);
 639        self.switch_mode(Mode::Insert, false, window, cx);
 640        self.update_editor(cx, |_, editor, cx| {
 641            editor.transact(window, cx, |editor, window, cx| {
 642                let selections = editor.selections.all::<Point>(cx);
 643                let snapshot = editor.buffer().read(cx).snapshot(cx);
 644
 645                let selection_start_rows: BTreeSet<u32> = selections
 646                    .into_iter()
 647                    .map(|selection| selection.start.row)
 648                    .collect();
 649                let edits = selection_start_rows
 650                    .into_iter()
 651                    .map(|row| {
 652                        let indent = snapshot
 653                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 654                            .chars()
 655                            .collect::<String>();
 656
 657                        let start_of_line = Point::new(row, 0);
 658                        (start_of_line..start_of_line, indent + "\n")
 659                    })
 660                    .collect::<Vec<_>>();
 661                editor.edit_with_autoindent(edits, cx);
 662                editor.change_selections(Default::default(), window, cx, |s| {
 663                    s.move_cursors_with(|map, cursor, _| {
 664                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 665                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 666                        (insert_point, SelectionGoal::None)
 667                    });
 668                });
 669            });
 670        });
 671    }
 672
 673    fn insert_line_below(
 674        &mut self,
 675        _: &InsertLineBelow,
 676        window: &mut Window,
 677        cx: &mut Context<Self>,
 678    ) {
 679        self.start_recording(cx);
 680        self.switch_mode(Mode::Insert, false, window, cx);
 681        self.update_editor(cx, |_, editor, cx| {
 682            let text_layout_details = editor.text_layout_details(window);
 683            editor.transact(window, cx, |editor, window, cx| {
 684                let selections = editor.selections.all::<Point>(cx);
 685                let snapshot = editor.buffer().read(cx).snapshot(cx);
 686
 687                let selection_end_rows: BTreeSet<u32> = selections
 688                    .into_iter()
 689                    .map(|selection| selection.end.row)
 690                    .collect();
 691                let edits = selection_end_rows
 692                    .into_iter()
 693                    .map(|row| {
 694                        let indent = snapshot
 695                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 696                            .chars()
 697                            .collect::<String>();
 698
 699                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 700                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 701                    })
 702                    .collect::<Vec<_>>();
 703                editor.change_selections(Default::default(), window, cx, |s| {
 704                    s.maybe_move_cursors_with(|map, cursor, goal| {
 705                        Motion::CurrentLine.move_point(
 706                            map,
 707                            cursor,
 708                            goal,
 709                            None,
 710                            &text_layout_details,
 711                        )
 712                    });
 713                });
 714                editor.edit_with_autoindent(edits, cx);
 715            });
 716        });
 717    }
 718
 719    fn insert_empty_line_above(
 720        &mut self,
 721        _: &InsertEmptyLineAbove,
 722        window: &mut Window,
 723        cx: &mut Context<Self>,
 724    ) {
 725        self.record_current_action(cx);
 726        let count = Vim::take_count(cx).unwrap_or(1);
 727        Vim::take_forced_motion(cx);
 728        self.update_editor(cx, |_, editor, cx| {
 729            editor.transact(window, cx, |editor, _, cx| {
 730                let selections = editor.selections.all::<Point>(cx);
 731
 732                let selection_start_rows: BTreeSet<u32> = selections
 733                    .into_iter()
 734                    .map(|selection| selection.start.row)
 735                    .collect();
 736                let edits = selection_start_rows
 737                    .into_iter()
 738                    .map(|row| {
 739                        let start_of_line = Point::new(row, 0);
 740                        (start_of_line..start_of_line, "\n".repeat(count))
 741                    })
 742                    .collect::<Vec<_>>();
 743                editor.edit(edits, cx);
 744            });
 745        });
 746    }
 747
 748    fn insert_empty_line_below(
 749        &mut self,
 750        _: &InsertEmptyLineBelow,
 751        window: &mut Window,
 752        cx: &mut Context<Self>,
 753    ) {
 754        self.record_current_action(cx);
 755        let count = Vim::take_count(cx).unwrap_or(1);
 756        Vim::take_forced_motion(cx);
 757        self.update_editor(cx, |_, editor, cx| {
 758            editor.transact(window, cx, |editor, window, cx| {
 759                let selections = editor.selections.all::<Point>(cx);
 760                let snapshot = editor.buffer().read(cx).snapshot(cx);
 761                let (_map, display_selections) = editor.selections.all_display(cx);
 762                let original_positions = display_selections
 763                    .iter()
 764                    .map(|s| (s.id, s.head()))
 765                    .collect::<HashMap<_, _>>();
 766
 767                let selection_end_rows: BTreeSet<u32> = selections
 768                    .into_iter()
 769                    .map(|selection| selection.end.row)
 770                    .collect();
 771                let edits = selection_end_rows
 772                    .into_iter()
 773                    .map(|row| {
 774                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 775                        (end_of_line..end_of_line, "\n".repeat(count))
 776                    })
 777                    .collect::<Vec<_>>();
 778                editor.edit(edits, cx);
 779
 780                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 781                    s.move_with(|_, selection| {
 782                        if let Some(position) = original_positions.get(&selection.id) {
 783                            selection.collapse_to(*position, SelectionGoal::None);
 784                        }
 785                    });
 786                });
 787            });
 788        });
 789    }
 790
 791    fn join_lines_impl(
 792        &mut self,
 793        insert_whitespace: bool,
 794        window: &mut Window,
 795        cx: &mut Context<Self>,
 796    ) {
 797        self.record_current_action(cx);
 798        let mut times = Vim::take_count(cx).unwrap_or(1);
 799        Vim::take_forced_motion(cx);
 800        if self.mode.is_visual() {
 801            times = 1;
 802        } else if times > 1 {
 803            // 2J joins two lines together (same as J or 1J)
 804            times -= 1;
 805        }
 806
 807        self.update_editor(cx, |_, editor, cx| {
 808            editor.transact(window, cx, |editor, window, cx| {
 809                for _ in 0..times {
 810                    editor.join_lines_impl(insert_whitespace, window, cx)
 811                }
 812            })
 813        });
 814        if self.mode.is_visual() {
 815            self.switch_mode(Mode::Normal, true, window, cx)
 816        }
 817    }
 818
 819    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 820        let count = Vim::take_count(cx);
 821        let forced_motion = Vim::take_forced_motion(cx);
 822        self.yank_motion(
 823            motion::Motion::CurrentLine,
 824            count,
 825            forced_motion,
 826            window,
 827            cx,
 828        )
 829    }
 830
 831    fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
 832        let count = Vim::take_count(cx);
 833        Vim::take_forced_motion(cx);
 834        self.update_editor(cx, |vim, editor, cx| {
 835            let selection = editor.selections.newest_anchor();
 836            let Some((buffer, point, _)) = editor
 837                .buffer()
 838                .read(cx)
 839                .point_to_buffer_point(selection.head(), cx)
 840            else {
 841                return;
 842            };
 843            let filename = if let Some(file) = buffer.read(cx).file() {
 844                if count.is_some() {
 845                    if let Some(local) = file.as_local() {
 846                        local.abs_path(cx).to_string_lossy().to_string()
 847                    } else {
 848                        file.full_path(cx).to_string_lossy().to_string()
 849                    }
 850                } else {
 851                    file.path().to_string_lossy().to_string()
 852                }
 853            } else {
 854                "[No Name]".into()
 855            };
 856            let buffer = buffer.read(cx);
 857            let lines = buffer.max_point().row + 1;
 858            let current_line = point.row;
 859            let percentage = current_line as f32 / lines as f32;
 860            let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 861            vim.status_label = Some(
 862                format!(
 863                    "{}{} {} lines --{:.0}%--",
 864                    filename,
 865                    modified,
 866                    lines,
 867                    percentage * 100.0,
 868                )
 869                .into(),
 870            );
 871            cx.notify();
 872        });
 873    }
 874
 875    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 876        self.record_current_action(cx);
 877        self.store_visual_marks(window, cx);
 878        self.update_editor(cx, |vim, editor, cx| {
 879            editor.transact(window, cx, |editor, window, cx| {
 880                let original_positions = vim.save_selection_starts(editor, cx);
 881                editor.toggle_comments(&Default::default(), window, cx);
 882                vim.restore_selection_cursors(editor, window, cx, original_positions);
 883            });
 884        });
 885        if self.mode.is_visual() {
 886            self.switch_mode(Mode::Normal, true, window, cx)
 887        }
 888    }
 889
 890    pub(crate) fn normal_replace(
 891        &mut self,
 892        text: Arc<str>,
 893        window: &mut Window,
 894        cx: &mut Context<Self>,
 895    ) {
 896        let is_return_char = text == "\n".into() || text == "\r".into();
 897        let count = Vim::take_count(cx).unwrap_or(1);
 898        Vim::take_forced_motion(cx);
 899        self.stop_recording(cx);
 900        self.update_editor(cx, |_, editor, cx| {
 901            editor.transact(window, cx, |editor, window, cx| {
 902                editor.set_clip_at_line_ends(false, cx);
 903                let (map, display_selections) = editor.selections.all_display(cx);
 904
 905                let mut edits = Vec::new();
 906                for selection in &display_selections {
 907                    let mut range = selection.range();
 908                    for _ in 0..count {
 909                        let new_point = movement::saturating_right(&map, range.end);
 910                        if range.end == new_point {
 911                            return;
 912                        }
 913                        range.end = new_point;
 914                    }
 915
 916                    edits.push((
 917                        range.start.to_offset(&map, Bias::Left)
 918                            ..range.end.to_offset(&map, Bias::Left),
 919                        text.repeat(if is_return_char { 0 } else { count }),
 920                    ));
 921                }
 922
 923                editor.edit(edits, cx);
 924                if is_return_char {
 925                    editor.newline(&editor::actions::Newline, window, cx);
 926                }
 927                editor.set_clip_at_line_ends(true, cx);
 928                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
 929                    s.move_with(|map, selection| {
 930                        let point = movement::saturating_left(map, selection.head());
 931                        selection.collapse_to(point, SelectionGoal::None)
 932                    });
 933                });
 934            });
 935        });
 936        self.pop_operator(window, cx);
 937    }
 938
 939    pub fn save_selection_starts(
 940        &self,
 941        editor: &Editor,
 942
 943        cx: &mut Context<Editor>,
 944    ) -> HashMap<usize, Anchor> {
 945        let (map, selections) = editor.selections.all_display(cx);
 946        selections
 947            .iter()
 948            .map(|selection| {
 949                (
 950                    selection.id,
 951                    map.display_point_to_anchor(selection.start, Bias::Right),
 952                )
 953            })
 954            .collect::<HashMap<_, _>>()
 955    }
 956
 957    pub fn restore_selection_cursors(
 958        &self,
 959        editor: &mut Editor,
 960        window: &mut Window,
 961        cx: &mut Context<Editor>,
 962        mut positions: HashMap<usize, Anchor>,
 963    ) {
 964        editor.change_selections(Default::default(), window, cx, |s| {
 965            s.move_with(|map, selection| {
 966                if let Some(anchor) = positions.remove(&selection.id) {
 967                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 968                }
 969            });
 970        });
 971    }
 972
 973    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 974        if self.temp_mode {
 975            self.switch_mode(Mode::Insert, true, window, cx);
 976        }
 977    }
 978}
 979#[cfg(test)]
 980mod test {
 981    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 982    use indoc::indoc;
 983    use language::language_settings::AllLanguageSettings;
 984    use settings::SettingsStore;
 985
 986    use crate::{
 987        VimSettings, motion,
 988        state::Mode::{self},
 989        test::{NeovimBackedTestContext, VimTestContext},
 990    };
 991
 992    #[gpui::test]
 993    async fn test_h(cx: &mut gpui::TestAppContext) {
 994        let mut cx = NeovimBackedTestContext::new(cx).await;
 995        cx.simulate_at_each_offset(
 996            "h",
 997            indoc! {"
 998            ˇThe qˇuick
 999            ˇbrown"
1000            },
1001        )
1002        .await
1003        .assert_matches();
1004    }
1005
1006    #[gpui::test]
1007    async fn test_backspace(cx: &mut gpui::TestAppContext) {
1008        let mut cx = NeovimBackedTestContext::new(cx).await;
1009        cx.simulate_at_each_offset(
1010            "backspace",
1011            indoc! {"
1012            ˇThe qˇuick
1013            ˇbrown"
1014            },
1015        )
1016        .await
1017        .assert_matches();
1018    }
1019
1020    #[gpui::test]
1021    async fn test_j(cx: &mut gpui::TestAppContext) {
1022        let mut cx = NeovimBackedTestContext::new(cx).await;
1023
1024        cx.set_shared_state(indoc! {"
1025            aaˇaa
1026            😃😃"
1027        })
1028        .await;
1029        cx.simulate_shared_keystrokes("j").await;
1030        cx.shared_state().await.assert_eq(indoc! {"
1031            aaaa
1032            😃ˇ😃"
1033        });
1034
1035        cx.simulate_at_each_offset(
1036            "j",
1037            indoc! {"
1038                ˇThe qˇuick broˇwn
1039                ˇfox jumps"
1040            },
1041        )
1042        .await
1043        .assert_matches();
1044    }
1045
1046    #[gpui::test]
1047    async fn test_enter(cx: &mut gpui::TestAppContext) {
1048        let mut cx = NeovimBackedTestContext::new(cx).await;
1049        cx.simulate_at_each_offset(
1050            "enter",
1051            indoc! {"
1052            ˇThe qˇuick broˇwn
1053            ˇfox jumps"
1054            },
1055        )
1056        .await
1057        .assert_matches();
1058    }
1059
1060    #[gpui::test]
1061    async fn test_k(cx: &mut gpui::TestAppContext) {
1062        let mut cx = NeovimBackedTestContext::new(cx).await;
1063        cx.simulate_at_each_offset(
1064            "k",
1065            indoc! {"
1066            ˇThe qˇuick
1067            ˇbrown fˇox jumˇps"
1068            },
1069        )
1070        .await
1071        .assert_matches();
1072    }
1073
1074    #[gpui::test]
1075    async fn test_l(cx: &mut gpui::TestAppContext) {
1076        let mut cx = NeovimBackedTestContext::new(cx).await;
1077        cx.simulate_at_each_offset(
1078            "l",
1079            indoc! {"
1080            ˇThe qˇuicˇk
1081            ˇbrowˇn"},
1082        )
1083        .await
1084        .assert_matches();
1085    }
1086
1087    #[gpui::test]
1088    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
1089        let mut cx = NeovimBackedTestContext::new(cx).await;
1090        cx.simulate_at_each_offset(
1091            "$",
1092            indoc! {"
1093            ˇThe qˇuicˇk
1094            ˇbrowˇn"},
1095        )
1096        .await
1097        .assert_matches();
1098        cx.simulate_at_each_offset(
1099            "0",
1100            indoc! {"
1101                ˇThe qˇuicˇk
1102                ˇbrowˇn"},
1103        )
1104        .await
1105        .assert_matches();
1106    }
1107
1108    #[gpui::test]
1109    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
1110        let mut cx = NeovimBackedTestContext::new(cx).await;
1111
1112        cx.simulate_at_each_offset(
1113            "shift-g",
1114            indoc! {"
1115                The ˇquick
1116
1117                brown fox jumps
1118                overˇ the lazy doˇg"},
1119        )
1120        .await
1121        .assert_matches();
1122        cx.simulate(
1123            "shift-g",
1124            indoc! {"
1125            The quiˇck
1126
1127            brown"},
1128        )
1129        .await
1130        .assert_matches();
1131        cx.simulate(
1132            "shift-g",
1133            indoc! {"
1134            The quiˇck
1135
1136            "},
1137        )
1138        .await
1139        .assert_matches();
1140    }
1141
1142    #[gpui::test]
1143    async fn test_w(cx: &mut gpui::TestAppContext) {
1144        let mut cx = NeovimBackedTestContext::new(cx).await;
1145        cx.simulate_at_each_offset(
1146            "w",
1147            indoc! {"
1148            The ˇquickˇ-ˇbrown
1149            ˇ
1150            ˇ
1151            ˇfox_jumps ˇover
1152            ˇthˇe"},
1153        )
1154        .await
1155        .assert_matches();
1156        cx.simulate_at_each_offset(
1157            "shift-w",
1158            indoc! {"
1159            The ˇquickˇ-ˇbrown
1160            ˇ
1161            ˇ
1162            ˇfox_jumps ˇover
1163            ˇthˇe"},
1164        )
1165        .await
1166        .assert_matches();
1167    }
1168
1169    #[gpui::test]
1170    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
1171        let mut cx = NeovimBackedTestContext::new(cx).await;
1172        cx.simulate_at_each_offset(
1173            "e",
1174            indoc! {"
1175            Thˇe quicˇkˇ-browˇn
1176
1177
1178            fox_jumpˇs oveˇr
1179            thˇe"},
1180        )
1181        .await
1182        .assert_matches();
1183        cx.simulate_at_each_offset(
1184            "shift-e",
1185            indoc! {"
1186            Thˇe quicˇkˇ-browˇn
1187
1188
1189            fox_jumpˇs oveˇr
1190            thˇe"},
1191        )
1192        .await
1193        .assert_matches();
1194    }
1195
1196    #[gpui::test]
1197    async fn test_b(cx: &mut gpui::TestAppContext) {
1198        let mut cx = NeovimBackedTestContext::new(cx).await;
1199        cx.simulate_at_each_offset(
1200            "b",
1201            indoc! {"
1202            ˇThe ˇquickˇ-ˇbrown
1203            ˇ
1204            ˇ
1205            ˇfox_jumps ˇover
1206            ˇthe"},
1207        )
1208        .await
1209        .assert_matches();
1210        cx.simulate_at_each_offset(
1211            "shift-b",
1212            indoc! {"
1213            ˇThe ˇquickˇ-ˇbrown
1214            ˇ
1215            ˇ
1216            ˇfox_jumps ˇover
1217            ˇthe"},
1218        )
1219        .await
1220        .assert_matches();
1221    }
1222
1223    #[gpui::test]
1224    async fn test_gg(cx: &mut gpui::TestAppContext) {
1225        let mut cx = NeovimBackedTestContext::new(cx).await;
1226        cx.simulate_at_each_offset(
1227            "g g",
1228            indoc! {"
1229                The qˇuick
1230
1231                brown fox jumps
1232                over ˇthe laˇzy dog"},
1233        )
1234        .await
1235        .assert_matches();
1236        cx.simulate(
1237            "g g",
1238            indoc! {"
1239
1240
1241                brown fox jumps
1242                over the laˇzy dog"},
1243        )
1244        .await
1245        .assert_matches();
1246        cx.simulate(
1247            "2 g g",
1248            indoc! {"
1249                ˇ
1250
1251                brown fox jumps
1252                over the lazydog"},
1253        )
1254        .await
1255        .assert_matches();
1256    }
1257
1258    #[gpui::test]
1259    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
1260        let mut cx = NeovimBackedTestContext::new(cx).await;
1261        cx.simulate_at_each_offset(
1262            "shift-g",
1263            indoc! {"
1264                The qˇuick
1265
1266                brown fox jumps
1267                over ˇthe laˇzy dog"},
1268        )
1269        .await
1270        .assert_matches();
1271        cx.simulate(
1272            "shift-g",
1273            indoc! {"
1274
1275
1276                brown fox jumps
1277                over the laˇzy dog"},
1278        )
1279        .await
1280        .assert_matches();
1281        cx.simulate(
1282            "2 shift-g",
1283            indoc! {"
1284                ˇ
1285
1286                brown fox jumps
1287                over the lazydog"},
1288        )
1289        .await
1290        .assert_matches();
1291    }
1292
1293    #[gpui::test]
1294    async fn test_a(cx: &mut gpui::TestAppContext) {
1295        let mut cx = NeovimBackedTestContext::new(cx).await;
1296        cx.simulate_at_each_offset("a", "The qˇuicˇk")
1297            .await
1298            .assert_matches();
1299    }
1300
1301    #[gpui::test]
1302    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1303        let mut cx = NeovimBackedTestContext::new(cx).await;
1304        cx.simulate_at_each_offset(
1305            "shift-a",
1306            indoc! {"
1307            ˇ
1308            The qˇuick
1309            brown ˇfox "},
1310        )
1311        .await
1312        .assert_matches();
1313    }
1314
1315    #[gpui::test]
1316    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1317        let mut cx = NeovimBackedTestContext::new(cx).await;
1318        cx.simulate("^", "The qˇuick").await.assert_matches();
1319        cx.simulate("^", " The qˇuick").await.assert_matches();
1320        cx.simulate("^", "ˇ").await.assert_matches();
1321        cx.simulate(
1322            "^",
1323            indoc! {"
1324                The qˇuick
1325                brown fox"},
1326        )
1327        .await
1328        .assert_matches();
1329        cx.simulate(
1330            "^",
1331            indoc! {"
1332                ˇ
1333                The quick"},
1334        )
1335        .await
1336        .assert_matches();
1337        // Indoc disallows trailing whitespace.
1338        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1339    }
1340
1341    #[gpui::test]
1342    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1343        let mut cx = NeovimBackedTestContext::new(cx).await;
1344        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1345        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1346        cx.simulate("shift-i", "ˇ").await.assert_matches();
1347        cx.simulate(
1348            "shift-i",
1349            indoc! {"
1350                The qˇuick
1351                brown fox"},
1352        )
1353        .await
1354        .assert_matches();
1355        cx.simulate(
1356            "shift-i",
1357            indoc! {"
1358                ˇ
1359                The quick"},
1360        )
1361        .await
1362        .assert_matches();
1363    }
1364
1365    #[gpui::test]
1366    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1367        let mut cx = NeovimBackedTestContext::new(cx).await;
1368        cx.simulate(
1369            "shift-d",
1370            indoc! {"
1371                The qˇuick
1372                brown fox"},
1373        )
1374        .await
1375        .assert_matches();
1376        cx.simulate(
1377            "shift-d",
1378            indoc! {"
1379                The quick
1380                ˇ
1381                brown fox"},
1382        )
1383        .await
1384        .assert_matches();
1385    }
1386
1387    #[gpui::test]
1388    async fn test_x(cx: &mut gpui::TestAppContext) {
1389        let mut cx = NeovimBackedTestContext::new(cx).await;
1390        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1391            .await
1392            .assert_matches();
1393        cx.simulate(
1394            "x",
1395            indoc! {"
1396                Tesˇt
1397                test"},
1398        )
1399        .await
1400        .assert_matches();
1401    }
1402
1403    #[gpui::test]
1404    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1405        let mut cx = NeovimBackedTestContext::new(cx).await;
1406        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1407            .await
1408            .assert_matches();
1409        cx.simulate(
1410            "shift-x",
1411            indoc! {"
1412                Test
1413                ˇtest"},
1414        )
1415        .await
1416        .assert_matches();
1417    }
1418
1419    #[gpui::test]
1420    async fn test_o(cx: &mut gpui::TestAppContext) {
1421        let mut cx = NeovimBackedTestContext::new(cx).await;
1422        cx.simulate("o", "ˇ").await.assert_matches();
1423        cx.simulate("o", "The ˇquick").await.assert_matches();
1424        cx.simulate_at_each_offset(
1425            "o",
1426            indoc! {"
1427                The qˇuick
1428                brown ˇfox
1429                jumps ˇover"},
1430        )
1431        .await
1432        .assert_matches();
1433        cx.simulate(
1434            "o",
1435            indoc! {"
1436                The quick
1437                ˇ
1438                brown fox"},
1439        )
1440        .await
1441        .assert_matches();
1442
1443        cx.assert_binding(
1444            "o",
1445            indoc! {"
1446                fn test() {
1447                    println!(ˇ);
1448                }"},
1449            Mode::Normal,
1450            indoc! {"
1451                fn test() {
1452                    println!();
1453                    ˇ
1454                }"},
1455            Mode::Insert,
1456        );
1457
1458        cx.assert_binding(
1459            "o",
1460            indoc! {"
1461                fn test(ˇ) {
1462                    println!();
1463                }"},
1464            Mode::Normal,
1465            indoc! {"
1466                fn test() {
1467                    ˇ
1468                    println!();
1469                }"},
1470            Mode::Insert,
1471        );
1472    }
1473
1474    #[gpui::test]
1475    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1476        let mut cx = NeovimBackedTestContext::new(cx).await;
1477        cx.simulate("shift-o", "ˇ").await.assert_matches();
1478        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1479        cx.simulate_at_each_offset(
1480            "shift-o",
1481            indoc! {"
1482            The qˇuick
1483            brown ˇfox
1484            jumps ˇover"},
1485        )
1486        .await
1487        .assert_matches();
1488        cx.simulate(
1489            "shift-o",
1490            indoc! {"
1491            The quick
1492            ˇ
1493            brown fox"},
1494        )
1495        .await
1496        .assert_matches();
1497
1498        // Our indentation is smarter than vims. So we don't match here
1499        cx.assert_binding(
1500            "shift-o",
1501            indoc! {"
1502                fn test() {
1503                    println!(ˇ);
1504                }"},
1505            Mode::Normal,
1506            indoc! {"
1507                fn test() {
1508                    ˇ
1509                    println!();
1510                }"},
1511            Mode::Insert,
1512        );
1513        cx.assert_binding(
1514            "shift-o",
1515            indoc! {"
1516                fn test(ˇ) {
1517                    println!();
1518                }"},
1519            Mode::Normal,
1520            indoc! {"
1521                ˇ
1522                fn test() {
1523                    println!();
1524                }"},
1525            Mode::Insert,
1526        );
1527    }
1528
1529    #[gpui::test]
1530    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
1531        let mut cx = NeovimBackedTestContext::new(cx).await;
1532        cx.simulate("[ space", "ˇ").await.assert_matches();
1533        cx.simulate("[ space", "The ˇquick").await.assert_matches();
1534        cx.simulate_at_each_offset(
1535            "3 [ space",
1536            indoc! {"
1537            The qˇuick
1538            brown ˇfox
1539            jumps ˇover"},
1540        )
1541        .await
1542        .assert_matches();
1543        cx.simulate_at_each_offset(
1544            "[ space",
1545            indoc! {"
1546            The qˇuick
1547            brown ˇfox
1548            jumps ˇover"},
1549        )
1550        .await
1551        .assert_matches();
1552        cx.simulate(
1553            "[ space",
1554            indoc! {"
1555            The quick
1556            ˇ
1557            brown fox"},
1558        )
1559        .await
1560        .assert_matches();
1561
1562        cx.simulate("] space", "ˇ").await.assert_matches();
1563        cx.simulate("] space", "The ˇquick").await.assert_matches();
1564        cx.simulate_at_each_offset(
1565            "3 ] space",
1566            indoc! {"
1567            The qˇuick
1568            brown ˇfox
1569            jumps ˇover"},
1570        )
1571        .await
1572        .assert_matches();
1573        cx.simulate_at_each_offset(
1574            "] space",
1575            indoc! {"
1576            The qˇuick
1577            brown ˇfox
1578            jumps ˇover"},
1579        )
1580        .await
1581        .assert_matches();
1582        cx.simulate(
1583            "] space",
1584            indoc! {"
1585            The quick
1586            ˇ
1587            brown fox"},
1588        )
1589        .await
1590        .assert_matches();
1591    }
1592
1593    #[gpui::test]
1594    async fn test_dd(cx: &mut gpui::TestAppContext) {
1595        let mut cx = NeovimBackedTestContext::new(cx).await;
1596        cx.simulate("d d", "ˇ").await.assert_matches();
1597        cx.simulate("d d", "The ˇquick").await.assert_matches();
1598        cx.simulate_at_each_offset(
1599            "d d",
1600            indoc! {"
1601            The qˇuick
1602            brown ˇfox
1603            jumps ˇover"},
1604        )
1605        .await
1606        .assert_matches();
1607        cx.simulate(
1608            "d d",
1609            indoc! {"
1610                The quick
1611                ˇ
1612                brown fox"},
1613        )
1614        .await
1615        .assert_matches();
1616    }
1617
1618    #[gpui::test]
1619    async fn test_cc(cx: &mut gpui::TestAppContext) {
1620        let mut cx = NeovimBackedTestContext::new(cx).await;
1621        cx.simulate("c c", "ˇ").await.assert_matches();
1622        cx.simulate("c c", "The ˇquick").await.assert_matches();
1623        cx.simulate_at_each_offset(
1624            "c c",
1625            indoc! {"
1626                The quˇick
1627                brown ˇfox
1628                jumps ˇover"},
1629        )
1630        .await
1631        .assert_matches();
1632        cx.simulate(
1633            "c c",
1634            indoc! {"
1635                The quick
1636                ˇ
1637                brown fox"},
1638        )
1639        .await
1640        .assert_matches();
1641    }
1642
1643    #[gpui::test]
1644    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1645        let mut cx = NeovimBackedTestContext::new(cx).await;
1646
1647        for count in 1..=5 {
1648            cx.simulate_at_each_offset(
1649                &format!("{count} w"),
1650                indoc! {"
1651                    ˇThe quˇickˇ browˇn
1652                    ˇ
1653                    ˇfox ˇjumpsˇ-ˇoˇver
1654                    ˇthe lazy dog
1655                "},
1656            )
1657            .await
1658            .assert_matches();
1659        }
1660    }
1661
1662    #[gpui::test]
1663    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1664        let mut cx = NeovimBackedTestContext::new(cx).await;
1665        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1666            .await
1667            .assert_matches();
1668    }
1669
1670    #[gpui::test]
1671    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1672        let mut cx = NeovimBackedTestContext::new(cx).await;
1673
1674        for count in 1..=3 {
1675            let test_case = indoc! {"
1676                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1677                ˇ    ˇbˇaaˇa ˇbˇbˇb
1678                ˇ
1679                ˇb
1680            "};
1681
1682            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1683                .await
1684                .assert_matches();
1685
1686            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1687                .await
1688                .assert_matches();
1689        }
1690    }
1691
1692    #[gpui::test]
1693    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1694        let mut cx = NeovimBackedTestContext::new(cx).await;
1695        let test_case = indoc! {"
1696            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1697            ˇ    ˇbˇaaˇa ˇbˇbˇb
1698            ˇ•••
1699            ˇb
1700            "
1701        };
1702
1703        for count in 1..=3 {
1704            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1705                .await
1706                .assert_matches();
1707
1708            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1709                .await
1710                .assert_matches();
1711        }
1712    }
1713
1714    #[gpui::test]
1715    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1716        let mut cx = VimTestContext::new(cx, true).await;
1717        cx.update_global(|store: &mut SettingsStore, cx| {
1718            store.update_user_settings::<VimSettings>(cx, |s| {
1719                s.use_smartcase_find = Some(true);
1720            });
1721        });
1722
1723        cx.assert_binding(
1724            "f 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-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            "t 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            "shift-t p",
1749            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1750            Mode::Normal,
1751            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1752            Mode::Normal,
1753        );
1754    }
1755
1756    #[gpui::test]
1757    async fn test_percent(cx: &mut TestAppContext) {
1758        let mut cx = NeovimBackedTestContext::new(cx).await;
1759        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1760            .await
1761            .assert_matches();
1762        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1763            .await
1764            .assert_matches();
1765        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1766            .await
1767            .assert_matches();
1768    }
1769
1770    #[gpui::test]
1771    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1772        let mut cx = NeovimBackedTestContext::new(cx).await;
1773
1774        // goes to current line end
1775        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1776        cx.simulate_shared_keystrokes("$").await;
1777        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1778
1779        // goes to next line end
1780        cx.simulate_shared_keystrokes("2 $").await;
1781        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1782
1783        // try to exceed the final line.
1784        cx.simulate_shared_keystrokes("4 $").await;
1785        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1786    }
1787
1788    #[gpui::test]
1789    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1790        let mut cx = VimTestContext::new(cx, true).await;
1791        cx.update(|_, cx| {
1792            cx.bind_keys(vec![
1793                KeyBinding::new(
1794                    "w",
1795                    motion::NextSubwordStart {
1796                        ignore_punctuation: false,
1797                    },
1798                    Some("Editor && VimControl && !VimWaiting && !menu"),
1799                ),
1800                KeyBinding::new(
1801                    "b",
1802                    motion::PreviousSubwordStart {
1803                        ignore_punctuation: false,
1804                    },
1805                    Some("Editor && VimControl && !VimWaiting && !menu"),
1806                ),
1807                KeyBinding::new(
1808                    "e",
1809                    motion::NextSubwordEnd {
1810                        ignore_punctuation: false,
1811                    },
1812                    Some("Editor && VimControl && !VimWaiting && !menu"),
1813                ),
1814                KeyBinding::new(
1815                    "g e",
1816                    motion::PreviousSubwordEnd {
1817                        ignore_punctuation: false,
1818                    },
1819                    Some("Editor && VimControl && !VimWaiting && !menu"),
1820                ),
1821            ]);
1822        });
1823
1824        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1825        // Special case: In 'cw', 'w' acts like 'e'
1826        cx.assert_binding(
1827            "c w",
1828            indoc! {"ˇassert_binding"},
1829            Mode::Normal,
1830            indoc! {"ˇ_binding"},
1831            Mode::Insert,
1832        );
1833
1834        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1835
1836        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1837
1838        cx.assert_binding_normal(
1839            "g e",
1840            indoc! {"assert_bindinˇg"},
1841            indoc! {"asserˇt_binding"},
1842        );
1843    }
1844
1845    #[gpui::test]
1846    async fn test_r(cx: &mut gpui::TestAppContext) {
1847        let mut cx = NeovimBackedTestContext::new(cx).await;
1848
1849        cx.set_shared_state("ˇhello\n").await;
1850        cx.simulate_shared_keystrokes("r -").await;
1851        cx.shared_state().await.assert_eq("ˇ-ello\n");
1852
1853        cx.set_shared_state("ˇhello\n").await;
1854        cx.simulate_shared_keystrokes("3 r -").await;
1855        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1856
1857        cx.set_shared_state("ˇhello\n").await;
1858        cx.simulate_shared_keystrokes("r - 2 l .").await;
1859        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1860
1861        cx.set_shared_state("ˇhello world\n").await;
1862        cx.simulate_shared_keystrokes("2 r - f w .").await;
1863        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1864
1865        cx.set_shared_state("ˇhello world\n").await;
1866        cx.simulate_shared_keystrokes("2 0 r - ").await;
1867        cx.shared_state().await.assert_eq("ˇhello world\n");
1868
1869        cx.set_shared_state("  helloˇ world\n").await;
1870        cx.simulate_shared_keystrokes("r enter").await;
1871        cx.shared_state().await.assert_eq("  hello\n ˇ world\n");
1872
1873        cx.set_shared_state("  helloˇ world\n").await;
1874        cx.simulate_shared_keystrokes("2 r enter").await;
1875        cx.shared_state().await.assert_eq("  hello\n ˇ orld\n");
1876    }
1877
1878    #[gpui::test]
1879    async fn test_gq(cx: &mut gpui::TestAppContext) {
1880        let mut cx = NeovimBackedTestContext::new(cx).await;
1881        cx.set_neovim_option("textwidth=5").await;
1882
1883        cx.update(|_, cx| {
1884            SettingsStore::update_global(cx, |settings, cx| {
1885                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1886                    settings.defaults.preferred_line_length = Some(5);
1887                });
1888            })
1889        });
1890
1891        cx.set_shared_state("ˇth th th th th th\n").await;
1892        cx.simulate_shared_keystrokes("g q q").await;
1893        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1894
1895        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1896            .await;
1897        cx.simulate_shared_keystrokes("v j g q").await;
1898        cx.shared_state()
1899            .await
1900            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1901    }
1902
1903    #[gpui::test]
1904    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1905        let mut cx = NeovimBackedTestContext::new(cx).await;
1906        cx.set_neovim_option("filetype=rust").await;
1907
1908        cx.set_shared_state("// helloˇ\n").await;
1909        cx.simulate_shared_keystrokes("o").await;
1910        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1911        cx.simulate_shared_keystrokes("x escape shift-o").await;
1912        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1913    }
1914
1915    #[gpui::test]
1916    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1917        let mut cx = NeovimBackedTestContext::new(cx).await;
1918        cx.set_shared_state("heˇllo\n").await;
1919        cx.simulate_shared_keystrokes("y y p").await;
1920        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1921    }
1922
1923    #[gpui::test]
1924    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1925        let mut cx = NeovimBackedTestContext::new(cx).await;
1926        cx.set_shared_state("heˇllo").await;
1927        cx.simulate_shared_keystrokes("y y p").await;
1928        cx.shared_state().await.assert_eq("hello\nˇhello");
1929    }
1930
1931    #[gpui::test]
1932    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1933        let mut cx = NeovimBackedTestContext::new(cx).await;
1934        cx.set_shared_state("heˇllo\nhello").await;
1935        cx.simulate_shared_keystrokes("2 y y p").await;
1936        cx.shared_state()
1937            .await
1938            .assert_eq("hello\nˇhello\nhello\nhello");
1939    }
1940
1941    #[gpui::test]
1942    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1943        let mut cx = NeovimBackedTestContext::new(cx).await;
1944        cx.set_shared_state("heˇllo").await;
1945        cx.simulate_shared_keystrokes("d d").await;
1946        cx.shared_state().await.assert_eq("ˇ");
1947        cx.simulate_shared_keystrokes("p p").await;
1948        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1949    }
1950
1951    #[gpui::test]
1952    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1953        let mut cx = NeovimBackedTestContext::new(cx).await;
1954
1955        cx.set_shared_state("heˇllo").await;
1956        cx.simulate_shared_keystrokes("v i w shift-i").await;
1957        cx.shared_state().await.assert_eq("ˇhello");
1958
1959        cx.set_shared_state(indoc! {"
1960            The quick brown
1961            fox ˇjumps over
1962            the lazy dog"})
1963            .await;
1964        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1965        cx.shared_state().await.assert_eq(indoc! {"
1966            The quick brown
1967            ˇfox jumps over
1968            the lazy dog"});
1969
1970        cx.set_shared_state(indoc! {"
1971            The quick brown
1972            fox ˇjumps over
1973            the lazy dog"})
1974            .await;
1975        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1976        cx.shared_state().await.assert_eq(indoc! {"
1977            The quick brown
1978            fox jˇumps over
1979            the lazy dog"});
1980    }
1981
1982    #[gpui::test]
1983    async fn test_jump_list(cx: &mut gpui::TestAppContext) {
1984        let mut cx = NeovimBackedTestContext::new(cx).await;
1985
1986        cx.set_shared_state(indoc! {"
1987            ˇfn a() { }
1988
1989
1990
1991
1992
1993            fn b() { }
1994
1995
1996
1997
1998
1999            fn b() { }"})
2000            .await;
2001        cx.simulate_shared_keystrokes("3 }").await;
2002        cx.shared_state().await.assert_matches();
2003        cx.simulate_shared_keystrokes("ctrl-o").await;
2004        cx.shared_state().await.assert_matches();
2005        cx.simulate_shared_keystrokes("ctrl-i").await;
2006        cx.shared_state().await.assert_matches();
2007        cx.simulate_shared_keystrokes("1 1 k").await;
2008        cx.shared_state().await.assert_matches();
2009        cx.simulate_shared_keystrokes("ctrl-o").await;
2010        cx.shared_state().await.assert_matches();
2011    }
2012
2013    #[gpui::test]
2014    async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
2015        let mut cx = NeovimBackedTestContext::new(cx).await;
2016
2017        cx.set_shared_state(indoc! {"
2018            ˇfn a() { }
2019            fn a() { }
2020            fn a() { }
2021        "})
2022            .await;
2023        // do a jump to reset vim's undo grouping
2024        cx.simulate_shared_keystrokes("shift-g").await;
2025        cx.shared_state().await.assert_matches();
2026        cx.simulate_shared_keystrokes("r a").await;
2027        cx.shared_state().await.assert_matches();
2028        cx.simulate_shared_keystrokes("shift-u").await;
2029        cx.shared_state().await.assert_matches();
2030        cx.simulate_shared_keystrokes("shift-u").await;
2031        cx.shared_state().await.assert_matches();
2032        cx.simulate_shared_keystrokes("g g shift-u").await;
2033        cx.shared_state().await.assert_matches();
2034    }
2035
2036    #[gpui::test]
2037    async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
2038        let mut cx = NeovimBackedTestContext::new(cx).await;
2039
2040        cx.set_shared_state(indoc! {"
2041            ˇfn a() { }
2042            fn a() { }
2043            fn a() { }
2044        "})
2045            .await;
2046        // do a jump to reset vim's undo grouping
2047        cx.simulate_shared_keystrokes("shift-g k").await;
2048        cx.shared_state().await.assert_matches();
2049        cx.simulate_shared_keystrokes("o h e l l o escape").await;
2050        cx.shared_state().await.assert_matches();
2051        cx.simulate_shared_keystrokes("shift-u").await;
2052        cx.shared_state().await.assert_matches();
2053        cx.simulate_shared_keystrokes("shift-u").await;
2054    }
2055
2056    #[gpui::test]
2057    async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
2058        let mut cx = NeovimBackedTestContext::new(cx).await;
2059
2060        cx.set_shared_state(indoc! {"
2061            ˇfn a() { }
2062            fn a() { }
2063            fn a() { }
2064        "})
2065            .await;
2066        // do a jump to reset vim's undo grouping
2067        cx.simulate_shared_keystrokes("x shift-g k").await;
2068        cx.shared_state().await.assert_matches();
2069        cx.simulate_shared_keystrokes("x f a x f { x").await;
2070        cx.shared_state().await.assert_matches();
2071        cx.simulate_shared_keystrokes("shift-u").await;
2072        cx.shared_state().await.assert_matches();
2073        cx.simulate_shared_keystrokes("shift-u").await;
2074        cx.shared_state().await.assert_matches();
2075        cx.simulate_shared_keystrokes("shift-u").await;
2076        cx.shared_state().await.assert_matches();
2077        cx.simulate_shared_keystrokes("shift-u").await;
2078        cx.shared_state().await.assert_matches();
2079    }
2080
2081    #[gpui::test]
2082    async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
2083        let mut cx = VimTestContext::new(cx, true).await;
2084
2085        cx.set_state(
2086            indoc! {"
2087            ˇone two ˇone
2088            two ˇone two
2089        "},
2090            Mode::Normal,
2091        );
2092        cx.simulate_keystrokes("3 r a");
2093        cx.assert_state(
2094            indoc! {"
2095            aaˇa two aaˇa
2096            two aaˇa two
2097        "},
2098            Mode::Normal,
2099        );
2100        cx.simulate_keystrokes("escape escape");
2101        cx.simulate_keystrokes("shift-u");
2102        cx.set_state(
2103            indoc! {"
2104            onˇe two onˇe
2105            two onˇe two
2106        "},
2107            Mode::Normal,
2108        );
2109    }
2110}