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