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