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