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(indoc! {"
326 The quick
327 brownˇ fox
328 jumps over
329 the lazy"}, ["d", "g", "g"])
330 .await;
331 cx.assert_neovim_compatible(indoc! {"
332 The quick
333 brown fox
334 jumps over
335 the lˇazy"}, ["d", "g", "g"])
336 .await;
337 cx.assert_neovim_compatible(
338 indoc! {"
339 The qˇuick
340 brown fox
341 jumps over
342 the lazy"},["d", "g", "g"]
343 )
344 .await;
345 cx.assert_neovim_compatible(
346 indoc! {"
347 ˇ
348 brown fox
349 jumps over
350 the lazy"},["d", "g", "g"]
351 )
352 .await;
353 }
354
355 #[gpui::test]
356 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
357 let mut cx = VimTestContext::new(cx, true).await;
358 cx.set_state(
359 indoc! {"
360 The quick brown
361 fox juˇmps over
362 the lazy dog"},
363 Mode::Normal,
364 );
365
366 // Canceling operator twice reverts to normal mode with no active operator
367 cx.simulate_keystrokes(["d", "escape", "k"]);
368 assert_eq!(cx.active_operator(), None);
369 assert_eq!(cx.mode(), Mode::Normal);
370 cx.assert_editor_state(indoc! {"
371 The quˇick brown
372 fox jumps over
373 the lazy dog"});
374 }
375
376 #[gpui::test]
377 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
378 let mut cx = VimTestContext::new(cx, true).await;
379 cx.set_state(
380 indoc! {"
381 The quick brown
382 fox juˇmps over
383 the lazy dog"},
384 Mode::Normal,
385 );
386
387 // Canceling operator twice reverts to normal mode with no active operator
388 cx.simulate_keystrokes(["d", "y"]);
389 assert_eq!(cx.active_operator(), None);
390 assert_eq!(cx.mode(), Mode::Normal);
391 }
392
393 #[gpui::test]
394 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
395 let mut cx = NeovimBackedTestContext::new(cx).await;
396 cx.set_shared_state(indoc! {"
397 The ˇquick brown
398 fox jumps over
399 the lazy dog"})
400 .await;
401 cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
402 cx.assert_shared_state(indoc! {"
403 the ˇlazy dog"})
404 .await;
405
406 cx.set_shared_state(indoc! {"
407 The ˇquick brown
408 fox jumps over
409 the lazy dog"})
410 .await;
411 cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
412 cx.assert_shared_state(indoc! {"
413 the ˇlazy dog"})
414 .await;
415
416 cx.set_shared_state(indoc! {"
417 The ˇquick brown
418 fox jumps over
419 the moon,
420 a star, and
421 the lazy dog"})
422 .await;
423 cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
424 cx.assert_shared_state(indoc! {"
425 the ˇlazy dog"})
426 .await;
427 }
428}