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