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