Add surrounds support for vim (#9400)

For #4965

There are still some minor issues: 
1. When change the surround and delete the surround, we should also
decide whether there are spaces inside after deleting/replacing
according to whether it is open parentheses, and replace them
accordingly, but at present, delete and change, haven't done this
adaptation for current pr, I'm not sure if I can fit it in the back or
if it needs to be fitted together.
2. In the selection mode, pressing s plus brackets should also trigger
the Add Surrounds function, but this MR has not adapted the selection
mode for the time being, I think we need to support different add
behaviors for the three selection modes.(Currently in select mode, s is
used for Substitute)
3. For the current change surrounds, if the user does not find the
bracket that needs to be matched after entering cs, but it is a valid
bracket, and will wait for the second input before failing, the better
practice here should be to return to normal mode if the first bracket is
not found
4. I reused BracketPair in language, but two of its properties weren't
used in this mr, so I'm not sure if I should create a new struct with
only start and end, which would have less code

I'm not sure which ones need to be changed in the first issue, and which
ones can be revised in the future, and it seems that they can be solved

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Hans 2024-04-09 01:41:06 +08:00 committed by GitHub
parent e826ef83e2
commit 44aed4a0cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1033 additions and 15 deletions

View File

@ -384,18 +384,46 @@
"d": "editor::Rename" // zed specific "d": "editor::Rename" // zed specific
} }
}, },
{
"context": "Editor && vim_mode == normal && vim_operator == c",
"bindings": {
"s": [
"vim::PushOperator",
{
"ChangeSurrounds": {}
}
]
}
},
{ {
"context": "Editor && vim_operator == d", "context": "Editor && vim_operator == d",
"bindings": { "bindings": {
"d": "vim::CurrentLine" "d": "vim::CurrentLine"
} }
}, },
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {
"s": ["vim::PushOperator", "DeleteSurrounds"]
}
},
{ {
"context": "Editor && vim_operator == y", "context": "Editor && vim_operator == y",
"bindings": { "bindings": {
"y": "vim::CurrentLine" "y": "vim::CurrentLine"
} }
}, },
{
"context": "Editor && vim_mode == normal && vim_operator == y",
"bindings": {
"s": [
"vim::PushOperator",
{
"AddSurrounds": {}
}
]
}
},
{ {
"context": "Editor && VimObject", "context": "Editor && VimObject",
"bindings": { "bindings": {

View File

@ -6,13 +6,14 @@ use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, To
use gpui::{px, Pixels, WindowTextSystem}; use gpui::{px, Pixels, WindowTextSystem};
use language::Point; use language::Point;
use multi_buffer::MultiBufferSnapshot; use multi_buffer::MultiBufferSnapshot;
use serde::Deserialize;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
/// Defines search strategy for items in `movement` module. /// Defines search strategy for items in `movement` module.
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas /// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
/// `FindRange::MultiLine` keeps going until the end of a string. /// `FindRange::MultiLine` keeps going until the end of a string.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub enum FindRange { pub enum FindRange {
SingleLine, SingleLine,
MultiLine, MultiLine,

View File

@ -14,12 +14,13 @@ use workspace::Workspace;
use crate::{ use crate::{
normal::normal_motion, normal::normal_motion,
state::{Mode, Operator}, state::{Mode, Operator},
surrounds::SurroundsType,
utils::coerce_punctuation, utils::coerce_punctuation,
visual::visual_motion, visual::visual_motion,
Vim, Vim,
}; };
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Motion { pub enum Motion {
Left, Left,
Backspace, Backspace,
@ -386,15 +387,31 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
} }
let count = Vim::update(cx, |vim, cx| vim.take_count(cx)); let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
let operator = Vim::read(cx).active_operator(); let active_operator = Vim::read(cx).active_operator();
let mut waiting_operator: Option<Operator> = None;
match Vim::read(cx).state().mode { match Vim::read(cx).state().mode {
Mode::Normal | Mode::Replace => normal_motion(motion, operator, count, cx), Mode::Normal | Mode::Replace => {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx), if active_operator == Some(Operator::AddSurrounds { target: None }) {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Motion(motion)),
});
} else {
normal_motion(motion.clone(), active_operator.clone(), count, cx)
}
}
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
visual_motion(motion.clone(), count, cx)
}
Mode::Insert => { Mode::Insert => {
// Shouldn't execute a motion in insert mode. Ignoring // Shouldn't execute a motion in insert mode. Ignoring
} }
} }
Vim::update(cx, |vim, cx| vim.clear_operator(cx)); Vim::update(cx, |vim, cx| {
vim.clear_operator(cx);
if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
}
});
} }
// Motion handling is specified here: // Motion handling is specified here:

