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