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