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 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 case::CaseTarget;
26use collections::BTreeSet;
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 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 VimSettings, motion,
634 state::Mode::{self},
635 test::{NeovimBackedTestContext, VimTestContext},
636 };
637
638 #[gpui::test]
639 async fn test_h(cx: &mut gpui::TestAppContext) {
640 let mut cx = NeovimBackedTestContext::new(cx).await;
641 cx.simulate_at_each_offset(
642 "h",
643 indoc! {"
644 ˇThe qˇuick
645 ˇbrown"
646 },
647 )
648 .await
649 .assert_matches();
650 }
651
652 #[gpui::test]
653 async fn test_backspace(cx: &mut gpui::TestAppContext) {
654 let mut cx = NeovimBackedTestContext::new(cx).await;
655 cx.simulate_at_each_offset(
656 "backspace",
657 indoc! {"
658 ˇThe qˇuick
659 ˇbrown"
660 },
661 )
662 .await
663 .assert_matches();
664 }
665
666 #[gpui::test]
667 async fn test_j(cx: &mut gpui::TestAppContext) {
668 let mut cx = NeovimBackedTestContext::new(cx).await;
669
670 cx.set_shared_state(indoc! {"
671 aaˇaa
672 😃😃"
673 })
674 .await;
675 cx.simulate_shared_keystrokes("j").await;
676 cx.shared_state().await.assert_eq(indoc! {"
677 aaaa
678 😃ˇ😃"
679 });
680
681 cx.simulate_at_each_offset(
682 "j",
683 indoc! {"
684 ˇThe qˇuick broˇwn
685 ˇfox jumps"
686 },
687 )
688 .await
689 .assert_matches();
690 }
691
692 #[gpui::test]
693 async fn test_enter(cx: &mut gpui::TestAppContext) {
694 let mut cx = NeovimBackedTestContext::new(cx).await;
695 cx.simulate_at_each_offset(
696 "enter",
697 indoc! {"
698 ˇThe qˇuick broˇwn
699 ˇfox jumps"
700 },
701 )
702 .await
703 .assert_matches();
704 }
705
706 #[gpui::test]
707 async fn test_k(cx: &mut gpui::TestAppContext) {
708 let mut cx = NeovimBackedTestContext::new(cx).await;
709 cx.simulate_at_each_offset(
710 "k",
711 indoc! {"
712 ˇThe qˇuick
713 ˇbrown fˇox jumˇps"
714 },
715 )
716 .await
717 .assert_matches();
718 }
719
720 #[gpui::test]
721 async fn test_l(cx: &mut gpui::TestAppContext) {
722 let mut cx = NeovimBackedTestContext::new(cx).await;
723 cx.simulate_at_each_offset(
724 "l",
725 indoc! {"
726 ˇThe qˇuicˇk
727 ˇbrowˇn"},
728 )
729 .await
730 .assert_matches();
731 }
732
733 #[gpui::test]
734 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
735 let mut cx = NeovimBackedTestContext::new(cx).await;
736 cx.simulate_at_each_offset(
737 "$",
738 indoc! {"
739 ˇThe qˇuicˇk
740 ˇbrowˇn"},
741 )
742 .await
743 .assert_matches();
744 cx.simulate_at_each_offset(
745 "0",
746 indoc! {"
747 ˇThe qˇuicˇk
748 ˇbrowˇn"},
749 )
750 .await
751 .assert_matches();
752 }
753
754 #[gpui::test]
755 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
756 let mut cx = NeovimBackedTestContext::new(cx).await;
757
758 cx.simulate_at_each_offset(
759 "shift-g",
760 indoc! {"
761 The ˇquick
762
763 brown fox jumps
764 overˇ the lazy doˇg"},
765 )
766 .await
767 .assert_matches();
768 cx.simulate(
769 "shift-g",
770 indoc! {"
771 The quiˇck
772
773 brown"},
774 )
775 .await
776 .assert_matches();
777 cx.simulate(
778 "shift-g",
779 indoc! {"
780 The quiˇck
781
782 "},
783 )
784 .await
785 .assert_matches();
786 }
787
788 #[gpui::test]
789 async fn test_w(cx: &mut gpui::TestAppContext) {
790 let mut cx = NeovimBackedTestContext::new(cx).await;
791 cx.simulate_at_each_offset(
792 "w",
793 indoc! {"
794 The ˇquickˇ-ˇbrown
795 ˇ
796 ˇ
797 ˇfox_jumps ˇover
798 ˇthˇe"},
799 )
800 .await
801 .assert_matches();
802 cx.simulate_at_each_offset(
803 "shift-w",
804 indoc! {"
805 The ˇquickˇ-ˇbrown
806 ˇ
807 ˇ
808 ˇfox_jumps ˇover
809 ˇthˇe"},
810 )
811 .await
812 .assert_matches();
813 }
814
815 #[gpui::test]
816 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
817 let mut cx = NeovimBackedTestContext::new(cx).await;
818 cx.simulate_at_each_offset(
819 "e",
820 indoc! {"
821 Thˇe quicˇkˇ-browˇn
822
823
824 fox_jumpˇs oveˇr
825 thˇe"},
826 )
827 .await
828 .assert_matches();
829 cx.simulate_at_each_offset(
830 "shift-e",
831 indoc! {"
832 Thˇe quicˇkˇ-browˇn
833
834
835 fox_jumpˇs oveˇr
836 thˇe"},
837 )
838 .await
839 .assert_matches();
840 }
841
842 #[gpui::test]
843 async fn test_b(cx: &mut gpui::TestAppContext) {
844 let mut cx = NeovimBackedTestContext::new(cx).await;
845 cx.simulate_at_each_offset(
846 "b",
847 indoc! {"
848 ˇThe ˇquickˇ-ˇbrown
849 ˇ
850 ˇ
851 ˇfox_jumps ˇover
852 ˇthe"},
853 )
854 .await
855 .assert_matches();
856 cx.simulate_at_each_offset(
857 "shift-b",
858 indoc! {"
859 ˇThe ˇquickˇ-ˇbrown
860 ˇ
861 ˇ
862 ˇfox_jumps ˇover
863 ˇthe"},
864 )
865 .await
866 .assert_matches();
867 }
868
869 #[gpui::test]
870 async fn test_gg(cx: &mut gpui::TestAppContext) {
871 let mut cx = NeovimBackedTestContext::new(cx).await;
872 cx.simulate_at_each_offset(
873 "g g",
874 indoc! {"
875 The qˇuick
876
877 brown fox jumps
878 over ˇthe laˇzy dog"},
879 )
880 .await
881 .assert_matches();
882 cx.simulate(
883 "g g",
884 indoc! {"
885
886
887 brown fox jumps
888 over the laˇzy dog"},
889 )
890 .await
891 .assert_matches();
892 cx.simulate(
893 "2 g g",
894 indoc! {"
895 ˇ
896
897 brown fox jumps
898 over the lazydog"},
899 )
900 .await
901 .assert_matches();
902 }
903
904 #[gpui::test]
905 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
906 let mut cx = NeovimBackedTestContext::new(cx).await;
907 cx.simulate_at_each_offset(
908 "shift-g",
909 indoc! {"
910 The qˇuick
911
912 brown fox jumps
913 over ˇthe laˇzy dog"},
914 )
915 .await
916 .assert_matches();
917 cx.simulate(
918 "shift-g",
919 indoc! {"
920
921
922 brown fox jumps
923 over the laˇzy dog"},
924 )
925 .await
926 .assert_matches();
927 cx.simulate(
928 "2 shift-g",
929 indoc! {"
930 ˇ
931
932 brown fox jumps
933 over the lazydog"},
934 )
935 .await
936 .assert_matches();
937 }
938
939 #[gpui::test]
940 async fn test_a(cx: &mut gpui::TestAppContext) {
941 let mut cx = NeovimBackedTestContext::new(cx).await;
942 cx.simulate_at_each_offset("a", "The qˇuicˇk")
943 .await
944 .assert_matches();
945 }
946
947 #[gpui::test]
948 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
949 let mut cx = NeovimBackedTestContext::new(cx).await;
950 cx.simulate_at_each_offset(
951 "shift-a",
952 indoc! {"
953 ˇ
954 The qˇuick
955 brown ˇfox "},
956 )
957 .await
958 .assert_matches();
959 }
960
961 #[gpui::test]
962 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
963 let mut cx = NeovimBackedTestContext::new(cx).await;
964 cx.simulate("^", "The qˇuick").await.assert_matches();
965 cx.simulate("^", " The qˇuick").await.assert_matches();
966 cx.simulate("^", "ˇ").await.assert_matches();
967 cx.simulate(
968 "^",
969 indoc! {"
970 The qˇuick
971 brown fox"},
972 )
973 .await
974 .assert_matches();
975 cx.simulate(
976 "^",
977 indoc! {"
978 ˇ
979 The quick"},
980 )
981 .await
982 .assert_matches();
983 // Indoc disallows trailing whitespace.
984 cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
985 }
986
987 #[gpui::test]
988 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
989 let mut cx = NeovimBackedTestContext::new(cx).await;
990 cx.simulate("shift-i", "The qˇuick").await.assert_matches();
991 cx.simulate("shift-i", " The qˇuick").await.assert_matches();
992 cx.simulate("shift-i", "ˇ").await.assert_matches();
993 cx.simulate(
994 "shift-i",
995 indoc! {"
996 The qˇuick
997 brown fox"},
998 )
999 .await
1000 .assert_matches();
1001 cx.simulate(
1002 "shift-i",
1003 indoc! {"
1004 ˇ
1005 The quick"},
1006 )
1007 .await
1008 .assert_matches();
1009 }
1010
1011 #[gpui::test]
1012 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1013 let mut cx = NeovimBackedTestContext::new(cx).await;
1014 cx.simulate(
1015 "shift-d",
1016 indoc! {"
1017 The qˇuick
1018 brown fox"},
1019 )
1020 .await
1021 .assert_matches();
1022 cx.simulate(
1023 "shift-d",
1024 indoc! {"
1025 The quick
1026 ˇ
1027 brown fox"},
1028 )
1029 .await
1030 .assert_matches();
1031 }
1032
1033 #[gpui::test]
1034 async fn test_x(cx: &mut gpui::TestAppContext) {
1035 let mut cx = NeovimBackedTestContext::new(cx).await;
1036 cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1037 .await
1038 .assert_matches();
1039 cx.simulate(
1040 "x",
1041 indoc! {"
1042 Tesˇt
1043 test"},
1044 )
1045 .await
1046 .assert_matches();
1047 }
1048
1049 #[gpui::test]
1050 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1051 let mut cx = NeovimBackedTestContext::new(cx).await;
1052 cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1053 .await
1054 .assert_matches();
1055 cx.simulate(
1056 "shift-x",
1057 indoc! {"
1058 Test
1059 ˇtest"},
1060 )
1061 .await
1062 .assert_matches();
1063 }
1064
1065 #[gpui::test]
1066 async fn test_o(cx: &mut gpui::TestAppContext) {
1067 let mut cx = NeovimBackedTestContext::new(cx).await;
1068 cx.simulate("o", "ˇ").await.assert_matches();
1069 cx.simulate("o", "The ˇquick").await.assert_matches();
1070 cx.simulate_at_each_offset(
1071 "o",
1072 indoc! {"
1073 The qˇuick
1074 brown ˇfox
1075 jumps ˇover"},
1076 )
1077 .await
1078 .assert_matches();
1079 cx.simulate(
1080 "o",
1081 indoc! {"
1082 The quick
1083 ˇ
1084 brown fox"},
1085 )
1086 .await
1087 .assert_matches();
1088
1089 cx.assert_binding(
1090 "o",
1091 indoc! {"
1092 fn test() {
1093 println!(ˇ);
1094 }"},
1095 Mode::Normal,
1096 indoc! {"
1097 fn test() {
1098 println!();
1099 ˇ
1100 }"},
1101 Mode::Insert,
1102 );
1103
1104 cx.assert_binding(
1105 "o",
1106 indoc! {"
1107 fn test(ˇ) {
1108 println!();
1109 }"},
1110 Mode::Normal,
1111 indoc! {"
1112 fn test() {
1113 ˇ
1114 println!();
1115 }"},
1116 Mode::Insert,
1117 );
1118 }
1119
1120 #[gpui::test]
1121 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1122 let mut cx = NeovimBackedTestContext::new(cx).await;
1123 cx.simulate("shift-o", "ˇ").await.assert_matches();
1124 cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1125 cx.simulate_at_each_offset(
1126 "shift-o",
1127 indoc! {"
1128 The qˇuick
1129 brown ˇfox
1130 jumps ˇover"},
1131 )
1132 .await
1133 .assert_matches();
1134 cx.simulate(
1135 "shift-o",
1136 indoc! {"
1137 The quick
1138 ˇ
1139 brown fox"},
1140 )
1141 .await
1142 .assert_matches();
1143
1144 // Our indentation is smarter than vims. So we don't match here
1145 cx.assert_binding(
1146 "shift-o",
1147 indoc! {"
1148 fn test() {
1149 println!(ˇ);
1150 }"},
1151 Mode::Normal,
1152 indoc! {"
1153 fn test() {
1154 ˇ
1155 println!();
1156 }"},
1157 Mode::Insert,
1158 );
1159 cx.assert_binding(
1160 "shift-o",
1161 indoc! {"
1162 fn test(ˇ) {
1163 println!();
1164 }"},
1165 Mode::Normal,
1166 indoc! {"
1167 ˇ
1168 fn test() {
1169 println!();
1170 }"},
1171 Mode::Insert,
1172 );
1173 }
1174
1175 #[gpui::test]
1176 async fn test_dd(cx: &mut gpui::TestAppContext) {
1177 let mut cx = NeovimBackedTestContext::new(cx).await;
1178 cx.simulate("d d", "ˇ").await.assert_matches();
1179 cx.simulate("d d", "The ˇquick").await.assert_matches();
1180 cx.simulate_at_each_offset(
1181 "d d",
1182 indoc! {"
1183 The qˇuick
1184 brown ˇfox
1185 jumps ˇover"},
1186 )
1187 .await
1188 .assert_matches();
1189 cx.simulate(
1190 "d d",
1191 indoc! {"
1192 The quick
1193 ˇ
1194 brown fox"},
1195 )
1196 .await
1197 .assert_matches();
1198 }
1199
1200 #[gpui::test]
1201 async fn test_cc(cx: &mut gpui::TestAppContext) {
1202 let mut cx = NeovimBackedTestContext::new(cx).await;
1203 cx.simulate("c c", "ˇ").await.assert_matches();
1204 cx.simulate("c c", "The ˇquick").await.assert_matches();
1205 cx.simulate_at_each_offset(
1206 "c c",
1207 indoc! {"
1208 The quˇick
1209 brown ˇfox
1210 jumps ˇover"},
1211 )
1212 .await
1213 .assert_matches();
1214 cx.simulate(
1215 "c c",
1216 indoc! {"
1217 The quick
1218 ˇ
1219 brown fox"},
1220 )
1221 .await
1222 .assert_matches();
1223 }
1224
1225 #[gpui::test]
1226 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1227 let mut cx = NeovimBackedTestContext::new(cx).await;
1228
1229 for count in 1..=5 {
1230 cx.simulate_at_each_offset(
1231 &format!("{count} w"),
1232 indoc! {"
1233 ˇThe quˇickˇ browˇn
1234 ˇ
1235 ˇfox ˇjumpsˇ-ˇoˇver
1236 ˇthe lazy dog
1237 "},
1238 )
1239 .await
1240 .assert_matches();
1241 }
1242 }
1243
1244 #[gpui::test]
1245 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1246 let mut cx = NeovimBackedTestContext::new(cx).await;
1247 cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1248 .await
1249 .assert_matches();
1250 }
1251
1252 #[gpui::test]
1253 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1254 let mut cx = NeovimBackedTestContext::new(cx).await;
1255
1256 for count in 1..=3 {
1257 let test_case = indoc! {"
1258 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1259 ˇ ˇbˇaaˇa ˇbˇbˇb
1260 ˇ
1261 ˇb
1262 "};
1263
1264 cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1265 .await
1266 .assert_matches();
1267
1268 cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1269 .await
1270 .assert_matches();
1271 }
1272 }
1273
1274 #[gpui::test]
1275 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1276 let mut cx = NeovimBackedTestContext::new(cx).await;
1277 let test_case = indoc! {"
1278 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1279 ˇ ˇbˇaaˇa ˇbˇbˇb
1280 ˇ•••
1281 ˇb
1282 "
1283 };
1284
1285 for count in 1..=3 {
1286 cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1287 .await
1288 .assert_matches();
1289
1290 cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1291 .await
1292 .assert_matches();
1293 }
1294 }
1295
1296 #[gpui::test]
1297 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1298 let mut cx = VimTestContext::new(cx, true).await;
1299 cx.update_global(|store: &mut SettingsStore, cx| {
1300 store.update_user_settings::<VimSettings>(cx, |s| {
1301 s.use_multiline_find = Some(true);
1302 });
1303 });
1304
1305 cx.assert_binding(
1306 "f l",
1307 indoc! {"
1308 ˇfunction print() {
1309 console.log('ok')
1310 }
1311 "},
1312 Mode::Normal,
1313 indoc! {"
1314 function print() {
1315 consoˇle.log('ok')
1316 }
1317 "},
1318 Mode::Normal,
1319 );
1320
1321 cx.assert_binding(
1322 "t l",
1323 indoc! {"
1324 ˇfunction print() {
1325 console.log('ok')
1326 }
1327 "},
1328 Mode::Normal,
1329 indoc! {"
1330 function print() {
1331 consˇole.log('ok')
1332 }
1333 "},
1334 Mode::Normal,
1335 );
1336 }
1337
1338 #[gpui::test]
1339 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1340 let mut cx = VimTestContext::new(cx, true).await;
1341 cx.update_global(|store: &mut SettingsStore, cx| {
1342 store.update_user_settings::<VimSettings>(cx, |s| {
1343 s.use_multiline_find = Some(true);
1344 });
1345 });
1346
1347 cx.assert_binding(
1348 "shift-f p",
1349 indoc! {"
1350 function print() {
1351 console.ˇlog('ok')
1352 }
1353 "},
1354 Mode::Normal,
1355 indoc! {"
1356 function ˇprint() {
1357 console.log('ok')
1358 }
1359 "},
1360 Mode::Normal,
1361 );
1362
1363 cx.assert_binding(
1364 "shift-t p",
1365 indoc! {"
1366 function print() {
1367 console.ˇlog('ok')
1368 }
1369 "},
1370 Mode::Normal,
1371 indoc! {"
1372 function pˇrint() {
1373 console.log('ok')
1374 }
1375 "},
1376 Mode::Normal,
1377 );
1378 }
1379
1380 #[gpui::test]
1381 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1382 let mut cx = VimTestContext::new(cx, true).await;
1383 cx.update_global(|store: &mut SettingsStore, cx| {
1384 store.update_user_settings::<VimSettings>(cx, |s| {
1385 s.use_smartcase_find = Some(true);
1386 });
1387 });
1388
1389 cx.assert_binding(
1390 "f p",
1391 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1392 Mode::Normal,
1393 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1394 Mode::Normal,
1395 );
1396
1397 cx.assert_binding(
1398 "shift-f p",
1399 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1400 Mode::Normal,
1401 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1402 Mode::Normal,
1403 );
1404
1405 cx.assert_binding(
1406 "t 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-t p",
1415 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1416 Mode::Normal,
1417 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1418 Mode::Normal,
1419 );
1420 }
1421
1422 #[gpui::test]
1423 async fn test_percent(cx: &mut TestAppContext) {
1424 let mut cx = NeovimBackedTestContext::new(cx).await;
1425 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1426 .await
1427 .assert_matches();
1428 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1429 .await
1430 .assert_matches();
1431 cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1432 .await
1433 .assert_matches();
1434 }
1435
1436 #[gpui::test]
1437 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1438 let mut cx = NeovimBackedTestContext::new(cx).await;
1439
1440 // goes to current line end
1441 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1442 cx.simulate_shared_keystrokes("$").await;
1443 cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1444
1445 // goes to next line end
1446 cx.simulate_shared_keystrokes("2 $").await;
1447 cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1448
1449 // try to exceed the final line.
1450 cx.simulate_shared_keystrokes("4 $").await;
1451 cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1452 }
1453
1454 #[gpui::test]
1455 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1456 let mut cx = VimTestContext::new(cx, true).await;
1457 cx.update(|_, cx| {
1458 cx.bind_keys(vec![
1459 KeyBinding::new(
1460 "w",
1461 motion::NextSubwordStart {
1462 ignore_punctuation: false,
1463 },
1464 Some("Editor && VimControl && !VimWaiting && !menu"),
1465 ),
1466 KeyBinding::new(
1467 "b",
1468 motion::PreviousSubwordStart {
1469 ignore_punctuation: false,
1470 },
1471 Some("Editor && VimControl && !VimWaiting && !menu"),
1472 ),
1473 KeyBinding::new(
1474 "e",
1475 motion::NextSubwordEnd {
1476 ignore_punctuation: false,
1477 },
1478 Some("Editor && VimControl && !VimWaiting && !menu"),
1479 ),
1480 KeyBinding::new(
1481 "g e",
1482 motion::PreviousSubwordEnd {
1483 ignore_punctuation: false,
1484 },
1485 Some("Editor && VimControl && !VimWaiting && !menu"),
1486 ),
1487 ]);
1488 });
1489
1490 cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1491 // Special case: In 'cw', 'w' acts like 'e'
1492 cx.assert_binding(
1493 "c w",
1494 indoc! {"ˇassert_binding"},
1495 Mode::Normal,
1496 indoc! {"ˇ_binding"},
1497 Mode::Insert,
1498 );
1499
1500 cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1501
1502 cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1503
1504 cx.assert_binding_normal(
1505 "g e",
1506 indoc! {"assert_bindinˇg"},
1507 indoc! {"asserˇt_binding"},
1508 );
1509 }
1510
1511 #[gpui::test]
1512 async fn test_r(cx: &mut gpui::TestAppContext) {
1513 let mut cx = NeovimBackedTestContext::new(cx).await;
1514
1515 cx.set_shared_state("ˇhello\n").await;
1516 cx.simulate_shared_keystrokes("r -").await;
1517 cx.shared_state().await.assert_eq("ˇ-ello\n");
1518
1519 cx.set_shared_state("ˇhello\n").await;
1520 cx.simulate_shared_keystrokes("3 r -").await;
1521 cx.shared_state().await.assert_eq("--ˇ-lo\n");
1522
1523 cx.set_shared_state("ˇhello\n").await;
1524 cx.simulate_shared_keystrokes("r - 2 l .").await;
1525 cx.shared_state().await.assert_eq("-eˇ-lo\n");
1526
1527 cx.set_shared_state("ˇhello world\n").await;
1528 cx.simulate_shared_keystrokes("2 r - f w .").await;
1529 cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1530
1531 cx.set_shared_state("ˇhello world\n").await;
1532 cx.simulate_shared_keystrokes("2 0 r - ").await;
1533 cx.shared_state().await.assert_eq("ˇhello world\n");
1534 }
1535
1536 #[gpui::test]
1537 async fn test_gq(cx: &mut gpui::TestAppContext) {
1538 let mut cx = NeovimBackedTestContext::new(cx).await;
1539 cx.set_neovim_option("textwidth=5").await;
1540
1541 cx.update(|_, cx| {
1542 SettingsStore::update_global(cx, |settings, cx| {
1543 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1544 settings.defaults.preferred_line_length = Some(5);
1545 });
1546 })
1547 });
1548
1549 cx.set_shared_state("ˇth th th th th th\n").await;
1550 cx.simulate_shared_keystrokes("g q q").await;
1551 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1552
1553 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1554 .await;
1555 cx.simulate_shared_keystrokes("v j g q").await;
1556 cx.shared_state()
1557 .await
1558 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1559 }
1560
1561 #[gpui::test]
1562 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1563 let mut cx = NeovimBackedTestContext::new(cx).await;
1564 cx.set_neovim_option("filetype=rust").await;
1565
1566 cx.set_shared_state("// helloˇ\n").await;
1567 cx.simulate_shared_keystrokes("o").await;
1568 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1569 cx.simulate_shared_keystrokes("x escape shift-o").await;
1570 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1571 }
1572
1573 #[gpui::test]
1574 async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1575 let mut cx = NeovimBackedTestContext::new(cx).await;
1576 cx.set_shared_state("heˇllo\n").await;
1577 cx.simulate_shared_keystrokes("y y p").await;
1578 cx.shared_state().await.assert_eq("hello\nˇhello\n");
1579 }
1580
1581 #[gpui::test]
1582 async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1583 let mut cx = NeovimBackedTestContext::new(cx).await;
1584 cx.set_shared_state("heˇllo").await;
1585 cx.simulate_shared_keystrokes("y y p").await;
1586 cx.shared_state().await.assert_eq("hello\nˇhello");
1587 }
1588
1589 #[gpui::test]
1590 async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1591 let mut cx = NeovimBackedTestContext::new(cx).await;
1592 cx.set_shared_state("heˇllo\nhello").await;
1593 cx.simulate_shared_keystrokes("2 y y p").await;
1594 cx.shared_state()
1595 .await
1596 .assert_eq("hello\nˇhello\nhello\nhello");
1597 }
1598
1599 #[gpui::test]
1600 async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1601 let mut cx = NeovimBackedTestContext::new(cx).await;
1602 cx.set_shared_state("heˇllo").await;
1603 cx.simulate_shared_keystrokes("d d").await;
1604 cx.shared_state().await.assert_eq("ˇ");
1605 cx.simulate_shared_keystrokes("p p").await;
1606 cx.shared_state().await.assert_eq("\nhello\nˇhello");
1607 }
1608
1609 #[gpui::test]
1610 async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1611 let mut cx = NeovimBackedTestContext::new(cx).await;
1612
1613 cx.set_shared_state("heˇllo").await;
1614 cx.simulate_shared_keystrokes("v i w shift-i").await;
1615 cx.shared_state().await.assert_eq("ˇhello");
1616
1617 cx.set_shared_state(indoc! {"
1618 The quick brown
1619 fox ˇjumps over
1620 the lazy dog"})
1621 .await;
1622 cx.simulate_shared_keystrokes("shift-v shift-i").await;
1623 cx.shared_state().await.assert_eq(indoc! {"
1624 The quick brown
1625 ˇfox jumps over
1626 the lazy dog"});
1627
1628 cx.set_shared_state(indoc! {"
1629 The quick brown
1630 fox ˇjumps over
1631 the lazy dog"})
1632 .await;
1633 cx.simulate_shared_keystrokes("shift-v shift-a").await;
1634 cx.shared_state().await.assert_eq(indoc! {"
1635 The quick brown
1636 fox jˇumps over
1637 the lazy dog"});
1638 }
1639}