use crate::{ Vim, motion::{self, Motion}, object::{Object, surrounding_markers}, state::Mode, }; use editor::{Bias, MultiBufferOffset, movement}; use gpui::{Context, Window}; use language::BracketPair; use std::sync::Arc; #[derive(Clone, Debug, PartialEq, Eq)] pub enum SurroundsType { Motion(Motion), Object(Object, bool), Selection, } impl Vim { pub fn add_surrounds( &mut self, text: Arc, target: SurroundsType, window: &mut Window, cx: &mut Context, ) { self.stop_recording(cx); let count = Vim::take_count(cx); let forced_motion = Vim::take_forced_motion(cx); let mode = self.mode; self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let pair = match find_surround_pair(&all_support_surround_pair(), &text) { Some(pair) => pair.clone(), None => BracketPair { start: text.to_string(), end: text.to_string(), close: true, surround: true, newline: false, }, }; let surround = pair.end != surround_alias((*text).as_ref()); let display_map = editor.display_snapshot(cx); let display_selections = editor.selections.all_adjusted_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); for selection in &display_selections { let range = match &target { SurroundsType::Object(object, around) => { object.range(&display_map, selection.clone(), *around, None) } SurroundsType::Motion(motion) => { motion .range( &display_map, selection.clone(), count, &text_layout_details, forced_motion, ) .map(|(mut range, _)| { // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace if let Motion::CurrentLine = motion { range.start = motion::first_non_whitespace( &display_map, false, range.start, ); range.end = movement::saturating_right( &display_map, motion::last_non_whitespace(&display_map, range.end, 1), ); } range }) } SurroundsType::Selection => Some(selection.range()), }; if let Some(range) = range { let start = range.start.to_offset(&display_map, Bias::Right); let end = range.end.to_offset(&display_map, Bias::Left); let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine { (format!("{}\n", pair.start), format!("\n{}", pair.end)) } else { let maybe_space = if surround { " " } else { "" }; ( format!("{}{}", pair.start, maybe_space), format!("{}{}", maybe_space, pair.end), ) }; let start_anchor = display_map.buffer_snapshot().anchor_before(start); edits.push((start..start, start_cursor_str)); edits.push((end..end, end_cursor_str)); anchors.push(start_anchor..start_anchor); } else { let start_anchor = display_map .buffer_snapshot() .anchor_before(selection.head().to_offset(&display_map, Bias::Left)); anchors.push(start_anchor..start_anchor); } } editor.edit(edits, cx); editor.set_clip_at_line_ends(true, cx); editor.change_selections(Default::default(), window, cx, |s| { if mode == Mode::VisualBlock { s.select_anchor_ranges(anchors.into_iter().take(1)) } else { s.select_anchor_ranges(anchors) } }); }); }); self.switch_mode(Mode::Normal, false, window, cx); } pub fn delete_surrounds( &mut self, text: Arc, window: &mut Window, cx: &mut Context, ) { self.stop_recording(cx); // only legitimate surrounds can be removed let pair = match find_surround_pair(&all_support_surround_pair(), &text) { Some(pair) => pair.clone(), None => return, }; let pair_object = match pair_to_object(&pair) { Some(pair_object) => pair_object, None => return, }; let surround = pair.end != *text; self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let display_map = editor.display_snapshot(cx); let display_selections = editor.selections.all_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); for selection in &display_selections { let start = selection.start.to_offset(&display_map, Bias::Left); if let Some(range) = pair_object.range(&display_map, selection.clone(), true, None) { // If the current parenthesis object is single-line, // then we need to filter whether it is the current line or not if !pair_object.is_multiline() { let is_same_row = selection.start.row() == range.start.row() && selection.end.row() == range.end.row(); if !is_same_row { anchors.push(start..start); continue; } } // This is a bit cumbersome, and it is written to deal with some special cases, as shown below // hello«ˇ "hello in a word" »again. // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces // In order to be able to accurately match and replace in this case, some cumbersome methods are used let mut chars_and_offset = display_map .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) .peekable(); while let Some((ch, offset)) = chars_and_offset.next() { if ch.to_string() == pair.start { let start = offset; let mut end = start + 1usize; if surround && let Some((next_ch, _)) = chars_and_offset.peek() && next_ch.eq(&' ') { end += 1; } edits.push((start..end, "")); anchors.push(start..start); break; } } let mut reverse_chars_and_offsets = display_map .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left)) .peekable(); while let Some((ch, offset)) = reverse_chars_and_offsets.next() { if ch.to_string() == pair.end { let mut start = offset; let end = start + 1usize; if surround && let Some((next_ch, _)) = reverse_chars_and_offsets.peek() && next_ch.eq(&' ') { start -= 1; } edits.push((start..end, "")); break; } } } else { anchors.push(start..start); } } editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(anchors); }); edits.sort_by_key(|(range, _)| range.start); editor.edit(edits, cx); editor.set_clip_at_line_ends(true, cx); }); }); } pub fn change_surrounds( &mut self, text: Arc, target: Object, opening: bool, window: &mut Window, cx: &mut Context, ) { if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) { self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let pair = match find_surround_pair(&all_support_surround_pair(), &text) { Some(pair) => pair.clone(), None => BracketPair { start: text.to_string(), end: text.to_string(), close: true, surround: true, newline: false, }, }; // A single space should be added if the new surround is a // bracket and not a quote (pair.start != pair.end) and if // the bracket used is the opening bracket. let add_space = !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref())); // Space should be preserved if either the surrounding // characters being updated are quotes // (will_replace_pair.start == will_replace_pair.end) or if // the bracket used in the command is not an opening // bracket. let preserve_space = will_replace_pair.start == will_replace_pair.end || !opening; let display_map = editor.display_snapshot(cx); let selections = editor.selections.all_adjusted_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); for selection in &selections { let start = selection.start.to_offset(&display_map, Bias::Left); if let Some(range) = target.range(&display_map, selection.clone(), true, None) { if !target.is_multiline() { let is_same_row = selection.start.row() == range.start.row() && selection.end.row() == range.end.row(); if !is_same_row { anchors.push(start..start); continue; } } // Keeps track of the length of the string that is // going to be edited on the start so we can ensure // that the end replacement string does not exceed // this value. Helpful when dealing with newlines. let mut edit_len = 0; let mut open_range_end = MultiBufferOffset(0); let mut chars_and_offset = display_map .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) .peekable(); while let Some((ch, offset)) = chars_and_offset.next() { if ch.to_string() == will_replace_pair.start { let mut open_str = pair.start.clone(); let start = offset; open_range_end = start + 1usize; while let Some((next_ch, _)) = chars_and_offset.next() && next_ch == ' ' { open_range_end += 1; if preserve_space { open_str.push(next_ch); } } if add_space { open_str.push(' '); }; edit_len = open_range_end - start; edits.push((start..open_range_end, open_str)); anchors.push(start..start); break; } } let mut reverse_chars_and_offsets = display_map .reverse_buffer_chars_at( range.end.to_offset(&display_map, Bias::Left), ) .peekable(); while let Some((ch, offset)) = reverse_chars_and_offsets.next() { if ch.to_string() == will_replace_pair.end { let mut close_str = String::new(); let mut start = offset; let end = start + 1usize; while let Some((next_ch, _)) = reverse_chars_and_offsets.next() && next_ch == ' ' && close_str.len() < edit_len - 1 && start > open_range_end { start -= 1; if preserve_space { close_str.push(next_ch); } } if add_space { close_str.push(' '); }; close_str.push_str(&pair.end); edits.push((start..end, close_str)); break; } } } else { anchors.push(start..start); } } let stable_anchors = editor .selections .disjoint_anchors_arc() .iter() .map(|selection| { let start = selection.start.bias_left(&display_map.buffer_snapshot()); start..start }) .collect::>(); edits.sort_by_key(|(range, _)| range.start); editor.edit(edits, cx); editor.set_clip_at_line_ends(true, cx); editor.change_selections(Default::default(), window, cx, |s| { s.select_anchor_ranges(stable_anchors); }); }); }); } } /// Checks if any of the current cursors are surrounded by a valid pair of brackets. /// /// This method supports multiple cursors and checks each cursor for a valid pair of brackets. /// A pair of brackets is considered valid if it is well-formed and properly closed. /// /// 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. /// If no valid pair of brackets is found for any cursor, the method returns `false`. pub fn check_and_move_to_valid_bracket_pair( &mut self, object: Object, window: &mut Window, cx: &mut Context, ) -> bool { let mut valid = false; if let Some(pair) = self.object_to_bracket_pair(object, cx) { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let display_map = editor.display_snapshot(cx); let selections = editor.selections.all_adjusted_display(&display_map); let mut anchors = Vec::new(); for selection in &selections { let start = selection.start.to_offset(&display_map, Bias::Left); if let Some(range) = object.range(&display_map, selection.clone(), true, None) { // If the current parenthesis object is single-line, // then we need to filter whether it is the current line or not if object.is_multiline() || (!object.is_multiline() && selection.start.row() == range.start.row() && selection.end.row() == range.end.row()) { valid = true; let chars_and_offset = display_map .buffer_chars_at( range.start.to_offset(&display_map, Bias::Left), ) .peekable(); for (ch, offset) in chars_and_offset { if ch.to_string() == pair.start { anchors.push(offset..offset); break; } } } else { anchors.push(start..start) } } else { anchors.push(start..start) } } editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(anchors); }); editor.set_clip_at_line_ends(true, cx); }); }); } valid } fn object_to_bracket_pair( &self, object: Object, cx: &mut Context, ) -> Option { match object { Object::Quotes => Some(BracketPair { start: "'".to_string(), end: "'".to_string(), close: true, surround: true, newline: false, }), Object::BackQuotes => Some(BracketPair { start: "`".to_string(), end: "`".to_string(), close: true, surround: true, newline: false, }), Object::DoubleQuotes => Some(BracketPair { start: "\"".to_string(), end: "\"".to_string(), close: true, surround: true, newline: false, }), Object::VerticalBars => Some(BracketPair { start: "|".to_string(), end: "|".to_string(), close: true, surround: true, newline: false, }), Object::Parentheses => Some(BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, surround: true, newline: false, }), Object::SquareBrackets => Some(BracketPair { start: "[".to_string(), end: "]".to_string(), close: true, surround: true, newline: false, }), Object::CurlyBrackets { .. } => Some(BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, surround: true, newline: false, }), Object::AngleBrackets => Some(BracketPair { start: "<".to_string(), end: ">".to_string(), close: true, surround: true, newline: false, }), Object::AnyBrackets => { // If we're dealing with `AnyBrackets`, which can map to multiple // bracket pairs, we'll need to first determine which `BracketPair` to // target. // As such, we keep track of the smallest range size, so // that in cases like `({ name: "John" })` if the cursor is // inside the curly brackets, we target the curly brackets // instead of the parentheses. let mut bracket_pair = None; let mut min_range_size = usize::MAX; let _ = self.editor.update(cx, |editor, cx| { let display_map = editor.display_snapshot(cx); let selections = editor.selections.all_adjusted_display(&display_map); // Even if there's multiple cursors, we'll simply rely on // the first one to understand what bracket pair to map to. // I believe we could, if worth it, go one step above and // have a `BracketPair` per selection, so that `AnyBracket` // could work in situations where the transformation below // could be done. // // ``` // (< name:ˇ'Zed' >) // <[ name:ˇ'DeltaDB' ]> // ``` // // After using `csb{`: // // ``` // (ˇ{ name:'Zed' }) // <ˇ{ name:'DeltaDB' }> // ``` if let Some(selection) = selections.first() { let relative_to = selection.head(); let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; let cursor_offset = relative_to.to_offset(&display_map, Bias::Left); for &(open, close) in bracket_pairs.iter() { if let Some(range) = surrounding_markers( &display_map, relative_to, true, false, open, close, ) { let start_offset = range.start.to_offset(&display_map, Bias::Left); let end_offset = range.end.to_offset(&display_map, Bias::Right); if cursor_offset >= start_offset && cursor_offset <= end_offset { let size = end_offset - start_offset; if size < min_range_size { min_range_size = size; bracket_pair = Some(BracketPair { start: open.to_string(), end: close.to_string(), close: true, surround: true, newline: false, }) } } } } } }); bracket_pair } _ => None, } } } fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { pairs .iter() .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch)) } fn surround_alias(ch: &str) -> &str { match ch { "b" => ")", "B" => "}", "a" => ">", "r" => "]", _ => ch, } } fn all_support_surround_pair() -> Vec { vec![ BracketPair { start: "{".into(), end: "}".into(), close: true, surround: true, newline: false, }, BracketPair { start: "'".into(), end: "'".into(), close: true, surround: true, newline: false, }, BracketPair { start: "`".into(), end: "`".into(), close: true, surround: true, newline: false, }, BracketPair { start: "\"".into(), end: "\"".into(), close: true, surround: true, newline: false, }, BracketPair { start: "(".into(), end: ")".into(), close: true, surround: true, newline: false, }, BracketPair { start: "|".into(), end: "|".into(), close: true, surround: true, newline: false, }, BracketPair { start: "[".into(), end: "]".into(), close: true, surround: true, newline: false, }, BracketPair { start: "<".into(), end: ">".into(), close: true, surround: true, newline: false, }, ] } fn pair_to_object(pair: &BracketPair) -> Option { match pair.start.as_str() { "'" => Some(Object::Quotes), "`" => Some(Object::BackQuotes), "\"" => Some(Object::DoubleQuotes), "|" => Some(Object::VerticalBars), "(" => Some(Object::Parentheses), "[" => Some(Object::SquareBrackets), "{" => Some(Object::CurlyBrackets), "<" => Some(Object::AngleBrackets), _ => None, } } #[cfg(test)] mod test { use gpui::KeyBinding; use indoc::indoc; use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext}; #[gpui::test] async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; // test add surrounds with around cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w {"); cx.assert_state( indoc! {" The ˇ{ quick } brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds not with around cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w }"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds with motion cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s $ }"); cx.assert_state( indoc! {" The quˇ{ick brown} fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds with multi cursor cx.set_state( indoc! {" The quˇick brown fox jumps over the laˇzy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w '"); cx.assert_state( indoc! {" The ˇ'quick' brown fox jumps over the ˇ'lazy' dog."}, Mode::Normal, ); // test multi cursor add surrounds with motion cx.set_state( indoc! {" The quˇick brown fox jumps over the laˇzy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s $ '"); cx.assert_state( indoc! {" The quˇ'ick brown' fox jumps over the laˇ'zy dog.'"}, Mode::Normal, ); // test multi cursor add surrounds with motion and custom string cx.set_state( indoc! {" The quˇick brown fox jumps over the laˇzy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s $ 1"); cx.assert_state( indoc! {" The quˇ1ick brown1 fox jumps over the laˇ1zy dog.1"}, Mode::Normal, ); // test add surrounds with motion current line cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s s {"); cx.assert_state( indoc! {" ˇ{ The quick brown } fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The quˇick brown• fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s s {"); cx.assert_state( indoc! {" ˇ{ The quick brown }• fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("2 y s s )"); cx.assert_state( indoc! {" ˇ({ The quick brown }• fox jumps over) the lazy dog."}, Mode::Normal, ); // test add surrounds around object cx.set_state( indoc! {" The [quˇick] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s a ] )"); cx.assert_state( indoc! {" The ˇ([quick]) brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds inside object cx.set_state( indoc! {" The [quˇick] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i ] )"); cx.assert_state( indoc! {" The [ˇ(quick)] brown fox jumps over the lazy dog."}, Mode::Normal, ); } #[gpui::test] async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.update(|_, cx| { cx.bind_keys([KeyBinding::new( "shift-s", PushAddSurrounds {}, Some("vim_mode == visual"), )]) }); // test add surrounds with around cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("v i w shift-s {"); cx.assert_state( indoc! {" The ˇ{ quick } brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds not with around cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("v i w shift-s }"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds with motion cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("v e shift-s }"); cx.assert_state( indoc! {" The quˇ{ick} brown fox jumps over the lazy dog."}, Mode::Normal, ); // test add surrounds with multi cursor cx.set_state( indoc! {" The quˇick brown fox jumps over the laˇzy dog."}, Mode::Normal, ); cx.simulate_keystrokes("v i w shift-s '"); cx.assert_state( indoc! {" The ˇ'quick' brown fox jumps over the ˇ'lazy' dog."}, Mode::Normal, ); // test add surrounds with visual block cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("ctrl-v i w j j shift-s '"); cx.assert_state( indoc! {" The ˇ'quick' brown fox 'jumps' over the 'lazy 'dog."}, Mode::Normal, ); // test add surrounds with visual line cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("j shift-v shift-s '"); cx.assert_state( indoc! {" The quick brown ˇ' fox jumps over ' the lazy dog."}, Mode::Normal, ); } #[gpui::test] async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; // test delete surround cx.set_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s {"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); // test delete not exist surrounds cx.set_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s ["); cx.assert_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); // test delete surround forward exist, in the surrounds plugin of other editors, // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment cx.set_state( indoc! {" The {quick} brˇown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s {"); cx.assert_state( indoc! {" The {quick} brˇown fox jumps over the lazy dog."}, Mode::Normal, ); // test cursor delete inner surrounds cx.set_state( indoc! {" The { quick brown fox jumˇps over } the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s {"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); // test multi cursor delete surrounds cx.set_state( indoc! {" The [quˇick] brown fox jumps over the [laˇzy] dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s ]"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the ˇlazy dog."}, Mode::Normal, ); // test multi cursor delete surrounds with around cx.set_state( indoc! {" Tˇhe [ quick ] brown fox jumps over the [laˇzy] dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s ["); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the ˇlazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" Tˇhe [ quick ] brown fox jumps over the [laˇzy ] dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s ["); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the ˇlazy dog."}, Mode::Normal, ); // test multi cursor delete different surrounds // the pair corresponding to the two cursors is the same, // so they are combined into one cursor cx.set_state( indoc! {" The [quˇick] brown fox jumps over the {laˇzy} dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s {"); cx.assert_state( indoc! {" The [quick] brown fox jumps over the ˇlazy dog."}, Mode::Normal, ); // test delete surround with multi cursor and nest surrounds cx.set_state( indoc! {" fn test_surround() { ifˇ 2 > 1 { ˇprintln!(\"it is fine\"); }; }"}, Mode::Normal, ); cx.simulate_keystrokes("d s }"); cx.assert_state( indoc! {" fn test_surround() ˇ if 2 > 1 ˇ println!(\"it is fine\"); ; "}, Mode::Normal, ); } #[gpui::test] async fn test_change_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.set_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s { ["); cx.assert_state( indoc! {" The ˇ[ quick ] brown fox jumps over the lazy dog."}, Mode::Normal, ); // test multi cursor change surrounds cx.set_state( indoc! {" The {quˇick} brown fox jumps over the {laˇzy} dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s { ["); cx.assert_state( indoc! {" The ˇ[ quick ] brown fox jumps over the ˇ[ lazy ] dog."}, Mode::Normal, ); // test multi cursor delete different surrounds with after cursor cx.set_state( indoc! {" Thˇe {quick} brown fox jumps over the {laˇzy} dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s { ["); cx.assert_state( indoc! {" The ˇ[ quick ] brown fox jumps over the ˇ[ lazy ] dog."}, Mode::Normal, ); // test multi cursor change surrount with not around cx.set_state( indoc! {" Thˇe { quick } brown fox jumps over the {laˇzy} dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s { ]"); cx.assert_state( indoc! {" The ˇ[quick] brown fox jumps over the ˇ[lazy] dog."}, Mode::Normal, ); // test multi cursor change with not exist surround cx.set_state( indoc! {" The {quˇick} brown fox jumps over the [laˇzy] dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s [ '"); cx.assert_state( indoc! {" The {quick} brown fox jumps over the ˇ'lazy' dog."}, Mode::Normal, ); // test change nesting surrounds cx.set_state( indoc! {" fn test_surround() { ifˇ 2 > 1 { ˇprintln!(\"it is fine\"); } };"}, Mode::Normal, ); cx.simulate_keystrokes("c s } ]"); cx.assert_state( indoc! {" fn test_surround() ˇ[ if 2 > 1 ˇ[ println!(\"it is fine\"); ] ];"}, Mode::Normal, ); // test spaces with quote change surrounds cx.set_state( indoc! {" fn test_surround() { \"ˇ \" };"}, Mode::Normal, ); cx.simulate_keystrokes("c s \" '"); cx.assert_state( indoc! {" fn test_surround() { ˇ' ' };"}, Mode::Normal, ); // Currently, the same test case but using the closing bracket `]` // actually removes a whitespace before the closing bracket, something // that might need to be fixed? cx.set_state( indoc! {" fn test_surround() { ifˇ 2 > 1 { ˇprintln!(\"it is fine\"); } };"}, Mode::Normal, ); cx.simulate_keystrokes("c s { ]"); cx.assert_state( indoc! {" fn test_surround() ˇ[ if 2 > 1 ˇ[ println!(\"it is fine\"); ] ];"}, Mode::Normal, ); // test change quotes. cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal); cx.simulate_keystrokes("c s ' \""); cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal); // test multi cursor change quotes cx.set_state( indoc! {" ' ˇstr ' some example text here ˇ' str ' "}, Mode::Normal, ); cx.simulate_keystrokes("c s ' \""); cx.assert_state( indoc! {" ˇ\" str \" some example text here ˇ\" str \" "}, Mode::Normal, ); // test quote to bracket spacing. cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); cx.simulate_keystrokes("c s ' {"); cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal); cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); cx.simulate_keystrokes("c s ' }"); cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal); } #[gpui::test] async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; // Update keybindings so that using `csb` triggers Vim's `AnyBrackets` // action. cx.update(|_, cx| { cx.bind_keys([KeyBinding::new( "b", AnyBrackets, Some("vim_operator == a || vim_operator == i || vim_operator == cs"), )]); }); cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal); cx.simulate_keystrokes("c s b ["); cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal); cx.simulate_keystrokes("c s b {"); cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal); cx.set_state(indoc! {""}, Mode::Normal); cx.simulate_keystrokes("c s b ["); cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal); cx.simulate_keystrokes("c s b ["); cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); cx.simulate_keystrokes("c s b }"); cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); cx.set_state( indoc! {" (< name: ˇ'Zed' >) (< nˇame: 'DeltaDB' >) "}, Mode::Normal, ); cx.simulate_keystrokes("c s b {"); cx.set_state( indoc! {" (ˇ{ name: 'Zed' }) (ˇ{ name: 'DeltaDB' }) "}, Mode::Normal, ); } // The following test cases all follow tpope/vim-surround's behaviour // and are more focused on how whitespace is handled. #[gpui::test] async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; // Changing quote to quote should never change the surrounding // whitespace. cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); cx.simulate_keystrokes("c s ' \""); cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal); cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal); cx.simulate_keystrokes("c s \" '"); cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); // Changing quote to bracket adds one more space when the opening // bracket is used, does not affect whitespace when the closing bracket // is used. cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); cx.simulate_keystrokes("c s ' {"); cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); cx.simulate_keystrokes("c s ' }"); cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); // Changing bracket to quote should remove all space when the // opening bracket is used and preserve all space when the // closing one is used. cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s { '"); cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal); cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s } '"); cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); // Changing bracket to bracket follows these rules: // * opening → opening – keeps only one space. // * opening → closing – removes all space. // * closing → opening – adds one space. // * closing → closing – does not change space. cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s { ["); cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s { ]"); cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal); cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s } ["); cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); cx.simulate_keystrokes("c s } ]"); cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); } #[gpui::test] async fn test_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w ["); cx.assert_state( indoc! {" The ˇ[ quick ] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s [ }"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s {"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("u"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); } #[gpui::test] async fn test_surround_aliases(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; // add aliases cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w b"); cx.assert_state( indoc! {" The ˇ(quick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w B"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w a"); cx.assert_state( indoc! {" The ˇ brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The quˇick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("y s i w r"); cx.assert_state( indoc! {" The ˇ[quick] brown fox jumps over the lazy dog."}, Mode::Normal, ); // change aliases cx.set_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s { b"); cx.assert_state( indoc! {" The ˇ(quick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The (quˇick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s ( B"); cx.assert_state( indoc! {" The ˇ{quick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The (quˇick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s ( a"); cx.assert_state( indoc! {" The ˇ brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s < b"); cx.assert_state( indoc! {" The ˇ(quick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The (quˇick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s ( r"); cx.assert_state( indoc! {" The ˇ[quick] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The [quˇick] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("c s [ b"); cx.assert_state( indoc! {" The ˇ(quick) brown fox jumps over the lazy dog."}, Mode::Normal, ); // delete alias cx.set_state( indoc! {" The {quˇick} brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s B"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The (quˇick) brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s b"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The [quˇick] brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s r"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.set_state( indoc! {" The brown fox jumps over the lazy dog."}, Mode::Normal, ); cx.simulate_keystrokes("d s a"); cx.assert_state( indoc! {" The ˇquick brown fox jumps over the lazy dog."}, Mode::Normal, ); } }