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