Re-applies https://github.com/zed-industries/zed/pull/30840 This PR re-applies the initial [PR](https://github.com/zed-industries/zed/pull/30840). As it was closed because it was hard to land, because of the many conflicts. This PR re-applies the changes for it. In several cases we were creating multiple display_map snapshots within the same root-level function call. Creating a display_map snapshot is quite slow, and in some cases we were creating the snapshot multiple times. Release Notes: - N/A
1687 lines
55 KiB
Rust
1687 lines
55 KiB
Rust
use crate::{
|
||
Vim,
|
||
motion::{self, Motion},
|
||
object::{Object, surrounding_markers},
|
||
state::Mode,
|
||
};
|
||
use editor::{Bias, 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<str>,
|
||
target: SurroundsType,
|
||
window: &mut Window,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
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<str>,
|
||
window: &mut Window,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
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 + 1;
|
||
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 + 1;
|
||
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<str>,
|
||
target: Object,
|
||
opening: bool,
|
||
window: &mut Window,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
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 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;
|
||
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;
|
||
}
|
||
}
|
||
|
||
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 + 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;
|
||
}
|
||
}
|
||
} 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::<Vec<_>>();
|
||
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<Self>,
|
||
) -> 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<Self>,
|
||
) -> Option<BracketPair> {
|
||
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<BracketPair> {
|
||
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<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,
|
||
}
|
||
}
|
||
|
||
#[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,
|
||
);
|
||
|
||
// 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! {"<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! {"(< 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 ˇ<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 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 ˇ<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 ( 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 <quˇick> 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,
|
||
);
|
||
}
|
||
}
|