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