Skip to repository content2531 lines · 86.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:31:36.371Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
normal.rs
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