1mod case;
2mod change;
3mod delete;
4mod increment;
5mod paste;
6pub(crate) mod repeat;
7mod scroll;
8pub(crate) mod search;
9pub mod substitute;
10mod yank;
11
12use std::sync::Arc;
13
14use crate::{
15 motion::{self, first_non_whitespace, next_line_end, right, Motion},
16 object::Object,
17 state::{Mode, Operator},
18 Vim,
19};
20use collections::HashSet;
21use editor::scroll::Autoscroll;
22use editor::{Bias, DisplayPoint};
23use gpui::{actions, ViewContext, WindowContext};
24use language::SelectionGoal;
25use log::error;
26use workspace::Workspace;
27
28use self::{
29 case::{change_case, convert_to_lower_case, convert_to_upper_case},
30 change::{change_motion, change_object},
31 delete::{delete_motion, delete_object},
32 yank::{yank_motion, yank_object},
33};
34
35actions!(
36 vim,
37 [
38 InsertAfter,
39 InsertBefore,
40 InsertFirstNonWhitespace,
41 InsertEndOfLine,
42 InsertLineAbove,
43 InsertLineBelow,
44 DeleteLeft,
45 DeleteRight,
46 ChangeToEndOfLine,
47 DeleteToEndOfLine,
48 Yank,
49 YankLine,
50 ChangeCase,
51 ConvertToUpperCase,
52 ConvertToLowerCase,
53 JoinLines,
54 Indent,
55 Outdent,
56 ]
57);
58
59pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
60 workspace.register_action(insert_after);
61 workspace.register_action(insert_before);
62 workspace.register_action(insert_first_non_whitespace);
63 workspace.register_action(insert_end_of_line);
64 workspace.register_action(insert_line_above);
65 workspace.register_action(insert_line_below);
66 workspace.register_action(change_case);
67 workspace.register_action(convert_to_upper_case);
68 workspace.register_action(convert_to_lower_case);
69 workspace.register_action(yank_line);
70
71 workspace.register_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
72 Vim::update(cx, |vim, cx| {
73 vim.record_current_action(cx);
74 let times = vim.take_count(cx);
75 delete_motion(vim, Motion::Left, times, cx);
76 })
77 });
78 workspace.register_action(|_: &mut Workspace, _: &DeleteRight, cx| {
79 Vim::update(cx, |vim, cx| {
80 vim.record_current_action(cx);
81 let times = vim.take_count(cx);
82 delete_motion(vim, Motion::Right, times, cx);
83 })
84 });
85 workspace.register_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
86 Vim::update(cx, |vim, cx| {
87 vim.start_recording(cx);
88 let times = vim.take_count(cx);
89 change_motion(
90 vim,
91 Motion::EndOfLine {
92 display_lines: false,
93 },
94 times,
95 cx,
96 );
97 })
98 });
99 workspace.register_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
100 Vim::update(cx, |vim, cx| {
101 vim.record_current_action(cx);
102 let times = vim.take_count(cx);
103 delete_motion(
104 vim,
105 Motion::EndOfLine {
106 display_lines: false,
107 },
108 times,
109 cx,
110 );
111 })
112 });
113 workspace.register_action(|_: &mut Workspace, _: &JoinLines, cx| {
114 Vim::update(cx, |vim, cx| {
115 vim.record_current_action(cx);
116 let mut times = vim.take_count(cx).unwrap_or(1);
117 if vim.state().mode.is_visual() {
118 times = 1;
119 } else if times > 1 {
120 // 2J joins two lines together (same as J or 1J)
121 times -= 1;
122 }
123
124 vim.update_active_editor(cx, |_, editor, cx| {
125 editor.transact(cx, |editor, cx| {
126 for _ in 0..times {
127 editor.join_lines(&Default::default(), cx)
128 }
129 })
130 });
131 if vim.state().mode.is_visual() {
132 vim.switch_mode(Mode::Normal, false, cx)
133 }
134 });
135 });
136
137 workspace.register_action(|_: &mut Workspace, _: &Indent, cx| {
138 Vim::update(cx, |vim, cx| {
139 vim.record_current_action(cx);
140 vim.update_active_editor(cx, |_, editor, cx| {
141 editor.transact(cx, |editor, cx| editor.indent(&Default::default(), cx))
142 });
143 if vim.state().mode.is_visual() {
144 vim.switch_mode(Mode::Normal, false, cx)
145 }
146 });
147 });
148
149 workspace.register_action(|_: &mut Workspace, _: &Outdent, cx| {
150 Vim::update(cx, |vim, cx| {
151 vim.record_current_action(cx);
152 vim.update_active_editor(cx, |_, editor, cx| {
153 editor.transact(cx, |editor, cx| editor.outdent(&Default::default(), cx))
154 });
155 if vim.state().mode.is_visual() {
156 vim.switch_mode(Mode::Normal, false, cx)
157 }
158 });
159 });
160
161 paste::register(workspace, cx);
162 repeat::register(workspace, cx);
163 scroll::register(workspace, cx);
164 search::register(workspace, cx);
165 substitute::register(workspace, cx);
166 increment::register(workspace, cx);
167}
168
169pub fn normal_motion(
170 motion: Motion,
171 operator: Option<Operator>,
172 times: Option<usize>,
173 cx: &mut WindowContext,
174) {
175 Vim::update(cx, |vim, cx| {
176 match operator {
177 None => move_cursor(vim, motion, times, cx),
178 Some(Operator::Change) => change_motion(vim, motion, times, cx),
179 Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
180 Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
181 Some(operator) => {
182 // Can't do anything for text objects, Ignoring
183 error!("Unexpected normal mode motion operator: {:?}", operator)
184 }
185 }
186 });
187}
188
189pub fn normal_object(object: Object, cx: &mut WindowContext) {
190 Vim::update(cx, |vim, cx| {
191 match vim.maybe_pop_operator() {
192 Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
193 Some(Operator::Change) => change_object(vim, object, around, cx),
194 Some(Operator::Delete) => delete_object(vim, object, around, cx),
195 Some(Operator::Yank) => yank_object(vim, object, around, cx),
196 _ => {
197 // Can't do anything for namespace operators. Ignoring
198 }
199 },
200 _ => {
201 // Can't do anything with change/delete/yank and text objects. Ignoring
202 }
203 }
204 vim.clear_operator(cx);
205 })
206}
207
208pub(crate) fn move_cursor(
209 vim: &mut Vim,
210 motion: Motion,
211 times: Option<usize>,
212 cx: &mut WindowContext,
213) {
214 vim.update_active_editor(cx, |_, editor, cx| {
215 let text_layout_details = editor.text_layout_details(cx);
216 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
217 s.move_cursors_with(|map, cursor, goal| {
218 motion
219 .move_point(map, cursor, goal, times, &text_layout_details)
220 .unwrap_or((cursor, goal))
221 })
222 })
223 });
224}
225
226fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
227 Vim::update(cx, |vim, cx| {
228 vim.start_recording(cx);
229 vim.switch_mode(Mode::Insert, false, cx);
230 vim.update_active_editor(cx, |_, editor, cx| {
231 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232 s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
233 });
234 });
235 });
236}
237
238fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
239 Vim::update(cx, |vim, cx| {
240 vim.start_recording(cx);
241 vim.switch_mode(Mode::Insert, false, cx);
242 });
243}
244
245fn insert_first_non_whitespace(
246 _: &mut Workspace,
247 _: &InsertFirstNonWhitespace,
248 cx: &mut ViewContext<Workspace>,
249) {
250 Vim::update(cx, |vim, cx| {
251 vim.start_recording(cx);
252 vim.switch_mode(Mode::Insert, false, cx);
253 vim.update_active_editor(cx, |_, editor, cx| {
254 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
255 s.move_cursors_with(|map, cursor, _| {
256 (
257 first_non_whitespace(map, false, cursor),
258 SelectionGoal::None,
259 )
260 });
261 });
262 });
263 });
264}
265
266fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
267 Vim::update(cx, |vim, cx| {
268 vim.start_recording(cx);
269 vim.switch_mode(Mode::Insert, false, cx);
270 vim.update_active_editor(cx, |_, editor, cx| {
271 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
272 s.move_cursors_with(|map, cursor, _| {
273 (next_line_end(map, cursor, 1), SelectionGoal::None)
274 });
275 });
276 });
277 });
278}
279
280fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
281 Vim::update(cx, |vim, cx| {
282 vim.start_recording(cx);
283 vim.switch_mode(Mode::Insert, false, cx);
284 vim.update_active_editor(cx, |_, editor, cx| {
285 editor.transact(cx, |editor, cx| {
286 let (map, old_selections) = editor.selections.all_display(cx);
287 let selection_start_rows: HashSet<u32> = old_selections
288 .into_iter()
289 .map(|selection| selection.start.row())
290 .collect();
291 let edits = selection_start_rows.into_iter().map(|row| {
292 let (indent, _) = map.line_indent(row);
293 let start_of_line =
294 motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
295 .to_point(&map);
296 let mut new_text = " ".repeat(indent as usize);
297 new_text.push('\n');
298 (start_of_line..start_of_line, new_text)
299 });
300 editor.edit_with_autoindent(edits, cx);
301 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
302 s.move_cursors_with(|map, cursor, _| {
303 let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
304 let insert_point = motion::end_of_line(map, false, previous_line);
305 (insert_point, SelectionGoal::None)
306 });
307 });
308 });
309 });
310 });
311}
312
313fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
314 Vim::update(cx, |vim, cx| {
315 vim.start_recording(cx);
316 vim.switch_mode(Mode::Insert, false, cx);
317 vim.update_active_editor(cx, |_, editor, cx| {
318 let text_layout_details = editor.text_layout_details(cx);
319 editor.transact(cx, |editor, cx| {
320 let (map, old_selections) = editor.selections.all_display(cx);
321
322 let selection_end_rows: HashSet<u32> = old_selections
323 .into_iter()
324 .map(|selection| selection.end.row())
325 .collect();
326 let edits = selection_end_rows.into_iter().map(|row| {
327 let (indent, _) = map.line_indent(row);
328 let end_of_line =
329 motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
330
331 let mut new_text = "\n".to_string();
332 new_text.push_str(&" ".repeat(indent as usize));
333 (end_of_line..end_of_line, new_text)
334 });
335 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
336 s.maybe_move_cursors_with(|map, cursor, goal| {
337 Motion::CurrentLine.move_point(
338 map,
339 cursor,
340 goal,
341 None,
342 &text_layout_details,
343 )
344 });
345 });
346 editor.edit_with_autoindent(edits, cx);
347 });
348 });
349 });
350}
351
352fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
353 Vim::update(cx, |vim, cx| {
354 let count = vim.take_count(cx);
355 yank_motion(vim, motion::Motion::CurrentLine, count, cx)
356 })
357}
358
359pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
360 Vim::update(cx, |vim, cx| {
361 vim.stop_recording();
362 vim.update_active_editor(cx, |_, editor, cx| {
363 editor.transact(cx, |editor, cx| {
364 editor.set_clip_at_line_ends(false, cx);
365 let (map, display_selections) = editor.selections.all_display(cx);
366 // Selections are biased right at the start. So we need to store
367 // anchors that are biased left so that we can restore the selections
368 // after the change
369 let stable_anchors = editor
370 .selections
371 .disjoint_anchors()
372 .into_iter()
373 .map(|selection| {
374 let start = selection.start.bias_left(&map.buffer_snapshot);
375 start..start
376 })
377 .collect::<Vec<_>>();
378
379 let edits = display_selections
380 .into_iter()
381 .map(|selection| {
382 let mut range = selection.range();
383 *range.end.column_mut() += 1;
384 range.end = map.clip_point(range.end, Bias::Right);
385
386 (
387 range.start.to_offset(&map, Bias::Left)
388 ..range.end.to_offset(&map, Bias::Left),
389 text.clone(),
390 )
391 })
392 .collect::<Vec<_>>();
393
394 editor.buffer().update(cx, |buffer, cx| {
395 buffer.edit(edits, None, cx);
396 });
397 editor.set_clip_at_line_ends(true, cx);
398 editor.change_selections(None, cx, |s| {
399 s.select_anchor_ranges(stable_anchors);
400 });
401 });
402 });
403 vim.pop_operator(cx)
404 });
405}
406
407#[cfg(test)]
408mod test {
409 use gpui::TestAppContext;
410 use indoc::indoc;
411 use settings::SettingsStore;
412
413 use crate::{
414 state::Mode::{self},
415 test::{NeovimBackedTestContext, VimTestContext},
416 VimSettings,
417 };
418
419 #[gpui::test]
420 async fn test_h(cx: &mut gpui::TestAppContext) {
421 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
422 cx.assert_all(indoc! {"
423 ˇThe qˇuick
424 ˇbrown"
425 })
426 .await;
427 }
428
429 #[gpui::test]
430 async fn test_backspace(cx: &mut gpui::TestAppContext) {
431 let mut cx = NeovimBackedTestContext::new(cx)
432 .await
433 .binding(["backspace"]);
434 cx.assert_all(indoc! {"
435 ˇThe qˇuick
436 ˇbrown"
437 })
438 .await;
439 }
440
441 #[gpui::test]
442 async fn test_j(cx: &mut gpui::TestAppContext) {
443 let mut cx = NeovimBackedTestContext::new(cx).await;
444
445 cx.set_shared_state(indoc! {"
446 aaˇaa
447 😃😃"
448 })
449 .await;
450 cx.simulate_shared_keystrokes(["j"]).await;
451 cx.assert_shared_state(indoc! {"
452 aaaa
453 😃ˇ😃"
454 })
455 .await;
456
457 for marked_position in cx.each_marked_position(indoc! {"
458 ˇThe qˇuick broˇwn
459 ˇfox jumps"
460 }) {
461 cx.assert_neovim_compatible(&marked_position, ["j"]).await;
462 }
463 }
464
465 #[gpui::test]
466 async fn test_enter(cx: &mut gpui::TestAppContext) {
467 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
468 cx.assert_all(indoc! {"
469 ˇThe qˇuick broˇwn
470 ˇfox jumps"
471 })
472 .await;
473 }
474
475 #[gpui::test]
476 async fn test_k(cx: &mut gpui::TestAppContext) {
477 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
478 cx.assert_all(indoc! {"
479 ˇThe qˇuick
480 ˇbrown fˇox jumˇps"
481 })
482 .await;
483 }
484
485 #[gpui::test]
486 async fn test_l(cx: &mut gpui::TestAppContext) {
487 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["l"]);
488 cx.assert_all(indoc! {"
489 ˇThe qˇuicˇk
490 ˇbrowˇn"})
491 .await;
492 }
493
494 #[gpui::test]
495 async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
496 let mut cx = NeovimBackedTestContext::new(cx).await;
497 cx.assert_binding_matches_all(
498 ["$"],
499 indoc! {"
500 ˇThe qˇuicˇk
501 ˇbrowˇn"},
502 )
503 .await;
504 cx.assert_binding_matches_all(
505 ["0"],
506 indoc! {"
507 ˇThe qˇuicˇk
508 ˇbrowˇn"},
509 )
510 .await;
511 }
512
513 #[gpui::test]
514 async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
515 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-g"]);
516
517 cx.assert_all(indoc! {"
518 The ˇquick
519
520 brown fox jumps
521 overˇ the lazy doˇg"})
522 .await;
523 cx.assert(indoc! {"
524 The quiˇck
525
526 brown"})
527 .await;
528 cx.assert(indoc! {"
529 The quiˇck
530
531 "})
532 .await;
533 }
534
535 #[gpui::test]
536 async fn test_w(cx: &mut gpui::TestAppContext) {
537 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["w"]);
538 cx.assert_all(indoc! {"
539 The ˇquickˇ-ˇbrown
540 ˇ
541 ˇ
542 ˇfox_jumps ˇover
543 ˇthˇe"})
544 .await;
545 let mut cx = cx.binding(["shift-w"]);
546 cx.assert_all(indoc! {"
547 The ˇquickˇ-ˇbrown
548 ˇ
549 ˇ
550 ˇfox_jumps ˇover
551 ˇthˇe"})
552 .await;
553 }
554
555 #[gpui::test]
556 async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
557 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
558 cx.assert_all(indoc! {"
559 Thˇe quicˇkˇ-browˇn
560
561
562 fox_jumpˇs oveˇr
563 thˇe"})
564 .await;
565 let mut cx = cx.binding(["shift-e"]);
566 cx.assert_all(indoc! {"
567 Thˇe quicˇkˇ-browˇn
568
569
570 fox_jumpˇs oveˇr
571 thˇe"})
572 .await;
573 }
574
575 #[gpui::test]
576 async fn test_b(cx: &mut gpui::TestAppContext) {
577 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["b"]);
578 cx.assert_all(indoc! {"
579 ˇThe ˇquickˇ-ˇbrown
580 ˇ
581 ˇ
582 ˇfox_jumps ˇover
583 ˇthe"})
584 .await;
585 let mut cx = cx.binding(["shift-b"]);
586 cx.assert_all(indoc! {"
587 ˇThe ˇquickˇ-ˇbrown
588 ˇ
589 ˇ
590 ˇfox_jumps ˇover
591 ˇthe"})
592 .await;
593 }
594
595 #[gpui::test]
596 async fn test_gg(cx: &mut gpui::TestAppContext) {
597 let mut cx = NeovimBackedTestContext::new(cx).await;
598 cx.assert_binding_matches_all(
599 ["g", "g"],
600 indoc! {"
601 The qˇuick
602
603 brown fox jumps
604 over ˇthe laˇzy dog"},
605 )
606 .await;
607 cx.assert_binding_matches(
608 ["g", "g"],
609 indoc! {"
610
611
612 brown fox jumps
613 over the laˇzy dog"},
614 )
615 .await;
616 cx.assert_binding_matches(
617 ["2", "g", "g"],
618 indoc! {"
619 ˇ
620
621 brown fox jumps
622 over the lazydog"},
623 )
624 .await;
625 }
626
627 #[gpui::test]
628 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
629 let mut cx = NeovimBackedTestContext::new(cx).await;
630 cx.assert_binding_matches_all(
631 ["shift-g"],
632 indoc! {"
633 The qˇuick
634
635 brown fox jumps
636 over ˇthe laˇzy dog"},
637 )
638 .await;
639 cx.assert_binding_matches(
640 ["shift-g"],
641 indoc! {"
642
643
644 brown fox jumps
645 over the laˇzy dog"},
646 )
647 .await;
648 cx.assert_binding_matches(
649 ["2", "shift-g"],
650 indoc! {"
651 ˇ
652
653 brown fox jumps
654 over the lazydog"},
655 )
656 .await;
657 }
658
659 #[gpui::test]
660 async fn test_a(cx: &mut gpui::TestAppContext) {
661 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
662 cx.assert_all("The qˇuicˇk").await;
663 }
664
665 #[gpui::test]
666 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
667 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
668 cx.assert_all(indoc! {"
669 ˇ
670 The qˇuick
671 brown ˇfox "})
672 .await;
673 }
674
675 #[gpui::test]
676 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
677 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
678 cx.assert("The qˇuick").await;
679 cx.assert(" The qˇuick").await;
680 cx.assert("ˇ").await;
681 cx.assert(indoc! {"
682 The qˇuick
683 brown fox"})
684 .await;
685 cx.assert(indoc! {"
686 ˇ
687 The quick"})
688 .await;
689 // Indoc disallows trailing whitespace.
690 cx.assert(" ˇ \nThe quick").await;
691 }
692
693 #[gpui::test]
694 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
695 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
696 cx.assert("The qˇuick").await;
697 cx.assert(" The qˇuick").await;
698 cx.assert("ˇ").await;
699 cx.assert(indoc! {"
700 The qˇuick
701 brown fox"})
702 .await;
703 cx.assert(indoc! {"
704 ˇ
705 The quick"})
706 .await;
707 }
708
709 #[gpui::test]
710 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
711 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
712 cx.assert(indoc! {"
713 The qˇuick
714 brown fox"})
715 .await;
716 cx.assert(indoc! {"
717 The quick
718 ˇ
719 brown fox"})
720 .await;
721 }
722
723 #[gpui::test]
724 async fn test_x(cx: &mut gpui::TestAppContext) {
725 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
726 cx.assert_all("ˇTeˇsˇt").await;
727 cx.assert(indoc! {"
728 Tesˇt
729 test"})
730 .await;
731 }
732
733 #[gpui::test]
734 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
735 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
736 cx.assert_all("ˇTˇeˇsˇt").await;
737 cx.assert(indoc! {"
738 Test
739 ˇtest"})
740 .await;
741 }
742
743 #[gpui::test]
744 async fn test_o(cx: &mut gpui::TestAppContext) {
745 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
746 cx.assert("ˇ").await;
747 cx.assert("The ˇquick").await;
748 cx.assert_all(indoc! {"
749 The qˇuick
750 brown ˇfox
751 jumps ˇover"})
752 .await;
753 cx.assert(indoc! {"
754 The quick
755 ˇ
756 brown fox"})
757 .await;
758
759 cx.assert_manual(
760 indoc! {"
761 fn test() {
762 println!(ˇ);
763 }"},
764 Mode::Normal,
765 indoc! {"
766 fn test() {
767 println!();
768 ˇ
769 }"},
770 Mode::Insert,
771 );
772
773 cx.assert_manual(
774 indoc! {"
775 fn test(ˇ) {
776 println!();
777 }"},
778 Mode::Normal,
779 indoc! {"
780 fn test() {
781 ˇ
782 println!();
783 }"},
784 Mode::Insert,
785 );
786 }
787
788 #[gpui::test]
789 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
790 let cx = NeovimBackedTestContext::new(cx).await;
791 let mut cx = cx.binding(["shift-o"]);
792 cx.assert("ˇ").await;
793 cx.assert("The ˇquick").await;
794 cx.assert_all(indoc! {"
795 The qˇuick
796 brown ˇfox
797 jumps ˇover"})
798 .await;
799 cx.assert(indoc! {"
800 The quick
801 ˇ
802 brown fox"})
803 .await;
804
805 // Our indentation is smarter than vims. So we don't match here
806 cx.assert_manual(
807 indoc! {"
808 fn test() {
809 println!(ˇ);
810 }"},
811 Mode::Normal,
812 indoc! {"
813 fn test() {
814 ˇ
815 println!();
816 }"},
817 Mode::Insert,
818 );
819 cx.assert_manual(
820 indoc! {"
821 fn test(ˇ) {
822 println!();
823 }"},
824 Mode::Normal,
825 indoc! {"
826 ˇ
827 fn test() {
828 println!();
829 }"},
830 Mode::Insert,
831 );
832 }
833
834 #[gpui::test]
835 async fn test_dd(cx: &mut gpui::TestAppContext) {
836 let mut cx = NeovimBackedTestContext::new(cx).await;
837 cx.assert_neovim_compatible("ˇ", ["d", "d"]).await;
838 cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await;
839 for marked_text in cx.each_marked_position(indoc! {"
840 The qˇuick
841 brown ˇfox
842 jumps ˇover"})
843 {
844 cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
845 }
846 cx.assert_neovim_compatible(
847 indoc! {"
848 The quick
849 ˇ
850 brown fox"},
851 ["d", "d"],
852 )
853 .await;
854 }
855
856 #[gpui::test]
857 async fn test_cc(cx: &mut gpui::TestAppContext) {
858 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
859 cx.assert("ˇ").await;
860 cx.assert("The ˇquick").await;
861 cx.assert_all(indoc! {"
862 The quˇick
863 brown ˇfox
864 jumps ˇover"})
865 .await;
866 cx.assert(indoc! {"
867 The quick
868 ˇ
869 brown fox"})
870 .await;
871 }
872
873 #[gpui::test]
874 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
875 let mut cx = NeovimBackedTestContext::new(cx).await;
876
877 for count in 1..=5 {
878 cx.assert_binding_matches_all(
879 [&count.to_string(), "w"],
880 indoc! {"
881 ˇThe quˇickˇ browˇn
882 ˇ
883 ˇfox ˇjumpsˇ-ˇoˇver
884 ˇthe lazy dog
885 "},
886 )
887 .await;
888 }
889 }
890
891 #[gpui::test]
892 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
893 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
894 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
895 }
896
897 #[gpui::test]
898 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
899 let mut cx = NeovimBackedTestContext::new(cx).await;
900
901 for count in 1..=3 {
902 let test_case = indoc! {"
903 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
904 ˇ ˇbˇaaˇa ˇbˇbˇb
905 ˇ
906 ˇb
907 "};
908
909 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
910 .await;
911
912 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
913 .await;
914 }
915 }
916
917 #[gpui::test]
918 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
919 let mut cx = NeovimBackedTestContext::new(cx).await;
920 let test_case = indoc! {"
921 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
922 ˇ ˇbˇaaˇa ˇbˇbˇb
923 ˇ•••
924 ˇb
925 "
926 };
927
928 for count in 1..=3 {
929 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
930 .await;
931
932 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
933 .await;
934 }
935 }
936
937 #[gpui::test]
938 async fn test_f_and_t_multiline(cx: &mut gpui::TestAppContext) {
939 let mut cx = VimTestContext::new(cx, true).await;
940 cx.update_global(|store: &mut SettingsStore, cx| {
941 store.update_user_settings::<VimSettings>(cx, |s| {
942 s.use_multiline_find = Some(true);
943 });
944 });
945
946 cx.assert_binding(
947 ["f", "l"],
948 indoc! {"
949 ˇfunction print() {
950 console.log('ok')
951 }
952 "},
953 Mode::Normal,
954 indoc! {"
955 function print() {
956 consoˇle.log('ok')
957 }
958 "},
959 Mode::Normal,
960 );
961
962 cx.assert_binding(
963 ["t", "l"],
964 indoc! {"
965 ˇfunction print() {
966 console.log('ok')
967 }
968 "},
969 Mode::Normal,
970 indoc! {"
971 function print() {
972 consˇole.log('ok')
973 }
974 "},
975 Mode::Normal,
976 );
977 }
978
979 #[gpui::test]
980 async fn test_capital_f_and_capital_t_multiline(cx: &mut gpui::TestAppContext) {
981 let mut cx = VimTestContext::new(cx, true).await;
982 cx.update_global(|store: &mut SettingsStore, cx| {
983 store.update_user_settings::<VimSettings>(cx, |s| {
984 s.use_multiline_find = Some(true);
985 });
986 });
987
988 cx.assert_binding(
989 ["shift-f", "p"],
990 indoc! {"
991 function print() {
992 console.ˇlog('ok')
993 }
994 "},
995 Mode::Normal,
996 indoc! {"
997 function ˇprint() {
998 console.log('ok')
999 }
1000 "},
1001 Mode::Normal,
1002 );
1003
1004 cx.assert_binding(
1005 ["shift-t", "p"],
1006 indoc! {"
1007 function print() {
1008 console.ˇlog('ok')
1009 }
1010 "},
1011 Mode::Normal,
1012 indoc! {"
1013 function pˇrint() {
1014 console.log('ok')
1015 }
1016 "},
1017 Mode::Normal,
1018 );
1019 }
1020
1021 #[gpui::test]
1022 async fn test_percent(cx: &mut TestAppContext) {
1023 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
1024 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
1025 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
1026 .await;
1027 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
1028 }
1029}