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