View File

@ -15,6 +15,7 @@ use crate::{
motion::{self, first_non_whitespace, next_line_end, right, Motion}, motion::{self, first_non_whitespace, next_line_end, right, Motion},
object::Object, object::Object,
state::{Mode, Operator}, state::{Mode, Operator},
surrounds::{check_and_move_to_valid_bracket_pair, SurroundsType},
Vim, Vim,
}; };
use collections::BTreeSet; use collections::BTreeSet;
@ -178,6 +179,7 @@ pub fn normal_motion(
Some(Operator::Change) => change_motion(vim, motion, times, cx), Some(Operator::Change) => change_motion(vim, motion, times, cx),
Some(Operator::Delete) => delete_motion(vim, motion, times, cx), Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx), Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
Some(Operator::AddSurrounds { target: None }) => {}
Some(operator) => { Some(operator) => {
// Can't do anything for text objects, Ignoring // Can't do anything for text objects, Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator) error!("Unexpected normal mode motion operator: {:?}", operator)
@ -188,21 +190,40 @@ pub fn normal_motion(
pub fn normal_object(object: Object, cx: &mut WindowContext) { pub fn normal_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
let mut waiting_operator: Option<Operator> = None;
match vim.maybe_pop_operator() { match vim.maybe_pop_operator() {
Some(Operator::Object { around }) => match vim.maybe_pop_operator() { Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
Some(Operator::Change) => change_object(vim, object, around, cx), Some(Operator::Change) => change_object(vim, object, around, cx),
Some(Operator::Delete) => delete_object(vim, object, around, cx), Some(Operator::Delete) => delete_object(vim, object, around, cx),
Some(Operator::Yank) => yank_object(vim, object, around, cx), Some(Operator::Yank) => yank_object(vim, object, around, cx),
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object)),
});
}
_ => { _ => {
// Can't do anything for namespace operators. Ignoring // Can't do anything for namespace operators. Ignoring
} }
}, },
Some(Operator::DeleteSurrounds) => {
waiting_operator = Some(Operator::DeleteSurrounds);
}
Some(Operator::ChangeSurrounds { target: None }) => {
if check_and_move_to_valid_bracket_pair(vim, object, cx) {
waiting_operator = Some(Operator::ChangeSurrounds {
target: Some(object),
});
}
}
_ => { _ => {
// Can't do anything with change/delete/yank and text objects. Ignoring // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring
} }
} }
vim.clear_operator(cx); vim.clear_operator(cx);
}) if let Some(operator) = waiting_operator {
vim.push_operator(operator, cx);
}
});
} }
pub(crate) fn move_cursor( pub(crate) fn move_cursor(

View File

@ -14,7 +14,7 @@ use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
use serde::Deserialize; use serde::Deserialize;
use workspace::Workspace; use workspace::Workspace;
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Object { pub enum Object {
Word { ignore_punctuation: bool }, Word { ignore_punctuation: bool },
Sentence, Sentence,

View File

@ -1,6 +1,7 @@
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range, sync::Arc};
use crate::motion::Motion; use crate::surrounds::SurroundsType;
use crate::{motion::Motion, object::Object};
use collections::HashMap; use collections::HashMap;
use editor::Anchor; use editor::Anchor;
use gpui::{Action, KeyContext}; use gpui::{Action, KeyContext};
@ -55,6 +56,9 @@ pub enum Operator {
Object { around: bool }, Object { around: bool },
FindForward { before: bool }, FindForward { before: bool },
FindBackward { after: bool }, FindBackward { after: bool },
AddSurrounds { target: Option<SurroundsType> },
ChangeSurrounds { target: Option<Object> },
DeleteSurrounds,
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@ -253,15 +257,21 @@ impl Operator {
Operator::FindForward { before: true } => "t", Operator::FindForward { before: true } => "t",
Operator::FindBackward { after: false } => "F", Operator::FindBackward { after: false } => "F",
Operator::FindBackward { after: true } => "T", Operator::FindBackward { after: true } => "T",
Operator::AddSurrounds { .. } => "ys",
Operator::ChangeSurrounds { .. } => "cs",
Operator::DeleteSurrounds => "ds",
} }
} }
pub fn context_flags(&self) -> &'static [&'static str] { pub fn context_flags(&self) -> &'static [&'static str] {
match self { match self {
Operator::Object { .. } => &["VimObject"], Operator::Object { .. } | Operator::ChangeSurrounds { target: None } => &["VimObject"],
Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace => { Operator::FindForward { .. }
&["VimWaiting"] | Operator::FindBackward { .. }
} | Operator::Replace
| Operator::AddSurrounds { target: Some(_) }
| Operator::ChangeSurrounds { .. }
| Operator::DeleteSurrounds => &["VimWaiting"],
_ => &[], _ => &[],
} }
} }

