Detailed changes
@@ -677,6 +677,19 @@ impl<'a> MutableSelectionsCollection<'a> {
});
}
+ pub fn maybe_move_cursors_with(
+ &mut self,
+ mut update_cursor_position: impl FnMut(
+ &DisplaySnapshot,
+ DisplayPoint,
+ SelectionGoal,
+ ) -> Option<(DisplayPoint, SelectionGoal)>,
+ ) {
+ self.move_cursors_with(|map, point, goal| {
+ update_cursor_position(map, point, goal).unwrap_or((point, goal))
+ })
+ }
+
pub fn replace_cursors_with(
&mut self,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
@@ -137,6 +137,11 @@ impl Motion {
)
}
+ pub fn infallible(self) -> bool {
+ use Motion::*;
+ matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
+ }
+
pub fn inclusive(self) -> bool {
use Motion::*;
match self {
@@ -164,9 +169,9 @@ impl Motion {
point: DisplayPoint,
goal: SelectionGoal,
times: usize,
- ) -> (DisplayPoint, SelectionGoal) {
+ ) -> Option<(DisplayPoint, SelectionGoal)> {
use Motion::*;
- match self {
+ let (new_point, goal) = match self {
Left => (left(map, point, times), SelectionGoal::None),
Backspace => (backspace(map, point, times), SelectionGoal::None),
Down => down(map, point, goal, times),
@@ -191,7 +196,9 @@ impl Motion {
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
Matching => (matching(map, point), SelectionGoal::None),
- }
+ };
+
+ (new_point != point || self.infallible()).then_some((new_point, goal))
}
// Expands a selection using self motion for an operator
@@ -201,12 +208,13 @@ impl Motion {
selection: &mut Selection<DisplayPoint>,
times: usize,
expand_to_surrounding_newline: bool,
- ) {
- let (new_head, goal) = self.move_point(map, selection.head(), selection.goal, times);
- selection.set_head(new_head, goal);
+ ) -> bool {
+ if let Some((new_head, goal)) =
+ self.move_point(map, selection.head(), selection.goal, times)
+ {
+ selection.set_head(new_head, goal);
- if self.linewise() {
- if selection.start != selection.end {
+ if self.linewise() {
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
if expand_to_surrounding_newline {
@@ -215,7 +223,7 @@ impl Motion {
*selection.end.column_mut() = 0;
selection.end = map.clip_point(selection.end, Bias::Right);
// Don't reset the end here
- return;
+ return true;
} else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row());
@@ -224,31 +232,33 @@ impl Motion {
}
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
- }
- } else {
- // If the motion is exclusive and the end of the motion is in column 1, the
- // end of the motion is moved to the end of the previous line and the motion
- // becomes inclusive. Example: "}" moves to the first line after a paragraph,
- // but "d}" will not include that line.
- let mut inclusive = self.inclusive();
- if !inclusive
- && self != Motion::Backspace
- && selection.end.row() > selection.start.row()
- && selection.end.column() == 0
- && selection.end.row() > 0
- {
- inclusive = true;
- *selection.end.row_mut() -= 1;
- *selection.end.column_mut() = 0;
- selection.end = map.clip_point(
- map.next_line_boundary(selection.end.to_point(map)).1,
- Bias::Left,
- );
- }
+ } else {
+ // If the motion is exclusive and the end of the motion is in column 1, the
+ // end of the motion is moved to the end of the previous line and the motion
+ // becomes inclusive. Example: "}" moves to the first line after a paragraph,
+ // but "d}" will not include that line.
+ let mut inclusive = self.inclusive();
+ if !inclusive
+ && self != Motion::Backspace
+ && selection.end.row() > selection.start.row()
+ && selection.end.column() == 0
+ {
+ inclusive = true;
+ *selection.end.row_mut() -= 1;
+ *selection.end.column_mut() = 0;
+ selection.end = map.clip_point(
+ map.next_line_boundary(selection.end.to_point(map)).1,
+ Bias::Left,
+ );
+ }
- if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
- *selection.end.column_mut() += 1;
+ if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
+ *selection.end.column_mut() += 1;
+ }
}
+ true
+ } else {
+ false
}
}
}
@@ -325,9 +335,7 @@ pub(crate) fn next_word_start(
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
- if at_newline {
- crossed_newline = true;
- }
+ crossed_newline |= at_newline;
found
})
}
@@ -350,7 +358,7 @@ fn next_word_end(
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
- // we have backtraced already
+ // we have backtracked already
if !map
.chars_at(point)
.nth(1)
@@ -115,7 +115,11 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal, times))
+ s.move_cursors_with(|map, cursor, goal| {
+ motion
+ .move_point(map, cursor, goal, times)
+ .unwrap_or((cursor, goal))
+ })
})
});
}
@@ -125,7 +129,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.move_cursors_with(|map, cursor, goal| {
+ s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::Right.move_point(map, cursor, goal, 1)
});
});
@@ -142,7 +146,7 @@ fn insert_first_non_whitespace(
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.move_cursors_with(|map, cursor, goal| {
+ s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
});
});
@@ -155,7 +159,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.move_cursors_with(|map, cursor, goal| {
+ s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
});
});
@@ -215,7 +219,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
(end_of_line..end_of_line, new_text)
});
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
- s.move_cursors_with(|map, cursor, goal| {
+ s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
});
});
@@ -1,27 +1,40 @@
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
-use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
+use editor::{
+ char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint,
+};
use gpui::MutableAppContext;
use language::Selection;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
+ // Some motions ignore failure when switching to normal mode
+ let mut motion_succeeded = matches!(
+ motion,
+ Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
+ );
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
- if let Motion::NextWordStart { ignore_punctuation } = motion {
- expand_changed_word_selection(map, selection, times, ignore_punctuation);
+ motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
+ {
+ expand_changed_word_selection(map, selection, times, ignore_punctuation)
} else {
- motion.expand_selection(map, selection, times, false);
- }
+ motion.expand_selection(map, selection, times, false)
+ };
});
});
copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
- vim.switch_mode(Mode::Insert, false, cx)
+
+ if motion_succeeded {
+ vim.switch_mode(Mode::Insert, false, cx)
+ } else {
+ vim.switch_mode(Mode::Normal, false, cx)
+ }
}
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
@@ -49,36 +62,45 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
}
}
-// From the docs https://vimhelp.org/change.txt.html#cw
-// Special case: When the cursor is in a word, "cw" and "cW" do not include the
-// white space after a word, they only change up to the end of the word. This is
-// because Vim interprets "cw" as change-word, and a word does not include the
-// following white space.
+// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
+// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
+// on a non-blank. This is because "cw" is interpreted as change-word, and a
+// word does not include the following white space. {Vi: "cw" when on a blank
+// followed by other blanks changes only the first blank; this is probably a
+// bug, because "dw" deletes all the blanks}
+//
+// NOT HANDLED YET
+// Another special case: When using the "w" motion in combination with an
+// operator and the last word moved over is at the end of a line, the end of
+// that word becomes the end of the operated text, not the first word in the
+// next line.
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
ignore_punctuation: bool,
-) {
- if times > 1 {
- Motion::NextWordStart { ignore_punctuation }.expand_selection(
- map,
- selection,
- times - 1,
- false,
- );
- }
-
- if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
- return;
+) -> bool {
+ if times == 1 {
+ let in_word = map
+ .chars_at(selection.head())
+ .next()
+ .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+ .unwrap_or_default();
+
+ if in_word {
+ selection.end = movement::find_boundary(map, selection.end, |left, right| {
+ let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+ left_kind != right_kind && left_kind != CharKind::Whitespace
+ });
+ true
+ } else {
+ Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
+ }
+ } else {
+ Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
}
-
- selection.end = movement::find_boundary(map, selection.end, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
-
- left_kind != right_kind || left == '\n' || right == '\n'
- });
}
#[cfg(test)]
@@ -143,7 +143,7 @@ mod test {
Test test
ˇ
test"},
- ExemptionFeatures::DeletionOnEmptyLine,
+ ExemptionFeatures::DeleteWordOnEmptyLine,
)
.await;
@@ -169,7 +169,7 @@ mod test {
Test test
ˇ
test"},
- ExemptionFeatures::DeletionOnEmptyLine,
+ ExemptionFeatures::OperatorLastNewlineRemains,
)
.await;
@@ -8,7 +8,10 @@ use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
-pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
+pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
+ ExemptionFeatures::DeletionOnEmptyLine,
+ ExemptionFeatures::OperatorAbortsOnFailedMotion,
+];
/// Enum representing features we have tests for but which don't work, yet. Used
/// to add exemptions and automatically
@@ -19,6 +22,10 @@ pub enum ExemptionFeatures {
DeletionOnEmptyLine,
// When a motion fails, it should should not apply linewise operations
OperatorAbortsOnFailedMotion,
+ // When an operator completes at the end of the file, an extra newline is left
+ OperatorLastNewlineRemains,
+ // Deleting a word on an empty line doesn't remove the newline
+ DeleteWordOnEmptyLine,
// OBJECTS
// Resulting position after the operation is slightly incorrect for unintuitive reasons.
@@ -30,20 +30,23 @@ pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
s.move_with(|map, selection| {
let was_reversed = selection.reversed;
- let (new_head, goal) =
- motion.move_point(map, selection.head(), selection.goal, times);
- selection.set_head(new_head, goal);
-
- if was_reversed && !selection.reversed {
- // Head was at the start of the selection, and now is at the end. We need to move the start
- // back by one if possible in order to compensate for this change.
- *selection.start.column_mut() = selection.start.column().saturating_sub(1);
- selection.start = map.clip_point(selection.start, Bias::Left);
- } else if !was_reversed && selection.reversed {
- // Head was at the end of the selection, and now is at the start. We need to move the end
- // forward by one if possible in order to compensate for this change.
- *selection.end.column_mut() = selection.end.column() + 1;
- selection.end = map.clip_point(selection.end, Bias::Right);
+ if let Some((new_head, goal)) =
+ motion.move_point(map, selection.head(), selection.goal, times)
+ {
+ selection.set_head(new_head, goal);
+
+ if was_reversed && !selection.reversed {
+ // Head was at the start of the selection, and now is at the end. We need to move the start
+ // back by one if possible in order to compensate for this change.
+ *selection.start.column_mut() =
+ selection.start.column().saturating_sub(1);
+ selection.start = map.clip_point(selection.start, Bias::Left);
+ } else if !was_reversed && selection.reversed {
+ // Head was at the end of the selection, and now is at the start. We need to move the end
+ // forward by one if possible in order to compensate for this change.
+ *selection.end.column_mut() = selection.end.column() + 1;
+ selection.end = map.clip_point(selection.end, Bias::Right);
+ }
}
});
});
@@ -1 +1 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]
@@ -1 +1 @@
-[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
@@ -1 +1 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
@@ -1 +1 @@
-[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
@@ -1 +1 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
+[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -1 +1 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
+[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
@@ -1 +1 @@
-[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
@@ -1 +1 @@