1mod case;
2mod change;
3mod delete;
4mod scroll;
5mod search;
6pub mod 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, 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::Mode::{self},
445 test::{ExemptionFeatures, NeovimBackedTestContext},
446 };
447
448 #[gpui::test]
449 async fn test_h(cx: &mut gpui::TestAppContext) {
450 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
451 cx.assert_all(indoc! {"
452 ˇThe qˇuick
453 ˇbrown"
454 })
455 .await;
456 }
457
458 #[gpui::test]
459 async fn test_backspace(cx: &mut gpui::TestAppContext) {
460 let mut cx = NeovimBackedTestContext::new(cx)
461 .await
462 .binding(["backspace"]);
463 cx.assert_all(indoc! {"
464 ˇThe qˇuick
465 ˇbrown"
466 })
467 .await;
468 }
469
470 #[gpui::test]
471 async fn test_j(cx: &mut gpui::TestAppContext) {
472 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
473 cx.assert_all(indoc! {"
474 ˇThe qˇuick broˇwn
475 ˇfox jumps"
476 })
477 .await;
478 }
479
480 #[gpui::test]
481 async fn test_enter(cx: &mut gpui::TestAppContext) {
482 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
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_gg(cx: &mut gpui::TestAppContext) {
612 let mut cx = NeovimBackedTestContext::new(cx).await;
613 cx.assert_binding_matches_all(
614 ["g", "g"],
615 indoc! {"
616 The qˇuick
617
618 brown fox jumps
619 over ˇthe laˇzy dog"},
620 )
621 .await;
622 cx.assert_binding_matches(
623 ["g", "g"],
624 indoc! {"
625
626
627 brown fox jumps
628 over the laˇzy dog"},
629 )
630 .await;
631 cx.assert_binding_matches(
632 ["2", "g", "g"],
633 indoc! {"
634 ˇ
635
636 brown fox jumps
637 over the lazydog"},
638 )
639 .await;
640 }
641
642 #[gpui::test]
643 async fn test_end_of_document(cx: &mut gpui::TestAppContext) {
644 let mut cx = NeovimBackedTestContext::new(cx).await;
645 cx.assert_binding_matches_all(
646 ["shift-g"],
647 indoc! {"
648 The qˇuick
649
650 brown fox jumps
651 over ˇthe laˇzy dog"},
652 )
653 .await;
654 cx.assert_binding_matches(
655 ["shift-g"],
656 indoc! {"
657
658
659 brown fox jumps
660 over the laˇzy dog"},
661 )
662 .await;
663 cx.assert_binding_matches(
664 ["2", "shift-g"],
665 indoc! {"
666 ˇ
667
668 brown fox jumps
669 over the lazydog"},
670 )
671 .await;
672 }
673
674 #[gpui::test]
675 async fn test_a(cx: &mut gpui::TestAppContext) {
676 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["a"]);
677 cx.assert_all("The qˇuicˇk").await;
678 }
679
680 #[gpui::test]
681 async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
682 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-a"]);
683 cx.assert_all(indoc! {"
684 ˇ
685 The qˇuick
686 brown ˇfox "})
687 .await;
688 }
689
690 #[gpui::test]
691 async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
692 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["^"]);
693 cx.assert("The qˇuick").await;
694 cx.assert(" The qˇuick").await;
695 cx.assert("ˇ").await;
696 cx.assert(indoc! {"
697 The qˇuick
698 brown fox"})
699 .await;
700 cx.assert(indoc! {"
701 ˇ
702 The quick"})
703 .await;
704 // Indoc disallows trailing whitespace.
705 cx.assert(" ˇ \nThe quick").await;
706 }
707
708 #[gpui::test]
709 async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
710 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-i"]);
711 cx.assert("The qˇuick").await;
712 cx.assert(" The qˇuick").await;
713 cx.assert("ˇ").await;
714 cx.assert(indoc! {"
715 The qˇuick
716 brown fox"})
717 .await;
718 cx.assert(indoc! {"
719 ˇ
720 The quick"})
721 .await;
722 }
723
724 #[gpui::test]
725 async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
726 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-d"]);
727 cx.assert(indoc! {"
728 The qˇuick
729 brown fox"})
730 .await;
731 cx.assert(indoc! {"
732 The quick
733 ˇ
734 brown fox"})
735 .await;
736 }
737
738 #[gpui::test]
739 async fn test_x(cx: &mut gpui::TestAppContext) {
740 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["x"]);
741 cx.assert_all("ˇTeˇsˇt").await;
742 cx.assert(indoc! {"
743 Tesˇt
744 test"})
745 .await;
746 }
747
748 #[gpui::test]
749 async fn test_delete_left(cx: &mut gpui::TestAppContext) {
750 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["shift-x"]);
751 cx.assert_all("ˇTˇeˇsˇt").await;
752 cx.assert(indoc! {"
753 Test
754 ˇtest"})
755 .await;
756 }
757
758 #[gpui::test]
759 async fn test_o(cx: &mut gpui::TestAppContext) {
760 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["o"]);
761 cx.assert("ˇ").await;
762 cx.assert("The ˇquick").await;
763 cx.assert_all(indoc! {"
764 The qˇuick
765 brown ˇfox
766 jumps ˇover"})
767 .await;
768 cx.assert(indoc! {"
769 The quick
770 ˇ
771 brown fox"})
772 .await;
773
774 cx.assert_manual(
775 indoc! {"
776 fn test() {
777 println!(ˇ);
778 }"},
779 Mode::Normal,
780 indoc! {"
781 fn test() {
782 println!();
783 ˇ
784 }"},
785 Mode::Insert,
786 );
787
788 cx.assert_manual(
789 indoc! {"
790 fn test(ˇ) {
791 println!();
792 }"},
793 Mode::Normal,
794 indoc! {"
795 fn test() {
796 ˇ
797 println!();
798 }"},
799 Mode::Insert,
800 );
801 }
802
803 #[gpui::test]
804 async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
805 let cx = NeovimBackedTestContext::new(cx).await;
806 let mut cx = cx.binding(["shift-o"]);
807 cx.assert("ˇ").await;
808 cx.assert("The ˇquick").await;
809 cx.assert_all(indoc! {"
810 The qˇuick
811 brown ˇfox
812 jumps ˇover"})
813 .await;
814 cx.assert(indoc! {"
815 The quick
816 ˇ
817 brown fox"})
818 .await;
819
820 // Our indentation is smarter than vims. So we don't match here
821 cx.assert_manual(
822 indoc! {"
823 fn test() {
824 println!(ˇ);
825 }"},
826 Mode::Normal,
827 indoc! {"
828 fn test() {
829 ˇ
830 println!();
831 }"},
832 Mode::Insert,
833 );
834 cx.assert_manual(
835 indoc! {"
836 fn test(ˇ) {
837 println!();
838 }"},
839 Mode::Normal,
840 indoc! {"
841 ˇ
842 fn test() {
843 println!();
844 }"},
845 Mode::Insert,
846 );
847 }
848
849 #[gpui::test]
850 async fn test_dd(cx: &mut gpui::TestAppContext) {
851 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
852 cx.assert("ˇ").await;
853 cx.assert("The ˇquick").await;
854 cx.assert_all(indoc! {"
855 The qˇuick
856 brown ˇfox
857 jumps ˇover"})
858 .await;
859 cx.assert_exempted(
860 indoc! {"
861 The quick
862 ˇ
863 brown fox"},
864 ExemptionFeatures::DeletionOnEmptyLine,
865 )
866 .await;
867 }
868
869 #[gpui::test]
870 async fn test_cc(cx: &mut gpui::TestAppContext) {
871 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "c"]);
872 cx.assert("ˇ").await;
873 cx.assert("The ˇquick").await;
874 cx.assert_all(indoc! {"
875 The quˇick
876 brown ˇfox
877 jumps ˇover"})
878 .await;
879 cx.assert(indoc! {"
880 The quick
881 ˇ
882 brown fox"})
883 .await;
884 }
885
886 #[gpui::test]
887 async fn test_p(cx: &mut gpui::TestAppContext) {
888 let mut cx = NeovimBackedTestContext::new(cx).await;
889 cx.set_shared_state(indoc! {"
890 The quick brown
891 fox juˇmps over
892 the lazy dog"})
893 .await;
894
895 cx.simulate_shared_keystrokes(["d", "d"]).await;
896 cx.assert_state_matches().await;
897
898 cx.simulate_shared_keystroke("p").await;
899 cx.assert_state_matches().await;
900
901 cx.set_shared_state(indoc! {"
902 The quick brown
903 fox ˇjumps over
904 the lazy dog"})
905 .await;
906 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
907 cx.set_shared_state(indoc! {"
908 The quick brown
909 fox jumps oveˇr
910 the lazy dog"})
911 .await;
912 cx.simulate_shared_keystroke("p").await;
913 cx.assert_state_matches().await;
914 }
915
916 #[gpui::test]
917 async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
918 let mut cx = NeovimBackedTestContext::new(cx).await;
919
920 for count in 1..=5 {
921 cx.assert_binding_matches_all(
922 [&count.to_string(), "w"],
923 indoc! {"
924 ˇThe quˇickˇ browˇn
925 ˇ
926 ˇfox ˇjumpsˇ-ˇoˇver
927 ˇthe lazy dog
928 "},
929 )
930 .await;
931 }
932 }
933
934 #[gpui::test]
935 async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
936 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
937 cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
938 }
939
940 #[gpui::test]
941 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
942 let mut cx = NeovimBackedTestContext::new(cx).await;
943 for count in 1..=3 {
944 let test_case = indoc! {"
945 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
946 ˇ ˇbˇaaˇa ˇbˇbˇb
947 ˇ
948 ˇb
949 "};
950
951 cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
952 .await;
953
954 cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
955 .await;
956 }
957 }
958
959 #[gpui::test]
960 async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
961 let mut cx = NeovimBackedTestContext::new(cx).await;
962 let test_case = indoc! {"
963 ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
964 ˇ ˇbˇaaˇa ˇbˇbˇb
965 ˇ•••
966 ˇb
967 "
968 };
969
970 for count in 1..=3 {
971 cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
972 .await;
973
974 cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
975 .await;
976 }
977 }
978
979 #[gpui::test]
980 async fn test_percent(cx: &mut TestAppContext) {
981 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
982 cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
983 cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
984 .await;
985 cx.assert_all("let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;").await;
986 }
987}