vim: Respect count for paragraphs (#33489)
Closes #32462 Release Notes: - vim: Paragraph objects now support counts (`d2ap`, `v2ap`, etc.) --------- Co-authored-by: Rift <no@e.mail> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
ba4fc1bcfc
commit
97c5c5a6e7
17 changed files with 182 additions and 54 deletions
|
@ -1609,7 +1609,7 @@ impl Vim {
|
|||
let snapshot = editor.snapshot(window, cx);
|
||||
let start = editor.selections.newest_display(cx);
|
||||
let range = object
|
||||
.range(&snapshot, start.clone(), around)
|
||||
.range(&snapshot, start.clone(), around, None)
|
||||
.unwrap_or(start.range());
|
||||
if range.start != start.start {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
|
|
|
@ -122,6 +122,7 @@ impl Vim {
|
|||
object: Object,
|
||||
around: bool,
|
||||
dir: IndentDirection,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -133,7 +134,7 @@ impl Vim {
|
|||
s.move_with(|map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
match dir {
|
||||
|
|
|
@ -277,40 +277,51 @@ impl Vim {
|
|||
self.exit_temporary_normal(window, cx);
|
||||
}
|
||||
|
||||
pub fn normal_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn normal_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut waiting_operator: Option<Operator> = None;
|
||||
match self.maybe_pop_operator() {
|
||||
Some(Operator::Object { around }) => match self.maybe_pop_operator() {
|
||||
Some(Operator::Change) => self.change_object(object, around, window, cx),
|
||||
Some(Operator::Delete) => self.delete_object(object, around, window, cx),
|
||||
Some(Operator::Yank) => self.yank_object(object, around, window, cx),
|
||||
Some(Operator::Change) => self.change_object(object, around, times, window, cx),
|
||||
Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
|
||||
Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
|
||||
Some(Operator::Indent) => {
|
||||
self.indent_object(object, around, IndentDirection::In, window, cx)
|
||||
self.indent_object(object, around, IndentDirection::In, times, window, cx)
|
||||
}
|
||||
Some(Operator::Outdent) => {
|
||||
self.indent_object(object, around, IndentDirection::Out, window, cx)
|
||||
self.indent_object(object, around, IndentDirection::Out, times, window, cx)
|
||||
}
|
||||
Some(Operator::AutoIndent) => {
|
||||
self.indent_object(object, around, IndentDirection::Auto, window, cx)
|
||||
self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
|
||||
}
|
||||
Some(Operator::ShellCommand) => {
|
||||
self.shell_command_object(object, around, window, cx);
|
||||
}
|
||||
Some(Operator::Rewrap) => self.rewrap_object(object, around, window, cx),
|
||||
Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
|
||||
Some(Operator::Lowercase) => {
|
||||
self.convert_object(object, around, ConvertTarget::LowerCase, window, cx)
|
||||
self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
|
||||
}
|
||||
Some(Operator::Uppercase) => {
|
||||
self.convert_object(object, around, ConvertTarget::UpperCase, window, cx)
|
||||
}
|
||||
Some(Operator::OppositeCase) => {
|
||||
self.convert_object(object, around, ConvertTarget::OppositeCase, window, cx)
|
||||
self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
|
||||
}
|
||||
Some(Operator::OppositeCase) => self.convert_object(
|
||||
object,
|
||||
around,
|
||||
ConvertTarget::OppositeCase,
|
||||
times,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
Some(Operator::Rot13) => {
|
||||
self.convert_object(object, around, ConvertTarget::Rot13, window, cx)
|
||||
self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
|
||||
}
|
||||
Some(Operator::Rot47) => {
|
||||
self.convert_object(object, around, ConvertTarget::Rot47, window, cx)
|
||||
self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
|
||||
}
|
||||
Some(Operator::AddSurrounds { target: None }) => {
|
||||
waiting_operator = Some(Operator::AddSurrounds {
|
||||
|
@ -318,7 +329,7 @@ impl Vim {
|
|||
});
|
||||
}
|
||||
Some(Operator::ToggleComments) => {
|
||||
self.toggle_comments_object(object, around, window, cx)
|
||||
self.toggle_comments_object(object, around, times, window, cx)
|
||||
}
|
||||
Some(Operator::ReplaceWithRegister) => {
|
||||
self.replace_with_register_object(object, around, window, cx)
|
||||
|
|
|
@ -105,6 +105,7 @@ impl Vim {
|
|||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -115,7 +116,7 @@ impl Vim {
|
|||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
objects_found |= object.expand_selection(map, selection, around);
|
||||
objects_found |= object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
if objects_found {
|
||||
|
|
|
@ -82,6 +82,7 @@ impl Vim {
|
|||
object: Object,
|
||||
around: bool,
|
||||
mode: ConvertTarget,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -92,7 +93,7 @@ impl Vim {
|
|||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
original_positions.insert(
|
||||
selection.id,
|
||||
map.display_point_to_anchor(selection.start, Bias::Left),
|
||||
|
|
|
@ -91,6 +91,7 @@ impl Vim {
|
|||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -103,7 +104,7 @@ impl Vim {
|
|||
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||
let mut move_selection_start_to_previous_line =
|
||||
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
|
||||
|
|
|
@ -240,7 +240,7 @@ impl Vim {
|
|||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, None);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ impl Vim {
|
|||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -57,7 +58,7 @@ impl Vim {
|
|||
s.move_with(|map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
editor.toggle_comments(&Default::default(), window, cx);
|
||||
|
|
|
@ -66,6 +66,7 @@ impl Vim {
|
|||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -75,7 +76,7 @@ impl Vim {
|
|||
let mut start_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
let start_position = (selection.start, selection.goal);
|
||||
start_positions.insert(selection.id, start_position);
|
||||
});
|
||||
|
|
|
@ -373,10 +373,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
|
||||
impl Vim {
|
||||
fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = Self::take_count(cx);
|
||||
|
||||
match self.mode {
|
||||
Mode::Normal => self.normal_object(object, window, cx),
|
||||
Mode::Normal => self.normal_object(object, count, window, cx),
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
||||
self.visual_object(object, window, cx)
|
||||
self.visual_object(object, count, window, cx)
|
||||
}
|
||||
Mode::Insert | Mode::Replace | Mode::HelixNormal => {
|
||||
// Shouldn't execute a text object in insert mode. Ignoring
|
||||
|
@ -485,6 +487,7 @@ impl Object {
|
|||
map: &DisplaySnapshot,
|
||||
selection: Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let relative_to = selection.head();
|
||||
match self {
|
||||
|
@ -503,7 +506,8 @@ impl Object {
|
|||
}
|
||||
}
|
||||
Object::Sentence => sentence(map, relative_to, around),
|
||||
Object::Paragraph => paragraph(map, relative_to, around),
|
||||
//change others later
|
||||
Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
|
||||
Object::Quotes => {
|
||||
surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
|
||||
}
|
||||
|
@ -692,8 +696,9 @@ impl Object {
|
|||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
) -> bool {
|
||||
if let Some(range) = self.range(map, selection.clone(), around) {
|
||||
if let Some(range) = self.range(map, selection.clone(), around, times) {
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
true
|
||||
|
@ -1399,30 +1404,37 @@ fn paragraph(
|
|||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
around: bool,
|
||||
times: usize,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let mut paragraph_start = start_of_paragraph(map, relative_to);
|
||||
let mut paragraph_end = end_of_paragraph(map, relative_to);
|
||||
|
||||
let paragraph_end_row = paragraph_end.row();
|
||||
let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
|
||||
let point = relative_to.to_point(map);
|
||||
let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
|
||||
for i in 0..times {
|
||||
let paragraph_end_row = paragraph_end.row();
|
||||
let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
|
||||
let point = relative_to.to_point(map);
|
||||
let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
|
||||
|
||||
if around {
|
||||
if paragraph_ends_with_eof {
|
||||
if current_line_is_empty {
|
||||
return None;
|
||||
}
|
||||
if around {
|
||||
if paragraph_ends_with_eof {
|
||||
if current_line_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
let paragraph_start_row = paragraph_start.row();
|
||||
if paragraph_start_row.0 != 0 {
|
||||
let previous_paragraph_last_line_start =
|
||||
DisplayPoint::new(paragraph_start_row - 1, 0);
|
||||
paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
|
||||
let paragraph_start_row = paragraph_start.row();
|
||||
if paragraph_start_row.0 != 0 {
|
||||
let previous_paragraph_last_line_start =
|
||||
Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map);
|
||||
paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
|
||||
}
|
||||
} else {
|
||||
let mut start_row = paragraph_end_row.0 + 1;
|
||||
if i > 0 {
|
||||
start_row += 1;
|
||||
}
|
||||
let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
|
||||
paragraph_end = end_of_paragraph(map, next_paragraph_start);
|
||||
}
|
||||
} else {
|
||||
let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
|
||||
paragraph_end = end_of_paragraph(map, next_paragraph_start);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ impl Vim {
|
|||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut selection = editor.selections.newest_display(cx);
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
object.expand_selection(&snapshot, &mut selection, around);
|
||||
object.expand_selection(&snapshot, &mut selection, around, None);
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(selection.start.to_point(&snapshot));
|
||||
|
|
|
@ -89,6 +89,7 @@ impl Vim {
|
|||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -100,7 +101,7 @@ impl Vim {
|
|||
s.move_with(|map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
editor.rewrap_impl(
|
||||
|
|
|
@ -52,7 +52,7 @@ impl Vim {
|
|||
for selection in &display_selections {
|
||||
let range = match &target {
|
||||
SurroundsType::Object(object, around) => {
|
||||
object.range(&display_map, selection.clone(), *around)
|
||||
object.range(&display_map, selection.clone(), *around, None)
|
||||
}
|
||||
SurroundsType::Motion(motion) => {
|
||||
motion
|
||||
|
@ -150,7 +150,9 @@ impl Vim {
|
|||
|
||||
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 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() {
|
||||
|
@ -247,7 +249,9 @@ impl Vim {
|
|||
|
||||
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 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();
|
||||
|
@ -348,7 +352,9 @@ impl Vim {
|
|||
|
||||
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 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()
|
||||
|
|
|
@ -2031,3 +2031,43 @@ async fn test_delete_unmatched_brace(cx: &mut gpui::TestAppContext) {
|
|||
.await
|
||||
.assert_eq(" oth(wow)\n oth(wow)\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paragraph_multi_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"
|
||||
Emacs is
|
||||
ˇa great
|
||||
|
||||
operating system
|
||||
|
||||
all it lacks
|
||||
is a
|
||||
|
||||
decent text editor
|
||||
"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("2 d a p").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"
|
||||
ˇall it lacks
|
||||
is a
|
||||
|
||||
decent text editor
|
||||
"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("d a p").await;
|
||||
cx.shared_clipboard()
|
||||
.await
|
||||
.assert_eq("all it lacks\nis a\n\n");
|
||||
|
||||
//reset to initial state
|
||||
cx.simulate_shared_keystrokes("2 u").await;
|
||||
|
||||
cx.simulate_shared_keystrokes("4 d a p").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"ˇ"});
|
||||
}
|
||||
|
|
|
@ -364,7 +364,13 @@ impl Vim {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn visual_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Vim>) {
|
||||
pub fn visual_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
count: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Vim>,
|
||||
) {
|
||||
if let Some(Operator::Object { around }) = self.active_operator() {
|
||||
self.pop_operator(window, cx);
|
||||
let current_mode = self.mode;
|
||||
|
@ -390,7 +396,7 @@ impl Vim {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some(range) = object.range(map, mut_selection, around) {
|
||||
if let Some(range) = object.range(map, mut_selection, around, count) {
|
||||
if !range.is_empty() {
|
||||
let expand_both_ways = object.always_expands_both_ways()
|
||||
|| selection.is_empty()
|
||||
|
@ -402,7 +408,7 @@ impl Vim {
|
|||
&& object.always_expands_both_ways()
|
||||
{
|
||||
if let Some(range) =
|
||||
object.range(map, selection.clone(), around)
|
||||
object.range(map, selection.clone(), around, count)
|
||||
{
|
||||
selection.start = range.start;
|
||||
selection.end = range.end;
|
||||
|
@ -1761,4 +1767,26 @@ mod test {
|
|||
});
|
||||
cx.shared_clipboard().await.assert_eq("quick\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_v2ap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"The
|
||||
quicˇk
|
||||
|
||||
brown
|
||||
fox"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v 2 a p").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"«The
|
||||
quick
|
||||
|
||||
brown
|
||||
fˇ»ox"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
18
crates/vim/test_data/test_paragraph_multi_delete.json
Normal file
18
crates/vim/test_data/test_paragraph_multi_delete.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{"Put":{"state":"Emacs is\nˇa great\n\noperating system\n\nall it lacks\nis a\n\ndecent text editor\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇall it lacks\nis a\n\ndecent text editor\n","mode":"Normal"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇdecent text editor\n","mode":"Normal"}}
|
||||
{"ReadRegister":{"name":"\"","value":"all it lacks\nis a\n\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"u"}
|
||||
{"Key":"4"}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Normal"}}
|
6
crates/vim/test_data/test_v2ap.json
Normal file
6
crates/vim/test_data/test_v2ap.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{"Put":{"state":"The\nquicˇk\n\nbrown\nfox"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"2"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"«The\nquick\n\nbrown\nfˇ»ox","mode":"VisualLine"}}
|
Loading…
Add table
Add a link
Reference in a new issue