Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:41:32.815Z 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

surround.rs

493 lines · 18.2 KB · rust
1use editor::display_map::DisplaySnapshot;
2use editor::{Bias, DisplayPoint, MultiBufferOffset};
3use gpui::{Context, Window};
4use multi_buffer::Anchor;
5use text::Selection;
6
7use crate::Vim;
8use crate::object::surrounding_markers;
9use crate::surrounds::{SURROUND_PAIRS, bracket_pair_for_str_helix, surround_pair_for_char_helix};
10
11/// Find the nearest surrounding bracket pair around the cursor.
12fn find_nearest_surrounding_pair(
13    display_map: &DisplaySnapshot,
14    cursor: DisplayPoint,
15) -> Option<(char, char)> {
16    let cursor_offset = cursor.to_offset(display_map, Bias::Left);
17    let mut best_pair: Option<(char, char)> = None;
18    let mut min_range_size = usize::MAX;
19
20    for pair in SURROUND_PAIRS {
21        if let Some(range) =
22            surrounding_markers(display_map, cursor, true, true, pair.open, pair.close)
23        {
24            let start_offset = range.start.to_offset(display_map, Bias::Left);
25            let end_offset = range.end.to_offset(display_map, Bias::Right);
26
27            if cursor_offset >= start_offset && cursor_offset <= end_offset {
28                let size = end_offset - start_offset;
29                if size < min_range_size {
30                    min_range_size = size;
31                    best_pair = Some((pair.open, pair.close));
32                }
33            }
34        }
35    }
36
37    best_pair
38}
39
40fn surrounding_markers_containing_cursor(
41    display_map: &DisplaySnapshot,
42    cursor: DisplayPoint,
43    open_marker: char,
44    close_marker: char,
45) -> Option<std::ops::Range<DisplayPoint>> {
46    let range = surrounding_markers(display_map, cursor, true, true, open_marker, close_marker)?;
47    let cursor_offset = cursor.to_offset(display_map, Bias::Left);
48    let start_offset = range.start.to_offset(display_map, Bias::Left);
49    let end_offset = range.end.to_offset(display_map, Bias::Right);
50
51    if cursor_offset >= start_offset && cursor_offset <= end_offset {
52        Some(range)
53    } else {
54        None
55    }
56}
57
58fn selection_cursor(map: &DisplaySnapshot, selection: &Selection<DisplayPoint>) -> DisplayPoint {
59    if selection.reversed || selection.is_empty() {
60        selection.head()
61    } else {
62        editor::movement::left(map, selection.head())
63    }
64}
65
66type SurroundEdits = Vec<(std::ops::Range<MultiBufferOffset>, String)>;
67type SurroundAnchors = Vec<std::ops::Range<Anchor>>;
68
69fn apply_helix_surround_edits<F>(
70    vim: &mut Vim,
71    window: &mut Window,
72    cx: &mut Context<Vim>,
73    mut build: F,
74) where
75    F: FnMut(&DisplaySnapshot, Vec<Selection<DisplayPoint>>) -> (SurroundEdits, SurroundAnchors),
76{
77    vim.update_editor(cx, |_, editor, cx| {
78        editor.transact(window, cx, |editor, window, cx| {
79            editor.set_clip_at_line_ends(false, cx);
80
81            let display_map = editor.display_snapshot(cx);
82            let selections = editor.selections.all_display(&display_map);
83            let (mut edits, anchors) = build(&display_map, selections);
84
85            edits.sort_by_key(|edit| edit.0.start);
86            editor.edit(edits, cx);
87
88            editor.change_selections(Default::default(), window, cx, |s| {
89                s.select_anchor_ranges(anchors);
90            });
91            editor.set_clip_at_line_ends(true, cx);
92        });
93    });
94}
95
96impl Vim {
97    /// ms - Add surrounding characters around selection.
98    pub fn helix_surround_add(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
99        self.stop_recording(cx);
100
101        let pair = bracket_pair_for_str_helix(text);
102
103        apply_helix_surround_edits(self, window, cx, |display_map, selections| {
104            let mut edits = Vec::new();
105            let mut anchors = Vec::new();
106
107            for selection in selections {
108                let range = selection.range();
109                let start = range.start.to_offset(display_map, Bias::Right);
110                let end = range.end.to_offset(display_map, Bias::Left);
111
112                let end_anchor = display_map.buffer_snapshot().anchor_before(end);
113                edits.push((end..end, pair.end.clone()));
114                edits.push((start..start, pair.start.clone()));
115                anchors.push(end_anchor..end_anchor);
116            }
117
118            (edits, anchors)
119        });
120    }
121
122    /// mr - Replace innermost surrounding pair containing the cursor.
123    pub fn helix_surround_replace(
124        &mut self,
125        old_char: char,
126        new_char: char,
127        window: &mut Window,
128        cx: &mut Context<Self>,
129    ) {
130        self.stop_recording(cx);
131
132        let new_char_str = new_char.to_string();
133        let new_pair = bracket_pair_for_str_helix(&new_char_str);
134
135        apply_helix_surround_edits(self, window, cx, |display_map, selections| {
136            let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
137            let mut anchors = Vec::new();
138
139            for selection in selections {
140                let cursor = selection_cursor(display_map, &selection);
141
142                // For 'm', find the nearest surrounding pair
143                let markers = match surround_pair_for_char_helix(old_char) {
144                    Some(pair) => Some((pair.open, pair.close)),
145                    None => find_nearest_surrounding_pair(display_map, cursor),
146                };
147
148                let Some((open_marker, close_marker)) = markers else {
149                    let offset = selection.head().to_offset(display_map, Bias::Left);
150                    let anchor = display_map.buffer_snapshot().anchor_before(offset);
151                    anchors.push(anchor..anchor);
152                    continue;
153                };
154
155                if let Some(range) = surrounding_markers_containing_cursor(
156                    display_map,
157                    cursor,
158                    open_marker,
159                    close_marker,
160                ) {
161                    let open_start = range.start.to_offset(display_map, Bias::Left);
162                    let open_end = open_start + open_marker.len_utf8();
163                    let close_end = range.end.to_offset(display_map, Bias::Left);
164                    let close_start = close_end - close_marker.len_utf8();
165
166                    edits.push((close_start..close_end, new_pair.end.clone()));
167                    edits.push((open_start..open_end, new_pair.start.clone()));
168
169                    let cursor_offset = cursor.to_offset(display_map, Bias::Left);
170                    let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
171                    anchors.push(anchor..anchor);
172                } else {
173                    let offset = selection.head().to_offset(display_map, Bias::Left);
174                    let anchor = display_map.buffer_snapshot().anchor_before(offset);
175                    anchors.push(anchor..anchor);
176                }
177            }
178
179            (edits, anchors)
180        });
181    }
182
183    /// md - Delete innermost surrounding pair containing the cursor.
184    pub fn helix_surround_delete(
185        &mut self,
186        target_char: char,
187        window: &mut Window,
188        cx: &mut Context<Self>,
189    ) {
190        self.stop_recording(cx);
191
192        apply_helix_surround_edits(self, window, cx, |display_map, selections| {
193            let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
194            let mut anchors = Vec::new();
195
196            for selection in selections {
197                let cursor = selection_cursor(display_map, &selection);
198
199                // For 'm', find the nearest surrounding pair
200                let markers = match surround_pair_for_char_helix(target_char) {
201                    Some(pair) => Some((pair.open, pair.close)),
202                    None => find_nearest_surrounding_pair(display_map, cursor),
203                };
204
205                let Some((open_marker, close_marker)) = markers else {
206                    let offset = selection.head().to_offset(display_map, Bias::Left);
207                    let anchor = display_map.buffer_snapshot().anchor_before(offset);
208                    anchors.push(anchor..anchor);
209                    continue;
210                };
211
212                if let Some(range) = surrounding_markers_containing_cursor(
213                    display_map,
214                    cursor,
215                    open_marker,
216                    close_marker,
217                ) {
218                    let open_start = range.start.to_offset(display_map, Bias::Left);
219                    let open_end = open_start + open_marker.len_utf8();
220                    let close_end = range.end.to_offset(display_map, Bias::Left);
221                    let close_start = close_end - close_marker.len_utf8();
222
223                    edits.push((close_start..close_end, String::new()));
224                    edits.push((open_start..open_end, String::new()));
225
226                    let cursor_offset = cursor.to_offset(display_map, Bias::Left);
227                    let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
228                    anchors.push(anchor..anchor);
229                } else {
230                    let offset = selection.head().to_offset(display_map, Bias::Left);
231                    let anchor = display_map.buffer_snapshot().anchor_before(offset);
232                    anchors.push(anchor..anchor);
233                }
234            }
235
236            (edits, anchors)
237        });
238    }
239}
240
241#[cfg(test)]
242mod test {
243    use indoc::indoc;
244
245    use crate::{state::Mode, test::VimTestContext};
246
247    #[gpui::test]
248    async fn test_helix_surround_add(cx: &mut gpui::TestAppContext) {
249        let mut cx = VimTestContext::new(cx, true).await;
250        cx.enable_helix();
251
252        cx.set_state("hello ˇworld", Mode::HelixNormal);
253        cx.simulate_keystrokes("m s (");
254        cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
255
256        cx.set_state("hello ˇworld", Mode::HelixNormal);
257        cx.simulate_keystrokes("m s )");
258        cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
259
260        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
261        cx.simulate_keystrokes("m s [");
262        cx.assert_state("hello [worlˇ]d", Mode::HelixNormal);
263
264        cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
265        cx.simulate_keystrokes("m s \"");
266        cx.assert_state("hello \"worlˇ\"d", Mode::HelixNormal);
267    }
268
269    #[gpui::test]
270    async fn test_helix_surround_delete(cx: &mut gpui::TestAppContext) {
271        let mut cx = VimTestContext::new(cx, true).await;
272        cx.enable_helix();
273
274        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
275        cx.simulate_keystrokes("m d (");
276        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
277
278        cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
279        cx.simulate_keystrokes("m d \"");
280        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
281
282        cx.set_state("hello woˇrld test", Mode::HelixNormal);
283        cx.simulate_keystrokes("m d (");
284        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
285
286        cx.set_state(
287            indoc! {"
288            \"heˇllo\"
289            fn world() {
290            }"},
291            Mode::HelixNormal,
292        );
293        cx.simulate_keystrokes("m d (");
294        cx.assert_state(
295            indoc! {"
296            \"heˇllo\"
297            fn world() {
298            }"},
299            Mode::HelixNormal,
300        );
301
302        cx.set_state("((woˇrld))", Mode::HelixNormal);
303        cx.simulate_keystrokes("m d (");
304        cx.assert_state("(woˇrld)", Mode::HelixNormal);
305    }
306
307    #[gpui::test]
308    async fn test_helix_surround_replace(cx: &mut gpui::TestAppContext) {
309        let mut cx = VimTestContext::new(cx, true).await;
310        cx.enable_helix();
311
312        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
313        cx.simulate_keystrokes("m r ( [");
314        cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
315
316        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
317        cx.simulate_keystrokes("m r ( ]");
318        cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
319
320        cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
321        cx.simulate_keystrokes("m r \" {");
322        cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
323
324        cx.set_state(
325            indoc! {"
326            \"heˇllo\"
327            fn world() {
328            }"},
329            Mode::HelixNormal,
330        );
331        cx.simulate_keystrokes("m r ( [");
332        cx.assert_state(
333            indoc! {"
334            \"heˇllo\"
335            fn world() {
336            }"},
337            Mode::HelixNormal,
338        );
339
340        cx.set_state("((woˇrld))", Mode::HelixNormal);
341        cx.simulate_keystrokes("m r ( [");
342        cx.assert_state("([woˇrld])", Mode::HelixNormal);
343    }
344
345    #[gpui::test]
346    async fn test_helix_surround_multiline(cx: &mut gpui::TestAppContext) {
347        let mut cx = VimTestContext::new(cx, true).await;
348        cx.enable_helix();
349
350        cx.set_state(
351            indoc! {"
352            function test() {
353                return ˇvalue;
354            }"},
355            Mode::HelixNormal,
356        );
357        cx.simulate_keystrokes("m d {");
358        cx.assert_state(
359            indoc! {"
360            function test() 
361                return ˇvalue;
362            "},
363            Mode::HelixNormal,
364        );
365    }
366
367    #[gpui::test]
368    async fn test_helix_surround_select_mode(cx: &mut gpui::TestAppContext) {
369        let mut cx = VimTestContext::new(cx, true).await;
370        cx.enable_helix();
371
372        cx.set_state("hello «worldˇ» test", Mode::HelixSelect);
373        cx.simulate_keystrokes("m s {");
374        cx.assert_state("hello {worldˇ} test", Mode::HelixNormal);
375    }
376
377    #[gpui::test]
378    async fn test_helix_surround_multi_cursor(cx: &mut gpui::TestAppContext) {
379        let mut cx = VimTestContext::new(cx, true).await;
380        cx.enable_helix();
381
382        cx.set_state(
383            indoc! {"
384            (heˇllo)
385            (woˇrld)"},
386            Mode::HelixNormal,
387        );
388        cx.simulate_keystrokes("m d (");
389        cx.assert_state(
390            indoc! {"
391            heˇllo
392            woˇrld"},
393            Mode::HelixNormal,
394        );
395    }
396
397    #[gpui::test]
398    async fn test_helix_surround_escape_cancels(cx: &mut gpui::TestAppContext) {
399        let mut cx = VimTestContext::new(cx, true).await;
400        cx.enable_helix();
401
402        cx.set_state("hello ˇworld", Mode::HelixNormal);
403        cx.simulate_keystrokes("m escape");
404        cx.assert_state("hello ˇworld", Mode::HelixNormal);
405
406        cx.set_state("hello (woˇrld)", Mode::HelixNormal);
407        cx.simulate_keystrokes("m r ( escape");
408        cx.assert_state("hello (woˇrld)", Mode::HelixNormal);
409    }
410
411    #[gpui::test]
412    async fn test_helix_surround_no_vim_aliases(cx: &mut gpui::TestAppContext) {
413        let mut cx = VimTestContext::new(cx, true).await;
414        cx.enable_helix();
415
416        // In Helix mode, 'b', 'B', 'r', 'a' are NOT aliases for brackets.
417        // They are treated as literal characters, so 'mdb' looks for 'b...b' surrounds.
418
419        // 'b' is not an alias - it looks for literal 'b...b', finds none, does nothing
420        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
421        cx.simulate_keystrokes("m d b");
422        cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
423
424        // 'B' looks for literal 'B...B', not {}
425        cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
426        cx.simulate_keystrokes("m d B");
427        cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
428
429        // 'r' looks for literal 'r...r', not []
430        cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
431        cx.simulate_keystrokes("m d r");
432        cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
433
434        // 'a' looks for literal 'a...a', not <>
435        cx.set_state("hello <woˇrld> test", Mode::HelixNormal);
436        cx.simulate_keystrokes("m d a");
437        cx.assert_state("hello <woˇrld> test", Mode::HelixNormal);
438
439        // Arbitrary chars work as symmetric pairs (Helix feature)
440        cx.set_state("hello *woˇrld* test", Mode::HelixNormal);
441        cx.simulate_keystrokes("m d *");
442        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
443
444        // ms (add) also doesn't use aliases - 'msb' adds literal 'b' surrounds
445        cx.set_state("hello ˇworld", Mode::HelixNormal);
446        cx.simulate_keystrokes("m s b");
447        cx.assert_state("hello bwˇborld", Mode::HelixNormal);
448
449        // mr (replace) also doesn't use aliases
450        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
451        cx.simulate_keystrokes("m r ( b");
452        cx.assert_state("hello bwoˇrldb test", Mode::HelixNormal);
453    }
454
455    #[gpui::test]
456    async fn test_helix_surround_match_nearest(cx: &mut gpui::TestAppContext) {
457        let mut cx = VimTestContext::new(cx, true).await;
458        cx.enable_helix();
459
460        // mdm - delete nearest surrounding pair
461        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
462        cx.simulate_keystrokes("m d m");
463        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
464
465        cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
466        cx.simulate_keystrokes("m d m");
467        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
468
469        cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
470        cx.simulate_keystrokes("m d m");
471        cx.assert_state("hello woˇrld test", Mode::HelixNormal);
472
473        // Nested - deletes innermost
474        cx.set_state("([woˇrld])", Mode::HelixNormal);
475        cx.simulate_keystrokes("m d m");
476        cx.assert_state("(woˇrld)", Mode::HelixNormal);
477
478        // mrm - replace nearest surrounding pair
479        cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
480        cx.simulate_keystrokes("m r m [");
481        cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
482
483        cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
484        cx.simulate_keystrokes("m r m (");
485        cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
486
487        // Nested - replaces innermost
488        cx.set_state("([woˇrld])", Mode::HelixNormal);
489        cx.simulate_keystrokes("m r m {");
490        cx.assert_state("({woˇrld})", Mode::HelixNormal);
491    }
492}
493
Served at tenant.openagents/omega Member data and write actions are omitted.