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