vim: Add some forced motion support (#27991)

Closes https://github.com/zed-industries/zed/issues/20971

Added `v` input to yank and delete to override default motion. The
global vim state tracking if the forced motion flag was passed handled
the same way that the count is. [The main chunk of code maps the motion
kind from the default to the overridden
kind](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1249-R1254).
To handle the case of deleting a single character (dv0) at the start of
a row I had to modify the control flow
[here](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1240-R1244).
Then to handle an exclusive delete till the end of the row (dv$) I
[saturated the endpoint with a left
bias](https://github.com/zed-industries/zed/pull/27991/files#diff-2dca6b7d1673c912d14e4edc74e415abbe3a4e6d6b37e0e2006d30828bf4bb9cR1281-R1286).

Test case: dv0


https://github.com/user-attachments/assets/613cf9fb-9732-425c-9179-025f3e107584

Test case: yvjp


https://github.com/user-attachments/assets/550b7c77-1eb8-41c3-894b-117eb50b7a5d

Release Notes:

- Added some forced motion support for delete and yank
This commit is contained in:
Peter Finn 2025-04-11 10:12:30 -07:00 committed by GitHub
parent 1df01eabfe
commit 08ce230bae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 485 additions and 58 deletions

View file

@ -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", {}]
}
},

View file

@ -24,6 +24,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
if self.change_list.is_empty() {
return;
}

View file

@ -234,6 +234,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Vim>,
) {
@ -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| {

View file

@ -18,6 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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>) {
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>) {
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<usize>,
forced_motion: bool,
dir: IndentDirection,
window: &mut Window,
cx: &mut Context<Self>,
@ -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 {

View file

@ -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);

View file

@ -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<Operator> = 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<DisplayPoint>,
times: Option<usize>,
text_layout_details: &TextLayoutDetails,
forced_motion: bool,
) -> Option<(Range<DisplayPoint>, 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<DisplayPoint>,
times: Option<usize>,
text_layout_details: &TextLayoutDetails,
forced_motion: bool,
) -> Option<MotionKind> {
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);
}
}

View file

@ -86,12 +86,14 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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>) {
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>) {
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>) {
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>) {
});
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<Operator>,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>) {
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<Self>) {
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<Self>,
) {
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| {

View file

@ -18,6 +18,7 @@ impl Vim {
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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)
}
}

View file

@ -25,6 +25,7 @@ impl Vim {
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
mode: ConvertTarget,
window: &mut Window,
cx: &mut Context<Self>,
@ -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();

View file

@ -18,6 +18,7 @@ impl Vim {
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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));

View file

@ -29,12 +29,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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)
});

View file

@ -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<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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,
);
});
});

View file

@ -170,6 +170,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
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<Self>,
) {
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();

View file

@ -55,6 +55,7 @@ impl Vim {
by: fn(c: Option<f32>) -> 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)
});

View file

@ -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::<BufferSearchBar>() {
@ -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();

View file

@ -13,6 +13,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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>) {
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,

View file

@ -9,6 +9,7 @@ impl Vim {
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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);

View file

@ -21,6 +21,7 @@ impl Vim {
&mut self,
motion: Motion,
times: Option<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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 };

View file

@ -27,6 +27,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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));

View file

@ -10,6 +10,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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<usize>,
forced_motion: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@ -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(

View file

@ -202,7 +202,7 @@ pub struct VimGlobals {
pub pre_count: Option<usize>,
/// post_count is the number after an operator is specified (2 in 3d2d)
pub post_count: Option<usize>,
pub forced_motion: bool,
pub stop_recording_after_next_action: bool,
pub ignore_current_insertion: bool,
pub recorded_count: Option<usize>,

View file

@ -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 {

View file

@ -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<String>,

View file

@ -142,6 +142,10 @@ impl VimTestContext {
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
}
pub fn forced_motion(&mut self) -> bool {
self.update_editor(|_, _, cx| cx.global::<VimGlobals>().forced_motion)
}
pub fn active_operator(&mut self) -> Option<Operator> {
self.update_editor(|editor, _, cx| {
editor

View file

@ -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::<VimGlobals>();
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<Self>) {
Vim::take_count(cx);
Vim::take_forced_motion(cx);
self.selected_register.take();
self.operator_stack.clear();
self.sync_vim_settings(window, cx);

View file

@ -85,6 +85,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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<Vim>) {
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<Self>) {
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<Self>,
) {
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<Self>,
) {
Vim::take_forced_motion(cx);
let count = Vim::take_count(cx).unwrap_or(1);
let Some(pane) = self.pane(window, cx) else {
return;

View file

@ -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"}}

View file

@ -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"}}

View file

@ -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"}}

View file

@ -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"}}