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