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