surrounds.rs

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