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.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 WindowContext) {
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::{
100 state::Mode,
101 test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
102 };
103
104 #[gpui::test]
105 async fn test_delete_h(cx: &mut gpui::TestAppContext) {
106 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "h"]);
107 cx.assert("Teˇst").await;
108 cx.assert("Tˇest").await;
109 cx.assert("ˇTest").await;
110 cx.assert(indoc! {"
111 Test
112 ˇtest"})
113 .await;
114 }
115
116 #[gpui::test]
117 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
118 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "l"]);
119 cx.assert("ˇTest").await;
120 cx.assert("Teˇst").await;
121 cx.assert("Tesˇt").await;
122 cx.assert(indoc! {"
123 Tesˇt
124 test"})
125 .await;
126 }
127
128 #[gpui::test]
129 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
130 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "w"]);
131 cx.assert("Teˇst").await;
132 cx.assert("Tˇest test").await;
133 cx.assert(indoc! {"
134 Test teˇst
135 test"})
136 .await;
137 cx.assert(indoc! {"
138 Test tesˇt
139 test"})
140 .await;
141 cx.assert_exempted(
142 indoc! {"
143 Test test
144 ˇ
145 test"},
146 ExemptionFeatures::DeleteWordOnEmptyLine,
147 )
148 .await;
149
150 let mut cx = cx.binding(["d", "shift-w"]);
151 cx.assert("Test teˇst-test test").await;
152 }
153
154 #[gpui::test]
155 async fn test_delete_e(cx: &mut gpui::TestAppContext) {
156 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
157 cx.assert("Teˇst Test").await;
158 cx.assert("Tˇest test").await;
159 cx.assert(indoc! {"
160 Test teˇst
161 test"})
162 .await;
163 cx.assert(indoc! {"
164 Test tesˇt
165 test"})
166 .await;
167 cx.assert_exempted(
168 indoc! {"
169 Test test
170 ˇ
171 test"},
172 ExemptionFeatures::OperatorLastNewlineRemains,
173 )
174 .await;
175
176 let mut cx = cx.binding(["d", "shift-e"]);
177 cx.assert("Test teˇst-test test").await;
178 }
179
180 #[gpui::test]
181 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
182 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "b"]);
183 cx.assert("Teˇst Test").await;
184 cx.assert("Test ˇtest").await;
185 cx.assert("Test1 test2 ˇtest3").await;
186 cx.assert(indoc! {"
187 Test test
188 ˇtest"})
189 .await;
190 cx.assert(indoc! {"
191 Test test
192 ˇ
193 test"})
194 .await;
195
196 let mut cx = cx.binding(["d", "shift-b"]);
197 cx.assert("Test test-test ˇtest").await;
198 }
199
200 #[gpui::test]
201 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
202 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "$"]);
203 cx.assert(indoc! {"
204 The qˇuick
205 brown fox"})
206 .await;
207 cx.assert(indoc! {"
208 The quick
209 ˇ
210 brown fox"})
211 .await;
212 }
213
214 #[gpui::test]
215 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
216 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "0"]);
217 cx.assert(indoc! {"
218 The qˇuick
219 brown fox"})
220 .await;
221 cx.assert(indoc! {"
222 The quick
223 ˇ
224 brown fox"})
225 .await;
226 }
227
228 #[gpui::test]
229 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
230 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "k"]);
231 cx.assert(indoc! {"
232 The quick
233 brown ˇfox
234 jumps over"})
235 .await;
236 cx.assert(indoc! {"
237 The quick
238 brown fox
239 jumps ˇover"})
240 .await;
241 cx.assert(indoc! {"
242 The qˇuick
243 brown fox
244 jumps over"})
245 .await;
246 cx.assert(indoc! {"
247 ˇbrown fox
248 jumps over"})
249 .await;
250 }
251
252 #[gpui::test]
253 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
254 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "j"]);
255 cx.assert(indoc! {"
256 The quick
257 brown ˇfox
258 jumps over"})
259 .await;
260 cx.assert(indoc! {"
261 The quick
262 brown fox
263 jumps ˇover"})
264 .await;
265 cx.assert(indoc! {"
266 The qˇuick
267 brown fox
268 jumps over"})
269 .await;
270 cx.assert(indoc! {"
271 The quick
272 brown fox
273 ˇ"})
274 .await;
275 }
276
277 #[gpui::test]
278 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
279 let mut cx = NeovimBackedTestContext::new(cx)
280 .await
281 .binding(["d", "shift-g"]);
282 cx.assert(indoc! {"
283 The quick
284 brownˇ fox
285 jumps over
286 the lazy"})
287 .await;
288 cx.assert(indoc! {"
289 The quick
290 brownˇ fox
291 jumps over
292 the lazy"})
293 .await;
294 cx.assert_exempted(
295 indoc! {"
296 The quick
297 brown fox
298 jumps over
299 the lˇazy"},
300 ExemptionFeatures::OperatorAbortsOnFailedMotion,
301 )
302 .await;
303 cx.assert_exempted(
304 indoc! {"
305 The quick
306 brown fox
307 jumps over
308 ˇ"},
309 ExemptionFeatures::OperatorAbortsOnFailedMotion,
310 )
311 .await;
312 }
313
314 #[gpui::test]
315 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
316 let mut cx = NeovimBackedTestContext::new(cx)
317 .await
318 .binding(["d", "g", "g"]);
319 cx.assert(indoc! {"
320 The quick
321 brownˇ fox
322 jumps over
323 the lazy"})
324 .await;
325 cx.assert(indoc! {"
326 The quick
327 brown fox
328 jumps over
329 the lˇazy"})
330 .await;
331 cx.assert_exempted(
332 indoc! {"
333 The qˇuick
334 brown fox
335 jumps over
336 the lazy"},
337 ExemptionFeatures::OperatorAbortsOnFailedMotion,
338 )
339 .await;
340 cx.assert_exempted(
341 indoc! {"
342 ˇ
343 brown fox
344 jumps over
345 the lazy"},
346 ExemptionFeatures::OperatorAbortsOnFailedMotion,
347 )
348 .await;
349 }
350
351 #[gpui::test]
352 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
353 let mut cx = VimTestContext::new(cx, true).await;
354 cx.set_state(
355 indoc! {"
356 The quick brown
357 fox juˇmps over
358 the lazy dog"},
359 Mode::Normal,
360 );
361
362 // Canceling operator twice reverts to normal mode with no active operator
363 cx.simulate_keystrokes(["d", "escape", "k"]);
364 assert_eq!(cx.active_operator(), None);
365 assert_eq!(cx.mode(), Mode::Normal);
366 cx.assert_editor_state(indoc! {"
367 The quˇick brown
368 fox jumps over
369 the lazy dog"});
370 }
371
372 #[gpui::test]
373 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
374 let mut cx = VimTestContext::new(cx, true).await;
375 cx.set_state(
376 indoc! {"
377 The quick brown
378 fox juˇmps over
379 the lazy dog"},
380 Mode::Normal,
381 );
382
383 // Canceling operator twice reverts to normal mode with no active operator
384 cx.simulate_keystrokes(["d", "y"]);
385 assert_eq!(cx.active_operator(), None);
386 assert_eq!(cx.mode(), Mode::Normal);
387 }
388}