907
crates/vim/src/surrounds.rs Normal file
View File

@ -0,0 +1,907 @@
use crate::{motion::Motion, object::Object, state::Mode, Vim};
use editor::{scroll::Autoscroll, Bias};
use gpui::WindowContext;
use language::BracketPair;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum SurroundsType {
Motion(Motion),
Object(Object),
}
pub fn add_surrounds(text: Arc<str>, target: SurroundsType, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, 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,
newline: false,
},
};
let surround = pair.end != *text;
let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
let mut edits = Vec::new();
let mut anchors = Vec::new();
for selection in &display_selections {
let range = match &target {
SurroundsType::Object(object) => {
object.range(&display_map, selection.clone(), false)
}
SurroundsType::Motion(motion) => motion.range(
&display_map,
selection.clone(),
Some(1),
true,
&text_layout_details,
),
};
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 =
format!("{}{}", pair.start, if surround { " " } else { "" });
let close_cursor_str =
format!("{}{}", if surround { " " } else { "" }, pair.end);
let start_anchor = display_map.buffer_snapshot.anchor_before(start);
edits.push((start..start, start_cursor_str));
edits.push((end..end, close_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.buffer().update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(anchors)
});
});
});
vim.switch_mode(Mode::Normal, false, cx);
});
}
pub fn delete_surrounds(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
// 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;
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, display_selections) = editor.selections.all_display(cx);
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) {
// 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 + 1;
if surround {
if let Some((next_ch, _)) = chars_and_offset.peek() {
if 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 + 1;
if surround {
if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
if next_ch.eq(&' ') {
start -= 1;
}
}
}
edits.push((start..end, ""));
break;
}
}
} else {
anchors.push(start..start);
}
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(anchors);
});
edits.sort_by_key(|(range, _)| range.start);
editor.buffer().update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
editor.set_clip_at_line_ends(true, cx);
});
});
});
}
pub fn change_surrounds(text: Arc<str>, target: Object, cx: &mut WindowContext) {
if let Some(will_replace_pair) = object_to_bracket_pair(target) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, 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,
newline: false,
},
};
let surround = pair.end != *text;
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
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) {
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;
}
}
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;
match chars_and_offset.peek() {
Some((next_ch, _)) => {
// If the next position is already a space or line break,
// we don't need to splice another space even under arround
if surround && !next_ch.is_whitespace() {
open_str.push_str(" ");
} else if !surround && next_ch.to_string() == " " {
end += 1;
}
}
None => {}
}
edits.push((start..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 = pair.end.clone();
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_str(0, " ")
} else if !surround && next_ch.to_string() == " " {
start -= 1;
}
}
edits.push((start..end, close_str));
break;
}
}
} else {
anchors.push(start..start);
}
}
let stable_anchors = editor
.selections
.disjoint_anchors()
.into_iter()
.map(|selection| {
let start = selection.start.bias_left(&display_map.buffer_snapshot);
start..start
})
.collect::<Vec<_>>();
edits.sort_by_key(|(range, _)| range.start);
editor.buffer().update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), 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(
vim: &mut Vim,
object: Object,
cx: &mut WindowContext,
) -> bool {
let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
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) {
// 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 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 {
anchors.push(offset..offset);
break;
}
}
} else {
anchors.push(start..start)
}
} else {
anchors.push(start..start)
}
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(anchors);
});
editor.set_clip_at_line_ends(true, cx);
});
});
}
return valid;
}
fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
pairs.iter().find(|pair| pair.start == ch || pair.end == ch)
}
fn all_support_surround_pair() -> Vec<BracketPair> {
return vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
},
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
newline: false,
},
BracketPair {
start: "`".into(),
end: "`".into(),
close: true,
newline: false,
},
BracketPair {
start: "\"".into(),
end: "\"".into(),
close: true,
newline: false,
},
BracketPair {
start: "(".into(),
end: ")".into(),
close: true,
newline: false,
},
BracketPair {
start: "|".into(),
end: "|".into(),
close: true,
newline: false,
},
BracketPair {
start: "[".into(),
end: "]".into(),
close: true,
newline: false,
},
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
},
BracketPair {
start: "<".into(),
end: ">".into(),
close: true,
newline: false,
},
];
}
fn pair_to_object(pair: &BracketPair) -> Option<Object> {
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,
}
}
fn object_to_bracket_pair(object: Object) -> Option<BracketPair> {
match object {
Object::Quotes => Some(BracketPair {
start: "'".to_string(),
end: "'".to_string(),
close: true,
newline: false,
}),
Object::BackQuotes => Some(BracketPair {
start: "`".to_string(),
end: "`".to_string(),
close: true,
newline: false,
}),
Object::DoubleQuotes => Some(BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
newline: false,
}),
Object::VerticalBars => Some(BracketPair {
start: "|".to_string(),
end: "|".to_string(),
close: true,
newline: false,
}),
Object::Parentheses => Some(BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
newline: false,
}),
Object::SquareBrackets => Some(BracketPair {
start: "[".to_string(),
end: "]".to_string(),
close: true,
newline: false,
}),
Object::CurlyBrackets => Some(BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: false,
}),
Object::AngleBrackets => Some(BracketPair {
start: "<".to_string(),
end: ">".to_string(),
close: true,
newline: false,
}),
_ => None,
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::{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 arround
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 arround
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,
);
}
#[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 arround
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 arround
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,
);
}
#[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,
);
}
}

