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