vim: Update change surrounds to match vim's behavior (#38721)
These changes refactor the whitespace handling logic for Vim's change surrounds command (`cs`), making its behavior closely match [tpope/vim-surround](https://github.com/tpope/vim-surround), following [this discussion](https://github.com/zed-industries/zed/issues/38169#issuecomment-3304129461). Zed's current implementation has two main differences when compared to [tpope/vim-surround](https://github.com/tpope/vim-surround): - It only considers whether a single space should be added or removed, instead of all the space that is between the surrounding character and the content - It only takes into consideration the new surrounding characters in order to determine whether to add or remove that space A review of [tpope/vim-surround](https://github.com/tpope/vim-surround)'s behavior reveals these rules for whitespace: * Quote to Quote * Whitespace is never changed * Quote to Bracket * If opening bracket, add one space * If closing bracket, do not add space * Bracket to Bracket * If opening to opening, keep only one space * If opening to closing, remove all space * If closing to opening, add one space * If closing to closing, do not change space * Bracket to Quote * If opening, remove all space * If closing, preserve all space Below is a table with examples for each scenario. A new test has also been added to specifically check the scenarios outlined above, `vim::surrounds::test::test_change_surrounds_vim`. | Type | Before | Command | After | |-------------------|-------------|---------|---------------| | Quote → Quote | `' a '` | `cs'"` | `" a "` | | Quote → Quote | `" a "` | `cs"'` | `' a '` | | Quote → Bracket | `' a '` | `cs'{` | `{ a }` | | Quote → Bracket | `' a '` | `cs'}` | `{ a }` | | Bracket → Bracket | `[ a ]` | `cs[{` | `{ a }` | | Bracket → Bracket | `[ a ]` | `cs[}` | `{a}` | | Bracket → Bracket | `[ a ]` | `cs]{` | `{ a }` | | Bracket → Bracket | `[ a ]` | `cs]}` | `{ a }` | | Bracket → Quote | `[ a ]` | `cs['` | `'a'` | | Bracket → Quote | `[ a ]` | `cs]'` | `' a '` | These changes diverge from [tpope/vim-surround](https://github.com/tpope/vim-surround) when handling newlines. For example, with the following snippet: ```rust fn test_surround() { if 2 > 1 { println!("place cursor here"); } }; ``` Placing the cursor inside the string and running any combination of `cs{[`, `cs{]`, `cs}[`, or `cs}]` would previously remove newline characters. With these changes, using commands like `cs}]` will now preserve newlines. Related to #38169 Closes #39334 Release Notes: - Improved Vim’s change surround command to closely match [tpope/vim-surround](https://github.com/tpope/vim-surround) behavior. --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
+56
-17
@@ -85,6 +85,41 @@ pub struct CandidateWithRanges {
|
||||
close_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// 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<I: Iterator<Item = (Range<usize>, Range<usize>)>>(
|
||||
candidates: Option<I>,
|
||||
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>) {
|
||||
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<Vim>) {
|
||||
|
||||
impl Vim {
|
||||
fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.object_impl(object, false, window, cx);
|
||||
}
|
||||
|
||||
fn object_impl(
|
||||
&mut self,
|
||||
object: Object,
|
||||
opening: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user