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