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