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::{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 if let Some(marks) = vim.marks.get("^") {
359 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
360 s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
361 });
362 }
363 });
364 }
365
366 fn insert_line_above(
367 &mut self,
368 _: &InsertLineAbove,
369 window: &mut Window,
370 cx: &mut Context<Self>,
371 ) {
372 self.start_recording(cx);
373 self.switch_mode(Mode::Insert, false, window, cx);
374 self.update_editor(window, cx, |_, editor, window, cx| {
375 editor.transact(window, cx, |editor, window, cx| {
376 let selections = editor.selections.all::<Point>(cx);
377 let snapshot = editor.buffer().read(cx).snapshot(cx);
378
379 let selection_start_rows: BTreeSet<u32> = selections
380 .into_iter()
381 .map(|selection| selection.start.row)
382 .collect();
383 let edits = selection_start_rows
384 .into_iter()
385 .map(|row| {
386 let indent = snapshot
387 .indent_and_comment_for_line(MultiBufferRow(row), cx)
388 .chars()
389 .collect::<String>();
390
391 let start_of_line = Point::new(row, 0);
392 (start_of_line..start_of_line, indent + "\n")
393 })
394 .collect::<Vec<_>>();
395 editor.edit_with_autoindent(edits, cx);
396 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
397 s.move_cursors_with(|map, cursor, _| {
398 let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
399 let insert_point = motion::end_of_line(map, false, previous_line, 1);
400 (insert_point, SelectionGoal::None)
401 });
402 });
403 });
404 });
405 }
406
407 fn insert_line_below(
408 &mut self,
409 _: &InsertLineBelow,
410 window: &mut Window,
411 cx: &mut Context<Self>,
412 ) {
413 self.start_recording(cx);
414 self.switch_mode(Mode::Insert, false, window, cx);
415 self.update_editor(window, cx, |_, editor, window, cx| {
416 let text_layout_details = editor.text_layout_details(window);
417 editor.transact(window, cx, |editor, window, cx| {
418 let selections = editor.selections.all::<Point>(cx);
419 let snapshot = editor.buffer().read(cx).snapshot(cx);
420
421 let selection_end_rows: BTreeSet<u32> = selections
422 .into_iter()
423 .map(|selection| selection.end.row)
424 .collect();
425 let edits = selection_end_rows
426 .into_iter()
427 .map(|row| {
428 let indent = snapshot
429 .indent_and_comment_for_line(MultiBufferRow(row), cx)
430 .chars()
431 .collect::<String>();
432
433 let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
434 (end_of_line..end_of_line, "\n".to_string() + &indent)
435 })
436 .collect::<Vec<_>>();
437 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
438 s.maybe_move_cursors_with(|map, cursor, goal| {
439 Motion::CurrentLine.move_point(
440 map,
441 cursor,
442 goal,
443 None,
444 &text_layout_details,
445 )
446 });
447 });
448 editor.edit_with_autoindent(edits, cx);
449 });
450 });
451 }
452
453 fn join_lines_impl(
454 &mut self,
455 insert_whitespace: bool,
456 window: &mut Window,
457 cx: &mut Context<Self>,
458 ) {
459 self.record_current_action(cx);
460 let mut times = Vim::take_count(cx).unwrap_or(1);
461 if self.mode.is_visual() {
462 times = 1;
463 } else if times > 1 {
464 // 2J joins two lines together (same as J or 1J)
465 times -= 1;
466 }
467
468 self.update_editor(window, cx, |_, editor, window, cx| {
469 editor.transact(window, cx, |editor, window, cx| {
470 for _ in 0..times {
471 editor.join_lines_impl(insert_whitespace, window, cx)
472 }
473 })
474 });
475 if self.mode.is_visual() {
476 self.switch_mode(Mode::Normal, true, window, cx)
477 }
478 }
479
480 fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context<Self>) {
481 let count = Vim::take_count(cx);
482 self.yank_motion(motion::Motion::CurrentLine, count, window, cx)
483 }
484
485 fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
486 let count = Vim::take_count(cx);
487 self.update_editor(window, cx, |vim, editor, _window, cx| {
488 let selection = editor.selections.newest_anchor();
489 if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
490 let filename = if let Some(file) = buffer.read(cx).file() {
491 if count.is_some() {
492 if let Some(local) = file.as_local() {
493 local.abs_path(cx).to_string_lossy().to_string()
494 } else {
495 file.full_path(cx).to_string_lossy().to_string()
496 }
497 } else {
498 file.path().to_string_lossy().to_string()
499 }
500 } else {
501 "[No Name]".into()
502 };
503 let buffer = buffer.read(cx);
504 let snapshot = buffer.snapshot();
505 let lines = buffer.max_point().row + 1;
506 let current_line = selection.head().text_anchor.to_point(&snapshot).row;
507 let percentage = current_line as f32 / lines as f32;
508 let modified = if buffer.is_dirty() { " [modified]" } else { "" };
509 vim.status_label = Some(
510 format!(
511 "{}{} {} lines --{:.0}%--",
512 filename,
513 modified,
514 lines,
515 percentage * 100.0,
516 )
517 .into(),
518 );
519 cx.notify();
520 }
521 });
522 }
523
524 fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
525 self.record_current_action(cx);
526 self.store_visual_marks(window, cx);
527 self.update_editor(window, cx, |vim, editor, window, cx| {
528 editor.transact(window, cx, |editor, window, cx| {
529 let original_positions = vim.save_selection_starts(editor, cx);
530 editor.toggle_comments(&Default::default(), window, cx);
531 vim.restore_selection_cursors(editor, window, cx, original_positions);
532 });
533 });
534 if self.mode.is_visual() {
535 self.switch_mode(Mode::Normal, true, window, cx)
536 }
537 }
538
539 pub(crate) fn normal_replace(
540 &mut self,
541 text: Arc<str>,
542 window: &mut Window,
543 cx: &mut Context<Self>,
544 ) {
545 let count = Vim::take_count(cx).unwrap_or(1);
546 self.stop_recording(cx);
547 self.update_editor(window, cx, |_, editor, window, cx| {
548 editor.transact(window, cx, |editor, window, cx| {
549 editor.set_clip_at_line_ends(false, cx);
550 let (map, display_selections) = editor.selections.all_display(cx);
551
552 let mut edits = Vec::new();
553 for selection in display_selections {
554 let mut range = selection.range();
555 for _ in 0..count {
556 let new_point = movement::saturating_right(&map, range.end);
557 if range.end == new_point {
558 return;
559 }
560 range.end = new_point;
561 }
562
563 edits.push((
564 range.start.to_offset(&map, Bias::Left)
565 ..range.end.to_offset(&map, Bias::Left),
566 text.repeat(count),
567 ))
568 }
569
570 editor.edit(edits, cx);
571 editor.set_clip_at_line_ends(true, cx);
572 editor.change_selections(None, window, cx, |s| {
573 s.move_with(|map, selection| {
574 let point = movement::saturating_left(map, selection.head());
575 selection.collapse_to(point, SelectionGoal::None)
576 });
577 });
578 });
579 });
580 self.pop_operator(window, cx);
581 }
582
583 pub fn save_selection_starts(
584 &self,
585 editor: &Editor,
586
587 cx: &mut Context<Editor>,
588 ) -> HashMap<usize, Anchor> {
589 let (map, selections) = editor.selections.all_display(cx);
590 selections
591 .iter()
592 .map(|selection| {
593 (
594 selection.id,
595 map.display_point_to_anchor(selection.start, Bias::Right),
596 )
597 })
598 .collect::<HashMap<_, _>>()
599 }
600
601 pub fn restore_selection_cursors(
602 &self,
603 editor: &mut Editor,
604 window: &mut Window,
605 cx: &mut Context<Editor>,
606 mut positions: HashMap<usize, Anchor>,
607 ) {
608 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
609 s.move_with(|map, selection| {
610 if let Some(anchor) = positions.remove(&selection.id) {
611 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
612 }
613 });
614 });
615 }
616
617 fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
618 if self.temp_mode {
619 self.switch_mode(Mode::Insert, true, window, cx);
620 }
621 }
622}
623#[cfg(test)]
624mod test {
625 use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
626 use indoc::indoc;
627 use language::language_settings::AllLanguageSettings;
628 use settings::SettingsStore;
629
630 use crate::{
631 motion,
632 state::Mode::{self},
633 test::{NeovimBackedTestContext, VimTestContext},
634 VimSettings,
635 };
636
637 #[gpui::test]
638 async fn test_h(cx: &mut gpui::TestAppContext) {
639 let mut cx = NeovimBackedTestContext::new(cx).await;
640 cx.simulate_at_each_offset(
641 "h",
642 indoc! {"
643 ˇThe qˇuick
644 ˇbrown"
645 },
646 )
647 .await
648 .assert_matches();
649 }
650
651 #[gpui::test]
652 async fn test_backspace(cx: &mut gpui::TestAppContext) {
653 let mut cx = NeovimBackedTestContext::new(cx).await;
654 cx.simulate_at_each_offset(
655 "backspace",
656 indoc! {"
657 ˇThe qˇuick
658 ˇbrown"
659 },
660 )
661 .await
662 .assert_matches();
663 }
664
665 #[gpui::test]
666 async fn test_j(cx: &mut gpui::TestAppContext) {
667 let mut cx = NeovimBackedTestContext::new(cx).await;
668
669 cx.set_shared_state(indoc! {"
670 aaˇaa
671 😃😃"
672 })
673 .await;
674 cx.simulate_shared_keystrokes("j").await;
675 cx.shared_state().await.assert_eq(indoc! {"
676 aaaa
677 😃ˇ😃"
678 });
679
680 cx.simulate_at_each_offset(
681 "j",
682 indoc! {"
683 ˇThe qˇuick broˇwn
684 ˇfox jumps"
685 },
686 )
687 .await
688 .assert_matches();
689 }
690
691 #[gpui::test]
692 async fn test_enter(cx: &mut gpui::TestAppContext) {
693 let mut cx = NeovimBackedTestContext::new(cx).await;
694 cx.simulate_at_each_offset(
695 "enter",
696 indoc! {"
697 ˇThe qˇuick broˇwn
698 ˇfox jumps"
699 },
700 )
701 .await
702 .assert_matches();
703 }
704
705 #[gpui::test]
706 async fn test_k(cx: &mut gpui::TestAppContext) {
707 let mut cx = NeovimBackedTestContext::new(cx).await;
708 cx.simulate_at_each_offset(
709 "k",
710 indoc! {"
711 ˇThe qˇuick
712 ˇbrown fˇox jumˇps"
713 },
714 )
715 .await
716 .assert_matches();
717 }
718
719 #[gpui::test]
720 async fn test_l(cx: &mut gpui::TestAppContext) {
721 let mut cx = NeovimBackedTestContext::new(cx).await;
722 cx.simulate_at_each_offset(
723 "l",
724 indoc! {"
725 ˇThe qˇuicˇk
726 ˇbrowˇn"},
727 )
728 .await
729 .assert_matches();
730 }
731
732 #[gpui::test]
733 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
734 let mut cx = NeovimBackedTestContext::new(cx).await;
735 cx.simulate_at_each_offset(
736 "$",
737 indoc! {"
738 ˇThe qˇuicˇk
739 ˇbrowˇn"},
740 )
741 .await
742 .assert_matches();
743 cx.simulate_at_each_offset(
744 "0",
745 indoc! {"
746 ˇThe qˇuicˇk
747 ˇbrowˇn"},
748 )
749 .await
750 .assert_matches();
751 }
752
753 #[gpui::test]
754 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
755 let mut cx = NeovimBackedTestContext::new(cx).await;
756
757 cx.simulate_at_each_offset(
758 "shift-g",
759 indoc! {"
760 The ˇquick
761
762 brown fox jumps
763 overˇ the lazy doˇg"},
764 )
765 .await
766 .assert_matches();
767 cx.simulate(
768 "shift-g",
769 indoc! {"
770 The quiˇck
771
772 brown"},
773 )
774 .await
775 .assert_matches();
776 cx.simulate(
777 "shift-g",
778 indoc! {"
779 The quiˇck
780
781 "},
782 )
783 .await
784 .assert_matches();
785 }
786
787 #[gpui::test]
788 async fn test_w(cx: &mut gpui::TestAppContext) {
789 let mut cx = NeovimBackedTestContext::new(cx).await;
790 cx.simulate_at_each_offset(
791 "w",
792 indoc! {"
793 The ˇquickˇ-ˇbrown
794 ˇ
795 ˇ
796 ˇfox_jumps ˇover
797 ˇthˇe"},
798 )
799 .await
800 .assert_matches();
801 cx.simulate_at_each_offset(
802 "shift-w",
803 indoc! {"
804 The ˇquickˇ-ˇbrown
805 ˇ
806 ˇ
807 ˇfox_jumps ˇover
808 ˇthˇe"},
809 )
810 .await
811 .assert_matches();
812 }
813
814 #[gpui::test]
815 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
816 let mut cx = NeovimBackedTestContext::new(cx).await;
817 cx.simulate_at_each_offset(
818 "e",
819 indoc! {"
820 Thˇe quicˇkˇ-browˇn
821
822
823 fox_jumpˇs oveˇr
824 thˇe"},
825 )
826 .await
827 .assert_matches();
828 cx.simulate_at_each_offset(
829 "shift-e",
830 indoc! {"
831 Thˇe quicˇkˇ-browˇn
832
833
834 fox_jumpˇs oveˇr
835 thˇe"},
836 )
837 .await
838 .assert_matches();
839 }
840
841 #[gpui::test]
842 async fn test_b(cx: &mut gpui::TestAppContext) {
843 let mut cx = NeovimBackedTestContext::new(cx).await;
844 cx.simulate_at_each_offset(
845 "b",
846 indoc! {"
847 ˇThe ˇquickˇ-ˇbrown
848 ˇ
849 ˇ
850 ˇfox_jumps ˇover
851 ˇthe"},
852 )
853 .await
854 .assert_matches();
855 cx.simulate_at_each_offset(
856 "shift-b",
857 indoc! {"
858 ˇThe ˇquickˇ-ˇbrown
859 ˇ
860 ˇ
861 ˇfox_jumps ˇover
862 ˇthe"},
863 )
864 .await
865 .assert_matches();
866 }
867
868 #[gpui::test]
869 async fn test_gg(cx: &mut gpui::TestAppContext) {
870 let mut cx = NeovimBackedTestContext::new(cx).await;
871 cx.simulate_at_each_offset(
872 "g g",
873 indoc! {"
874 The qˇuick
875
876 brown fox jumps
877 over ˇthe laˇzy dog"},
878 )
879 .await
880 .assert_matches();
881 cx.simulate(
882 "g g",
883 indoc! {"
884
885
886 brown fox jumps
887 over the laˇzy dog"},
888 )
889 .await
890 .assert_matches();
891 cx.simulate(
892 "2 g g",
893 indoc! {"
894 ˇ
895
896 brown fox jumps
897 over the lazydog"},
898 )
899 .await
900 .assert_matches();
901 }
902
903 #[gpui::test]
904 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
905 let mut cx = NeovimBackedTestContext::new(cx).await;
906 cx.simulate_at_each_offset(
907 "shift-g",
908 indoc! {"
909 The qˇuick
910
911 brown fox jumps
912 over ˇthe laˇzy dog"},
913 )
914 .await
915 .assert_matches();
916 cx.simulate(
917 "shift-g",
918 indoc! {"
919
920
921 brown fox jumps
922 over the laˇzy dog"},
923 )
924 .await
925 .assert_matches();
926 cx.simulate(
927 "2 shift-g",
928 indoc! {"
929 ˇ
930
931 brown fox jumps
932 over the lazydog"},
933 )
934 .await
935 .assert_matches();
936 }
937
938 #[gpui::test]
939 async fn test_a(cx: &mut gpui::TestAppContext) {
940 let mut cx = NeovimBackedTestContext::new(cx).await;
941 cx.simulate_at_each_offset("a", "The qˇuicˇk")
942 .await
943 .assert_matches();
944 }
945
946 #[gpui::test]
947 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
948 let mut cx = NeovimBackedTestContext::new(cx).await;
949 cx.simulate_at_each_offset(
950 "shift-a",
951 indoc! {"
952 ˇ
953 The qˇuick
954 brown ˇfox "},
955 )
956 .await
957 .assert_matches();
958 }
959
960 #[gpui::test]
961 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
962 let mut cx = NeovimBackedTestContext::new(cx).await;
963 cx.simulate("^", "The qˇuick").await.assert_matches();
964 cx.simulate("^", " The qˇuick").await.assert_matches();
965 cx.simulate("^", "ˇ").await.assert_matches();
966 cx.simulate(
967 "^",
968 indoc! {"
969 The qˇuick
970 brown fox"},
971 )
972 .await
973 .assert_matches();
974 cx.simulate(
975 "^",
976 indoc! {"
977 ˇ
978 The quick"},
979 )
980 .await
981 .assert_matches();
982 // Indoc disallows trailing whitespace.
983 cx.simulate("^", " ˇ \nThe quick").await.assert_matches();
984 }
985
986 #[gpui::test]
987 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
988 let mut cx = NeovimBackedTestContext::new(cx).await;
989 cx.simulate("shift-i", "The qˇuick").await.assert_matches();
990 cx.simulate("shift-i", " The qˇuick").await.assert_matches();
991 cx.simulate("shift-i", "ˇ").await.assert_matches();
992 cx.simulate(
993 "shift-i",
994 indoc! {"
995 The qˇuick
996 brown fox"},
997 )
998 .await
999 .assert_matches();
1000 cx.simulate(
1001 "shift-i",
1002 indoc! {"
1003 ˇ
1004 The quick"},
1005 )
1006 .await
1007 .assert_matches();
1008 }
1009
1010 #[gpui::test]
1011 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
1012 let mut cx = NeovimBackedTestContext::new(cx).await;
1013 cx.simulate(
1014 "shift-d",
1015 indoc! {"
1016 The qˇuick
1017 brown fox"},
1018 )
1019 .await
1020 .assert_matches();
1021 cx.simulate(
1022 "shift-d",
1023 indoc! {"
1024 The quick
1025 ˇ
1026 brown fox"},
1027 )
1028 .await
1029 .assert_matches();
1030 }
1031
1032 #[gpui::test]
1033 async fn test_x(cx: &mut gpui::TestAppContext) {
1034 let mut cx = NeovimBackedTestContext::new(cx).await;
1035 cx.simulate_at_each_offset("x", "ˇTeˇsˇt")
1036 .await
1037 .assert_matches();
1038 cx.simulate(
1039 "x",
1040 indoc! {"
1041 Tesˇt
1042 test"},
1043 )
1044 .await
1045 .assert_matches();
1046 }
1047
1048 #[gpui::test]
1049 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
1050 let mut cx = NeovimBackedTestContext::new(cx).await;
1051 cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt")
1052 .await
1053 .assert_matches();
1054 cx.simulate(
1055 "shift-x",
1056 indoc! {"
1057 Test
1058 ˇtest"},
1059 )
1060 .await
1061 .assert_matches();
1062 }
1063
1064 #[gpui::test]
1065 async fn test_o(cx: &mut gpui::TestAppContext) {
1066 let mut cx = NeovimBackedTestContext::new(cx).await;
1067 cx.simulate("o", "ˇ").await.assert_matches();
1068 cx.simulate("o", "The ˇquick").await.assert_matches();
1069 cx.simulate_at_each_offset(
1070 "o",
1071 indoc! {"
1072 The qˇuick
1073 brown ˇfox
1074 jumps ˇover"},
1075 )
1076 .await
1077 .assert_matches();
1078 cx.simulate(
1079 "o",
1080 indoc! {"
1081 The quick
1082 ˇ
1083 brown fox"},
1084 )
1085 .await
1086 .assert_matches();
1087
1088 cx.assert_binding(
1089 "o",
1090 indoc! {"
1091 fn test() {
1092 println!(ˇ);
1093 }"},
1094 Mode::Normal,
1095 indoc! {"
1096 fn test() {
1097 println!();
1098 ˇ
1099 }"},
1100 Mode::Insert,
1101 );
1102
1103 cx.assert_binding(
1104 "o",
1105 indoc! {"
1106 fn test(ˇ) {
1107 println!();
1108 }"},
1109 Mode::Normal,
1110 indoc! {"
1111 fn test() {
1112 ˇ
1113 println!();
1114 }"},
1115 Mode::Insert,
1116 );
1117 }
1118
1119 #[gpui::test]
1120 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
1121 let mut cx = NeovimBackedTestContext::new(cx).await;
1122 cx.simulate("shift-o", "ˇ").await.assert_matches();
1123 cx.simulate("shift-o", "The ˇquick").await.assert_matches();
1124 cx.simulate_at_each_offset(
1125 "shift-o",
1126 indoc! {"
1127 The qˇuick
1128 brown ˇfox
1129 jumps ˇover"},
1130 )
1131 .await
1132 .assert_matches();
1133 cx.simulate(
1134 "shift-o",
1135 indoc! {"
1136 The quick
1137 ˇ
1138 brown fox"},
1139 )
1140 .await
1141 .assert_matches();
1142
1143 // Our indentation is smarter than vims. So we don't match here
1144 cx.assert_binding(
1145 "shift-o",
1146 indoc! {"
1147 fn test() {
1148 println!(ˇ);
1149 }"},
1150 Mode::Normal,
1151 indoc! {"
1152 fn test() {
1153 ˇ
1154 println!();
1155 }"},
1156 Mode::Insert,
1157 );
1158 cx.assert_binding(
1159 "shift-o",
1160 indoc! {"
1161 fn test(ˇ) {
1162 println!();
1163 }"},
1164 Mode::Normal,
1165 indoc! {"
1166 ˇ
1167 fn test() {
1168 println!();
1169 }"},
1170 Mode::Insert,
1171 );
1172 }
1173
1174 #[gpui::test]
1175 async fn test_dd(cx: &mut gpui::TestAppContext) {
1176 let mut cx = NeovimBackedTestContext::new(cx).await;
1177 cx.simulate("d d", "ˇ").await.assert_matches();
1178 cx.simulate("d d", "The ˇquick").await.assert_matches();
1179 cx.simulate_at_each_offset(
1180 "d d",
1181 indoc! {"
1182 The qˇuick
1183 brown ˇfox
1184 jumps ˇover"},
1185 )
1186 .await
1187 .assert_matches();
1188 cx.simulate(
1189 "d d",
1190 indoc! {"
1191 The quick
1192 ˇ
1193 brown fox"},
1194 )
1195 .await
1196 .assert_matches();
1197 }
1198
1199 #[gpui::test]
1200 async fn test_cc(cx: &mut gpui::TestAppContext) {
1201 let mut cx = NeovimBackedTestContext::new(cx).await;
1202 cx.simulate("c c", "ˇ").await.assert_matches();
1203 cx.simulate("c c", "The ˇquick").await.assert_matches();
1204 cx.simulate_at_each_offset(
1205 "c c",
1206 indoc! {"
1207 The quˇick
1208 brown ˇfox
1209 jumps ˇover"},
1210 )
1211 .await
1212 .assert_matches();
1213 cx.simulate(
1214 "c c",
1215 indoc! {"
1216 The quick
1217 ˇ
1218 brown fox"},
1219 )
1220 .await
1221 .assert_matches();
1222 }
1223
1224 #[gpui::test]
1225 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
1226 let mut cx = NeovimBackedTestContext::new(cx).await;
1227
1228 for count in 1..=5 {
1229 cx.simulate_at_each_offset(
1230 &format!("{count} w"),
1231 indoc! {"
1232 ˇThe quˇickˇ browˇn
1233 ˇ
1234 ˇfox ˇjumpsˇ-ˇoˇver
1235 ˇthe lazy dog
1236 "},
1237 )
1238 .await
1239 .assert_matches();
1240 }
1241 }
1242
1243 #[gpui::test]
1244 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
1245 let mut cx = NeovimBackedTestContext::new(cx).await;
1246 cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest")
1247 .await
1248 .assert_matches();
1249 }
1250
1251 #[gpui::test]
1252 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1253 let mut cx = NeovimBackedTestContext::new(cx).await;
1254
1255 for count in 1..=3 {
1256 let test_case = indoc! {"
1257 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1258 ˇ ˇbˇaaˇa ˇbˇbˇb
1259 ˇ
1260 ˇb
1261 "};
1262
1263 cx.simulate_at_each_offset(&format!("{count} f b"), test_case)
1264 .await
1265 .assert_matches();
1266
1267 cx.simulate_at_each_offset(&format!("{count} t b"), test_case)
1268 .await
1269 .assert_matches();
1270 }
1271 }
1272
1273 #[gpui::test]
1274 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
1275 let mut cx = NeovimBackedTestContext::new(cx).await;
1276 let test_case = indoc! {"
1277 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
1278 ˇ ˇbˇaaˇa ˇbˇbˇb
1279 ˇ•••
1280 ˇb
1281 "
1282 };
1283
1284 for count in 1..=3 {
1285 cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case)
1286 .await
1287 .assert_matches();
1288
1289 cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case)
1290 .await
1291 .assert_matches();
1292 }
1293 }
1294
1295 #[gpui::test]
1296 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
1297 let mut cx = VimTestContext::new(cx, true).await;
1298 cx.update_global(|store: &mut SettingsStore, cx| {
1299 store.update_user_settings::<VimSettings>(cx, |s| {
1300 s.use_multiline_find = Some(true);
1301 });
1302 });
1303
1304 cx.assert_binding(
1305 "f l",
1306 indoc! {"
1307 ˇfunction print() {
1308 console.log('ok')
1309 }
1310 "},
1311 Mode::Normal,
1312 indoc! {"
1313 function print() {
1314 consoˇle.log('ok')
1315 }
1316 "},
1317 Mode::Normal,
1318 );
1319
1320 cx.assert_binding(
1321 "t l",
1322 indoc! {"
1323 ˇfunction print() {
1324 console.log('ok')
1325 }
1326 "},
1327 Mode::Normal,
1328 indoc! {"
1329 function print() {
1330 consˇole.log('ok')
1331 }
1332 "},
1333 Mode::Normal,
1334 );
1335 }
1336
1337 #[gpui::test]
1338 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
1339 let mut cx = VimTestContext::new(cx, true).await;
1340 cx.update_global(|store: &mut SettingsStore, cx| {
1341 store.update_user_settings::<VimSettings>(cx, |s| {
1342 s.use_multiline_find = Some(true);
1343 });
1344 });
1345
1346 cx.assert_binding(
1347 "shift-f p",
1348 indoc! {"
1349 function print() {
1350 console.ˇlog('ok')
1351 }
1352 "},
1353 Mode::Normal,
1354 indoc! {"
1355 function ˇprint() {
1356 console.log('ok')
1357 }
1358 "},
1359 Mode::Normal,
1360 );
1361
1362 cx.assert_binding(
1363 "shift-t p",
1364 indoc! {"
1365 function print() {
1366 console.ˇlog('ok')
1367 }
1368 "},
1369 Mode::Normal,
1370 indoc! {"
1371 function pˇrint() {
1372 console.log('ok')
1373 }
1374 "},
1375 Mode::Normal,
1376 );
1377 }
1378
1379 #[gpui::test]
1380 async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
1381 let mut cx = VimTestContext::new(cx, true).await;
1382 cx.update_global(|store: &mut SettingsStore, cx| {
1383 store.update_user_settings::<VimSettings>(cx, |s| {
1384 s.use_smartcase_find = Some(true);
1385 });
1386 });
1387
1388 cx.assert_binding(
1389 "f p",
1390 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1391 Mode::Normal,
1392 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1393 Mode::Normal,
1394 );
1395
1396 cx.assert_binding(
1397 "shift-f p",
1398 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1399 Mode::Normal,
1400 indoc! {"fmt.ˇPrintln(\"Hello, World!\")"},
1401 Mode::Normal,
1402 );
1403
1404 cx.assert_binding(
1405 "t p",
1406 indoc! {"ˇfmt.Println(\"Hello, World!\")"},
1407 Mode::Normal,
1408 indoc! {"fmtˇ.Println(\"Hello, World!\")"},
1409 Mode::Normal,
1410 );
1411
1412 cx.assert_binding(
1413 "shift-t p",
1414 indoc! {"fmt.Printlnˇ(\"Hello, World!\")"},
1415 Mode::Normal,
1416 indoc! {"fmt.Pˇrintln(\"Hello, World!\")"},
1417 Mode::Normal,
1418 );
1419 }
1420
1421 #[gpui::test]
1422 async fn test_percent(cx: &mut TestAppContext) {
1423 let mut cx = NeovimBackedTestContext::new(cx).await;
1424 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;")
1425 .await
1426 .assert_matches();
1427 cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1428 .await
1429 .assert_matches();
1430 cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;")
1431 .await
1432 .assert_matches();
1433 }
1434
1435 #[gpui::test]
1436 async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) {
1437 let mut cx = NeovimBackedTestContext::new(cx).await;
1438
1439 // goes to current line end
1440 cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await;
1441 cx.simulate_shared_keystrokes("$").await;
1442 cx.shared_state().await.assert_eq("aˇa\nbb\ncc");
1443
1444 // goes to next line end
1445 cx.simulate_shared_keystrokes("2 $").await;
1446 cx.shared_state().await.assert_eq("aa\nbˇb\ncc");
1447
1448 // try to exceed the final line.
1449 cx.simulate_shared_keystrokes("4 $").await;
1450 cx.shared_state().await.assert_eq("aa\nbb\ncˇc");
1451 }
1452
1453 #[gpui::test]
1454 async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
1455 let mut cx = VimTestContext::new(cx, true).await;
1456 cx.update(|_, cx| {
1457 cx.bind_keys(vec![
1458 KeyBinding::new(
1459 "w",
1460 motion::NextSubwordStart {
1461 ignore_punctuation: false,
1462 },
1463 Some("Editor && VimControl && !VimWaiting && !menu"),
1464 ),
1465 KeyBinding::new(
1466 "b",
1467 motion::PreviousSubwordStart {
1468 ignore_punctuation: false,
1469 },
1470 Some("Editor && VimControl && !VimWaiting && !menu"),
1471 ),
1472 KeyBinding::new(
1473 "e",
1474 motion::NextSubwordEnd {
1475 ignore_punctuation: false,
1476 },
1477 Some("Editor && VimControl && !VimWaiting && !menu"),
1478 ),
1479 KeyBinding::new(
1480 "g e",
1481 motion::PreviousSubwordEnd {
1482 ignore_punctuation: false,
1483 },
1484 Some("Editor && VimControl && !VimWaiting && !menu"),
1485 ),
1486 ]);
1487 });
1488
1489 cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"});
1490 // Special case: In 'cw', 'w' acts like 'e'
1491 cx.assert_binding(
1492 "c w",
1493 indoc! {"ˇassert_binding"},
1494 Mode::Normal,
1495 indoc! {"ˇ_binding"},
1496 Mode::Insert,
1497 );
1498
1499 cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"});
1500
1501 cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"});
1502
1503 cx.assert_binding_normal(
1504 "g e",
1505 indoc! {"assert_bindinˇg"},
1506 indoc! {"asserˇt_binding"},
1507 );
1508 }
1509
1510 #[gpui::test]
1511 async fn test_r(cx: &mut gpui::TestAppContext) {
1512 let mut cx = NeovimBackedTestContext::new(cx).await;
1513
1514 cx.set_shared_state("ˇhello\n").await;
1515 cx.simulate_shared_keystrokes("r -").await;
1516 cx.shared_state().await.assert_eq("ˇ-ello\n");
1517
1518 cx.set_shared_state("ˇhello\n").await;
1519 cx.simulate_shared_keystrokes("3 r -").await;
1520 cx.shared_state().await.assert_eq("--ˇ-lo\n");
1521
1522 cx.set_shared_state("ˇhello\n").await;
1523 cx.simulate_shared_keystrokes("r - 2 l .").await;
1524 cx.shared_state().await.assert_eq("-eˇ-lo\n");
1525
1526 cx.set_shared_state("ˇhello world\n").await;
1527 cx.simulate_shared_keystrokes("2 r - f w .").await;
1528 cx.shared_state().await.assert_eq("--llo -ˇ-rld\n");
1529
1530 cx.set_shared_state("ˇhello world\n").await;
1531 cx.simulate_shared_keystrokes("2 0 r - ").await;
1532 cx.shared_state().await.assert_eq("ˇhello world\n");
1533 }
1534
1535 #[gpui::test]
1536 async fn test_gq(cx: &mut gpui::TestAppContext) {
1537 let mut cx = NeovimBackedTestContext::new(cx).await;
1538 cx.set_neovim_option("textwidth=5").await;
1539
1540 cx.update(|_, cx| {
1541 SettingsStore::update_global(cx, |settings, cx| {
1542 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1543 settings.defaults.preferred_line_length = Some(5);
1544 });
1545 })
1546 });
1547
1548 cx.set_shared_state("ˇth th th th th th\n").await;
1549 cx.simulate_shared_keystrokes("g q q").await;
1550 cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n");
1551
1552 cx.set_shared_state("ˇth th th th th th\nth th th th th th\n")
1553 .await;
1554 cx.simulate_shared_keystrokes("v j g q").await;
1555 cx.shared_state()
1556 .await
1557 .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n");
1558 }
1559
1560 #[gpui::test]
1561 async fn test_o_comment(cx: &mut gpui::TestAppContext) {
1562 let mut cx = NeovimBackedTestContext::new(cx).await;
1563 cx.set_neovim_option("filetype=rust").await;
1564
1565 cx.set_shared_state("// helloˇ\n").await;
1566 cx.simulate_shared_keystrokes("o").await;
1567 cx.shared_state().await.assert_eq("// hello\n// ˇ\n");
1568 cx.simulate_shared_keystrokes("x escape shift-o").await;
1569 cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
1570 }
1571
1572 #[gpui::test]
1573 async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
1574 let mut cx = NeovimBackedTestContext::new(cx).await;
1575 cx.set_shared_state("heˇllo\n").await;
1576 cx.simulate_shared_keystrokes("y y p").await;
1577 cx.shared_state().await.assert_eq("hello\nˇhello\n");
1578 }
1579
1580 #[gpui::test]
1581 async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1582 let mut cx = NeovimBackedTestContext::new(cx).await;
1583 cx.set_shared_state("heˇllo").await;
1584 cx.simulate_shared_keystrokes("y y p").await;
1585 cx.shared_state().await.assert_eq("hello\nˇhello");
1586 }
1587
1588 #[gpui::test]
1589 async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1590 let mut cx = NeovimBackedTestContext::new(cx).await;
1591 cx.set_shared_state("heˇllo\nhello").await;
1592 cx.simulate_shared_keystrokes("2 y y p").await;
1593 cx.shared_state()
1594 .await
1595 .assert_eq("hello\nˇhello\nhello\nhello");
1596 }
1597
1598 #[gpui::test]
1599 async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
1600 let mut cx = NeovimBackedTestContext::new(cx).await;
1601 cx.set_shared_state("heˇllo").await;
1602 cx.simulate_shared_keystrokes("d d").await;
1603 cx.shared_state().await.assert_eq("ˇ");
1604 cx.simulate_shared_keystrokes("p p").await;
1605 cx.shared_state().await.assert_eq("\nhello\nˇhello");
1606 }
1607
1608 #[gpui::test]
1609 async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
1610 let mut cx = NeovimBackedTestContext::new(cx).await;
1611
1612 cx.set_shared_state("heˇllo").await;
1613 cx.simulate_shared_keystrokes("v i w shift-i").await;
1614 cx.shared_state().await.assert_eq("ˇhello");
1615
1616 cx.set_shared_state(indoc! {"
1617 The quick brown
1618 fox ˇjumps over
1619 the lazy dog"})
1620 .await;
1621 cx.simulate_shared_keystrokes("shift-v shift-i").await;
1622 cx.shared_state().await.assert_eq(indoc! {"
1623 The quick brown
1624 ˇfox jumps over
1625 the lazy dog"});
1626
1627 cx.set_shared_state(indoc! {"
1628 The quick brown
1629 fox ˇjumps over
1630 the lazy dog"})
1631 .await;
1632 cx.simulate_shared_keystrokes("shift-v shift-a").await;
1633 cx.shared_state().await.assert_eq(indoc! {"
1634 The quick brown
1635 fox jˇumps over
1636 the lazy dog"});
1637 }
1638}