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