diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 40c96ed5d8..452731ebc2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -539,6 +539,7 @@ "bindings": { "d": "vim::CurrentLine", "s": "vim::PushDeleteSurrounds", + "v": "vim::PushForcedMotion", // "d v" "o": "editor::ToggleSelectedDiffHunks", // "d o" "shift-o": "git::ToggleStaged", "p": "git::Restore", // "d p" @@ -587,6 +588,7 @@ "context": "vim_operator == y", "bindings": { "y": "vim::CurrentLine", + "v": "vim::PushForcedMotion", "s": ["vim::PushAddSurrounds", {}] } }, diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index baa72dbc1f..0d0a5898b2 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -24,6 +24,7 @@ impl Vim { cx: &mut Context, ) { let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); if self.change_list.is_empty() { return; } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 9245701bf3..15f008f91d 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -234,6 +234,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { return; }; let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let n = if count > 1 { format!(".,.+{}", count.saturating_sub(1)) } else { @@ -1323,6 +1324,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -1335,7 +1337,13 @@ impl Vim { let start = editor.selections.newest_display(cx); let text_layout_details = editor.text_layout_details(window); let (mut range, _) = motion - .range(&snapshot, start.clone(), times, &text_layout_details) + .range( + &snapshot, + start.clone(), + times, + &text_layout_details, + forced_motion, + ) .unwrap_or((start.range(), MotionKind::Exclusive)); if range.start != start.start { editor.change_selections(None, window, cx, |s| { diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index 3f0ed5251f..ac708a7e89 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -18,6 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Indent, window, cx| { vim.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); vim.update_editor(window, cx, |vim, editor, window, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -36,6 +37,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Outdent, window, cx| { vim.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); vim.update_editor(window, cx, |vim, editor, window, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -54,6 +56,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| { vim.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); vim.update_editor(window, cx, |vim, editor, window, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -75,6 +78,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, dir: IndentDirection, window: &mut Window, cx: &mut Context, @@ -88,7 +92,13 @@ impl Vim { s.move_with(|map, selection| { let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, &text_layout_details); + motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); }); }); match dir { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 550d5b57fd..561ceec0a8 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -23,6 +23,7 @@ impl Vim { return; } let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); self.stop_recording_immediately(action.boxed_clone(), cx); if count <= 1 || Vim::globals(cx).dot_replaying { self.create_mark("^".into(), window, cx); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index bbe1ef01a6..fcf1e07749 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -650,6 +650,7 @@ impl Vim { } let count = Vim::take_count(cx); + let forced_motion = Vim::take_forced_motion(cx); let active_operator = self.active_operator(); let mut waiting_operator: Option = None; match self.mode { @@ -659,7 +660,14 @@ impl Vim { target: Some(SurroundsType::Motion(motion)), }); } else { - self.normal_motion(motion.clone(), active_operator.clone(), count, window, cx) + self.normal_motion( + motion.clone(), + active_operator.clone(), + count, + forced_motion, + window, + cx, + ) } } Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { @@ -1183,7 +1191,6 @@ impl Motion { SelectionGoal::None, ), }; - (new_point != point || infallible).then_some((new_point, goal)) } @@ -1194,6 +1201,7 @@ impl Motion { selection: Selection, times: Option, text_layout_details: &TextLayoutDetails, + forced_motion: bool, ) -> Option<(Range, MotionKind)> { if let Motion::ZedSearchResult { prior_selections, @@ -1221,18 +1229,29 @@ impl Motion { return None; } } - - let (new_head, goal) = self.move_point( + let maybe_new_point = self.move_point( map, selection.head(), selection.goal, times, text_layout_details, - )?; + ); + + let (new_head, goal) = match (maybe_new_point, forced_motion) { + (Some((p, g)), _) => Some((p, g)), + (None, false) => None, + (None, true) => Some((selection.head(), selection.goal)), + }?; + let mut selection = selection.clone(); selection.set_head(new_head, goal); - let mut kind = self.default_kind(); + let mut kind = match (self.default_kind(), forced_motion) { + (MotionKind::Linewise, true) => MotionKind::Exclusive, + (MotionKind::Exclusive, true) => MotionKind::Inclusive, + (MotionKind::Inclusive, true) => MotionKind::Exclusive, + (kind, false) => kind, + }; if let Motion::NextWordStart { ignore_punctuation: _, @@ -1259,6 +1278,12 @@ impl Motion { } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() { let start_point = selection.start.to_point(map); let mut end_point = selection.end.to_point(map); + let mut next_point = selection.end; + *next_point.column_mut() += 1; + next_point = map.clip_point(next_point, Bias::Right); + if next_point.to_point(map) == end_point && forced_motion { + selection.end = movement::saturating_left(map, selection.end); + } if end_point.row > start_point.row { let first_non_blank_of_start_row = map @@ -1304,8 +1329,15 @@ impl Motion { selection: &mut Selection, times: Option, text_layout_details: &TextLayoutDetails, + forced_motion: bool, ) -> Option { - let (range, kind) = self.range(map, selection.clone(), times, text_layout_details)?; + let (range, kind) = self.range( + map, + selection.clone(), + times, + text_layout_details, + forced_motion, + )?; selection.start = range.start; selection.end = range.end; Some(kind) @@ -3816,6 +3848,7 @@ mod test { Mode::Normal, ); } + #[gpui::test] async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; @@ -3823,4 +3856,147 @@ mod test { cx.simulate_shared_keystrokes("delete").await; cx.shared_state().await.assert_eq("aˇb"); } + + #[gpui::test] + async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v 0").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇhe quick brown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick bˇrown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v 0").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v 0").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇ + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + } + + #[gpui::test] + async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v $").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v $").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇx + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + } + + #[gpui::test] + async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("y v j p").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick brown fox + ˇthe quick brown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick bˇrown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("y v j p").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick brˇrown fox + jumped overown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("y v j p").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick brown foxˇx + jumped over the la + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick brown fox + jˇumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("y v k p").await; + cx.shared_state().await.assert_eq(indoc! {" + thˇhe quick brown fox + je quick brown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + } + + #[gpui::test] + async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇthe quick brown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v e").await; + cx.shared_state().await.assert_eq(indoc! {" + ˇe quick brown fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick bˇrown fox + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v e").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick bˇn fox + jumped over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + + cx.set_shared_state(indoc! {" + the quick brown foˇx + jumped over the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("d v e").await; + cx.shared_state().await.assert_eq(indoc! {" + the quick brown foˇd over the lazy dog"}); + assert_eq!(cx.cx.forced_motion(), false); + } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 43657ffd73..7781891050 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -86,12 +86,14 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| { vim.record_current_action(cx); let times = Vim::take_count(cx); - vim.delete_motion(Motion::Left, times, window, cx); + let forced_motion = Vim::take_forced_motion(cx); + vim.delete_motion(Motion::Left, times, forced_motion, window, cx); }); Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| { vim.record_current_action(cx); let times = Vim::take_count(cx); - vim.delete_motion(Motion::Right, times, window, cx); + let forced_motion = Vim::take_forced_motion(cx); + vim.delete_motion(Motion::Right, times, forced_motion, window, cx); }); Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| { @@ -111,11 +113,13 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| { vim.start_recording(cx); let times = Vim::take_count(cx); + let forced_motion = Vim::take_forced_motion(cx); vim.change_motion( Motion::EndOfLine { display_lines: false, }, times, + forced_motion, window, cx, ); @@ -123,11 +127,13 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| { vim.record_current_action(cx); let times = Vim::take_count(cx); + let forced_motion = Vim::take_forced_motion(cx); vim.delete_motion( Motion::EndOfLine { display_lines: false, }, times, + forced_motion, window, cx, ); @@ -142,6 +148,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Undo, window, cx| { let times = Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.update_editor(window, cx, |_, editor, window, cx| { for _ in 0..times.unwrap_or(1) { editor.undo(&editor::actions::Undo, window, cx); @@ -150,6 +157,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &Redo, window, cx| { let times = Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.update_editor(window, cx, |_, editor, window, cx| { for _ in 0..times.unwrap_or(1) { editor.redo(&editor::actions::Redo, window, cx); @@ -170,48 +178,93 @@ impl Vim { motion: Motion, operator: Option, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { match operator { None => self.move_cursor(motion, times, window, cx), - Some(Operator::Change) => self.change_motion(motion, times, window, cx), - Some(Operator::Delete) => self.delete_motion(motion, times, window, cx), - Some(Operator::Yank) => self.yank_motion(motion, times, window, cx), + Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx), + Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx), + Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx), Some(Operator::AddSurrounds { target: None }) => {} - Some(Operator::Indent) => { - self.indent_motion(motion, times, IndentDirection::In, window, cx) - } - Some(Operator::Rewrap) => self.rewrap_motion(motion, times, window, cx), - Some(Operator::Outdent) => { - self.indent_motion(motion, times, IndentDirection::Out, window, cx) - } - Some(Operator::AutoIndent) => { - self.indent_motion(motion, times, IndentDirection::Auto, window, cx) - } - Some(Operator::ShellCommand) => self.shell_command_motion(motion, times, window, cx), - Some(Operator::Lowercase) => { - self.convert_motion(motion, times, ConvertTarget::LowerCase, window, cx) - } - Some(Operator::Uppercase) => { - self.convert_motion(motion, times, ConvertTarget::UpperCase, window, cx) - } - Some(Operator::OppositeCase) => { - self.convert_motion(motion, times, ConvertTarget::OppositeCase, window, cx) - } - Some(Operator::Rot13) => { - self.convert_motion(motion, times, ConvertTarget::Rot13, window, cx) - } - Some(Operator::Rot47) => { - self.convert_motion(motion, times, ConvertTarget::Rot47, window, cx) + Some(Operator::Indent) => self.indent_motion( + motion, + times, + forced_motion, + IndentDirection::In, + window, + cx, + ), + Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx), + Some(Operator::Outdent) => self.indent_motion( + motion, + times, + forced_motion, + IndentDirection::Out, + window, + cx, + ), + Some(Operator::AutoIndent) => self.indent_motion( + motion, + times, + forced_motion, + IndentDirection::Auto, + window, + cx, + ), + Some(Operator::ShellCommand) => { + self.shell_command_motion(motion, times, forced_motion, window, cx) } + Some(Operator::Lowercase) => self.convert_motion( + motion, + times, + forced_motion, + ConvertTarget::LowerCase, + window, + cx, + ), + Some(Operator::Uppercase) => self.convert_motion( + motion, + times, + forced_motion, + ConvertTarget::UpperCase, + window, + cx, + ), + Some(Operator::OppositeCase) => self.convert_motion( + motion, + times, + forced_motion, + ConvertTarget::OppositeCase, + window, + cx, + ), + Some(Operator::Rot13) => self.convert_motion( + motion, + times, + forced_motion, + ConvertTarget::Rot13, + window, + cx, + ), + Some(Operator::Rot47) => self.convert_motion( + motion, + times, + forced_motion, + ConvertTarget::Rot47, + window, + cx, + ), Some(Operator::ToggleComments) => { - self.toggle_comments_motion(motion, times, window, cx) + self.toggle_comments_motion(motion, times, forced_motion, window, cx) } Some(Operator::ReplaceWithRegister) => { - self.replace_with_register_motion(motion, times, window, cx) + self.replace_with_register_motion(motion, times, forced_motion, window, cx) + } + Some(Operator::Exchange) => { + self.exchange_motion(motion, times, forced_motion, window, cx) } - Some(Operator::Exchange) => self.exchange_motion(motion, times, window, cx), Some(operator) => { // Can't do anything for text objects, Ignoring error!("Unexpected normal mode motion operator: {:?}", operator) @@ -492,6 +545,7 @@ impl Vim { ) { self.record_current_action(cx); let mut times = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); if self.mode.is_visual() { times = 1; } else if times > 1 { @@ -513,11 +567,19 @@ impl Vim { fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context) { let count = Vim::take_count(cx); - self.yank_motion(motion::Motion::CurrentLine, count, window, cx) + let forced_motion = Vim::take_forced_motion(cx); + self.yank_motion( + motion::Motion::CurrentLine, + count, + forced_motion, + window, + cx, + ) } fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context) { let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); self.update_editor(window, cx, |vim, editor, _window, cx| { let selection = editor.selections.newest_anchor(); if let Some((_, buffer, _)) = editor.active_excerpt(cx) { @@ -577,6 +639,7 @@ impl Vim { cx: &mut Context, ) { let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); self.stop_recording(cx); self.update_editor(window, cx, |_, editor, window, cx| { editor.transact(window, cx, |editor, window, cx| { diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 199ac8b0c7..7e27cda949 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -18,6 +18,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -59,6 +60,7 @@ impl Vim { selection, times, &text_layout_details, + forced_motion, ); if let Motion::CurrentLine = motion { let mut start_offset = @@ -181,7 +183,7 @@ fn expand_changed_word_selection( } else { Motion::NextWordStart { ignore_punctuation } }; - motion.expand_selection(map, selection, times, text_layout_details) + motion.expand_selection(map, selection, times, text_layout_details, false) } } diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index af0154d3c2..31aac771c2 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -25,6 +25,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, mode: ConvertTarget, window: &mut Window, cx: &mut Context, @@ -39,7 +40,13 @@ impl Vim { s.move_with(|map, selection| { let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, &text_layout_details); + motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); }); }); match mode { @@ -185,6 +192,7 @@ impl Vim { self.record_current_action(cx); self.store_visual_marks(window, cx); let count = Vim::take_count(cx).unwrap_or(1) as u32; + Vim::take_forced_motion(cx); self.update_editor(window, cx, |vim, editor, window, cx| { let mut ranges = Vec::new(); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index afd6bc402c..583e775fc6 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -18,6 +18,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -33,9 +34,13 @@ impl Vim { s.move_with(|map, selection| { let original_head = selection.head(); original_columns.insert(selection.id, original_head.column()); - let kind = - motion.expand_selection(map, selection, times, &text_layout_details); - + let kind = motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); ranges_to_copy .push(selection.start.to_point(map)..selection.end.to_point(map)); diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 194a5c8803..e092249e32 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -29,12 +29,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, action: &Increment, window, cx| { vim.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let step = if action.step { count as i32 } else { 0 }; vim.increment(count as i64, step, window, cx) }); Vim::action(editor, cx, |vim, action: &Decrement, window, cx| { vim.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let step = if action.step { -1 * (count as i32) } else { 0 }; vim.increment(-(count as i64), step, window, cx) }); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 2aaa2a4b7c..3d0a3e44c8 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -28,6 +28,7 @@ impl Vim { self.record_current_action(cx); self.store_visual_marks(window, cx); let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); self.update_editor(window, cx, |vim, editor, window, cx| { let text_layout_details = editor.text_layout_details(window); @@ -247,6 +248,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -258,7 +260,13 @@ impl Vim { editor.set_clip_at_line_ends(false, cx); editor.change_selections(None, window, cx, |s| { s.move_with(|map, selection| { - motion.expand_selection(map, selection, times, &text_layout_details); + motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); }); }); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index d396d0ae4d..49f07954ff 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -170,6 +170,7 @@ impl Vim { cx: &mut Context, ) { let mut count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); self.clear_operator(window, cx); let globals = Vim::globals(cx); @@ -201,6 +202,7 @@ impl Vim { cx: &mut Context, ) { let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| { let actions = globals.recorded_actions.clone(); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 0b87a3b345..dfca3aa280 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -55,6 +55,7 @@ impl Vim { by: fn(c: Option) -> ScrollAmount, ) { let amount = by(Vim::take_count(cx).map(|c| c as f32)); + Vim::take_forced_motion(cx); self.update_editor(window, cx, |_, editor, window, cx| { scroll_editor(editor, move_cursor, &amount, window, cx) }); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 98972097ae..da8f65c1cf 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -138,6 +138,7 @@ impl Vim { Direction::Next }; let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let prior_selections = self.editor_selections(window, cx); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { @@ -261,6 +262,7 @@ impl Vim { return; }; let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let prior_selections = self.editor_selections(window, cx); let success = pane.update(cx, |pane, cx| { @@ -303,6 +305,7 @@ impl Vim { return; }; let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); let prior_selections = self.editor_selections(window, cx); let cursor_word = self.editor_cursor_word(window, cx); let vim = cx.entity().clone(); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 78c9ec5b3f..1199356995 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -13,6 +13,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Substitute, window, cx| { vim.start_recording(cx); let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.substitute(count, vim.mode == Mode::VisualLine, window, cx); }); @@ -22,6 +23,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { vim.switch_mode(Mode::VisualLine, false, window, cx) } let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.substitute(count, true, window, cx) }); } @@ -47,6 +49,7 @@ impl Vim { selection, count, &text_layout_details, + false, ); } if line_mode { @@ -60,6 +63,7 @@ impl Vim { selection, None, &text_layout_details, + false, ); if let Some((point, _)) = (Motion::FirstNonWhitespace { display_lines: false, diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 363215ffe2..1df381acbe 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -9,6 +9,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -21,7 +22,13 @@ impl Vim { s.move_with(|map, selection| { let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, &text_layout_details); + motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); }); }); editor.toggle_comments(&Default::default(), window, cx); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 0ec19f654b..6f83b954b2 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -21,6 +21,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -33,8 +34,19 @@ impl Vim { editor.change_selections(None, window, cx, |s| { s.move_with(|map, selection| { let original_position = (selection.head(), selection.goal); - original_positions.insert(selection.id, original_position); - kind = motion.expand_selection(map, selection, times, &text_layout_details); + kind = motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); + if kind == Some(MotionKind::Exclusive) { + original_positions + .insert(selection.id, (selection.start, selection.goal)); + } else { + original_positions.insert(selection.id, original_position); + } }) }); let Some(kind) = kind else { return }; diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 26437550a1..f975aefa33 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -27,6 +27,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { return; } let count = Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.undo_replace(count, window, cx) }); } @@ -179,6 +180,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -188,7 +190,13 @@ impl Vim { let text_layout_details = editor.text_layout_details(window); let mut selection = editor.selections.newest_display(cx); let snapshot = editor.snapshot(window, cx); - motion.expand_selection(&snapshot, &mut selection, times, &text_layout_details); + motion.expand_selection( + &snapshot, + &mut selection, + times, + &text_layout_details, + forced_motion, + ); 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 f7f234c742..b5d69ef0ae 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -10,6 +10,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| { vim.record_current_action(cx); Vim::take_count(cx); + Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); vim.update_editor(window, cx, |vim, editor, window, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -43,6 +44,7 @@ impl Vim { &mut self, motion: Motion, times: Option, + forced_motion: bool, window: &mut Window, cx: &mut Context, ) { @@ -55,7 +57,13 @@ impl Vim { s.move_with(|map, selection| { let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); selection_starts.insert(selection.id, anchor); - motion.expand_selection(map, selection, times, &text_layout_details); + motion.expand_selection( + map, + selection, + times, + &text_layout_details, + forced_motion, + ); }); }); editor.rewrap_impl( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6e7f753def..6b1a87aec7 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -202,7 +202,7 @@ pub struct VimGlobals { pub pre_count: Option, /// post_count is the number after an operator is specified (2 in 3d2d) pub post_count: Option, - + pub forced_motion: bool, pub stop_recording_after_next_action: bool, pub ignore_current_insertion: bool, pub recorded_count: Option, diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 3c450292e1..6697742e4d 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -27,6 +27,7 @@ impl Vim { ) { 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(window, cx, |_, editor, window, cx| { let text_layout_details = editor.text_layout_details(window); @@ -55,7 +56,13 @@ impl Vim { } SurroundsType::Motion(motion) => { motion - .range(&display_map, selection.clone(), count, &text_layout_details) + .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 { diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index e2189da86b..053e1e587e 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -13,7 +13,7 @@ use super::{VimTestContext, neovim_connection::NeovimConnection}; use crate::state::{Mode, VimGlobals}; pub struct NeovimBackedTestContext { - cx: VimTestContext, + pub(crate) cx: VimTestContext, pub(crate) neovim: NeovimConnection, last_set_state: Option, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 32e8de3af5..188ae1c248 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -142,6 +142,10 @@ impl VimTestContext { self.update_editor(|editor, _, cx| editor.addon::().unwrap().entity.read(cx).mode) } + pub fn forced_motion(&mut self) -> bool { + self.update_editor(|_, _, cx| cx.global::().forced_motion) + } + pub fn active_operator(&mut self) -> Option { self.update_editor(|editor, _, cx| { editor diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c4efb2b513..a1ecab13c3 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -145,6 +145,7 @@ actions!( PushDeleteSurrounds, PushMark, ToggleMarksView, + PushForcedMotion, PushIndent, PushOutdent, PushAutoIndent, @@ -233,6 +234,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ResizePaneRight, window, cx| { let count = Vim::take_count(cx).unwrap_or(1) as f32; + Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { return; @@ -248,6 +250,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ResizePaneLeft, window, cx| { let count = Vim::take_count(cx).unwrap_or(1) as f32; + Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { return; @@ -263,6 +266,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ResizePaneUp, window, cx| { let count = Vim::take_count(cx).unwrap_or(1) as f32; + Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); workspace.resize_pane(Axis::Vertical, height * count, window, cx); @@ -270,6 +274,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ResizePaneDown, window, cx| { let count = Vim::take_count(cx).unwrap_or(1) as f32; + Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); workspace.resize_pane(Axis::Vertical, -height * count, window, cx); @@ -472,7 +477,9 @@ impl Vim { vim.switch_mode(Mode::HelixNormal, false, window, cx) }, ); - + Vim::action(editor, cx, |_, _: &PushForcedMotion, _, cx| { + Vim::globals(cx).forced_motion = true; + }); Vim::action(editor, cx, |vim, action: &PushObject, window, cx| { vim.push_operator( Operator::Object { @@ -907,6 +914,7 @@ impl Vim { self.current_tx.take(); self.current_anchor.take(); } + Vim::take_forced_motion(cx); if mode != Mode::Insert && mode != Mode::Replace { Vim::take_count(cx); } @@ -1011,6 +1019,13 @@ impl Vim { count } + pub fn take_forced_motion(cx: &mut App) -> bool { + let global_state = cx.global_mut::(); + let forced_motion = global_state.forced_motion; + global_state.forced_motion = false; + forced_motion + } + pub fn cursor_shape(&self, cx: &mut App) -> CursorShape { match self.mode { Mode::Normal => { @@ -1372,6 +1387,7 @@ impl Vim { fn clear_operator(&mut self, window: &mut Window, cx: &mut Context) { Vim::take_count(cx); + Vim::take_forced_motion(cx); self.selected_register.take(); self.operator_stack.clear(); self.sync_vim_settings(window, cx); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index a96d49a43c..6827c2c055 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -85,6 +85,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| { let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); for _ in 0..count { vim.update_editor(window, cx, |_, editor, window, cx| { editor.select_larger_syntax_node(&Default::default(), window, cx); @@ -97,6 +98,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { cx, |vim, _: &SelectSmallerSyntaxNode, window, cx| { let count = Vim::take_count(cx).unwrap_or(1); + Vim::take_forced_motion(cx); for _ in 0..count { vim.update_editor(window, cx, |_, editor, window, cx| { editor.select_smaller_syntax_node(&Default::default(), window, cx); @@ -682,6 +684,7 @@ impl Vim { } pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { + Vim::take_forced_motion(cx); let count = Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); self.update_editor(window, cx, |_, editor, window, cx| { @@ -704,6 +707,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { + Vim::take_forced_motion(cx); let count = Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); self.update_editor(window, cx, |_, editor, window, cx| { @@ -725,6 +729,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { + Vim::take_forced_motion(cx); let count = Vim::take_count(cx).unwrap_or(1); let Some(pane) = self.pane(window, cx) else { return; diff --git a/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json new file mode 100644 index 0000000000..4df916befb --- /dev/null +++ b/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json @@ -0,0 +1,10 @@ +{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"$"} +{"Get":{"state":"the quick brown foˇx\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"$"} +{"Get":{"state":"ˇx\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json new file mode 100644 index 0000000000..8aae77c8de --- /dev/null +++ b/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json @@ -0,0 +1,15 @@ +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"0"} +{"Get":{"state":"ˇhe quick brown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"0"} +{"Get":{"state":"ˇown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"0"} +{"Get":{"state":"ˇ\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_yank.json b/crates/vim/test_data/test_forced_motion_yank.json new file mode 100644 index 0000000000..208c22d689 --- /dev/null +++ b/crates/vim/test_data/test_forced_motion_yank.json @@ -0,0 +1,24 @@ +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"y"} +{"Key":"v"} +{"Key":"j"} +{"Key":"p"} +{"Get":{"state":"the quick brown fox\nˇthe quick brown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} +{"Key":"y"} +{"Key":"v"} +{"Key":"j"} +{"Key":"p"} +{"Get":{"state":"the quick brˇrown fox\njumped overown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} +{"Key":"y"} +{"Key":"v"} +{"Key":"j"} +{"Key":"p"} +{"Get":{"state":"the quick brown foxˇx\njumped over the la\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick brown fox\njˇumped over the lazy dog"}} +{"Key":"y"} +{"Key":"v"} +{"Key":"k"} +{"Key":"p"} +{"Get":{"state":"thˇhe quick brown fox\nje quick brown fox\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_inclusive_to_exclusive_delete.json b/crates/vim/test_data/test_inclusive_to_exclusive_delete.json new file mode 100644 index 0000000000..3d25b9fc67 --- /dev/null +++ b/crates/vim/test_data/test_inclusive_to_exclusive_delete.json @@ -0,0 +1,15 @@ +{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"e"} +{"Get":{"state":"ˇe quick brown fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"e"} +{"Get":{"state":"the quick bˇn fox\njumped over the lazy dog","mode":"Normal"}} +{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} +{"Key":"d"} +{"Key":"v"} +{"Key":"e"} +{"Get":{"state":"the quick brown foˇd over the lazy dog","mode":"Normal"}}