normal.rs

   1mod change;
   2mod convert;
   3mod delete;
   4mod increment;
   5pub(crate) mod mark;
   6mod paste;
   7pub(crate) mod repeat;
   8mod scroll;
   9pub(crate) mod search;
  10pub mod substitute;
  11mod toggle_comments;
  12pub(crate) mod yank;
  13
  14use std::collections::HashMap;
  15use std::sync::Arc;
  16
  17use crate::{
  18    Vim,
  19    indent::IndentDirection,
  20    motion::{self, Motion, first_non_whitespace, next_line_end, right},
  21    object::Object,
  22    state::{Mark, Mode, Operator},
  23    surrounds::SurroundsType,
  24};
  25use collections::BTreeSet;
  26use convert::ConvertTarget;
  27use editor::Anchor;
  28use editor::Bias;
  29use editor::Editor;
  30use editor::scroll::Autoscroll;
  31use editor::{display_map::ToDisplayPoint, movement};
  32use gpui::{Context, Window, actions};
  33use language::{Point, SelectionGoal, ToPoint};
  34use log::error;
  35use multi_buffer::MultiBufferRow;
  36
  37actions!(
  38    vim,
  39    [
  40        InsertAfter,
  41        InsertBefore,
  42        InsertFirstNonWhitespace,
  43        InsertEndOfLine,
  44        InsertLineAbove,
  45        InsertLineBelow,
  46        InsertAtPrevious,
  47        JoinLines,
  48        JoinLinesNoWhitespace,
  49        DeleteLeft,
  50        DeleteRight,
  51        HelixDelete,
  52        ChangeToEndOfLine,
  53        DeleteToEndOfLine,
  54        Yank,
  55        YankLine,
  56        ChangeCase,
  57        ConvertToUpperCase,
  58        ConvertToLowerCase,
  59        ConvertToRot13,
  60        ConvertToRot47,
  61        ToggleComments,
  62        ShowLocation,
  63        Undo,
  64        Redo,
  65    ]
  66);
  67
  68pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  69    Vim::action(editor, cx, Vim::insert_after);
  70    Vim::action(editor, cx, Vim::insert_before);
  71    Vim::action(editor, cx, Vim::insert_first_non_whitespace);
  72    Vim::action(editor, cx, Vim::insert_end_of_line);
  73    Vim::action(editor, cx, Vim::insert_line_above);
  74    Vim::action(editor, cx, Vim::insert_line_below);
  75    Vim::action(editor, cx, Vim::insert_at_previous);
  76    Vim::action(editor, cx, Vim::change_case);
  77    Vim::action(editor, cx, Vim::convert_to_upper_case);
  78    Vim::action(editor, cx, Vim::convert_to_lower_case);
  79    Vim::action(editor, cx, Vim::convert_to_rot13);
  80    Vim::action(editor, cx, Vim::convert_to_rot47);
  81    Vim::action(editor, cx, Vim::yank_line);
  82    Vim::action(editor, cx, Vim::toggle_comments);
  83    Vim::action(editor, cx, Vim::paste);
  84    Vim::action(editor, cx, Vim::show_location);
  85
  86    Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
  87        vim.record_current_action(cx);
  88        let times = Vim::take_count(cx);
  89        vim.delete_motion(Motion::Left, times, window, cx);
  90    });
  91    Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| {
  92        vim.record_current_action(cx);
  93        let times = Vim::take_count(cx);
  94        vim.delete_motion(Motion::Right, times, window, cx);
  95    });
  96
  97    Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
  98        vim.record_current_action(cx);
  99        vim.update_editor(window, cx, |_, editor, window, cx| {
 100            editor.change_selections(None, window, cx, |s| {
 101                s.move_with(|map, selection| {
 102                    if selection.is_empty() {
 103                        selection.end = movement::right(map, selection.end)
 104                    }
 105                })
 106            })
 107        });
 108        vim.visual_delete(false, window, cx);
 109    });
 110
 111    Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
 112        vim.start_recording(cx);
 113        let times = Vim::take_count(cx);
 114        vim.change_motion(
 115            Motion::EndOfLine {
 116                display_lines: false,
 117            },
 118            times,
 119            window,
 120            cx,
 121        );
 122    });
 123    Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| {
 124        vim.record_current_action(cx);
 125        let times = Vim::take_count(cx);
 126        vim.delete_motion(
 127            Motion::EndOfLine {
 128                display_lines: false,
 129            },
 130            times,
 131            window,
 132            cx,
 133        );
 134    });
 135    Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| {
 136        vim.join_lines_impl(true, window, cx);
 137    });
 138
 139    Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| {
 140        vim.join_lines_impl(false, window, cx);
 141    });
 142
 143    Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
 144        let times = Vim::take_count(cx);
 145        vim.update_editor(window, cx, |_, editor, window, cx| {
 146            for _ in 0..times.unwrap_or(1) {
 147                editor.undo(&editor::actions::Undo, window, cx);
 148            }
 149        });
 150    });
 151    Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
 152        let times = Vim::take_count(cx);
 153        vim.update_editor(window, cx, |_, editor, window, cx| {
 154            for _ in 0..times.unwrap_or(1) {
 155                editor.redo(&editor::actions::Redo, window, cx);
 156            }
 157        });
 158    });
 159
 160    repeat::register(editor, cx);
 161    scroll::register(editor, cx);
 162    search::register(editor, cx);
 163    substitute::register(editor, cx);
 164    increment::register(editor, cx);
 165}
 166
 167impl Vim {
 168    pub fn normal_motion(
 169        &mut self,
 170        motion: Motion,
 171        operator: Option<Operator>,
 172        times: Option<usize>,
 173        window: &mut Window,
 174        cx: &mut Context<Self>,
 175    ) {
 176        match operator {
 177            None => self.move_cursor(motion, times, window, cx),
 178            Some(Operator::Change) => self.change_motion(motion, times, window, cx),
 179            Some(Operator::Delete) => self.delete_motion(motion, times, window, cx),
 180            Some(Operator::Yank) => self.yank_motion(motion, times, window, cx),
 181            Some(Operator::AddSurrounds { target: None }) => {}
 182            Some(Operator::Indent) => {
 183                self.indent_motion(motion, times, IndentDirection::In, window, cx)
 184            }
 185            Some(Operator::Rewrap) => self.rewrap_motion(motion, times, window, cx),
 186            Some(Operator::Outdent) => {
 187                self.indent_motion(motion, times, IndentDirection::Out, window, cx)
 188            }
 189            Some(Operator::AutoIndent) => {
 190                self.indent_motion(motion, times, IndentDirection::Auto, window, cx)
 191            }
 192            Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, window, cx),
 193            Some(Operator::Lowercase) => {
 194                self.convert_motion(motion, times, ConvertTarget::LowerCase, window, cx)
 195            }
 196            Some(Operator::Uppercase) => {
 197                self.convert_motion(motion, times, ConvertTarget::UpperCase, window, cx)
 198            }
 199            Some(Operator::OppositeCase) => {
 200                self.convert_motion(motion, times, ConvertTarget::OppositeCase, window, cx)
 201            }
 202            Some(Operator::Rot13) => {
 203                self.convert_motion(motion, times, ConvertTarget::Rot13, window, cx)
 204            }
 205            Some(Operator::Rot47) => {
 206                self.convert_motion(motion, times, ConvertTarget::Rot47, window, cx)
 207            }
 208            Some(Operator::ToggleComments) => {
 209                self.toggle_comments_motion(motion, times, window, cx)
 210            }
 211            Some(Operator::ReplaceWithRegister) => {
 212                self.replace_with_register_motion(motion, times, window, cx)
 213            }
 214            Some(Operator::Exchange) => self.exchange_motion(motion, times, window, cx),
 215            Some(operator) => {
 216                // Can't do anything for text objects, Ignoring
 217                error!("Unexpected normal mode motion operator: {:?}", operator)
 218            }
 219        }
 220        // Exit temporary normal mode (if active).
 221        self.exit_temporary_normal(window, cx);
 222    }
 223
 224    pub fn normal_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
 225        let mut waiting_operator: Option<Operator> = None;
 226        match self.maybe_pop_operator() {
 227            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
 228                Some(Operator::Change) => self.change_object(object, around, window, cx),
 229                Some(Operator::Delete) => self.delete_object(object, around, window, cx),
 230                Some(Operator::Yank) => self.yank_object(object, around, window, cx),
 231                Some(Operator::Indent) => {
 232                    self.indent_object(object, around, IndentDirection::In, window, cx)
 233                }
 234                Some(Operator::Outdent) => {
 235                    self.indent_object(object, around, IndentDirection::Out, window, cx)
 236                }
 237                Some(Operator::AutoIndent) => {
 238                    self.indent_object(object, around, IndentDirection::Auto, window, cx)
 239                }
 240                Some(Operator::ShellCommand) => {
 241                    self.shell_command_object(object, around, window, cx);
 242                }
 243                Some(Operator::Rewrap) => self.rewrap_object(object, around, window, cx),
 244                Some(Operator::Lowercase) => {
 245                    self.convert_object(object, around, ConvertTarget::LowerCase, window, cx)
 246                }
 247                Some(Operator::Uppercase) => {
 248                    self.convert_object(object, around, ConvertTarget::UpperCase, window, cx)
 249                }
 250                Some(Operator::OppositeCase) => {
 251                    self.convert_object(object, around, ConvertTarget::OppositeCase, window, cx)
 252                }
 253                Some(Operator::Rot13) => {
 254                    self.convert_object(object, around, ConvertTarget::Rot13, window, cx)
 255                }
 256                Some(Operator::Rot47) => {
 257                    self.convert_object(object, around, ConvertTarget::Rot47, window, cx)
 258                }
 259                Some(Operator::AddSurrounds { target: None }) => {
 260                    waiting_operator = Some(Operator::AddSurrounds {
 261                        target: Some(SurroundsType::Object(object, around)),
 262                    });
 263                }
 264                Some(Operator::ToggleComments) => {
 265                    self.toggle_comments_object(object, around, window, cx)
 266                }
 267                Some(Operator::ReplaceWithRegister) => {
 268                    self.replace_with_register_object(object, around, window, cx)
 269                }
 270                Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
 271                _ => {
 272                    // Can't do anything for namespace operators. Ignoring
 273                }
 274            },
 275            Some(Operator::DeleteSurrounds) => {
 276                waiting_operator = Some(Operator::DeleteSurrounds);
 277            }
 278            Some(Operator::ChangeSurrounds { target: None }) => {
 279                if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
 280                    waiting_operator = Some(Operator::ChangeSurrounds {
 281                        target: Some(object),
 282                    });
 283                }
 284            }
 285            _ => {
 286                // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
 287            }
 288        }
 289        self.clear_operator(window, cx);
 290        if let Some(operator) = waiting_operator {
 291            self.push_operator(operator, window, cx);
 292        }
 293    }
 294
 295    pub(crate) fn move_cursor(
 296        &mut self,
 297        motion: Motion,
 298        times: Option<usize>,
 299        window: &mut Window,
 300        cx: &mut Context<Self>,
 301    ) {
 302        self.update_editor(window, cx, |_, editor, window, cx| {
 303            let text_layout_details = editor.text_layout_details(window);
 304            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 305                s.move_cursors_with(|map, cursor, goal| {
 306                    motion
 307                        .move_point(map, cursor, goal, times, &text_layout_details)
 308                        .unwrap_or((cursor, goal))
 309                })
 310            })
 311        });
 312    }
 313
 314    fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
 315        self.start_recording(cx);
 316        self.switch_mode(Mode::Insert, false, window, cx);
 317        self.update_editor(window, cx, |_, editor, window, cx| {
 318            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 319                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
 320            });
 321        });
 322    }
 323
 324    fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
 325        self.start_recording(cx);
 326        if self.mode.is_visual() {
 327            let current_mode = self.mode;
 328            self.update_editor(window, cx, |_, editor, window, cx| {
 329                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 330                    s.move_with(|map, selection| {
 331                        if current_mode == Mode::VisualLine {
 332                            let start_of_line = motion::start_of_line(map, false, selection.start);
 333                            selection.collapse_to(start_of_line, SelectionGoal::None)
 334                        } else {
 335                            selection.collapse_to(selection.start, SelectionGoal::None)
 336                        }
 337                    });
 338                });
 339            });
 340        }
 341        self.switch_mode(Mode::Insert, false, window, cx);
 342    }
 343
 344    fn insert_first_non_whitespace(
 345        &mut self,
 346        _: &InsertFirstNonWhitespace,
 347        window: &mut Window,
 348        cx: &mut Context<Self>,
 349    ) {
 350        self.start_recording(cx);
 351        self.switch_mode(Mode::Insert, false, window, cx);
 352        self.update_editor(window, cx, |_, editor, window, cx| {
 353            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 354                s.move_cursors_with(|map, cursor, _| {
 355                    (
 356                        first_non_whitespace(map, false, cursor),
 357                        SelectionGoal::None,
 358                    )
 359                });
 360            });
 361        });
 362    }
 363
 364    fn insert_end_of_line(
 365        &mut self,
 366        _: &InsertEndOfLine,
 367        window: &mut Window,
 368        cx: &mut Context<Self>,
 369    ) {
 370        self.start_recording(cx);
 371        self.switch_mode(Mode::Insert, false, window, cx);
 372        self.update_editor(window, cx, |_, editor, window, cx| {
 373            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 374                s.move_cursors_with(|map, cursor, _| {
 375                    (next_line_end(map, cursor, 1), SelectionGoal::None)
 376                });
 377            });
 378        });
 379    }
 380
 381    fn insert_at_previous(
 382        &mut self,
 383        _: &InsertAtPrevious,
 384        window: &mut Window,
 385        cx: &mut Context<Self>,
 386    ) {
 387        self.start_recording(cx);
 388        self.switch_mode(Mode::Insert, false, window, cx);
 389        self.update_editor(window, cx, |vim, editor, window, cx| {
 390            let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
 391                return;
 392            };
 393
 394            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 395                s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
 396            });
 397        });
 398    }
 399
 400    fn insert_line_above(
 401        &mut self,
 402        _: &InsertLineAbove,
 403        window: &mut Window,
 404        cx: &mut Context<Self>,
 405    ) {
 406        self.start_recording(cx);
 407        self.switch_mode(Mode::Insert, false, window, cx);
 408        self.update_editor(window, cx, |_, editor, window, cx| {
 409            editor.transact(window, cx, |editor, window, cx| {
 410                let selections = editor.selections.all::<Point>(cx);
 411                let snapshot = editor.buffer().read(cx).snapshot(cx);
 412
 413                let selection_start_rows: BTreeSet<u32> = selections
 414                    .into_iter()
 415                    .map(|selection| selection.start.row)
 416                    .collect();
 417                let edits = selection_start_rows
 418                    .into_iter()
 419                    .map(|row| {
 420                        let indent = snapshot
 421                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 422                            .chars()
 423                            .collect::<String>();
 424
 425                        let start_of_line = Point::new(row, 0);
 426                        (start_of_line..start_of_line, indent + "\n")
 427                    })
 428                    .collect::<Vec<_>>();
 429                editor.edit_with_autoindent(edits, cx);
 430                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 431                    s.move_cursors_with(|map, cursor, _| {
 432                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
 433                        let insert_point = motion::end_of_line(map, false, previous_line, 1);
 434                        (insert_point, SelectionGoal::None)
 435                    });
 436                });
 437            });
 438        });
 439    }
 440
 441    fn insert_line_below(
 442        &mut self,
 443        _: &InsertLineBelow,
 444        window: &mut Window,
 445        cx: &mut Context<Self>,
 446    ) {
 447        self.start_recording(cx);
 448        self.switch_mode(Mode::Insert, false, window, cx);
 449        self.update_editor(window, cx, |_, editor, window, cx| {
 450            let text_layout_details = editor.text_layout_details(window);
 451            editor.transact(window, cx, |editor, window, cx| {
 452                let selections = editor.selections.all::<Point>(cx);
 453                let snapshot = editor.buffer().read(cx).snapshot(cx);
 454
 455                let selection_end_rows: BTreeSet<u32> = selections
 456                    .into_iter()
 457                    .map(|selection| selection.end.row)
 458                    .collect();
 459                let edits = selection_end_rows
 460                    .into_iter()
 461                    .map(|row| {
 462                        let indent = snapshot
 463                            .indent_and_comment_for_line(MultiBufferRow(row), cx)
 464                            .chars()
 465                            .collect::<String>();
 466
 467                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
 468                        (end_of_line..end_of_line, "\n".to_string() + &indent)
 469                    })
 470                    .collect::<Vec<_>>();
 471                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 472                    s.maybe_move_cursors_with(|map, cursor, goal| {
 473                        Motion::CurrentLine.move_point(
 474                            map,
 475                            cursor,
 476                            goal,
 477                            None,
 478                            &text_layout_details,
 479                        )
 480                    });
 481                });
 482                editor.edit_with_autoindent(edits, cx);
 483            });
 484        });
 485    }
 486
 487    fn join_lines_impl(
 488        &mut self,
 489        insert_whitespace: bool,
 490        window: &mut Window,
 491        cx: &mut Context<Self>,
 492    ) {
 493        self.record_current_action(cx);
 494        let mut times = Vim::take_count(cx).unwrap_or(1);
 495        if self.mode.is_visual() {
 496            times = 1;
 497        } else if times > 1 {
 498            // 2J joins two lines together (same as J or 1J)
 499            times -= 1;
 500        }
 501
 502        self.update_editor(window, cx, |_, editor, window, cx| {
 503            editor.transact(window, cx, |editor, window, cx| {
 504                for _ in 0..times {
 505                    editor.join_lines_impl(insert_whitespace, window, cx)
 506                }
 507            })
 508        });
 509        if self.mode.is_visual() {
 510            self.switch_mode(Mode::Normal, true, window, cx)
 511        }
 512    }
 513
 514    fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
 515        let count = Vim::take_count(cx);
 516        self.yank_motion(motion::Motion::CurrentLine, count, window, cx)
 517    }
 518
 519    fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
 520        let count = Vim::take_count(cx);
 521        self.update_editor(window, cx, |vim, editor, _window, cx| {
 522            let selection = editor.selections.newest_anchor();
 523            if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
 524                let filename = if let Some(file) = buffer.read(cx).file() {
 525                    if count.is_some() {
 526                        if let Some(local) = file.as_local() {
 527                            local.abs_path(cx).to_string_lossy().to_string()
 528                        } else {
 529                            file.full_path(cx).to_string_lossy().to_string()
 530                        }
 531                    } else {
 532                        file.path().to_string_lossy().to_string()
 533                    }
 534                } else {
 535                    "[No Name]".into()
 536                };
 537                let buffer = buffer.read(cx);
 538                let snapshot = buffer.snapshot();
 539                let lines = buffer.max_point().row + 1;
 540                let current_line = selection.head().text_anchor.to_point(&snapshot).row;
 541                let percentage = current_line as f32 / lines as f32;
 542                let modified = if buffer.is_dirty() { " [modified]" } else { "" };
 543                vim.status_label = Some(
 544                    format!(
 545                        "{}{} {} lines --{:.0}%--",
 546                        filename,
 547                        modified,
 548                        lines,
 549                        percentage * 100.0,
 550                    )
 551                    .into(),
 552                );
 553                cx.notify();
 554            }
 555        });
 556    }
 557
 558    fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
 559        self.record_current_action(cx);
 560        self.store_visual_marks(window, cx);
 561        self.update_editor(window, cx, |vim, editor, window, cx| {
 562            editor.transact(window, cx, |editor, window, cx| {
 563                let original_positions = vim.save_selection_starts(editor, cx);
 564                editor.toggle_comments(&Default::default(), window, cx);
 565                vim.restore_selection_cursors(editor, window, cx, original_positions);
 566            });
 567        });
 568        if self.mode.is_visual() {
 569            self.switch_mode(Mode::Normal, true, window, cx)
 570        }
 571    }
 572
 573    pub(crate) fn normal_replace(
 574        &mut self,
 575        text: Arc<str>,
 576        window: &mut Window,
 577        cx: &mut Context<Self>,
 578    ) {
 579        let count = Vim::take_count(cx).unwrap_or(1);
 580        self.stop_recording(cx);
 581        self.update_editor(window, cx, |_, editor, window, cx| {
 582            editor.transact(window, cx, |editor, window, cx| {
 583                editor.set_clip_at_line_ends(false, cx);
 584                let (map, display_selections) = editor.selections.all_display(cx);
 585
 586                let mut edits = Vec::new();
 587                for selection in display_selections {
 588                    let mut range = selection.range();
 589                    for _ in 0..count {
 590                        let new_point = movement::saturating_right(&map, range.end);
 591                        if range.end == new_point {
 592                            return;
 593                        }
 594                        range.end = new_point;
 595                    }
 596
 597                    edits.push((
 598                        range.start.to_offset(&map, Bias::Left)
 599                            ..range.end.to_offset(&map, Bias::Left),
 600                        text.repeat(count),
 601                    ))
 602                }
 603
 604                editor.edit(edits, cx);
 605                editor.set_clip_at_line_ends(true, cx);
 606                editor.change_selections(None, window, cx, |s| {
 607                    s.move_with(|map, selection| {
 608                        let point = movement::saturating_left(map, selection.head());
 609                        selection.collapse_to(point, SelectionGoal::None)
 610                    });
 611                });
 612            });
 613        });
 614        self.pop_operator(window, cx);
 615    }
 616
 617    pub fn save_selection_starts(
 618        &self,
 619        editor: &Editor,
 620
 621        cx: &mut Context<Editor>,
 622    ) -> HashMap<usize, Anchor> {
 623        let (map, selections) = editor.selections.all_display(cx);
 624        selections
 625            .iter()
 626            .map(|selection| {
 627                (
 628                    selection.id,
 629                    map.display_point_to_anchor(selection.start, Bias::Right),
 630                )
 631            })
 632            .collect::<HashMap<_, _>>()
 633    }
 634
 635    pub fn restore_selection_cursors(
 636        &self,
 637        editor: &mut Editor,
 638        window: &mut Window,
 639        cx: &mut Context<Editor>,
 640        mut positions: HashMap<usize, Anchor>,
 641    ) {
 642        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 643            s.move_with(|map, selection| {
 644                if let Some(anchor) = positions.remove(&selection.id) {
 645                    selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 646                }
 647            });
 648        });
 649    }
 650
 651    fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 652        if self.temp_mode {
 653            self.switch_mode(Mode::Insert, true, window, cx);
 654        }
 655    }
 656}
 657#[cfg(test)]
 658mod test {
 659    use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
 660    use indoc::indoc;
 661    use language::language_settings::AllLanguageSettings;
 662    use settings::SettingsStore;
 663
 664    use crate::{
 665        VimSettings, motion,
 666        state::Mode::{self},
 667        test::{NeovimBackedTestContext, VimTestContext},
 668    };
 669
 670    #[gpui::test]
 671    async fn test_h(cx: &mut gpui::TestAppContext) {
 672        let mut cx = NeovimBackedTestContext::new(cx).await;
 673        cx.simulate_at_each_offset(
 674            "h",
 675            indoc! {"
 676            ˇThe qˇuick
 677            ˇbrown"
 678            },
 679        )
 680        .await
 681        .assert_matches();
 682    }
 683
 684    #[gpui::test]
 685    async fn test_backspace(cx: &mut gpui::TestAppContext) {
 686        let mut cx = NeovimBackedTestContext::new(cx).await;
 687        cx.simulate_at_each_offset(
 688            "backspace",
 689            indoc! {"
 690            ˇThe qˇuick
 691            ˇbrown"
 692            },
 693        )
 694        .await
 695        .assert_matches();
 696    }
 697
 698    #[gpui::test]
 699    async fn test_j(cx: &mut gpui::TestAppContext) {
 700        let mut cx = NeovimBackedTestContext::new(cx).await;
 701
 702        cx.set_shared_state(indoc! {"
 703            aaˇaa
 704            😃😃"
 705        })
 706        .await;
 707        cx.simulate_shared_keystrokes("j").await;
 708        cx.shared_state().await.assert_eq(indoc! {"
 709            aaaa
 710            😃ˇ😃"
 711        });
 712
 713        cx.simulate_at_each_offset(
 714            "j",
 715            indoc! {"
 716                ˇThe qˇuick broˇwn
 717                ˇfox jumps"
 718            },
 719        )
 720        .await
 721        .assert_matches();
 722    }
 723
 724    #[gpui::test]
 725    async fn test_enter(cx: &mut gpui::TestAppContext) {
 726        let mut cx = NeovimBackedTestContext::new(cx).await;
 727        cx.simulate_at_each_offset(
 728            "enter",
 729            indoc! {"
 730            ˇThe qˇuick broˇwn
 731            ˇfox jumps"
 732            },
 733        )
 734        .await
 735        .assert_matches();
 736    }
 737
 738    #[gpui::test]
 739    async fn test_k(cx: &mut gpui::TestAppContext) {
 740        let mut cx = NeovimBackedTestContext::new(cx).await;
 741        cx.simulate_at_each_offset(
 742            "k",
 743            indoc! {"
 744            ˇThe qˇuick
 745            ˇbrown fˇox jumˇps"
 746            },
 747        )
 748        .await
 749        .assert_matches();
 750    }
 751
 752    #[gpui::test]
 753    async fn test_l(cx: &mut gpui::TestAppContext) {
 754        let mut cx = NeovimBackedTestContext::new(cx).await;
 755        cx.simulate_at_each_offset(
 756            "l",
 757            indoc! {"
 758            ˇThe qˇuicˇk
 759            ˇbrowˇn"},
 760        )
 761        .await
 762        .assert_matches();
 763    }
 764
 765    #[gpui::test]
 766    async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
 767        let mut cx = NeovimBackedTestContext::new(cx).await;
 768        cx.simulate_at_each_offset(
 769            "$",
 770            indoc! {"
 771            ˇThe qˇuicˇk
 772            ˇbrowˇn"},
 773        )
 774        .await
 775        .assert_matches();
 776        cx.simulate_at_each_offset(
 777            "0",
 778            indoc! {"
 779                ˇThe qˇuicˇk
 780                ˇbrowˇn"},
 781        )
 782        .await
 783        .assert_matches();
 784    }
 785
 786    #[gpui::test]
 787    async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
 788        let mut cx = NeovimBackedTestContext::new(cx).await;
 789
 790        cx.simulate_at_each_offset(
 791            "shift-g",
 792            indoc! {"
 793                The ˇquick
 794
 795                brown fox jumps
 796                overˇ the lazy doˇg"},
 797        )
 798        .await
 799        .assert_matches();
 800        cx.simulate(
 801            "shift-g",
 802            indoc! {"
 803            The quiˇck
 804
 805            brown"},
 806        )
 807        .await
 808        .assert_matches();
 809        cx.simulate(
 810            "shift-g",
 811            indoc! {"
 812            The quiˇck
 813
 814            "},
 815        )
 816        .await
 817        .assert_matches();
 818    }
 819
 820    #[gpui::test]
 821    async fn test_w(cx: &mut gpui::TestAppContext) {
 822        let mut cx = NeovimBackedTestContext::new(cx).await;
 823        cx.simulate_at_each_offset(
 824            "w",
 825            indoc! {"
 826            The ˇquickˇ-ˇbrown
 827            ˇ
 828            ˇ
 829            ˇfox_jumps ˇover
 830            ˇthˇe"},
 831        )
 832        .await
 833        .assert_matches();
 834        cx.simulate_at_each_offset(
 835            "shift-w",
 836            indoc! {"
 837            The ˇquickˇ-ˇbrown
 838            ˇ
 839            ˇ
 840            ˇfox_jumps ˇover
 841            ˇthˇe"},
 842        )
 843        .await
 844        .assert_matches();
 845    }
 846
 847    #[gpui::test]
 848    async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
 849        let mut cx = NeovimBackedTestContext::new(cx).await;
 850        cx.simulate_at_each_offset(
 851            "e",
 852            indoc! {"
 853            Thˇe quicˇkˇ-browˇn
 854
 855
 856            fox_jumpˇs oveˇr
 857            thˇe"},
 858        )
 859        .await
 860        .assert_matches();
 861        cx.simulate_at_each_offset(
 862            "shift-e",
 863            indoc! {"
 864            Thˇe quicˇkˇ-browˇn
 865
 866
 867            fox_jumpˇs oveˇr
 868            thˇe"},
 869        )
 870        .await
 871        .assert_matches();
 872    }
 873
 874    #[gpui::test]
 875    async fn test_b(cx: &mut gpui::TestAppContext) {
 876        let mut cx = NeovimBackedTestContext::new(cx).await;
 877        cx.simulate_at_each_offset(
 878            "b",
 879            indoc! {"
 880            ˇThe ˇquickˇ-ˇbrown
 881            ˇ
 882            ˇ
 883            ˇfox_jumps ˇover
 884            ˇthe"},
 885        )
 886        .await
 887        .assert_matches();
 888        cx.simulate_at_each_offset(
 889            "shift-b",
 890            indoc! {"
 891            ˇThe ˇquickˇ-ˇbrown
 892            ˇ
 893            ˇ
 894            ˇfox_jumps ˇover
 895            ˇthe"},
 896        )
 897        .await
 898        .assert_matches();
 899    }
 900
 901    #[gpui::test]
 902    async fn test_gg(cx: &mut gpui::TestAppContext) {
 903        let mut cx = NeovimBackedTestContext::new(cx).await;
 904        cx.simulate_at_each_offset(
 905            "g g",
 906            indoc! {"
 907                The qˇuick
 908
 909                brown fox jumps
 910                over ˇthe laˇzy dog"},
 911        )
 912        .await
 913        .assert_matches();
 914        cx.simulate(
 915            "g g",
 916            indoc! {"
 917
 918
 919                brown fox jumps
 920                over the laˇzy dog"},
 921        )
 922        .await
 923        .assert_matches();
 924        cx.simulate(
 925            "2 g g",
 926            indoc! {"
 927                ˇ
 928
 929                brown fox jumps
 930                over the lazydog"},
 931        )
 932        .await
 933        .assert_matches();
 934    }
 935
 936    #[gpui::test]
 937    async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
 938        let mut cx = NeovimBackedTestContext::new(cx).await;
 939        cx.simulate_at_each_offset(
 940            "shift-g",
 941            indoc! {"
 942                The qˇuick
 943
 944                brown fox jumps
 945                over ˇthe laˇzy dog"},
 946        )
 947        .await
 948        .assert_matches();
 949        cx.simulate(
 950            "shift-g",
 951            indoc! {"
 952
 953
 954                brown fox jumps
 955                over the laˇzy dog"},
 956        )
 957        .await
 958        .assert_matches();
 959        cx.simulate(
 960            "2 shift-g",
 961            indoc! {"
 962                ˇ
 963
 964                brown fox jumps
 965                over the lazydog"},
 966        )
 967        .await
 968        .assert_matches();
 969    }
 970
 971    #[gpui::test]
 972    async fn test_a(cx: &mut gpui::TestAppContext) {
 973        let mut cx = NeovimBackedTestContext::new(cx).await;
 974        cx.simulate_at_each_offset("a", "The qˇuicˇk")
 975            .await
 976            .assert_matches();
 977    }
 978
 979    #[gpui::test]
 980    async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
 981        let mut cx = NeovimBackedTestContext::new(cx).await;
 982        cx.simulate_at_each_offset(
 983            "shift-a",
 984            indoc! {"
 985            ˇ
 986            The qˇuick
 987            brown ˇfox "},
 988        )
 989        .await
 990        .assert_matches();
 991    }
 992
 993    #[gpui::test]
 994    async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
 995        let mut cx = NeovimBackedTestContext::new(cx).await;
 996        cx.simulate("^", "The qˇuick").await.assert_matches();
 997        cx.simulate("^", " The qˇuick").await.assert_matches();
 998        cx.simulate("^", "ˇ").await.assert_matches();
 999        cx.simulate(
1000            "^",
1001            indoc! {"
1002                The qˇuick
1003                brown fox"},
1004        )
1005        .await
1006        .assert_matches();
1007        cx.simulate(
1008            "^",
1009            indoc! {"
1010                ˇ
1011                The quick"},
1012        )
1013        .await
1014        .assert_matches();
1015        // Indoc disallows trailing whitespace.
1016        cx.simulate("^", "   ˇ \nThe quick").await.assert_matches();
1017    }
1018
1019    #[gpui::test]
1020    async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
1021        let mut cx = NeovimBackedTestContext::new(cx).await;
1022        cx.simulate("shift-i", "The qˇuick").await.assert_matches();
1023        cx.simulate("shift-i", " The qˇuick").await.assert_matches();
1024        cx.simulate("shift-i", "ˇ").await.assert_matches();
1025        cx.simulate(
1026            "shift-i",
1027            indoc! {"
1028                The qˇuick
1029                brown fox"},
1030        )
1031        .await
1032        .assert_matches();
1033        cx.simulate(
1034            "shift-i",
1035            indoc! {"
1036                ˇ
1037                The quick"},
1038        )
1039        .await
1040        .assert_matches();
1041    }
1042
1043    #[gpui::test]
1044    async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1045        let mut cx = NeovimBackedTestContext::new(cx).await;
1046        cx.simulate(
1047            "shift-d",
1048            indoc! {"
1049                The qˇuick
1050                brown fox"},
1051        )
1052        .await
1053        .assert_matches();
1054        cx.simulate(
1055            "shift-d",
1056            indoc! {"
1057                The quick
1058                ˇ
1059                brown fox"},
1060        )
1061        .await
1062        .assert_matches();
1063    }
1064
1065    #[gpui::test]
1066    async fn test_x(cx: &mut gpui::TestAppContext) {
1067        let mut cx = NeovimBackedTestContext::new(cx).await;
1068        cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1069            .await
1070            .assert_matches();
1071        cx.simulate(
1072            "x",
1073            indoc! {"
1074                Tesˇt
1075                test"},
1076        )
1077        .await
1078        .assert_matches();
1079    }
1080
1081    #[gpui::test]
1082    async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1083        let mut cx = NeovimBackedTestContext::new(cx).await;
1084        cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1085            .await
1086            .assert_matches();
1087        cx.simulate(
1088            "shift-x",
1089            indoc! {"
1090                Test
1091                ˇtest"},
1092        )
1093        .await
1094        .assert_matches();
1095    }
1096
1097    #[gpui::test]
1098    async fn test_o(cx: &mut gpui::TestAppContext) {
1099        let mut cx = NeovimBackedTestContext::new(cx).await;
1100        cx.simulate("o", "ˇ").await.assert_matches();
1101        cx.simulate("o", "The ˇquick").await.assert_matches();
1102        cx.simulate_at_each_offset(
1103            "o",
1104            indoc! {"
1105                The qˇuick
1106                brown ˇfox
1107                jumps ˇover"},
1108        )
1109        .await
1110        .assert_matches();
1111        cx.simulate(
1112            "o",
1113            indoc! {"
1114                The quick
1115                ˇ
1116                brown fox"},
1117        )
1118        .await
1119        .assert_matches();
1120
1121        cx.assert_binding(
1122            "o",
1123            indoc! {"
1124                fn test() {
1125                    println!(ˇ);
1126                }"},
1127            Mode::Normal,
1128            indoc! {"
1129                fn test() {
1130                    println!();
1131                    ˇ
1132                }"},
1133            Mode::Insert,
1134        );
1135
1136        cx.assert_binding(
1137            "o",
1138            indoc! {"
1139                fn test(ˇ) {
1140                    println!();
1141                }"},
1142            Mode::Normal,
1143            indoc! {"
1144                fn test() {
1145                    ˇ
1146                    println!();
1147                }"},
1148            Mode::Insert,
1149        );
1150    }
1151
1152    #[gpui::test]
1153    async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1154        let mut cx = NeovimBackedTestContext::new(cx).await;
1155        cx.simulate("shift-o", "ˇ").await.assert_matches();
1156        cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1157        cx.simulate_at_each_offset(
1158            "shift-o",
1159            indoc! {"
1160            The qˇuick
1161            brown ˇfox
1162            jumps ˇover"},
1163        )
1164        .await
1165        .assert_matches();
1166        cx.simulate(
1167            "shift-o",
1168            indoc! {"
1169            The quick
1170            ˇ
1171            brown fox"},
1172        )
1173        .await
1174        .assert_matches();
1175
1176        // Our indentation is smarter than vims. So we don't match here
1177        cx.assert_binding(
1178            "shift-o",
1179            indoc! {"
1180                fn test() {
1181                    println!(ˇ);
1182                }"},
1183            Mode::Normal,
1184            indoc! {"
1185                fn test() {
1186                    ˇ
1187                    println!();
1188                }"},
1189            Mode::Insert,
1190        );
1191        cx.assert_binding(
1192            "shift-o",
1193            indoc! {"
1194                fn test(ˇ) {
1195                    println!();
1196                }"},
1197            Mode::Normal,
1198            indoc! {"
1199                ˇ
1200                fn test() {
1201                    println!();
1202                }"},
1203            Mode::Insert,
1204        );
1205    }
1206
1207    #[gpui::test]
1208    async fn test_dd(cx: &mut gpui::TestAppContext) {
1209        let mut cx = NeovimBackedTestContext::new(cx).await;
1210        cx.simulate("d d", "ˇ").await.assert_matches();
1211        cx.simulate("d d", "The ˇquick").await.assert_matches();
1212        cx.simulate_at_each_offset(
1213            "d d",
1214            indoc! {"
1215            The qˇuick
1216            brown ˇfox
1217            jumps ˇover"},
1218        )
1219        .await
1220        .assert_matches();
1221        cx.simulate(
1222            "d d",
1223            indoc! {"
1224                The quick
1225                ˇ
1226                brown fox"},
1227        )
1228        .await
1229        .assert_matches();
1230    }
1231
1232    #[gpui::test]
1233    async fn test_cc(cx: &mut gpui::TestAppContext) {
1234        let mut cx = NeovimBackedTestContext::new(cx).await;
1235        cx.simulate("c c", "ˇ").await.assert_matches();
1236        cx.simulate("c c", "The ˇquick").await.assert_matches();
1237        cx.simulate_at_each_offset(
1238            "c c",
1239            indoc! {"
1240                The quˇick
1241                brown ˇfox
1242                jumps ˇover"},
1243        )
1244        .await
1245        .assert_matches();
1246        cx.simulate(
1247            "c c",
1248            indoc! {"
1249                The quick
1250                ˇ
1251                brown fox"},
1252        )
1253        .await
1254        .assert_matches();
1255    }
1256
1257    #[gpui::test]
1258    async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1259        let mut cx = NeovimBackedTestContext::new(cx).await;
1260
1261        for count in 1..=5 {
1262            cx.simulate_at_each_offset(
1263                &format!("{count} w"),
1264                indoc! {"
1265                    ˇThe quˇickˇ browˇn
1266                    ˇ
1267                    ˇfox ˇjumpsˇ-ˇoˇver
1268                    ˇthe lazy dog
1269                "},
1270            )
1271            .await
1272            .assert_matches();
1273        }
1274    }
1275
1276    #[gpui::test]
1277    async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1278        let mut cx = NeovimBackedTestContext::new(cx).await;
1279        cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1280            .await
1281            .assert_matches();
1282    }
1283
1284    #[gpui::test]
1285    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1286        let mut cx = NeovimBackedTestContext::new(cx).await;
1287
1288        for count in 1..=3 {
1289            let test_case = indoc! {"
1290                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1291                ˇ    ˇbˇaaˇa ˇbˇbˇb
1292                ˇ
1293                ˇb
1294            "};
1295
1296            cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1297                .await
1298                .assert_matches();
1299
1300            cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1301                .await
1302                .assert_matches();
1303        }
1304    }
1305
1306    #[gpui::test]
1307    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1308        let mut cx = NeovimBackedTestContext::new(cx).await;
1309        let test_case = indoc! {"
1310            ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
1311            ˇ    ˇbˇaaˇa ˇbˇbˇb
1312            ˇ•••
1313            ˇb
1314            "
1315        };
1316
1317        for count in 1..=3 {
1318            cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1319                .await
1320                .assert_matches();
1321
1322            cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1323                .await
1324                .assert_matches();
1325        }
1326    }
1327
1328    #[gpui::test]
1329    async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1330        let mut cx = VimTestContext::new(cx, true).await;
1331        cx.update_global(|store: &mut SettingsStore, cx| {
1332            store.update_user_settings::<VimSettings>(cx, |s| {
1333                s.use_multiline_find = Some(true);
1334            });
1335        });
1336
1337        cx.assert_binding(
1338            "f l",
1339            indoc! {"
1340            ˇfunction print() {
1341                console.log('ok')
1342            }
1343            "},
1344            Mode::Normal,
1345            indoc! {"
1346            function print() {
1347                consoˇle.log('ok')
1348            }
1349            "},
1350            Mode::Normal,
1351        );
1352
1353        cx.assert_binding(
1354            "t l",
1355            indoc! {"
1356            ˇfunction print() {
1357                console.log('ok')
1358            }
1359            "},
1360            Mode::Normal,
1361            indoc! {"
1362            function print() {
1363                consˇole.log('ok')
1364            }
1365            "},
1366            Mode::Normal,
1367        );
1368    }
1369
1370    #[gpui::test]
1371    async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1372        let mut cx = VimTestContext::new(cx, true).await;
1373        cx.update_global(|store: &mut SettingsStore, cx| {
1374            store.update_user_settings::<VimSettings>(cx, |s| {
1375                s.use_multiline_find = Some(true);
1376            });
1377        });
1378
1379        cx.assert_binding(
1380            "shift-f p",
1381            indoc! {"
1382            function print() {
1383                console.ˇlog('ok')
1384            }
1385            "},
1386            Mode::Normal,
1387            indoc! {"
1388            function ˇprint() {
1389                console.log('ok')
1390            }
1391            "},
1392            Mode::Normal,
1393        );
1394
1395        cx.assert_binding(
1396            "shift-t p",
1397            indoc! {"
1398            function print() {
1399                console.ˇlog('ok')
1400            }
1401            "},
1402            Mode::Normal,
1403            indoc! {"
1404            function pˇrint() {
1405                console.log('ok')
1406            }
1407            "},
1408            Mode::Normal,
1409        );
1410    }
1411
1412    #[gpui::test]
1413    async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1414        let mut cx = VimTestContext::new(cx, true).await;
1415        cx.update_global(|store: &mut SettingsStore, cx| {
1416            store.update_user_settings::<VimSettings>(cx, |s| {
1417                s.use_smartcase_find = Some(true);
1418            });
1419        });
1420
1421        cx.assert_binding(
1422            "f p",
1423            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1424            Mode::Normal,
1425            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1426            Mode::Normal,
1427        );
1428
1429        cx.assert_binding(
1430            "shift-f p",
1431            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1432            Mode::Normal,
1433            indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1434            Mode::Normal,
1435        );
1436
1437        cx.assert_binding(
1438            "t p",
1439            indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1440            Mode::Normal,
1441            indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1442            Mode::Normal,
1443        );
1444
1445        cx.assert_binding(
1446            "shift-t p",
1447            indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1448            Mode::Normal,
1449            indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1450            Mode::Normal,
1451        );
1452    }
1453
1454    #[gpui::test]
1455    async fn test_percent(cx: &mut TestAppContext) {
1456        let mut cx = NeovimBackedTestContext::new(cx).await;
1457        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1458            .await
1459            .assert_matches();
1460        cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1461            .await
1462            .assert_matches();
1463        cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1464            .await
1465            .assert_matches();
1466    }
1467
1468    #[gpui::test]
1469    async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1470        let mut cx = NeovimBackedTestContext::new(cx).await;
1471
1472        // goes to current line end
1473        cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1474        cx.simulate_shared_keystrokes("$").await;
1475        cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1476
1477        // goes to next line end
1478        cx.simulate_shared_keystrokes("2 $").await;
1479        cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1480
1481        // try to exceed the final line.
1482        cx.simulate_shared_keystrokes("4 $").await;
1483        cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1484    }
1485
1486    #[gpui::test]
1487    async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1488        let mut cx = VimTestContext::new(cx, true).await;
1489        cx.update(|_, cx| {
1490            cx.bind_keys(vec![
1491                KeyBinding::new(
1492                    "w",
1493                    motion::NextSubwordStart {
1494                        ignore_punctuation: false,
1495                    },
1496                    Some("Editor && VimControl && !VimWaiting && !menu"),
1497                ),
1498                KeyBinding::new(
1499                    "b",
1500                    motion::PreviousSubwordStart {
1501                        ignore_punctuation: false,
1502                    },
1503                    Some("Editor && VimControl && !VimWaiting && !menu"),
1504                ),
1505                KeyBinding::new(
1506                    "e",
1507                    motion::NextSubwordEnd {
1508                        ignore_punctuation: false,
1509                    },
1510                    Some("Editor && VimControl && !VimWaiting && !menu"),
1511                ),
1512                KeyBinding::new(
1513                    "g e",
1514                    motion::PreviousSubwordEnd {
1515                        ignore_punctuation: false,
1516                    },
1517                    Some("Editor && VimControl && !VimWaiting && !menu"),
1518                ),
1519            ]);
1520        });
1521
1522        cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1523        // Special case: In 'cw', 'w' acts like 'e'
1524        cx.assert_binding(
1525            "c w",
1526            indoc! {"ˇassert_binding"},
1527            Mode::Normal,
1528            indoc! {"ˇ_binding"},
1529            Mode::Insert,
1530        );
1531
1532        cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1533
1534        cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1535
1536        cx.assert_binding_normal(
1537            "g e",
1538            indoc! {"assert_bindinˇg"},
1539            indoc! {"asserˇt_binding"},
1540        );
1541    }
1542
1543    #[gpui::test]
1544    async fn test_r(cx: &mut gpui::TestAppContext) {
1545        let mut cx = NeovimBackedTestContext::new(cx).await;
1546
1547        cx.set_shared_state("ˇhello\n").await;
1548        cx.simulate_shared_keystrokes("r -").await;
1549        cx.shared_state().await.assert_eq("ˇ-ello\n");
1550
1551        cx.set_shared_state("ˇhello\n").await;
1552        cx.simulate_shared_keystrokes("3 r -").await;
1553        cx.shared_state().await.assert_eq("--ˇ-lo\n");
1554
1555        cx.set_shared_state("ˇhello\n").await;
1556        cx.simulate_shared_keystrokes("r - 2 l .").await;
1557        cx.shared_state().await.assert_eq("-eˇ-lo\n");
1558
1559        cx.set_shared_state("ˇhello world\n").await;
1560        cx.simulate_shared_keystrokes("2 r - f w .").await;
1561        cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1562
1563        cx.set_shared_state("ˇhello world\n").await;
1564        cx.simulate_shared_keystrokes("2 0 r - ").await;
1565        cx.shared_state().await.assert_eq("ˇhello world\n");
1566    }
1567
1568    #[gpui::test]
1569    async fn test_gq(cx: &mut gpui::TestAppContext) {
1570        let mut cx = NeovimBackedTestContext::new(cx).await;
1571        cx.set_neovim_option("textwidth=5").await;
1572
1573        cx.update(|_, cx| {
1574            SettingsStore::update_global(cx, |settings, cx| {
1575                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1576                    settings.defaults.preferred_line_length = Some(5);
1577                });
1578            })
1579        });
1580
1581        cx.set_shared_state("ˇth th th th th th\n").await;
1582        cx.simulate_shared_keystrokes("g q q").await;
1583        cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1584
1585        cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1586            .await;
1587        cx.simulate_shared_keystrokes("v j g q").await;
1588        cx.shared_state()
1589            .await
1590            .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1591    }
1592
1593    #[gpui::test]
1594    async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1595        let mut cx = NeovimBackedTestContext::new(cx).await;
1596        cx.set_neovim_option("filetype=rust").await;
1597
1598        cx.set_shared_state("// helloˇ\n").await;
1599        cx.simulate_shared_keystrokes("o").await;
1600        cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1601        cx.simulate_shared_keystrokes("x escape shift-o").await;
1602        cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1603    }
1604
1605    #[gpui::test]
1606    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1607        let mut cx = NeovimBackedTestContext::new(cx).await;
1608        cx.set_shared_state("heˇllo\n").await;
1609        cx.simulate_shared_keystrokes("y y p").await;
1610        cx.shared_state().await.assert_eq("hello\nˇhello\n");
1611    }
1612
1613    #[gpui::test]
1614    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1615        let mut cx = NeovimBackedTestContext::new(cx).await;
1616        cx.set_shared_state("heˇllo").await;
1617        cx.simulate_shared_keystrokes("y y p").await;
1618        cx.shared_state().await.assert_eq("hello\nˇhello");
1619    }
1620
1621    #[gpui::test]
1622    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1623        let mut cx = NeovimBackedTestContext::new(cx).await;
1624        cx.set_shared_state("heˇllo\nhello").await;
1625        cx.simulate_shared_keystrokes("2 y y p").await;
1626        cx.shared_state()
1627            .await
1628            .assert_eq("hello\nˇhello\nhello\nhello");
1629    }
1630
1631    #[gpui::test]
1632    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1633        let mut cx = NeovimBackedTestContext::new(cx).await;
1634        cx.set_shared_state("heˇllo").await;
1635        cx.simulate_shared_keystrokes("d d").await;
1636        cx.shared_state().await.assert_eq("ˇ");
1637        cx.simulate_shared_keystrokes("p p").await;
1638        cx.shared_state().await.assert_eq("\nhello\nˇhello");
1639    }
1640
1641    #[gpui::test]
1642    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1643        let mut cx = NeovimBackedTestContext::new(cx).await;
1644
1645        cx.set_shared_state("heˇllo").await;
1646        cx.simulate_shared_keystrokes("v i w shift-i").await;
1647        cx.shared_state().await.assert_eq("ˇhello");
1648
1649        cx.set_shared_state(indoc! {"
1650            The quick brown
1651            fox ˇjumps over
1652            the lazy dog"})
1653            .await;
1654        cx.simulate_shared_keystrokes("shift-v shift-i").await;
1655        cx.shared_state().await.assert_eq(indoc! {"
1656            The quick brown
1657            ˇfox jumps over
1658            the lazy dog"});
1659
1660        cx.set_shared_state(indoc! {"
1661            The quick brown
1662            fox ˇjumps over
1663            the lazy dog"})
1664            .await;
1665        cx.simulate_shared_keystrokes("shift-v shift-a").await;
1666        cx.shared_state().await.assert_eq(indoc! {"
1667            The quick brown
1668            fox jˇumps over
1669            the lazy dog"});
1670    }
1671}