Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:34:26.721Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

normal.rs

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