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