Skip to repository content776 lines · 24.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:35:31.959Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
delete.rs
1use crate::{
2 Vim,
3 motion::{Motion, MotionKind},
4 object::Object,
5 state::Mode,
6};
7use collections::{HashMap, HashSet};
8use editor::{
9 Bias, DisplayPoint, EditPredictionRequestTrigger,
10 display_map::{DisplaySnapshot, ToDisplayPoint},
11};
12use gpui::{Context, Window};
13use language::{Point, Selection};
14use multi_buffer::MultiBufferRow;
15
16impl Vim {
17 pub fn delete_motion(
18 &mut self,
19 motion: Motion,
20 times: Option<usize>,
21 forced_motion: bool,
22 window: &mut Window,
23 cx: &mut Context<Self>,
24 ) {
25 self.stop_recording(cx);
26 self.update_editor(cx, |vim, editor, cx| {
27 let text_layout_details = editor.text_layout_details(window, cx);
28 editor.transact(window, cx, |editor, window, cx| {
29 editor.set_clip_at_line_ends(false, cx);
30 let mut original_columns: HashMap<_, _> = Default::default();
31 let mut motion_kind = None;
32 let mut ranges_to_copy = Vec::new();
33 editor.change_selections(Default::default(), window, cx, |s| {
34 s.move_with(&mut |map, selection| {
35 let original_head = selection.head();
36 original_columns.insert(selection.id, original_head.column());
37 let kind = motion.expand_selection(
38 map,
39 selection,
40 times,
41 &text_layout_details,
42 forced_motion,
43 );
44 ranges_to_copy
45 .push(selection.start.to_point(map)..selection.end.to_point(map));
46
47 // When deleting line-wise, we always want to delete a newline.
48 // If there is one after the current line, it goes; otherwise we
49 // pick the one before.
50 if kind == Some(MotionKind::Linewise) {
51 let start = selection.start.to_point(map);
52 let end = selection.end.to_point(map);
53 if end.row < map.buffer_snapshot().max_point().row {
54 selection.end = Point::new(end.row + 1, 0).to_display_point(map)
55 } else if start.row > 0 {
56 selection.start = Point::new(
57 start.row - 1,
58 map.buffer_snapshot()
59 .line_len(MultiBufferRow(start.row - 1)),
60 )
61 .to_display_point(map)
62 }
63 }
64 if let Some(kind) = kind {
65 motion_kind.get_or_insert(kind);
66 }
67 });
68 });
69 let Some(kind) = motion_kind else { return };
70 vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx);
71 editor.delete_selections_with_linked_edits(window, cx);
72
73 // Fixup cursor position after the deletion
74 editor.set_clip_at_line_ends(true, cx);
75 editor.change_selections(Default::default(), window, cx, |s| {
76 s.move_with(&mut |map, selection| {
77 let mut cursor = selection.head();
78 if kind.linewise()
79 && let Some(column) = original_columns.get(&selection.id)
80 {
81 *cursor.column_mut() = *column
82 }
83 cursor = map.clip_point(cursor, Bias::Left);
84 selection.collapse_to(cursor, selection.goal)
85 });
86 });
87 editor.refresh_edit_prediction(
88 true,
89 false,
90 EditPredictionRequestTrigger::BufferEdit,
91 window,
92 cx,
93 );
94 });
95 });
96 }
97
98 pub fn delete_object(
99 &mut self,
100 object: Object,
101 around: bool,
102 times: Option<usize>,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) {
106 self.stop_recording(cx);
107 self.update_editor(cx, |vim, editor, cx| {
108 editor.transact(window, cx, |editor, window, cx| {
109 editor.set_clip_at_line_ends(false, cx);
110 // Emulates behavior in vim where if we expanded backwards to include a newline
111 // the cursor gets set back to the start of the line
112 let mut should_move_to_start: HashSet<_> = Default::default();
113
114 // Emulates behavior in vim where after deletion the cursor should try to move
115 // to the same column it was before deletion if the line is not empty or only
116 // contains whitespace
117 let mut column_before_move: HashMap<_, _> = Default::default();
118 let target_mode = object.target_visual_mode(vim.mode, around);
119
120 editor.change_selections(Default::default(), window, cx, |s| {
121 s.move_with(&mut |map, selection| {
122 let cursor_point = selection.head().to_point(map);
123 if target_mode == Mode::VisualLine {
124 column_before_move.insert(selection.id, cursor_point.column);
125 }
126
127 object.expand_selection(map, selection, around, times);
128 let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
129 let mut move_selection_start_to_previous_line =
130 |map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
131 let start = selection.start.to_offset(map, Bias::Left);
132 if selection.start.row().0 > 0 {
133 should_move_to_start.insert(selection.id);
134 selection.start =
135 (start - '\n'.len_utf8()).to_display_point(map);
136 }
137 };
138 let range = selection.start.to_offset(map, Bias::Left)
139 ..selection.end.to_offset(map, Bias::Right);
140 let contains_only_newlines = map
141 .buffer_chars_at(range.start)
142 .take_while(|(_, p)| p < &range.end)
143 .all(|(char, _)| char == '\n')
144 && !offset_range.is_empty();
145 let end_at_newline = map
146 .buffer_chars_at(range.end)
147 .next()
148 .map(|(c, _)| c == '\n')
149 .unwrap_or(false);
150
151 // If expanded range contains only newlines and
152 // the object is around or sentence, expand to include a newline
153 // at the end or start
154 if (around || object == Object::Sentence) && contains_only_newlines {
155 if end_at_newline {
156 move_selection_end_to_next_line(map, selection);
157 } else {
158 move_selection_start_to_previous_line(map, selection);
159 }
160 }
161
162 // Does post-processing for the trailing newline and EOF
163 // when not cancelled.
164 let cancelled = around && selection.start == selection.end;
165 if object == Object::Paragraph && !cancelled {
166 // EOF check should be done before including a trailing newline.
167 if ends_at_eof(map, selection) {
168 move_selection_start_to_previous_line(map, selection);
169 }
170
171 if end_at_newline {
172 move_selection_end_to_next_line(map, selection);
173 }
174 }
175 });
176 });
177 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
178 editor.delete_selections_with_linked_edits(window, cx);
179
180 // Fixup cursor position after the deletion
181 editor.set_clip_at_line_ends(true, cx);
182 editor.change_selections(Default::default(), window, cx, |s| {
183 s.move_with(&mut |map, selection| {
184 let mut cursor = selection.head();
185 if should_move_to_start.contains(&selection.id) {
186 *cursor.column_mut() = 0;
187 } else if let Some(column) = column_before_move.get(&selection.id)
188 && *column > 0
189 {
190 let mut cursor_point = cursor.to_point(map);
191 cursor_point.column = *column;
192 cursor = map
193 .buffer_snapshot()
194 .clip_point(cursor_point, Bias::Left)
195 .to_display_point(map);
196 }
197 cursor = map.clip_point(cursor, Bias::Left);
198 selection.collapse_to(cursor, selection.goal)
199 });
200 });
201 editor.refresh_edit_prediction(
202 true,
203 false,
204 EditPredictionRequestTrigger::BufferEdit,
205 window,
206 cx,
207 );
208 });
209 });
210 }
211}
212
213fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
214 let end = selection.end.to_offset(map, Bias::Left);
215 selection.end = (end + '\n'.len_utf8()).to_display_point(map);
216}
217
218fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) -> bool {
219 selection.end.to_point(map) == map.buffer_snapshot().max_point()
220}
221
222#[cfg(test)]
223mod test {
224 use indoc::indoc;
225
226 use crate::{
227 state::Mode,
228 test::{NeovimBackedTestContext, VimTestContext},
229 };
230
231 #[gpui::test]
232 async fn test_delete_h(cx: &mut gpui::TestAppContext) {
233 let mut cx = NeovimBackedTestContext::new(cx).await;
234 cx.simulate("d h", "Teˇst").await.assert_matches();
235 cx.simulate("d h", "Tˇest").await.assert_matches();
236 cx.simulate("d h", "ˇTest").await.assert_matches();
237 cx.simulate(
238 "d h",
239 indoc! {"
240 Test
241 ˇtest"},
242 )
243 .await
244 .assert_matches();
245 }
246
247 #[gpui::test]
248 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
249 let mut cx = NeovimBackedTestContext::new(cx).await;
250 cx.simulate("d l", "ˇTest").await.assert_matches();
251 cx.simulate("d l", "Teˇst").await.assert_matches();
252 cx.simulate("d l", "Tesˇt").await.assert_matches();
253 cx.simulate(
254 "d l",
255 indoc! {"
256 Tesˇt
257 test"},
258 )
259 .await
260 .assert_matches();
261 }
262
263 #[gpui::test]
264 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
265 let mut cx = NeovimBackedTestContext::new(cx).await;
266 cx.simulate(
267 "d w",
268 indoc! {"
269 Test tesˇt
270 test"},
271 )
272 .await
273 .assert_matches();
274
275 cx.simulate("d w", "Teˇst").await.assert_matches();
276 cx.simulate("d w", "Tˇest test").await.assert_matches();
277 cx.simulate(
278 "d w",
279 indoc! {"
280 Test teˇst
281 test"},
282 )
283 .await
284 .assert_matches();
285 cx.simulate(
286 "d w",
287 indoc! {"
288 Test tesˇt
289 test"},
290 )
291 .await
292 .assert_matches();
293
294 cx.simulate(
295 "d w",
296 indoc! {"
297 Test test
298 ˇ
299 test"},
300 )
301 .await
302 .assert_matches();
303
304 cx.simulate("d shift-w", "Test teˇst-test test")
305 .await
306 .assert_matches();
307 }
308
309 #[gpui::test]
310 async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
311 let mut cx = NeovimBackedTestContext::new(cx).await;
312 cx.simulate("d e", "Teˇst Test\n").await.assert_matches();
313 cx.simulate("d e", "Tˇest test\n").await.assert_matches();
314 cx.simulate(
315 "d e",
316 indoc! {"
317 Test teˇst
318 test"},
319 )
320 .await
321 .assert_matches();
322 cx.simulate(
323 "d e",
324 indoc! {"
325 Test tesˇt
326 test"},
327 )
328 .await
329 .assert_matches();
330
331 cx.simulate("d e", "Test teˇst-test test")
332 .await
333 .assert_matches();
334 }
335
336 #[gpui::test]
337 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
338 let mut cx = NeovimBackedTestContext::new(cx).await;
339 cx.simulate("d b", "Teˇst Test").await.assert_matches();
340 cx.simulate("d b", "Test ˇtest").await.assert_matches();
341 cx.simulate("d b", "Test1 test2 ˇtest3")
342 .await
343 .assert_matches();
344 cx.simulate(
345 "d b",
346 indoc! {"
347 Test test
348 ˇtest"},
349 )
350 .await
351 .assert_matches();
352 cx.simulate(
353 "d b",
354 indoc! {"
355 Test test
356 ˇ
357 test"},
358 )
359 .await
360 .assert_matches();
361
362 cx.simulate("d shift-b", "Test test-test ˇtest")
363 .await
364 .assert_matches();
365 }
366
367 #[gpui::test]
368 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
369 let mut cx = NeovimBackedTestContext::new(cx).await;
370 cx.simulate(
371 "d $",
372 indoc! {"
373 The qˇuick
374 brown fox"},
375 )
376 .await
377 .assert_matches();
378 cx.simulate(
379 "d $",
380 indoc! {"
381 The quick
382 ˇ
383 brown fox"},
384 )
385 .await
386 .assert_matches();
387 }
388
389 #[gpui::test]
390 async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) {
391 let mut cx = NeovimBackedTestContext::new(cx).await;
392 cx.simulate(
393 "d }",
394 indoc! {"
395 ˇhello world.
396
397 hello world."},
398 )
399 .await
400 .assert_matches();
401
402 cx.simulate(
403 "d }",
404 indoc! {"
405 ˇhello world.
406 hello world."},
407 )
408 .await
409 .assert_matches();
410 }
411
412 #[gpui::test]
413 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
414 let mut cx = NeovimBackedTestContext::new(cx).await;
415 cx.simulate(
416 "d 0",
417 indoc! {"
418 The qˇuick
419 brown fox"},
420 )
421 .await
422 .assert_matches();
423 cx.simulate(
424 "d 0",
425 indoc! {"
426 The quick
427 ˇ
428 brown fox"},
429 )
430 .await
431 .assert_matches();
432 }
433
434 #[gpui::test]
435 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
436 let mut cx = NeovimBackedTestContext::new(cx).await;
437 cx.simulate(
438 "d k",
439 indoc! {"
440 The quick
441 brown ˇfox
442 jumps over"},
443 )
444 .await
445 .assert_matches();
446 cx.simulate(
447 "d k",
448 indoc! {"
449 The quick
450 brown fox
451 jumps ˇover"},
452 )
453 .await
454 .assert_matches();
455 cx.simulate(
456 "d k",
457 indoc! {"
458 The qˇuick
459 brown fox
460 jumps over"},
461 )
462 .await
463 .assert_matches();
464 cx.simulate(
465 "d k",
466 indoc! {"
467 ˇbrown fox
468 jumps over"},
469 )
470 .await
471 .assert_matches();
472 }
473
474 #[gpui::test]
475 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
476 let mut cx = NeovimBackedTestContext::new(cx).await;
477 cx.simulate(
478 "d j",
479 indoc! {"
480 The quick
481 brown ˇfox
482 jumps over"},
483 )
484 .await
485 .assert_matches();
486 cx.simulate(
487 "d j",
488 indoc! {"
489 The quick
490 brown fox
491 jumps ˇover"},
492 )
493 .await
494 .assert_matches();
495 cx.simulate(
496 "d j",
497 indoc! {"
498 The qˇuick
499 brown fox
500 jumps over"},
501 )
502 .await
503 .assert_matches();
504 cx.simulate(
505 "d j",
506 indoc! {"
507 The quick
508 brown fox
509 ˇ"},
510 )
511 .await
512 .assert_matches();
513 }
514
515 #[gpui::test]
516 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
517 let mut cx = NeovimBackedTestContext::new(cx).await;
518 cx.simulate(
519 "d shift-g",
520 indoc! {"
521 The quick
522 brownˇ fox
523 jumps over
524 the lazy"},
525 )
526 .await
527 .assert_matches();
528 cx.simulate(
529 "d shift-g",
530 indoc! {"
531 The quick
532 brownˇ fox
533 jumps over
534 the lazy"},
535 )
536 .await
537 .assert_matches();
538 cx.simulate(
539 "d shift-g",
540 indoc! {"
541 The quick
542 brown fox
543 jumps over
544 the lˇazy"},
545 )
546 .await
547 .assert_matches();
548 cx.simulate(
549 "d shift-g",
550 indoc! {"
551 The quick
552 brown fox
553 jumps over
554 ˇ"},
555 )
556 .await
557 .assert_matches();
558 }
559
560 #[gpui::test]
561 async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
562 let mut cx = NeovimBackedTestContext::new(cx).await;
563 cx.simulate(
564 "d 3 shift-g",
565 indoc! {"
566 The quick
567 brownˇ fox
568 jumps over
569 the lazy"},
570 )
571 .await
572 .assert_matches();
573 cx.simulate(
574 "d 3 shift-g",
575 indoc! {"
576 The quick
577 brown fox
578 jumps over
579 the lˇazy"},
580 )
581 .await
582 .assert_matches();
583 cx.simulate(
584 "d 2 shift-g",
585 indoc! {"
586 The quick
587 brown fox
588 jumps over
589 ˇ"},
590 )
591 .await
592 .assert_matches();
593 }
594
595 #[gpui::test]
596 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
597 let mut cx = NeovimBackedTestContext::new(cx).await;
598 cx.simulate(
599 "d g g",
600 indoc! {"
601 The quick
602 brownˇ fox
603 jumps over
604 the lazy"},
605 )
606 .await
607 .assert_matches();
608 cx.simulate(
609 "d g g",
610 indoc! {"
611 The quick
612 brown fox
613 jumps over
614 the lˇazy"},
615 )
616 .await
617 .assert_matches();
618 cx.simulate(
619 "d g g",
620 indoc! {"
621 The qˇuick
622 brown fox
623 jumps over
624 the lazy"},
625 )
626 .await
627 .assert_matches();
628 cx.simulate(
629 "d g g",
630 indoc! {"
631 ˇ
632 brown fox
633 jumps over
634 the lazy"},
635 )
636 .await
637 .assert_matches();
638 }
639
640 #[gpui::test]
641 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
642 let mut cx = VimTestContext::new(cx, true).await;
643 cx.set_state(
644 indoc! {"
645 The quick brown
646 fox juˇmps over
647 the lazy dog"},
648 Mode::Normal,
649 );
650
651 // Canceling operator twice reverts to normal mode with no active operator
652 cx.simulate_keystrokes("d escape k");
653 assert_eq!(cx.active_operator(), None);
654 assert_eq!(cx.mode(), Mode::Normal);
655 cx.assert_editor_state(indoc! {"
656 The quˇick brown
657 fox jumps over
658 the lazy dog"});
659 }
660
661 #[gpui::test]
662 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
663 let mut cx = VimTestContext::new(cx, true).await;
664 cx.set_state(
665 indoc! {"
666 The quick brown
667 fox juˇmps over
668 the lazy dog"},
669 Mode::Normal,
670 );
671
672 // Canceling operator twice reverts to normal mode with no active operator
673 cx.simulate_keystrokes("d y");
674 assert_eq!(cx.active_operator(), None);
675 assert_eq!(cx.mode(), Mode::Normal);
676 }
677
678 #[gpui::test]
679 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
680 let mut cx = NeovimBackedTestContext::new(cx).await;
681 cx.set_shared_state(indoc! {"
682 The ˇquick brown
683 fox jumps over
684 the lazy dog"})
685 .await;
686 cx.simulate_shared_keystrokes("d 2 d").await;
687 cx.shared_state().await.assert_eq(indoc! {"
688 the ˇlazy dog"});
689
690 cx.set_shared_state(indoc! {"
691 The ˇquick brown
692 fox jumps over
693 the lazy dog"})
694 .await;
695 cx.simulate_shared_keystrokes("2 d d").await;
696 cx.shared_state().await.assert_eq(indoc! {"
697 the ˇlazy dog"});
698
699 cx.set_shared_state(indoc! {"
700 The ˇquick brown
701 fox jumps over
702 the moon,
703 a star, and
704 the lazy dog"})
705 .await;
706 cx.simulate_shared_keystrokes("2 d 2 d").await;
707 cx.shared_state().await.assert_eq(indoc! {"
708 the ˇlazy dog"});
709 }
710
711 #[gpui::test]
712 async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
713 let mut cx = NeovimBackedTestContext::new(cx).await;
714 cx.simulate("d t x", "ˇax").await.assert_matches();
715 cx.simulate("d t x", "aˇx").await.assert_matches();
716 }
717
718 #[gpui::test]
719 async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
720 let mut cx = NeovimBackedTestContext::new(cx).await;
721 // cx.simulate(
722 // "d )",
723 // indoc! {"
724 // Fiˇrst. Second. Third.
725 // Fourth.
726 // "},
727 // )
728 // .await
729 // .assert_matches();
730
731 // cx.simulate(
732 // "d )",
733 // indoc! {"
734 // First. Secˇond. Third.
735 // Fourth.
736 // "},
737 // )
738 // .await
739 // .assert_matches();
740
741 // // Two deletes
742 // cx.simulate(
743 // "d ) d )",
744 // indoc! {"
745 // First. Second. Thirˇd.
746 // Fourth.
747 // "},
748 // )
749 // .await
750 // .assert_matches();
751
752 // Should delete whole line if done on first column
753 cx.simulate(
754 "d )",
755 indoc! {"
756 ˇFirst.
757 Fourth.
758 "},
759 )
760 .await
761 .assert_matches();
762
763 // Backwards it should also delete the whole first line
764 cx.simulate(
765 "d (",
766 indoc! {"
767 First.
768 ˇSecond.
769 Fourth.
770 "},
771 )
772 .await
773 .assert_matches();
774 }
775}
776