surrounds.rs

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