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