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