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