This PR introduces a new `MultiBufferOffset` new type wrapping size. The goal of this is to make it clear at the type level when we are interacting with offsets of a multi buffer versus offsets of a language / text buffer. This improves readability of things quite a bit by making it clear what kind of offsets one is working with while also reducing accidental bugs by using the wrong kin of offset for the wrong API. This PR also uncovered two minor bugs due to that. Does not yet introduce the MultiBufferPoint equivalent, that is for a follow up PR. Release Notes: - N/A *or* Added/Fixed/Improved ...
1706 lines
56 KiB
Rust
1706 lines
56 KiB
Rust
use crate::{
|
||
Vim,
|
||
motion::{self, Motion},
|
||
object::{Object, surrounding_markers},
|
||
state::Mode,
|
||
};
|
||
use editor::{Bias, MultiBufferOffset, movement};
|
||
use gpui::{Context, Window};
|
||
use language::BracketPair;
|
||
|
||
use std::sync::Arc;
|
||
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum SurroundsType {
|
||
Motion(Motion),
|
||
Object(Object, bool),
|
||
Selection,
|
||
}
|
||
|
||
impl Vim {
|
||
pub fn add_surrounds(
|
||
&mut self,
|
||
text: Arc<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 + 1usize;
|
||
if surround
|
||
&& let Some((next_ch, _)) = chars_and_offset.peek()
|
||
&& next_ch.eq(&' ')
|
||
{
|
||
end += 1;
|
||
}
|
||
edits.push((start..end, ""));
|
||
anchors.push(start..start);
|
||
break;
|
||
}
|
||
}
|
||
let mut reverse_chars_and_offsets = display_map
|
||
.reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
|
||
.peekable();
|
||
while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
|
||
if ch.to_string() == pair.end {
|
||
let mut start = offset;
|
||
let end = start + 1usize;
|
||
if surround
|
||
&& let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
|
||
&& next_ch.eq(&' ')
|
||
{
|
||
start -= 1;
|
||
}
|
||
edits.push((start..end, ""));
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
anchors.push(start..start);
|
||
}
|
||
}
|
||
|
||
editor.change_selections(Default::default(), window, cx, |s| {
|
||
s.select_ranges(anchors);
|
||
});
|
||
edits.sort_by_key(|(range, _)| range.start);
|
||
editor.edit(edits, cx);
|
||
editor.set_clip_at_line_ends(true, cx);
|
||
});
|
||
});
|
||
}
|
||
|
||
pub fn change_surrounds(
|
||
&mut self,
|
||
text: Arc<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 open_range_end = MultiBufferOffset(0);
|
||
let mut chars_and_offset = display_map
|
||
.buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
|
||
.peekable();
|
||
|
||
while let Some((ch, offset)) = chars_and_offset.next() {
|
||
if ch.to_string() == will_replace_pair.start {
|
||
let mut open_str = pair.start.clone();
|
||
let start = offset;
|
||
open_range_end = start + 1usize;
|
||
while let Some((next_ch, _)) = chars_and_offset.next()
|
||
&& next_ch == ' '
|
||
{
|
||
open_range_end += 1;
|
||
|
||
if preserve_space {
|
||
open_str.push(next_ch);
|
||
}
|
||
}
|
||
|
||
if add_space {
|
||
open_str.push(' ');
|
||
};
|
||
|
||
edit_len = open_range_end - start;
|
||
edits.push((start..open_range_end, open_str));
|
||
anchors.push(start..start);
|
||
break;
|
||
}
|
||
}
|
||
|
||
let mut reverse_chars_and_offsets = display_map
|
||
.reverse_buffer_chars_at(
|
||
range.end.to_offset(&display_map, Bias::Left),
|
||
)
|
||
.peekable();
|
||
while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
|
||
if ch.to_string() == will_replace_pair.end {
|
||
let mut close_str = String::new();
|
||
let mut start = offset;
|
||
let end = start + 1usize;
|
||
while let Some((next_ch, _)) = reverse_chars_and_offsets.next()
|
||
&& next_ch == ' '
|
||
&& close_str.len() < edit_len - 1
|
||
&& start > open_range_end
|
||
{
|
||
start -= 1;
|
||
|
||
if preserve_space {
|
||
close_str.push(next_ch);
|
||
}
|
||
}
|
||
|
||
if add_space {
|
||
close_str.push(' ');
|
||
};
|
||
|
||
close_str.push_str(&pair.end);
|
||
edits.push((start..end, close_str));
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
anchors.push(start..start);
|
||
}
|
||
}
|
||
|
||
let stable_anchors = editor
|
||
.selections
|
||
.disjoint_anchors_arc()
|
||
.iter()
|
||
.map(|selection| {
|
||
let start = selection.start.bias_left(&display_map.buffer_snapshot());
|
||
start..start
|
||
})
|
||
.collect::<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,
|
||
);
|
||
|
||
// test spaces with quote change surrounds
|
||
cx.set_state(
|
||
indoc! {"
|
||
fn test_surround() {
|
||
\"ˇ \"
|
||
};"},
|
||
Mode::Normal,
|
||
);
|
||
cx.simulate_keystrokes("c s \" '");
|
||
cx.assert_state(
|
||
indoc! {"
|
||
fn test_surround() {
|
||
ˇ' '
|
||
};"},
|
||
Mode::Normal,
|
||
);
|
||
|
||
// Currently, the same test case but using the closing bracket `]`
|
||
// actually removes a whitespace before the closing bracket, something
|
||
// that might need to be fixed?
|
||
cx.set_state(
|
||
indoc! {"
|
||
fn test_surround() {
|
||
ifˇ 2 > 1 {
|
||
ˇprintln!(\"it is fine\");
|
||
}
|
||
};"},
|
||
Mode::Normal,
|
||
);
|
||
cx.simulate_keystrokes("c s { ]");
|
||
cx.assert_state(
|
||
indoc! {"
|
||
fn test_surround() ˇ[
|
||
if 2 > 1 ˇ[
|
||
println!(\"it is fine\");
|
||
]
|
||
];"},
|
||
Mode::Normal,
|
||
);
|
||
|
||
// test change quotes.
|
||
cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal);
|
||
cx.simulate_keystrokes("c s ' \"");
|
||
cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal);
|
||
|
||
// test multi cursor change quotes
|
||
cx.set_state(
|
||
indoc! {"
|
||
' ˇstr '
|
||
some example text here
|
||
ˇ' str '
|
||
"},
|
||
Mode::Normal,
|
||
);
|
||
cx.simulate_keystrokes("c s ' \"");
|
||
cx.assert_state(
|
||
indoc! {"
|
||
ˇ\" str \"
|
||
some example text here
|
||
ˇ\" str \"
|
||
"},
|
||
Mode::Normal,
|
||
);
|
||
|
||
// test quote to bracket spacing.
|
||
cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
|
||
cx.simulate_keystrokes("c s ' {");
|
||
cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);
|
||
|
||
cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
|
||
cx.simulate_keystrokes("c s ' }");
|
||
cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
|
||
let mut cx = VimTestContext::new(cx, true).await;
|
||
|
||
// Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
|
||
// action.
|
||
cx.update(|_, cx| {
|
||
cx.bind_keys([KeyBinding::new(
|
||
"b",
|
||
AnyBrackets,
|
||
Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
|
||
)]);
|
||
});
|
||
|
||
cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
|
||
cx.simulate_keystrokes("c s b [");
|
||
cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
|
||
|
||
cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
|
||
cx.simulate_keystrokes("c s b {");
|
||
cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);
|
||
|
||
cx.set_state(indoc! {"<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,
|
||
);
|
||
}
|
||
}
|