diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 4c4d6b5175..83df86d0e8 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -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| { diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index c8762c563a..b10fff8b5d 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -122,6 +122,7 @@ impl Vim { object: Object, around: bool, dir: IndentDirection, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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 { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2003c8b754..f25467aec4 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -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) { + pub fn normal_object( + &mut self, + object: Object, + times: Option, + window: &mut Window, + cx: &mut Context, + ) { let mut waiting_operator: Option = 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) diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index da8d38ea13..9485f17477 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -105,6 +105,7 @@ impl Vim { &mut self, object: Object, around: bool, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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 { diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index 4621e3ab89..25b425e847 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -82,6 +82,7 @@ impl Vim { object: Object, around: bool, mode: ConvertTarget, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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), diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 141346c99f..ccbb3dd0fd 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -91,6 +91,7 @@ impl Vim { &mut self, object: Object, around: bool, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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| { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 0dade838f5..67ca6314af 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -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); }); }); diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 3b578c44cb..636ea9eec2 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -46,6 +46,7 @@ impl Vim { &mut self, object: Object, around: bool, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 6beb81b2b6..f8cc3ca7dd 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -66,6 +66,7 @@ impl Vim { &mut self, object: Object, around: bool, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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); }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 2486619608..2cec4e254a 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -373,10 +373,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { impl Vim { fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { + 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, around: bool, + times: Option, ) -> Option> { 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, around: bool, + times: Option, ) -> 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> { 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); } } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index bf0d977531..15753e8290 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -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)); diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index e03a3308fc..c1d157accb 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -89,6 +89,7 @@ impl Vim { &mut self, object: Object, around: bool, + times: Option, window: &mut Window, cx: &mut Context, ) { @@ -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( diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 852433bc8e..1f77ebda4a 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -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() diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 346f78c1ca..e62d8c58ef 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -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! {"ˇ"}); +} diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 2d72881b7a..c3da5d2143 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -364,7 +364,13 @@ impl Vim { }) } - pub fn visual_object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { + pub fn visual_object( + &mut self, + object: Object, + count: Option, + window: &mut Window, + cx: &mut Context, + ) { 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" + }); + } } diff --git a/crates/vim/test_data/test_paragraph_multi_delete.json b/crates/vim/test_data/test_paragraph_multi_delete.json new file mode 100644 index 0000000000..f706827a24 --- /dev/null +++ b/crates/vim/test_data/test_paragraph_multi_delete.json @@ -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"}} diff --git a/crates/vim/test_data/test_v2ap.json b/crates/vim/test_data/test_v2ap.json new file mode 100644 index 0000000000..7b4d31a5dc --- /dev/null +++ b/crates/vim/test_data/test_v2ap.json @@ -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"}}