diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4d296667ff572a644d4f6b37e1704c0b250a652c..c90b439c6abb60f4e3d826c171d7e2491fce1d90 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -580,18 +580,18 @@ // "q": "vim::AnyQuotes", "q": "vim::MiniQuotes", "|": "vim::VerticalBars", - "(": "vim::Parentheses", + "(": ["vim::Parentheses", { "opening": true }], ")": "vim::Parentheses", "b": "vim::Parentheses", // "b": "vim::AnyBrackets", // "b": "vim::MiniBrackets", - "[": "vim::SquareBrackets", + "[": ["vim::SquareBrackets", { "opening": true }], "]": "vim::SquareBrackets", "r": "vim::SquareBrackets", - "{": "vim::CurlyBrackets", + "{": ["vim::CurlyBrackets", { "opening": true }], "}": "vim::CurlyBrackets", "shift-b": "vim::CurlyBrackets", - "<": "vim::AngleBrackets", + "<": ["vim::AngleBrackets", { "opening": true }], ">": "vim::AngleBrackets", "a": "vim::Argument", "i": "vim::IndentObj", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index bf45129021de7d4c4c0aa003bc05681f1622359a..9386eab58a389b4917cdf33078ac7397ffd01796 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -450,6 +450,7 @@ impl Vim { &mut self, object: Object, times: Option, + opening: bool, window: &mut Window, cx: &mut Context, ) { @@ -520,10 +521,11 @@ impl Vim { Some(Operator::DeleteSurrounds) => { waiting_operator = Some(Operator::DeleteSurrounds); } - Some(Operator::ChangeSurrounds { target: None }) => { + Some(Operator::ChangeSurrounds { target: None, .. }) => { if self.check_and_move_to_valid_bracket_pair(object, window, cx) { waiting_operator = Some(Operator::ChangeSurrounds { target: Some(object), + opening, }); } } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 41b5a17c21d11a753836e26ca640c74312f7b7d2..5d0ac722872f3c39067a668c4ed5d56847c61898 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -85,6 +85,41 @@ pub struct CandidateWithRanges { close_range: Range, } +/// Selects text at the same indentation level. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] +#[action(namespace = vim)] +#[serde(deny_unknown_fields)] +struct Parentheses { + #[serde(default)] + opening: bool, +} + +/// Selects text at the same indentation level. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] +#[action(namespace = vim)] +#[serde(deny_unknown_fields)] +struct SquareBrackets { + #[serde(default)] + opening: bool, +} + +/// Selects text at the same indentation level. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] +#[action(namespace = vim)] +#[serde(deny_unknown_fields)] +struct AngleBrackets { + #[serde(default)] + opening: bool, +} +/// Selects text at the same indentation level. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] +#[action(namespace = vim)] +#[serde(deny_unknown_fields)] +struct CurlyBrackets { + #[serde(default)] + opening: bool, +} + fn cover_or_next, Range)>>( candidates: Option, caret: DisplayPoint, @@ -275,18 +310,10 @@ actions!( DoubleQuotes, /// Selects text within vertical bars (pipes). VerticalBars, - /// Selects text within parentheses. - Parentheses, /// Selects text within the nearest brackets. MiniBrackets, /// Selects text within any type of brackets. AnyBrackets, - /// Selects text within square brackets. - SquareBrackets, - /// Selects text within curly brackets. - CurlyBrackets, - /// Selects text within angle brackets. - AngleBrackets, /// Selects a function argument. Argument, /// Selects an HTML/XML tag. @@ -350,17 +377,17 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| { vim.object(Object::DoubleQuotes, window, cx) }); - Vim::action(editor, cx, |vim, _: &Parentheses, window, cx| { - vim.object(Object::Parentheses, window, cx) + Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| { + vim.object_impl(Object::Parentheses, action.opening, window, cx) }); - Vim::action(editor, cx, |vim, _: &SquareBrackets, window, cx| { - vim.object(Object::SquareBrackets, window, cx) + Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| { + vim.object_impl(Object::SquareBrackets, action.opening, window, cx) }); - Vim::action(editor, cx, |vim, _: &CurlyBrackets, window, cx| { - vim.object(Object::CurlyBrackets, window, cx) + Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| { + vim.object_impl(Object::CurlyBrackets, action.opening, window, cx) }); - Vim::action(editor, cx, |vim, _: &AngleBrackets, window, cx| { - vim.object(Object::AngleBrackets, window, cx) + Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| { + vim.object_impl(Object::AngleBrackets, action.opening, window, cx) }); Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| { vim.object(Object::VerticalBars, window, cx) @@ -394,10 +421,22 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { impl Vim { fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { + self.object_impl(object, false, window, cx); + } + + fn object_impl( + &mut self, + object: Object, + opening: bool, + window: &mut Window, + cx: &mut Context, + ) { let count = Self::take_count(cx); match self.mode { - Mode::Normal | Mode::HelixNormal => self.normal_object(object, count, window, cx), + Mode::Normal | Mode::HelixNormal => { + self.normal_object(object, count, opening, window, cx) + } Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { self.visual_object(object, count, window, cx) } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 3458a92442a3ec76ebce581bf798fc57509f7d53..88a100fc2abb90005256548395959c596167c148 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -109,6 +109,9 @@ pub enum Operator { }, ChangeSurrounds { target: Option, + /// Represents whether the opening bracket was used for the target + /// object. + opening: bool, }, DeleteSurrounds, Mark, @@ -1077,7 +1080,9 @@ impl Operator { | Operator::Replace | Operator::Digraph { .. } | Operator::Literal { .. } - | Operator::ChangeSurrounds { target: Some(_) } + | Operator::ChangeSurrounds { + target: Some(_), .. + } | Operator::DeleteSurrounds => true, Operator::Change | Operator::Delete @@ -1094,7 +1099,7 @@ impl Operator { | Operator::ReplaceWithRegister | Operator::Exchange | Operator::Object { .. } - | Operator::ChangeSurrounds { target: None } + | Operator::ChangeSurrounds { target: None, .. } | Operator::OppositeCase | Operator::ToggleComments | Operator::HelixMatch @@ -1121,7 +1126,7 @@ impl Operator { | Operator::Rewrap | Operator::ShellCommand | Operator::AddSurrounds { target: None } - | Operator::ChangeSurrounds { target: None } + | Operator::ChangeSurrounds { target: None, .. } | Operator::DeleteSurrounds | Operator::Exchange | Operator::HelixNext { .. } diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 78fa02f69599c767aa1e196e09e631bdcfc006f0..e1b46f56a9e8b934e8c8e55d144b8eb325352375 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -221,6 +221,7 @@ impl Vim { &mut self, text: Arc, target: Object, + opening: bool, window: &mut Window, cx: &mut Context, ) { @@ -241,16 +242,19 @@ impl Vim { }, }; - // Determines whether space should be added after - // and before the surround pairs. - // Space is only added in the following cases: - // - new surround is not quote and is opening bracket (({[<) - // - new surround is quote and original was also quote - let surround = if pair.start != pair.end { - pair.end != surround_alias((*text).as_ref()) - } else { - will_replace_pair.start == will_replace_pair.end - }; + // 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, selections) = editor.selections.all_adjusted_display(cx); let mut edits = Vec::new(); @@ -269,23 +273,36 @@ impl Vim { 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 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; let mut end = start + 1; - if let Some((next_ch, _)) = chars_and_offset.peek() { - // If the next position is already a space or line break, - // we don't need to splice another space even under around - if surround && !next_ch.is_whitespace() { - open_str.push(' '); - } else if !surround && next_ch.to_string() == " " { - end += 1; + while let Some((next_ch, _)) = chars_and_offset.next() + && next_ch.to_string() == " " + { + end += 1; + + if preserve_space { + open_str.push(next_ch); } } + + if add_space { + open_str.push(' '); + }; + + edit_len = end - start; edits.push((start..end, open_str)); anchors.push(start..start); break; @@ -299,16 +316,25 @@ impl Vim { .peekable(); while let Some((ch, offset)) = reverse_chars_and_offsets.next() { if ch.to_string() == will_replace_pair.end { - let mut close_str = pair.end.clone(); + let mut close_str = String::new(); let mut start = offset; let end = start + 1; - if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() { - if surround && !next_ch.is_whitespace() { - close_str.insert(0, ' ') - } else if !surround && next_ch.to_string() == " " { - start -= 1; + while let Some((next_ch, _)) = reverse_chars_and_offsets.next() + && next_ch.to_string() == " " + && close_str.len() < edit_len - 1 + { + 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; } @@ -448,7 +474,7 @@ impl Vim { surround: true, newline: false, }), - Object::CurlyBrackets => Some(BracketPair { + Object::CurlyBrackets { .. } => Some(BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, @@ -1194,7 +1220,30 @@ mod test { };"}, Mode::Normal, ); - cx.simulate_keystrokes("c s { ["); + cx.simulate_keystrokes("c s } ]"); + cx.assert_state( + indoc! {" + fn test_surround() ˇ[ + if 2 > 1 ˇ[ + println!(\"it is fine\"); + ] + ];"}, + 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() ˇ[ @@ -1270,7 +1319,7 @@ mod test { cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); - cx.simulate_keystrokes("c s b {"); + cx.simulate_keystrokes("c s b }"); cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); cx.set_state( @@ -1290,6 +1339,66 @@ mod test { ); } + // 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; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e01d1065b99aa6791bf79d8df26fe354c562284c..4999a4b04c2005ad2371b4e82c7f23578269c7bc 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -678,6 +678,7 @@ impl Vim { vim.push_operator( Operator::ChangeSurrounds { target: action.target, + opening: false, }, window, cx, @@ -945,6 +946,7 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx) }); + return; } } else if window.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() @@ -1780,10 +1782,10 @@ impl Vim { } _ => self.clear_operator(window, cx), }, - Some(Operator::ChangeSurrounds { target }) => match self.mode { + Some(Operator::ChangeSurrounds { target, opening }) => match self.mode { Mode::Normal => { if let Some(target) = target { - self.change_surrounds(text, target, window, cx); + self.change_surrounds(text, target, opening, window, cx); self.clear_operator(window, cx); } }