Skip to repository content493 lines · 18.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:41:32.815Z 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
surround.rs
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