1use crate::{motion::Motion, object::Object, utils::copy_selections_content, 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::{ExemptionFeatures, 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.binding(["d", "h"]);
167 cx.assert("Teˇst").await;
168 cx.assert("Tˇest").await;
169 cx.assert("ˇTest").await;
170 cx.assert(indoc! {"
171 Test
172 ˇtest"})
173 .await;
174 }
175
176 #[gpui::test]
177 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
178 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "l"]);
179 cx.assert("ˇTest").await;
180 cx.assert("Teˇst").await;
181 cx.assert("Tesˇt").await;
182 cx.assert(indoc! {"
183 Tesˇt
184 test"})
185 .await;
186 }
187
188 #[gpui::test]
189 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
190 let mut cx = NeovimBackedTestContext::new(cx).await;
191 cx.assert_neovim_compatible(
192 indoc! {"
193 Test tesˇt
194 test"},
195 ["d", "w"],
196 )
197 .await;
198
199 cx.assert_neovim_compatible("Teˇst", ["d", "w"]).await;
200 cx.assert_neovim_compatible("Tˇest test", ["d", "w"]).await;
201 cx.assert_neovim_compatible(
202 indoc! {"
203 Test teˇst
204 test"},
205 ["d", "w"],
206 )
207 .await;
208 cx.assert_neovim_compatible(
209 indoc! {"
210 Test tesˇt
211 test"},
212 ["d", "w"],
213 )
214 .await;
215
216 cx.assert_neovim_compatible(
217 indoc! {"
218 Test test
219 ˇ
220 test"},
221 ["d", "w"],
222 )
223 .await;
224
225 let mut cx = cx.binding(["d", "shift-w"]);
226 cx.assert_neovim_compatible("Test teˇst-test test", ["d", "shift-w"])
227 .await;
228 }
229
230 #[gpui::test]
231 async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
232 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
233 // cx.assert("Teˇst Test").await;
234 // cx.assert("Tˇest test").await;
235 cx.assert(indoc! {"
236 Test teˇst
237 test"})
238 .await;
239 cx.assert(indoc! {"
240 Test tesˇt
241 test"})
242 .await;
243 cx.assert_exempted(
244 indoc! {"
245 Test test
246 ˇ
247 test"},
248 ExemptionFeatures::OperatorLastNewlineRemains,
249 )
250 .await;
251
252 let mut cx = cx.binding(["d", "shift-e"]);
253 cx.assert("Test teˇst-test test").await;
254 }
255
256 #[gpui::test]
257 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
258 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "b"]);
259 cx.assert("Teˇst Test").await;
260 cx.assert("Test ˇtest").await;
261 cx.assert("Test1 test2 ˇtest3").await;
262 cx.assert(indoc! {"
263 Test test
264 ˇtest"})
265 .await;
266 cx.assert(indoc! {"
267 Test test
268 ˇ
269 test"})
270 .await;
271
272 let mut cx = cx.binding(["d", "shift-b"]);
273 cx.assert("Test test-test ˇtest").await;
274 }
275
276 #[gpui::test]
277 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
278 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "$"]);
279 cx.assert(indoc! {"
280 The qˇuick
281 brown fox"})
282 .await;
283 cx.assert(indoc! {"
284 The quick
285 ˇ
286 brown fox"})
287 .await;
288 }
289
290 #[gpui::test]
291 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
292 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "0"]);
293 cx.assert(indoc! {"
294 The qˇuick
295 brown fox"})
296 .await;
297 cx.assert(indoc! {"
298 The quick
299 ˇ
300 brown fox"})
301 .await;
302 }
303
304 #[gpui::test]
305 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
306 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "k"]);
307 cx.assert(indoc! {"
308 The quick
309 brown ˇfox
310 jumps over"})
311 .await;
312 cx.assert(indoc! {"
313 The quick
314 brown fox
315 jumps ˇover"})
316 .await;
317 cx.assert(indoc! {"
318 The qˇuick
319 brown fox
320 jumps over"})
321 .await;
322 cx.assert(indoc! {"
323 ˇbrown fox
324 jumps over"})
325 .await;
326 }
327
328 #[gpui::test]
329 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
330 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "j"]);
331 cx.assert(indoc! {"
332 The quick
333 brown ˇfox
334 jumps over"})
335 .await;
336 cx.assert(indoc! {"
337 The quick
338 brown fox
339 jumps ˇover"})
340 .await;
341 cx.assert(indoc! {"
342 The qˇuick
343 brown fox
344 jumps over"})
345 .await;
346 cx.assert(indoc! {"
347 The quick
348 brown fox
349 ˇ"})
350 .await;
351 }
352
353 #[gpui::test]
354 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
355 let mut cx = NeovimBackedTestContext::new(cx).await;
356 cx.assert_neovim_compatible(
357 indoc! {"
358 The quick
359 brownˇ fox
360 jumps over
361 the lazy"},
362 ["d", "shift-g"],
363 )
364 .await;
365 cx.assert_neovim_compatible(
366 indoc! {"
367 The quick
368 brownˇ fox
369 jumps over
370 the lazy"},
371 ["d", "shift-g"],
372 )
373 .await;
374 cx.assert_neovim_compatible(
375 indoc! {"
376 The quick
377 brown fox
378 jumps over
379 the lˇazy"},
380 ["d", "shift-g"],
381 )
382 .await;
383 cx.assert_neovim_compatible(
384 indoc! {"
385 The quick
386 brown fox
387 jumps over
388 ˇ"},
389 ["d", "shift-g"],
390 )
391 .await;
392 }
393
394 #[gpui::test]
395 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
396 let mut cx = NeovimBackedTestContext::new(cx)
397 .await
398 .binding(["d", "g", "g"]);
399 cx.assert_neovim_compatible(
400 indoc! {"
401 The quick
402 brownˇ fox
403 jumps over
404 the lazy"},
405 ["d", "g", "g"],
406 )
407 .await;
408 cx.assert_neovim_compatible(
409 indoc! {"
410 The quick
411 brown fox
412 jumps over
413 the lˇazy"},
414 ["d", "g", "g"],
415 )
416 .await;
417 cx.assert_neovim_compatible(
418 indoc! {"
419 The qˇuick
420 brown fox
421 jumps over
422 the lazy"},
423 ["d", "g", "g"],
424 )
425 .await;
426 cx.assert_neovim_compatible(
427 indoc! {"
428 ˇ
429 brown fox
430 jumps over
431 the lazy"},
432 ["d", "g", "g"],
433 )
434 .await;
435 }
436
437 #[gpui::test]
438 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
439 let mut cx = VimTestContext::new(cx, true).await;
440 cx.set_state(
441 indoc! {"
442 The quick brown
443 fox juˇmps over
444 the lazy dog"},
445 Mode::Normal,
446 );
447
448 // Canceling operator twice reverts to normal mode with no active operator
449 cx.simulate_keystrokes(["d", "escape", "k"]);
450 assert_eq!(cx.active_operator(), None);
451 assert_eq!(cx.mode(), Mode::Normal);
452 cx.assert_editor_state(indoc! {"
453 The quˇick brown
454 fox jumps over
455 the lazy dog"});
456 }
457
458 #[gpui::test]
459 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
460 let mut cx = VimTestContext::new(cx, true).await;
461 cx.set_state(
462 indoc! {"
463 The quick brown
464 fox juˇmps over
465 the lazy dog"},
466 Mode::Normal,
467 );
468
469 // Canceling operator twice reverts to normal mode with no active operator
470 cx.simulate_keystrokes(["d", "y"]);
471 assert_eq!(cx.active_operator(), None);
472 assert_eq!(cx.mode(), Mode::Normal);
473 }
474
475 #[gpui::test]
476 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
477 let mut cx = NeovimBackedTestContext::new(cx).await;
478 cx.set_shared_state(indoc! {"
479 The ˇquick brown
480 fox jumps over
481 the lazy dog"})
482 .await;
483 cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
484 cx.assert_shared_state(indoc! {"
485 the ˇlazy dog"})
486 .await;
487
488 cx.set_shared_state(indoc! {"
489 The ˇquick brown
490 fox jumps over
491 the lazy dog"})
492 .await;
493 cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
494 cx.assert_shared_state(indoc! {"
495 the ˇlazy dog"})
496 .await;
497
498 cx.set_shared_state(indoc! {"
499 The ˇquick brown
500 fox jumps over
501 the moon,
502 a star, and
503 the lazy dog"})
504 .await;
505 cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
506 cx.assert_shared_state(indoc! {"
507 the ˇlazy dog"})
508 .await;
509 }
510
511 #[gpui::test]
512 async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
513 let mut cx = NeovimBackedTestContext::new(cx).await;
514 cx.assert_neovim_compatible("ˇax", ["d", "t", "x"]).await;
515 cx.assert_neovim_compatible("aˇx", ["d", "t", "x"]).await;
516 }
517}