1use crate::{
2 Vim,
3 motion::{Motion, MotionKind},
4 object::Object,
5};
6use collections::{HashMap, HashSet};
7use editor::{
8 Bias, DisplayPoint,
9 display_map::{DisplaySnapshot, ToDisplayPoint},
10 scroll::Autoscroll,
11};
12use gpui::{Context, Window};
13use language::{Point, Selection};
14use multi_buffer::MultiBufferRow;
15
16impl Vim {
17 pub fn delete_motion(
18 &mut self,
19 motion: Motion,
20 times: Option<usize>,
21 forced_motion: bool,
22 window: &mut Window,
23 cx: &mut Context<Self>,
24 ) {
25 self.stop_recording(cx);
26 self.update_editor(window, cx, |vim, editor, window, cx| {
27 let text_layout_details = editor.text_layout_details(window);
28 editor.transact(window, cx, |editor, window, cx| {
29 editor.set_clip_at_line_ends(false, cx);
30 let mut original_columns: HashMap<_, _> = Default::default();
31 let mut motion_kind = None;
32 let mut ranges_to_copy = Vec::new();
33 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
34 s.move_with(|map, selection| {
35 let original_head = selection.head();
36 original_columns.insert(selection.id, original_head.column());
37 let kind = motion.expand_selection(
38 map,
39 selection,
40 times,
41 &text_layout_details,
42 forced_motion,
43 );
44 ranges_to_copy
45 .push(selection.start.to_point(map)..selection.end.to_point(map));
46
47 // When deleting line-wise, we always want to delete a newline.
48 // If there is one after the current line, it goes; otherwise we
49 // pick the one before.
50 if kind == Some(MotionKind::Linewise) {
51 let start = selection.start.to_point(map);
52 let end = selection.end.to_point(map);
53 if end.row < map.buffer_snapshot.max_point().row {
54 selection.end = Point::new(end.row + 1, 0).to_display_point(map)
55 } else if start.row > 0 {
56 selection.start = Point::new(
57 start.row - 1,
58 map.buffer_snapshot.line_len(MultiBufferRow(start.row - 1)),
59 )
60 .to_display_point(map)
61 }
62 }
63 if let Some(kind) = kind {
64 motion_kind.get_or_insert(kind);
65 }
66 });
67 });
68 let Some(kind) = motion_kind else { return };
69 vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx);
70 editor.insert("", window, cx);
71
72 // Fixup cursor position after the deletion
73 editor.set_clip_at_line_ends(true, cx);
74 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
75 s.move_with(|map, selection| {
76 let mut cursor = selection.head();
77 if kind.linewise() {
78 if let Some(column) = original_columns.get(&selection.id) {
79 *cursor.column_mut() = *column
80 }
81 }
82 cursor = map.clip_point(cursor, Bias::Left);
83 selection.collapse_to(cursor, selection.goal)
84 });
85 });
86 editor.refresh_inline_completion(true, false, window, cx);
87 });
88 });
89 }
90
91 pub fn delete_object(
92 &mut self,
93 object: Object,
94 around: bool,
95 window: &mut Window,
96 cx: &mut Context<Self>,
97 ) {
98 self.stop_recording(cx);
99 self.update_editor(window, cx, |vim, editor, window, cx| {
100 editor.transact(window, cx, |editor, window, cx| {
101 editor.set_clip_at_line_ends(false, cx);
102 // Emulates behavior in vim where if we expanded backwards to include a newline
103 // the cursor gets set back to the start of the line
104 let mut should_move_to_start: HashSet<_> = Default::default();
105 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
106 s.move_with(|map, selection| {
107 object.expand_selection(map, selection, around);
108 let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
109 let mut move_selection_start_to_previous_line =
110 |map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
111 let start = selection.start.to_offset(map, Bias::Left);
112 if selection.start.row().0 > 0 {
113 should_move_to_start.insert(selection.id);
114 selection.start =
115 (start - '\n'.len_utf8()).to_display_point(map);
116 }
117 };
118 let range = selection.start.to_offset(map, Bias::Left)
119 ..selection.end.to_offset(map, Bias::Right);
120 let contains_only_newlines = map
121 .buffer_chars_at(range.start)
122 .take_while(|(_, p)| p < &range.end)
123 .all(|(char, _)| char == '\n')
124 && !offset_range.is_empty();
125 let end_at_newline = map
126 .buffer_chars_at(range.end)
127 .next()
128 .map(|(c, _)| c == '\n')
129 .unwrap_or(false);
130
131 // If expanded range contains only newlines and
132 // the object is around or sentence, expand to include a newline
133 // at the end or start
134 if (around || object == Object::Sentence) && contains_only_newlines {
135 if end_at_newline {
136 move_selection_end_to_next_line(map, selection);
137 } else {
138 move_selection_start_to_previous_line(map, selection);
139 }
140 }
141
142 // Does post-processing for the trailing newline and EOF
143 // when not cancelled.
144 let cancelled = around && selection.start == selection.end;
145 if object == Object::Paragraph && !cancelled {
146 // EOF check should be done before including a trailing newline.
147 if ends_at_eof(map, selection) {
148 move_selection_start_to_previous_line(map, selection);
149 }
150
151 if end_at_newline {
152 move_selection_end_to_next_line(map, selection);
153 }
154 }
155 });
156 });
157 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
158 editor.insert("", window, cx);
159
160 // Fixup cursor position after the deletion
161 editor.set_clip_at_line_ends(true, cx);
162 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
163 s.move_with(|map, selection| {
164 let mut cursor = selection.head();
165 if should_move_to_start.contains(&selection.id) {
166 *cursor.column_mut() = 0;
167 }
168 cursor = map.clip_point(cursor, Bias::Left);
169 selection.collapse_to(cursor, selection.goal)
170 });
171 });
172 editor.refresh_inline_completion(true, false, window, cx);
173 });
174 });
175 }
176}
177
178fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
179 let end = selection.end.to_offset(map, Bias::Left);
180 selection.end = (end + '\n'.len_utf8()).to_display_point(map);
181}
182
183fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) -> bool {
184 selection.end.to_point(map) == map.buffer_snapshot.max_point()
185}
186
187#[cfg(test)]
188mod test {
189 use indoc::indoc;
190
191 use crate::{
192 state::Mode,
193 test::{NeovimBackedTestContext, VimTestContext},
194 };
195
196 #[gpui::test]
197 async fn test_delete_h(cx: &mut gpui::TestAppContext) {
198 let mut cx = NeovimBackedTestContext::new(cx).await;
199 cx.simulate("d h", "Teˇst").await.assert_matches();
200 cx.simulate("d h", "Tˇest").await.assert_matches();
201 cx.simulate("d h", "ˇTest").await.assert_matches();
202 cx.simulate(
203 "d h",
204 indoc! {"
205 Test
206 ˇtest"},
207 )
208 .await
209 .assert_matches();
210 }
211
212 #[gpui::test]
213 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
214 let mut cx = NeovimBackedTestContext::new(cx).await;
215 cx.simulate("d l", "ˇTest").await.assert_matches();
216 cx.simulate("d l", "Teˇst").await.assert_matches();
217 cx.simulate("d l", "Tesˇt").await.assert_matches();
218 cx.simulate(
219 "d l",
220 indoc! {"
221 Tesˇt
222 test"},
223 )
224 .await
225 .assert_matches();
226 }
227
228 #[gpui::test]
229 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
230 let mut cx = NeovimBackedTestContext::new(cx).await;
231 cx.simulate(
232 "d w",
233 indoc! {"
234 Test tesˇt
235 test"},
236 )
237 .await
238 .assert_matches();
239
240 cx.simulate("d w", "Teˇst").await.assert_matches();
241 cx.simulate("d w", "Tˇest test").await.assert_matches();
242 cx.simulate(
243 "d w",
244 indoc! {"
245 Test teˇst
246 test"},
247 )
248 .await
249 .assert_matches();
250 cx.simulate(
251 "d w",
252 indoc! {"
253 Test tesˇt
254 test"},
255 )
256 .await
257 .assert_matches();
258
259 cx.simulate(
260 "d w",
261 indoc! {"
262 Test test
263 ˇ
264 test"},
265 )
266 .await
267 .assert_matches();
268
269 cx.simulate("d shift-w", "Test teˇst-test test")
270 .await
271 .assert_matches();
272 }
273
274 #[gpui::test]
275 async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
276 let mut cx = NeovimBackedTestContext::new(cx).await;
277 cx.simulate("d e", "Teˇst Test\n").await.assert_matches();
278 cx.simulate("d e", "Tˇest test\n").await.assert_matches();
279 cx.simulate(
280 "d e",
281 indoc! {"
282 Test teˇst
283 test"},
284 )
285 .await
286 .assert_matches();
287 cx.simulate(
288 "d e",
289 indoc! {"
290 Test tesˇt
291 test"},
292 )
293 .await
294 .assert_matches();
295
296 cx.simulate("d e", "Test teˇst-test test")
297 .await
298 .assert_matches();
299 }
300
301 #[gpui::test]
302 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
303 let mut cx = NeovimBackedTestContext::new(cx).await;
304 cx.simulate("d b", "Teˇst Test").await.assert_matches();
305 cx.simulate("d b", "Test ˇtest").await.assert_matches();
306 cx.simulate("d b", "Test1 test2 ˇtest3")
307 .await
308 .assert_matches();
309 cx.simulate(
310 "d b",
311 indoc! {"
312 Test test
313 ˇtest"},
314 )
315 .await
316 .assert_matches();
317 cx.simulate(
318 "d b",
319 indoc! {"
320 Test test
321 ˇ
322 test"},
323 )
324 .await
325 .assert_matches();
326
327 cx.simulate("d shift-b", "Test test-test ˇtest")
328 .await
329 .assert_matches();
330 }
331
332 #[gpui::test]
333 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
334 let mut cx = NeovimBackedTestContext::new(cx).await;
335 cx.simulate(
336 "d $",
337 indoc! {"
338 The qˇuick
339 brown fox"},
340 )
341 .await
342 .assert_matches();
343 cx.simulate(
344 "d $",
345 indoc! {"
346 The quick
347 ˇ
348 brown fox"},
349 )
350 .await
351 .assert_matches();
352 }
353
354 #[gpui::test]
355 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
356 let mut cx = NeovimBackedTestContext::new(cx).await;
357 cx.simulate(
358 "d 0",
359 indoc! {"
360 The qˇuick
361 brown fox"},
362 )
363 .await
364 .assert_matches();
365 cx.simulate(
366 "d 0",
367 indoc! {"
368 The quick
369 ˇ
370 brown fox"},
371 )
372 .await
373 .assert_matches();
374 }
375
376 #[gpui::test]
377 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
378 let mut cx = NeovimBackedTestContext::new(cx).await;
379 cx.simulate(
380 "d k",
381 indoc! {"
382 The quick
383 brown ˇfox
384 jumps over"},
385 )
386 .await
387 .assert_matches();
388 cx.simulate(
389 "d k",
390 indoc! {"
391 The quick
392 brown fox
393 jumps ˇover"},
394 )
395 .await
396 .assert_matches();
397 cx.simulate(
398 "d k",
399 indoc! {"
400 The qˇuick
401 brown fox
402 jumps over"},
403 )
404 .await
405 .assert_matches();
406 cx.simulate(
407 "d k",
408 indoc! {"
409 ˇbrown fox
410 jumps over"},
411 )
412 .await
413 .assert_matches();
414 }
415
416 #[gpui::test]
417 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
418 let mut cx = NeovimBackedTestContext::new(cx).await;
419 cx.simulate(
420 "d j",
421 indoc! {"
422 The quick
423 brown ˇfox
424 jumps over"},
425 )
426 .await
427 .assert_matches();
428 cx.simulate(
429 "d j",
430 indoc! {"
431 The quick
432 brown fox
433 jumps ˇover"},
434 )
435 .await
436 .assert_matches();
437 cx.simulate(
438 "d j",
439 indoc! {"
440 The qˇuick
441 brown fox
442 jumps over"},
443 )
444 .await
445 .assert_matches();
446 cx.simulate(
447 "d j",
448 indoc! {"
449 The quick
450 brown fox
451 ˇ"},
452 )
453 .await
454 .assert_matches();
455 }
456
457 #[gpui::test]
458 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
459 let mut cx = NeovimBackedTestContext::new(cx).await;
460 cx.simulate(
461 "d shift-g",
462 indoc! {"
463 The quick
464 brownˇ fox
465 jumps over
466 the lazy"},
467 )
468 .await
469 .assert_matches();
470 cx.simulate(
471 "d shift-g",
472 indoc! {"
473 The quick
474 brownˇ fox
475 jumps over
476 the lazy"},
477 )
478 .await
479 .assert_matches();
480 cx.simulate(
481 "d shift-g",
482 indoc! {"
483 The quick
484 brown fox
485 jumps over
486 the lˇazy"},
487 )
488 .await
489 .assert_matches();
490 cx.simulate(
491 "d shift-g",
492 indoc! {"
493 The quick
494 brown fox
495 jumps over
496 ˇ"},
497 )
498 .await
499 .assert_matches();
500 }
501
502 #[gpui::test]
503 async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
504 let mut cx = NeovimBackedTestContext::new(cx).await;
505 cx.simulate(
506 "d 3 shift-g",
507 indoc! {"
508 The quick
509 brownˇ fox
510 jumps over
511 the lazy"},
512 )
513 .await
514 .assert_matches();
515 cx.simulate(
516 "d 3 shift-g",
517 indoc! {"
518 The quick
519 brown fox
520 jumps over
521 the lˇazy"},
522 )
523 .await
524 .assert_matches();
525 cx.simulate(
526 "d 2 shift-g",
527 indoc! {"
528 The quick
529 brown fox
530 jumps over
531 ˇ"},
532 )
533 .await
534 .assert_matches();
535 }
536
537 #[gpui::test]
538 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
539 let mut cx = NeovimBackedTestContext::new(cx).await;
540 cx.simulate(
541 "d g g",
542 indoc! {"
543 The quick
544 brownˇ fox
545 jumps over
546 the lazy"},
547 )
548 .await
549 .assert_matches();
550 cx.simulate(
551 "d g g",
552 indoc! {"
553 The quick
554 brown fox
555 jumps over
556 the lˇazy"},
557 )
558 .await
559 .assert_matches();
560 cx.simulate(
561 "d g g",
562 indoc! {"
563 The qˇuick
564 brown fox
565 jumps over
566 the lazy"},
567 )
568 .await
569 .assert_matches();
570 cx.simulate(
571 "d g g",
572 indoc! {"
573 ˇ
574 brown fox
575 jumps over
576 the lazy"},
577 )
578 .await
579 .assert_matches();
580 }
581
582 #[gpui::test]
583 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
584 let mut cx = VimTestContext::new(cx, true).await;
585 cx.set_state(
586 indoc! {"
587 The quick brown
588 fox juˇmps over
589 the lazy dog"},
590 Mode::Normal,
591 );
592
593 // Canceling operator twice reverts to normal mode with no active operator
594 cx.simulate_keystrokes("d escape k");
595 assert_eq!(cx.active_operator(), None);
596 assert_eq!(cx.mode(), Mode::Normal);
597 cx.assert_editor_state(indoc! {"
598 The quˇick brown
599 fox jumps over
600 the lazy dog"});
601 }
602
603 #[gpui::test]
604 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
605 let mut cx = VimTestContext::new(cx, true).await;
606 cx.set_state(
607 indoc! {"
608 The quick brown
609 fox juˇmps over
610 the lazy dog"},
611 Mode::Normal,
612 );
613
614 // Canceling operator twice reverts to normal mode with no active operator
615 cx.simulate_keystrokes("d y");
616 assert_eq!(cx.active_operator(), None);
617 assert_eq!(cx.mode(), Mode::Normal);
618 }
619
620 #[gpui::test]
621 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
622 let mut cx = NeovimBackedTestContext::new(cx).await;
623 cx.set_shared_state(indoc! {"
624 The ˇquick brown
625 fox jumps over
626 the lazy dog"})
627 .await;
628 cx.simulate_shared_keystrokes("d 2 d").await;
629 cx.shared_state().await.assert_eq(indoc! {"
630 the ˇlazy dog"});
631
632 cx.set_shared_state(indoc! {"
633 The ˇquick brown
634 fox jumps over
635 the lazy dog"})
636 .await;
637 cx.simulate_shared_keystrokes("2 d d").await;
638 cx.shared_state().await.assert_eq(indoc! {"
639 the ˇlazy dog"});
640
641 cx.set_shared_state(indoc! {"
642 The ˇquick brown
643 fox jumps over
644 the moon,
645 a star, and
646 the lazy dog"})
647 .await;
648 cx.simulate_shared_keystrokes("2 d 2 d").await;
649 cx.shared_state().await.assert_eq(indoc! {"
650 the ˇlazy dog"});
651 }
652
653 #[gpui::test]
654 async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
655 let mut cx = NeovimBackedTestContext::new(cx).await;
656 cx.simulate("d t x", "ˇax").await.assert_matches();
657 cx.simulate("d t x", "aˇx").await.assert_matches();
658 }
659
660 #[gpui::test]
661 async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
662 let mut cx = NeovimBackedTestContext::new(cx).await;
663 // cx.simulate(
664 // "d )",
665 // indoc! {"
666 // Fiˇrst. Second. Third.
667 // Fourth.
668 // "},
669 // )
670 // .await
671 // .assert_matches();
672
673 // cx.simulate(
674 // "d )",
675 // indoc! {"
676 // First. Secˇond. Third.
677 // Fourth.
678 // "},
679 // )
680 // .await
681 // .assert_matches();
682
683 // // Two deletes
684 // cx.simulate(
685 // "d ) d )",
686 // indoc! {"
687 // First. Second. Thirˇd.
688 // Fourth.
689 // "},
690 // )
691 // .await
692 // .assert_matches();
693
694 // Should delete whole line if done on first column
695 cx.simulate(
696 "d )",
697 indoc! {"
698 ˇFirst.
699 Fourth.
700 "},
701 )
702 .await
703 .assert_matches();
704
705 // Backwards it should also delete the whole first line
706 cx.simulate(
707 "d (",
708 indoc! {"
709 First.
710 ˇSecond.
711 Fourth.
712 "},
713 )
714 .await
715 .assert_matches();
716 }
717}