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 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}