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