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