View File

@ -12,6 +12,7 @@ mod normal;
mod object; mod object;
mod replace; mod replace;
mod state; mod state;
mod surrounds;
mod utils; mod utils;
mod visual; mod visual;
@ -37,6 +38,7 @@ use serde_derive::Serialize;
use settings::{update_settings_file, Settings, SettingsStore}; use settings::{update_settings_file, Settings, SettingsStore};
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState}; use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use surrounds::{add_surrounds, change_surrounds, delete_surrounds};
use ui::BorrowAppContext; use ui::BorrowAppContext;
use visual::{visual_block_motion, visual_replace}; use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace}; use workspace::{self, Workspace};
@ -170,7 +172,14 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext)
} }
Vim::update(cx, |vim, cx| match vim.active_operator() { Vim::update(cx, |vim, cx| match vim.active_operator() {
Some(Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace) => {} Some(
Operator::FindForward { .. }
| Operator::FindBackward { .. }
| Operator::Replace
| Operator::AddSurrounds { .. }
| Operator::ChangeSurrounds { .. }
| Operator::DeleteSurrounds,
) => {}
Some(_) => { Some(_) => {
vim.clear_operator(cx); vim.clear_operator(cx);
} }
@ -622,6 +631,31 @@ impl Vim {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
}, },
Some(Operator::AddSurrounds { target }) => match Vim::read(cx).state().mode {
Mode::Normal => {
if let Some(target) = target {
add_surrounds(text, target, cx);
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
}
}
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
},
Some(Operator::ChangeSurrounds { target }) => match Vim::read(cx).state().mode {
Mode::Normal => {
if let Some(target) = target {
change_surrounds(text, target, cx);
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
}
}
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
},
Some(Operator::DeleteSurrounds) => match Vim::read(cx).state().mode {
Mode::Normal => {
delete_surrounds(text, cx);
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
}
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
},
_ => match Vim::read(cx).state().mode { _ => match Vim::read(cx).state().mode {
Mode::Replace => multi_replace(text, cx), Mode::Replace => multi_replace(text, cx),
_ => {} _ => {}