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