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).await;
282 cx.assert_neovim_compatible(
283 indoc! {"
284 The quick
285 brownˇ fox
286 jumps over
287 the lazy"},
288 ["d", "shift-g"],
289 )
290 .await;
291 cx.assert_neovim_compatible(
292 indoc! {"
293 The quick
294 brownˇ fox
295 jumps over
296 the lazy"},
297 ["d", "shift-g"],
298 )
299 .await;
300 cx.assert_neovim_compatible(
301 indoc! {"
302 The quick
303 brown fox
304 jumps over
305 the lˇazy"},
306 ["d", "shift-g"],
307 )
308 .await;
309 cx.assert_neovim_compatible(
310 indoc! {"
311 The quick
312 brown fox
313 jumps over
314 ˇ"},
315 ["d", "shift-g"],
316 )
317 .await;
318 }
319
320 #[gpui::test]
321 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
322 let mut cx = NeovimBackedTestContext::new(cx)
323 .await
324 .binding(["d", "g", "g"]);
325 cx.assert_neovim_compatible(
326 indoc! {"
327 The quick
328 brownˇ fox
329 jumps over
330 the lazy"},
331 ["d", "g", "g"],
332 )
333 .await;
334 cx.assert_neovim_compatible(
335 indoc! {"
336 The quick
337 brown fox
338 jumps over
339 the lˇazy"},
340 ["d", "g", "g"],
341 )
342 .await;
343 cx.assert_neovim_compatible(
344 indoc! {"
345 The qˇuick
346 brown fox
347 jumps over
348 the lazy"},
349 ["d", "g", "g"],
350 )
351 .await;
352 cx.assert_neovim_compatible(
353 indoc! {"
354 ˇ
355 brown fox
356 jumps over
357 the lazy"},
358 ["d", "g", "g"],
359 )
360 .await;
361 }
362
363 #[gpui::test]
364 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
365 let mut cx = VimTestContext::new(cx, true).await;
366 cx.set_state(
367 indoc! {"
368 The quick brown
369 fox juˇmps over
370 the lazy dog"},
371 Mode::Normal,
372 );
373
374 // Canceling operator twice reverts to normal mode with no active operator
375 cx.simulate_keystrokes(["d", "escape", "k"]);
376 assert_eq!(cx.active_operator(), None);
377 assert_eq!(cx.mode(), Mode::Normal);
378 cx.assert_editor_state(indoc! {"
379 The quˇick brown
380 fox jumps over
381 the lazy dog"});
382 }
383
384 #[gpui::test]
385 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
386 let mut cx = VimTestContext::new(cx, true).await;
387 cx.set_state(
388 indoc! {"
389 The quick brown
390 fox juˇmps over
391 the lazy dog"},
392 Mode::Normal,
393 );
394
395 // Canceling operator twice reverts to normal mode with no active operator
396 cx.simulate_keystrokes(["d", "y"]);
397 assert_eq!(cx.active_operator(), None);
398 assert_eq!(cx.mode(), Mode::Normal);
399 }
400
401 #[gpui::test]
402 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
403 let mut cx = NeovimBackedTestContext::new(cx).await;
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(["d", "2", "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 lazy dog"})
418 .await;
419 cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
420 cx.assert_shared_state(indoc! {"
421 the ˇlazy dog"})
422 .await;
423
424 cx.set_shared_state(indoc! {"
425 The ˇquick brown
426 fox jumps over
427 the moon,
428 a star, and
429 the lazy dog"})
430 .await;
431 cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
432 cx.assert_shared_state(indoc! {"
433 the ˇlazy dog"})
434 .await;
435 }
436}