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