Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:35:31.959Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

delete.rs

776 lines · 24.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.