surrounds.rs

  1use crate::{motion::Motion, object::Object, state::Mode, Vim};
  2use editor::{scroll::Autoscroll, Bias};
  3use gpui::WindowContext;
  4use language::BracketPair;
  5use serde::Deserialize;
  6use std::sync::Arc;
  7#[derive(Clone, Debug, PartialEq, Eq)]
  8pub enum SurroundsType {
  9    Motion(Motion),
 10    Object(Object),
 11}
 12
 13// This exists so that we can have Deserialize on Operators, but not on Motions.
 14impl<'de> Deserialize<'de> for SurroundsType {
 15    fn deserialize<D>(_: D) -> Result<Self, D::Error>
 16    where
 17        D: serde::Deserializer<'de>,
 18    {
 19        Err(serde::de::Error::custom("Cannot deserialize SurroundsType"))
 20    }
 21}
 22
 23pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
 24    Vim::update(cx, |vim, cx| {
 25        vim.stop_recording();
 26        vim.update_active_editor(cx, |_, editor, cx| {
 27            let text_layout_details = editor.text_layout_details(cx);
 28            editor.transact(cx, |editor, cx| {
 29                editor.set_clip_at_line_ends(false, cx);
 30
 31                let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 32                    Some(pair) => pair.clone(),
 33                    None => BracketPair {
 34                        start: text.to_string(),
 35                        end: text.to_string(),
 36                        close: true,
 37                        newline: false,
 38                    },
 39                };
 40                let surround = pair.end != *text;
 41                let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
 42                let mut edits = Vec::new();
 43                let mut anchors = Vec::new();
 44
 45                for selection in &display_selections {
 46                    let range = match &target {
 47                        SurroundsType::Object(object) => {
 48                            object.range(&display_map, selection.clone(), false)
 49                        }
 50                        SurroundsType::Motion(motion) => motion.range(
 51                            &display_map,
 52                            selection.clone(),
 53                            Some(1),
 54                            true,
 55                            &text_layout_details,
 56                        ),
 57                    };
 58
 59                    if let Some(range) = range {
 60                        let start = range.start.to_offset(&display_map, Bias::Right);
 61                        let end = range.end.to_offset(&display_map, Bias::Left);
 62                        let start_cursor_str =
 63                            format!("{}{}", pair.start, if surround { " " } else { "" });
 64                        let close_cursor_str =
 65                            format!("{}{}", if surround { " " } else { "" }, pair.end);
 66                        let start_anchor = display_map.buffer_snapshot.anchor_before(start);
 67
 68                        edits.push((start..start, start_cursor_str));
 69                        edits.push((end..end, close_cursor_str));
 70                        anchors.push(start_anchor..start_anchor);
 71                    } else {
 72                        let start_anchor = display_map
 73                            .buffer_snapshot
 74                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
 75                        anchors.push(start_anchor..start_anchor);
 76                    }
 77                }
 78
 79                editor.buffer().update(cx, |buffer, cx| {
 80                    buffer.edit(edits, None, cx);
 81                });
 82                editor.set_clip_at_line_ends(true, cx);
 83                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 84                    s.select_anchor_ranges(anchors)
 85                });
 86            });
 87        });
 88        vim.switch_mode(Mode::Normal, false, cx);
 89    });
 90}
 91
 92pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
 93    Vim::update(cx, |vim, cx| {
 94        vim.stop_recording();
 95
 96        // only legitimate surrounds can be removed
 97        let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
 98            Some(pair) => pair.clone(),
 99            None => return,
100        };
101        let pair_object = match pair_to_object(&pair) {
102            Some(pair_object) => pair_object,
103            None => return,
104        };
105        let surround = pair.end != *text;
106
107        vim.update_active_editor(cx, |_, editor, cx| {
108            editor.transact(cx, |editor, cx| {
109                editor.set_clip_at_line_ends(false, cx);
110
111                let (display_map, display_selections) = editor.selections.all_display(cx);
112                let mut edits = Vec::new();
113                let mut anchors = Vec::new();
114
115                for selection in &display_selections {
116                    let start = selection.start.to_offset(&display_map, Bias::Left);
117                    if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
118                        // If the current parenthesis object is single-line,
119                        // then we need to filter whether it is the current line or not
120                        if !pair_object.is_multiline() {
121                            let is_same_row = selection.start.row() == range.start.row()
122                                && selection.end.row() == range.end.row();
123                            if !is_same_row {
124                                anchors.push(start..start);
125                                continue;
126                            }
127                        }
128                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
129                        // hello«ˇ  "hello in a word"  »again.
130                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
131                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
132                        let mut chars_and_offset = display_map
133                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
134                            .peekable();
135                        while let Some((ch, offset)) = chars_and_offset.next() {
136                            if ch.to_string() == pair.start {
137                                let start = offset;
138                                let mut end = start + 1;
139                                if surround {
140                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
141                                        if next_ch.eq(&' ') {
142                                            end += 1;
143                                        }
144                                    }
145                                }
146                                edits.push((start..end, ""));
147                                anchors.push(start..start);
148                                break;
149                            }
150                        }
151                        let mut reverse_chars_and_offsets = display_map
152                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
153                            .peekable();
154                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
155                            if ch.to_string() == pair.end {
156                                let mut start = offset;
157                                let end = start + 1;
158                                if surround {
159                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
160                                        if next_ch.eq(&' ') {
161                                            start -= 1;
162                                        }
163                                    }
164                                }
165                                edits.push((start..end, ""));
166                                break;
167                            }
168                        }
169                    } else {
170                        anchors.push(start..start);
171                    }
172                }
173
174                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
175                    s.select_ranges(anchors);
176                });
177                edits.sort_by_key(|(range, _)| range.start);
178                editor.buffer().update(cx, |buffer, cx| {
179                    buffer.edit(edits, None, cx);
180                });
181                editor.set_clip_at_line_ends(true, cx);
182            });
183        });
184    });
185}
186
187pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
188    if let Some(will_replace_pair) = object_to_bracket_pair(target) {
189        Vim::update(cx, |vim, cx| {
190            vim.stop_recording();
191            vim.update_active_editor(cx, |_, editor, cx| {
192                editor.transact(cx, |editor, cx| {
193                    editor.set_clip_at_line_ends(false, cx);
194
195                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
196                        Some(pair) => pair.clone(),
197                        None => BracketPair {
198                            start: text.to_string(),
199                            end: text.to_string(),
200                            close: true,
201                            newline: false,
202                        },
203                    };
204                    let surround = pair.end != *text;
205                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
206                    let mut edits = Vec::new();
207                    let mut anchors = Vec::new();
208
209                    for selection in &selections {
210                        let start = selection.start.to_offset(&display_map, Bias::Left);
211                        if let Some(range) = target.range(&display_map, selection.clone(), true) {
212                            if !target.is_multiline() {
213                                let is_same_row = selection.start.row() == range.start.row()
214                                    && selection.end.row() == range.end.row();
215                                if !is_same_row {
216                                    anchors.push(start..start);
217                                    continue;
218                                }
219                            }
220                            let mut chars_and_offset = display_map
221                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
222                                .peekable();
223                            while let Some((ch, offset)) = chars_and_offset.next() {
224                                if ch.to_string() == will_replace_pair.start {
225                                    let mut open_str = pair.start.clone();
226                                    let start = offset;
227                                    let mut end = start + 1;
228                                    match chars_and_offset.peek() {
229                                        Some((next_ch, _)) => {
230                                            // If the next position is already a space or line break,
231                                            // we don't need to splice another space even under arround
232                                            if surround && !next_ch.is_whitespace() {
233                                                open_str.push_str(" ");
234                                            } else if !surround && next_ch.to_string() == " " {
235                                                end += 1;
236                                            }
237                                        }
238                                        None => {}
239                                    }
240                                    edits.push((start..end, open_str));
241                                    anchors.push(start..start);
242                                    break;
243                                }
244                            }
245
246                            let mut reverse_chars_and_offsets = display_map
247                                .reverse_buffer_chars_at(
248                                    range.end.to_offset(&display_map, Bias::Left),
249                                )
250                                .peekable();
251                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
252                                if ch.to_string() == will_replace_pair.end {
253                                    let mut close_str = pair.end.clone();
254                                    let mut start = offset;
255                                    let end = start + 1;
256                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
257                                        if surround && !next_ch.is_whitespace() {
258                                            close_str.insert_str(0, " ")
259                                        } else if !surround && next_ch.to_string() == " " {
260                                            start -= 1;
261                                        }
262                                    }
263                                    edits.push((start..end, close_str));
264                                    break;
265                                }
266                            }
267                        } else {
268                            anchors.push(start..start);
269                        }
270                    }
271
272                    let stable_anchors = editor
273                        .selections
274                        .disjoint_anchors()
275                        .into_iter()
276                        .map(|selection| {
277                            let start = selection.start.bias_left(&display_map.buffer_snapshot);
278                            start..start
279                        })
280                        .collect::<Vec<_>>();
281                    edits.sort_by_key(|(range, _)| range.start);
282                    editor.buffer().update(cx, |buffer, cx| {
283                        buffer.edit(edits, None, cx);
284                    });
285                    editor.set_clip_at_line_ends(true, cx);
286                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
287                        s.select_anchor_ranges(stable_anchors);
288                    });
289                });
290            });
291        });
292    }
293}
294
295/// Checks if any of the current cursors are surrounded by a valid pair of brackets.
296///
297/// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
298/// A pair of brackets is considered valid if it is well-formed and properly closed.
299///
300/// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
301/// If no valid pair of brackets is found for any cursor, the method returns `false`.
302pub fn check_and_move_to_valid_bracket_pair(
303    vim: &mut Vim,
304    object: Object,
305    cx: &mut WindowContext,
306) -> bool {
307    let mut valid = false;
308    if let Some(pair) = object_to_bracket_pair(object) {
309        vim.update_active_editor(cx, |_, editor, cx| {
310            editor.transact(cx, |editor, cx| {
311                editor.set_clip_at_line_ends(false, cx);
312                let (display_map, selections) = editor.selections.all_adjusted_display(cx);
313                let mut anchors = Vec::new();
314
315                for selection in &selections {
316                    let start = selection.start.to_offset(&display_map, Bias::Left);
317                    if let Some(range) = object.range(&display_map, selection.clone(), true) {
318                        // If the current parenthesis object is single-line,
319                        // then we need to filter whether it is the current line or not
320                        if object.is_multiline()
321                            || (!object.is_multiline()
322                                && selection.start.row() == range.start.row()
323                                && selection.end.row() == range.end.row())
324                        {
325                            valid = true;
326                            let mut chars_and_offset = display_map
327                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
328                                .peekable();
329                            while let Some((ch, offset)) = chars_and_offset.next() {
330                                if ch.to_string() == pair.start {
331                                    anchors.push(offset..offset);
332                                    break;
333                                }
334                            }
335                        } else {
336                            anchors.push(start..start)
337                        }
338                    } else {
339                        anchors.push(start..start)
340                    }
341                }
342                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
343                    s.select_ranges(anchors);
344                });
345                editor.set_clip_at_line_ends(true, cx);
346            });
347        });
348    }
349    return valid;
350}
351
352fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
353    pairs.iter().find(|pair| pair.start == ch || pair.end == ch)
354}
355
356fn all_support_surround_pair() -> Vec<BracketPair> {
357    return vec![
358        BracketPair {
359            start: "{".into(),
360            end: "}".into(),
361            close: true,
362            newline: false,
363        },
364        BracketPair {
365            start: "'".into(),
366            end: "'".into(),
367            close: true,
368            newline: false,
369        },
370        BracketPair {
371            start: "`".into(),
372            end: "`".into(),
373            close: true,
374            newline: false,
375        },
376        BracketPair {
377            start: "\"".into(),
378            end: "\"".into(),
379            close: true,
380            newline: false,
381        },
382        BracketPair {
383            start: "(".into(),
384            end: ")".into(),
385            close: true,
386            newline: false,
387        },
388        BracketPair {
389            start: "|".into(),
390            end: "|".into(),
391            close: true,
392            newline: false,
393        },
394        BracketPair {
395            start: "[".into(),
396            end: "]".into(),
397            close: true,
398            newline: false,
399        },
400        BracketPair {
401            start: "{".into(),
402            end: "}".into(),
403            close: true,
404            newline: false,
405        },
406        BracketPair {
407            start: "<".into(),
408            end: ">".into(),
409            close: true,
410            newline: false,
411        },
412    ];
413}
414
415fn pair_to_object(pair: &BracketPair) -> Option<Object> {
416    match pair.start.as_str() {
417        "'" => Some(Object::Quotes),
418        "`" => Some(Object::BackQuotes),
419        "\"" => Some(Object::DoubleQuotes),
420        "|" => Some(Object::VerticalBars),
421        "(" => Some(Object::Parentheses),
422        "[" => Some(Object::SquareBrackets),
423        "{" => Some(Object::CurlyBrackets),
424        "<" => Some(Object::AngleBrackets),
425        _ => None,
426    }
427}
428
429fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
430    match object {
431        Object::Quotes => Some(BracketPair {
432            start: "'".to_string(),
433            end: "'".to_string(),
434            close: true,
435            newline: false,
436        }),
437        Object::BackQuotes => Some(BracketPair {
438            start: "`".to_string(),
439            end: "`".to_string(),
440            close: true,
441            newline: false,
442        }),
443        Object::DoubleQuotes => Some(BracketPair {
444            start: "\"".to_string(),
445            end: "\"".to_string(),
446            close: true,
447            newline: false,
448        }),
449        Object::VerticalBars => Some(BracketPair {
450            start: "|".to_string(),
451            end: "|".to_string(),
452            close: true,
453            newline: false,
454        }),
455        Object::Parentheses => Some(BracketPair {
456            start: "(".to_string(),
457            end: ")".to_string(),
458            close: true,
459            newline: false,
460        }),
461        Object::SquareBrackets => Some(BracketPair {
462            start: "[".to_string(),
463            end: "]".to_string(),
464            close: true,
465            newline: false,
466        }),
467        Object::CurlyBrackets => Some(BracketPair {
468            start: "{".to_string(),
469            end: "}".to_string(),
470            close: true,
471            newline: false,
472        }),
473        Object::AngleBrackets => Some(BracketPair {
474            start: "<".to_string(),
475            end: ">".to_string(),
476            close: true,
477            newline: false,
478        }),
479        _ => None,
480    }
481}
482
483#[cfg(test)]
484mod test {
485    use indoc::indoc;
486
487    use crate::{state::Mode, test::VimTestContext};
488
489    #[gpui::test]
490    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
491        let mut cx = VimTestContext::new(cx, true).await;
492
493        // test add surrounds with arround
494        cx.set_state(
495            indoc! {"
496            The quˇick brown
497            fox jumps over
498            the lazy dog."},
499            Mode::Normal,
500        );
501        cx.simulate_keystrokes(["y", "s", "i", "w", "{"]);
502        cx.assert_state(
503            indoc! {"
504            The ˇ{ quick } brown
505            fox jumps over
506            the lazy dog."},
507            Mode::Normal,
508        );
509
510        // test add surrounds not with arround
511        cx.set_state(
512            indoc! {"
513            The quˇick brown
514            fox jumps over
515            the lazy dog."},
516            Mode::Normal,
517        );
518        cx.simulate_keystrokes(["y", "s", "i", "w", "}"]);
519        cx.assert_state(
520            indoc! {"
521            The ˇ{quick} brown
522            fox jumps over
523            the lazy dog."},
524            Mode::Normal,
525        );
526
527        // test add surrounds with motion
528        cx.set_state(
529            indoc! {"
530            The quˇick brown
531            fox jumps over
532            the lazy dog."},
533            Mode::Normal,
534        );
535        cx.simulate_keystrokes(["y", "s", "$", "}"]);
536        cx.assert_state(
537            indoc! {"
538            The quˇ{ick brown}
539            fox jumps over
540            the lazy dog."},
541            Mode::Normal,
542        );
543
544        // test add surrounds with multi cursor
545        cx.set_state(
546            indoc! {"
547            The quˇick brown
548            fox jumps over
549            the laˇzy dog."},
550            Mode::Normal,
551        );
552        cx.simulate_keystrokes(["y", "s", "i", "w", "'"]);
553        cx.assert_state(
554            indoc! {"
555            The ˇ'quick' brown
556            fox jumps over
557            the ˇ'lazy' dog."},
558            Mode::Normal,
559        );
560
561        // test multi cursor add surrounds with motion
562        cx.set_state(
563            indoc! {"
564            The quˇick brown
565            fox jumps over
566            the laˇzy dog."},
567            Mode::Normal,
568        );
569        cx.simulate_keystrokes(["y", "s", "$", "'"]);
570        cx.assert_state(
571            indoc! {"
572            The quˇ'ick brown'
573            fox jumps over
574            the laˇ'zy dog.'"},
575            Mode::Normal,
576        );
577
578        // test multi cursor add surrounds with motion and custom string
579        cx.set_state(
580            indoc! {"
581            The quˇick brown
582            fox jumps over
583            the laˇzy dog."},
584            Mode::Normal,
585        );
586        cx.simulate_keystrokes(["y", "s", "$", "1"]);
587        cx.assert_state(
588            indoc! {"
589            The quˇ1ick brown1
590            fox jumps over
591            the laˇ1zy dog.1"},
592            Mode::Normal,
593        );
594    }
595
596    #[gpui::test]
597    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
598        let mut cx = VimTestContext::new(cx, true).await;
599
600        // test delete surround
601        cx.set_state(
602            indoc! {"
603            The {quˇick} brown
604            fox jumps over
605            the lazy dog."},
606            Mode::Normal,
607        );
608        cx.simulate_keystrokes(["d", "s", "{"]);
609        cx.assert_state(
610            indoc! {"
611            The ˇquick brown
612            fox jumps over
613            the lazy dog."},
614            Mode::Normal,
615        );
616
617        // test delete not exist surrounds
618        cx.set_state(
619            indoc! {"
620            The {quˇick} brown
621            fox jumps over
622            the lazy dog."},
623            Mode::Normal,
624        );
625        cx.simulate_keystrokes(["d", "s", "["]);
626        cx.assert_state(
627            indoc! {"
628            The {quˇick} brown
629            fox jumps over
630            the lazy dog."},
631            Mode::Normal,
632        );
633
634        // test delete surround forward exist, in the surrounds plugin of other editors,
635        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
636        cx.set_state(
637            indoc! {"
638            The {quick} brˇown
639            fox jumps over
640            the lazy dog."},
641            Mode::Normal,
642        );
643        cx.simulate_keystrokes(["d", "s", "{"]);
644        cx.assert_state(
645            indoc! {"
646            The {quick} brˇown
647            fox jumps over
648            the lazy dog."},
649            Mode::Normal,
650        );
651
652        // test cursor delete inner surrounds
653        cx.set_state(
654            indoc! {"
655            The { quick brown
656            fox jumˇps over }
657            the lazy dog."},
658            Mode::Normal,
659        );
660        cx.simulate_keystrokes(["d", "s", "{"]);
661        cx.assert_state(
662            indoc! {"
663            The ˇquick brown
664            fox jumps over
665            the lazy dog."},
666            Mode::Normal,
667        );
668
669        // test multi cursor delete surrounds
670        cx.set_state(
671            indoc! {"
672            The [quˇick] brown
673            fox jumps over
674            the [laˇzy] dog."},
675            Mode::Normal,
676        );
677        cx.simulate_keystrokes(["d", "s", "]"]);
678        cx.assert_state(
679            indoc! {"
680            The ˇquick brown
681            fox jumps over
682            the ˇlazy dog."},
683            Mode::Normal,
684        );
685
686        // test multi cursor delete surrounds with arround
687        cx.set_state(
688            indoc! {"
689            Tˇhe [ quick ] brown
690            fox jumps over
691            the [laˇzy] dog."},
692            Mode::Normal,
693        );
694        cx.simulate_keystrokes(["d", "s", "["]);
695        cx.assert_state(
696            indoc! {"
697            The ˇquick brown
698            fox jumps over
699            the ˇlazy dog."},
700            Mode::Normal,
701        );
702
703        cx.set_state(
704            indoc! {"
705            Tˇhe [ quick ] brown
706            fox jumps over
707            the [laˇzy ] dog."},
708            Mode::Normal,
709        );
710        cx.simulate_keystrokes(["d", "s", "["]);
711        cx.assert_state(
712            indoc! {"
713            The ˇquick brown
714            fox jumps over
715            the ˇlazy dog."},
716            Mode::Normal,
717        );
718
719        // test multi cursor delete different surrounds
720        // the pair corresponding to the two cursors is the same,
721        // so they are combined into one cursor
722        cx.set_state(
723            indoc! {"
724            The [quˇick] brown
725            fox jumps over
726            the {laˇzy} dog."},
727            Mode::Normal,
728        );
729        cx.simulate_keystrokes(["d", "s", "{"]);
730        cx.assert_state(
731            indoc! {"
732            The [quick] brown
733            fox jumps over
734            the ˇlazy dog."},
735            Mode::Normal,
736        );
737
738        // test delete surround with multi cursor and nest surrounds
739        cx.set_state(
740            indoc! {"
741            fn test_surround() {
742                ifˇ 2 > 1 {
743                    ˇprintln!(\"it is fine\");
744                };
745            }"},
746            Mode::Normal,
747        );
748        cx.simulate_keystrokes(["d", "s", "}"]);
749        cx.assert_state(
750            indoc! {"
751            fn test_surround() ˇ
752                if 2 > 1 ˇ
753                    println!(\"it is fine\");
754                ;
755            "},
756            Mode::Normal,
757        );
758    }
759
760    #[gpui::test]
761    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
762        let mut cx = VimTestContext::new(cx, true).await;
763
764        cx.set_state(
765            indoc! {"
766            The {quˇick} brown
767            fox jumps over
768            the lazy dog."},
769            Mode::Normal,
770        );
771        cx.simulate_keystrokes(["c", "s", "{", "["]);
772        cx.assert_state(
773            indoc! {"
774            The ˇ[ quick ] brown
775            fox jumps over
776            the lazy dog."},
777            Mode::Normal,
778        );
779
780        // test multi cursor change surrounds
781        cx.set_state(
782            indoc! {"
783            The {quˇick} brown
784            fox jumps over
785            the {laˇzy} dog."},
786            Mode::Normal,
787        );
788        cx.simulate_keystrokes(["c", "s", "{", "["]);
789        cx.assert_state(
790            indoc! {"
791            The ˇ[ quick ] brown
792            fox jumps over
793            the ˇ[ lazy ] dog."},
794            Mode::Normal,
795        );
796
797        // test multi cursor delete different surrounds with after cursor
798        cx.set_state(
799            indoc! {"
800            Thˇe {quick} brown
801            fox jumps over
802            the {laˇzy} dog."},
803            Mode::Normal,
804        );
805        cx.simulate_keystrokes(["c", "s", "{", "["]);
806        cx.assert_state(
807            indoc! {"
808            The ˇ[ quick ] brown
809            fox jumps over
810            the ˇ[ lazy ] dog."},
811            Mode::Normal,
812        );
813
814        // test multi cursor change surrount with not arround
815        cx.set_state(
816            indoc! {"
817            Thˇe { quick } brown
818            fox jumps over
819            the {laˇzy} dog."},
820            Mode::Normal,
821        );
822        cx.simulate_keystrokes(["c", "s", "{", "]"]);
823        cx.assert_state(
824            indoc! {"
825            The ˇ[quick] brown
826            fox jumps over
827            the ˇ[lazy] dog."},
828            Mode::Normal,
829        );
830
831        // test multi cursor change with not exist surround
832        cx.set_state(
833            indoc! {"
834            The {quˇick} brown
835            fox jumps over
836            the [laˇzy] dog."},
837            Mode::Normal,
838        );
839        cx.simulate_keystrokes(["c", "s", "[", "'"]);
840        cx.assert_state(
841            indoc! {"
842            The {quick} brown
843            fox jumps over
844            the ˇ'lazy' dog."},
845            Mode::Normal,
846        );
847
848        // test change nesting surrounds
849        cx.set_state(
850            indoc! {"
851            fn test_surround() {
852                ifˇ 2 > 1 {
853                    ˇprintln!(\"it is fine\");
854                }
855            };"},
856            Mode::Normal,
857        );
858        cx.simulate_keystrokes(["c", "s", "{", "["]);
859        cx.assert_state(
860            indoc! {"
861            fn test_surround() ˇ[
862                if 2 > 1 ˇ[
863                    println!(\"it is fine\");
864                ]
865            ];"},
866            Mode::Normal,
867        );
868    }
869
870    #[gpui::test]
871    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
872        let mut cx = VimTestContext::new(cx, true).await;
873
874        cx.set_state(
875            indoc! {"
876            The quˇick brown
877            fox jumps over
878            the lazy dog."},
879            Mode::Normal,
880        );
881        cx.simulate_keystrokes(["y", "s", "i", "w", "["]);
882        cx.assert_state(
883            indoc! {"
884            The ˇ[ quick ] brown
885            fox jumps over
886            the lazy dog."},
887            Mode::Normal,
888        );
889
890        cx.simulate_keystrokes(["c", "s", "[", "}"]);
891        cx.assert_state(
892            indoc! {"
893            The ˇ{quick} brown
894            fox jumps over
895            the lazy dog."},
896            Mode::Normal,
897        );
898
899        cx.simulate_keystrokes(["d", "s", "{"]);
900        cx.assert_state(
901            indoc! {"
902            The ˇquick brown
903            fox jumps over
904            the lazy dog."},
905            Mode::Normal,
906        );
907
908        cx.simulate_keystrokes(["u"]);
909        cx.assert_state(
910            indoc! {"
911            The ˇ{quick} brown
912            fox jumps over
913            the lazy dog."},
914            Mode::Normal,
915        );
916    }
917}