@@ -42,6 +42,18 @@ pub enum Motion {
PreviousWordEnd {
ignore_punctuation: bool,
},
+ NextSubwordStart {
+ ignore_punctuation: bool,
+ },
+ NextSubwordEnd {
+ ignore_punctuation: bool,
+ },
+ PreviousSubwordStart {
+ ignore_punctuation: bool,
+ },
+ PreviousSubwordEnd {
+ ignore_punctuation: bool,
+ },
FirstNonWhitespace {
display_lines: bool,
},
@@ -110,6 +122,34 @@ struct PreviousWordEnd {
ignore_punctuation: bool,
}
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct NextSubwordStart {
+ #[serde(default)]
+ pub(crate) ignore_punctuation: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct NextSubwordEnd {
+ #[serde(default)]
+ pub(crate) ignore_punctuation: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct PreviousSubwordStart {
+ #[serde(default)]
+ pub(crate) ignore_punctuation: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct PreviousSubwordEnd {
+ #[serde(default)]
+ pub(crate) ignore_punctuation: bool,
+}
+
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Up {
@@ -153,10 +193,14 @@ impl_actions!(
FirstNonWhitespace,
Down,
Up,
+ NextWordStart,
+ NextWordEnd,
PreviousWordStart,
PreviousWordEnd,
- NextWordEnd,
- NextWordStart
+ NextSubwordStart,
+ NextSubwordEnd,
+ PreviousSubwordStart,
+ PreviousSubwordEnd,
]
);
@@ -264,6 +308,31 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
+ workspace.register_action(
+ |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
+ motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
+ },
+ );
+ workspace.register_action(
+ |_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
+ motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
+ },
+ );
+ workspace.register_action(
+ |_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
+ motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
+ },
+ );
+ workspace.register_action(
+ |_: &mut Workspace,
+ &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
+ cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
+ );
+ workspace.register_action(
+ |_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
+ motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
+ },
+ );
workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
motion(Motion::NextLineStart, cx)
});
@@ -304,11 +373,6 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
motion(Motion::WindowBottom, cx)
});
- workspace.register_action(
- |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
- motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
- },
- );
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
@@ -349,7 +413,6 @@ impl Motion {
| WindowBottom
| EndOfParagraph => true,
EndOfLine { .. }
- | NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
@@ -360,8 +423,13 @@ impl Motion {
| EndOfLineDownward
| GoToColumn
| NextWordStart { .. }
+ | NextWordEnd { .. }
| PreviousWordStart { .. }
| PreviousWordEnd { .. }
+ | NextSubwordStart { .. }
+ | NextSubwordEnd { .. }
+ | PreviousSubwordStart { .. }
+ | PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFind { .. }
@@ -376,7 +444,6 @@ impl Motion {
Down { .. }
| Up { .. }
| EndOfLine { .. }
- | NextWordEnd { .. }
| Matching
| FindForward { .. }
| RepeatFind { .. }
@@ -391,14 +458,19 @@ impl Motion {
| EndOfLineDownward
| GoToColumn
| NextWordStart { .. }
+ | NextWordEnd { .. }
| PreviousWordStart { .. }
+ | PreviousWordEnd { .. }
+ | NextSubwordStart { .. }
+ | NextSubwordEnd { .. }
+ | PreviousSubwordStart { .. }
+ | PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFindReversed { .. }
| WindowTop
| WindowMiddle
| WindowBottom
- | PreviousWordEnd { .. }
| NextLineStart => false,
}
}
@@ -413,13 +485,15 @@ impl Motion {
| CurrentLine
| EndOfLine { .. }
| EndOfLineDownward
- | NextWordEnd { .. }
| Matching
| FindForward { .. }
| WindowTop
| WindowMiddle
| WindowBottom
+ | NextWordEnd { .. }
| PreviousWordEnd { .. }
+ | NextSubwordEnd { .. }
+ | PreviousSubwordEnd { .. }
| NextLineStart => true,
Left
| Backspace
@@ -432,6 +506,8 @@ impl Motion {
| GoToColumn
| NextWordStart { .. }
| PreviousWordStart { .. }
+ | NextSubwordStart { .. }
+ | PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
@@ -473,7 +549,7 @@ impl Motion {
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
- next_word_end(map, point, *ignore_punctuation, times),
+ next_word_end(map, point, *ignore_punctuation, times, true),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
@@ -484,6 +560,22 @@ impl Motion {
previous_word_end(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
+ NextSubwordStart { ignore_punctuation } => (
+ next_subword_start(map, point, *ignore_punctuation, times),
+ SelectionGoal::None,
+ ),
+ NextSubwordEnd { ignore_punctuation } => (
+ next_subword_end(map, point, *ignore_punctuation, times, true),
+ SelectionGoal::None,
+ ),
+ PreviousSubwordStart { ignore_punctuation } => (
+ previous_subword_start(map, point, *ignore_punctuation, times),
+ SelectionGoal::None,
+ ),
+ PreviousSubwordEnd { ignore_punctuation } => (
+ previous_subword_end(map, point, *ignore_punctuation, times),
+ SelectionGoal::None,
+ ),
FirstNonWhitespace { display_lines } => (
first_non_whitespace(map, *display_lines, point),
SelectionGoal::None,
@@ -819,6 +911,25 @@ pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize
point
}
+pub(crate) fn next_char(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ allow_cross_newline: bool,
+) -> DisplayPoint {
+ let mut new_point = point;
+ let mut max_column = map.line_len(new_point.row());
+ if !allow_cross_newline {
+ max_column -= 1;
+ }
+ if new_point.column() < max_column {
+ *new_point.column_mut() += 1;
+ } else if new_point < map.max_point() && allow_cross_newline {
+ *new_point.row_mut() += 1;
+ *new_point.column_mut() = 0;
+ }
+ new_point
+}
+
pub(crate) fn next_word_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
@@ -848,22 +959,17 @@ pub(crate) fn next_word_start(
point
}
-fn next_word_end(
+pub(crate) fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
+ allow_cross_newline: bool,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
- let mut new_point = point;
- if new_point.column() < map.line_len(new_point.row()) {
- *new_point.column_mut() += 1;
- } else if new_point < map.max_point() {
- *new_point.row_mut() += 1;
- *new_point.column_mut() = 0;
- }
-
+ let new_point = next_char(map, point, allow_cross_newline);
+ let mut need_next_char = false;
let new_point = movement::find_boundary_exclusive(
map,
new_point,
@@ -871,10 +977,21 @@ fn next_word_end(
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ let at_newline = right == '\n';
+
+ if !allow_cross_newline && at_newline {
+ need_next_char = true;
+ return true;
+ }
left_kind != right_kind && left_kind != CharKind::Whitespace
},
);
+ let new_point = if need_next_char {
+ next_char(map, new_point, true)
+ } else {
+ new_point
+ };
let new_point = map.clip_point(new_point, Bias::Left);
if point == new_point {
break;
@@ -913,6 +1030,210 @@ fn previous_word_start(
point
}
+fn previous_word_end(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ let mut point = point.to_point(map);
+
+ if point.column < map.buffer_snapshot.line_len(point.row) {
+ point.column += 1;
+ }
+ for _ in 0..times {
+ let new_point = movement::find_preceding_boundary_point(
+ &map.buffer_snapshot,
+ point,
+ FindRange::MultiLine,
+ |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ match (left_kind, right_kind) {
+ (CharKind::Punctuation, CharKind::Whitespace)
+ | (CharKind::Punctuation, CharKind::Word)
+ | (CharKind::Word, CharKind::Whitespace)
+ | (CharKind::Word, CharKind::Punctuation) => true,
+ (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
+ _ => false,
+ }
+ },
+ );
+ if new_point == point {
+ break;
+ }
+ point = new_point;
+ }
+ movement::saturating_left(map, point.to_display_point(map))
+}
+
+fn next_subword_start(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ for _ in 0..times {
+ let mut crossed_newline = false;
+ let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ let at_newline = right == '\n';
+
+ let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
+ let is_subword_start =
+ left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
+
+ let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
+ || at_newline && crossed_newline
+ || at_newline && left == '\n'; // Prevents skipping repeated empty lines
+
+ crossed_newline |= at_newline;
+ found
+ });
+ if point == new_point {
+ break;
+ }
+ point = new_point;
+ }
+ point
+}
+
+pub(crate) fn next_subword_end(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+ allow_cross_newline: bool,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ for _ in 0..times {
+ let new_point = next_char(map, point, allow_cross_newline);
+
+ let mut crossed_newline = false;
+ let mut need_backtrack = false;
+ let new_point =
+ movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ let at_newline = right == '\n';
+
+ if !allow_cross_newline && at_newline {
+ return true;
+ }
+
+ let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
+ let is_subword_end =
+ left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
+
+ let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
+
+ if found && (is_word_end || is_subword_end) {
+ need_backtrack = true;
+ }
+
+ crossed_newline |= at_newline;
+ found
+ });
+ let mut new_point = map.clip_point(new_point, Bias::Left);
+ if need_backtrack {
+ *new_point.column_mut() -= 1;
+ }
+ if point == new_point {
+ break;
+ }
+ point = new_point;
+ }
+ point
+}
+
+fn previous_subword_start(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ for _ in 0..times {
+ let mut crossed_newline = false;
+ // This works even though find_preceding_boundary is called for every character in the line containing
+ // cursor because the newline is checked only once.
+ let new_point = movement::find_preceding_boundary_display_point(
+ map,
+ point,
+ FindRange::MultiLine,
+ |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ let at_newline = right == '\n';
+
+ let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
+ let is_subword_start =
+ left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
+
+ let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
+ || at_newline && crossed_newline
+ || at_newline && left == '\n'; // Prevents skipping repeated empty lines
+
+ crossed_newline |= at_newline;
+
+ found
+ },
+ );
+ if point == new_point {
+ break;
+ }
+ point = new_point;
+ }
+ point
+}
+
+fn previous_subword_end(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ let mut point = point.to_point(map);
+
+ if point.column < map.buffer_snapshot.line_len(point.row) {
+ point.column += 1;
+ }
+ for _ in 0..times {
+ let new_point = movement::find_preceding_boundary_point(
+ &map.buffer_snapshot,
+ point,
+ FindRange::MultiLine,
+ |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+
+ let is_subword_end =
+ left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
+
+ if is_subword_end {
+ return true;
+ }
+
+ match (left_kind, right_kind) {
+ (CharKind::Word, CharKind::Whitespace)
+ | (CharKind::Word, CharKind::Punctuation) => true,
+ (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
+ _ => false,
+ }
+ },
+ );
+ if new_point == point {
+ break;
+ }
+ point = new_point;
+ }
+ movement::saturating_left(map, point.to_display_point(map))
+}
+
pub(crate) fn first_non_whitespace(
map: &DisplaySnapshot,
display_lines: bool,
@@ -1217,44 +1538,6 @@ fn window_bottom(
}
}
-fn previous_word_end(
- map: &DisplaySnapshot,
- point: DisplayPoint,
- ignore_punctuation: bool,
- times: usize,
-) -> DisplayPoint {
- let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
- let mut point = point.to_point(map);
-
- if point.column < map.buffer_snapshot.line_len(point.row) {
- point.column += 1;
- }
- for _ in 0..times {
- let new_point = movement::find_preceding_boundary_point(
- &map.buffer_snapshot,
- point,
- FindRange::MultiLine,
- |left, right| {
- let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
- let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
- match (left_kind, right_kind) {
- (CharKind::Punctuation, CharKind::Whitespace)
- | (CharKind::Punctuation, CharKind::Word)
- | (CharKind::Word, CharKind::Whitespace)
- | (CharKind::Word, CharKind::Punctuation) => true,
- (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
- _ => false,
- }
- },
- );
- if new_point == point {
- break;
- }
- point = new_point;
- }
- movement::saturating_left(map, point.to_display_point(map))
-}
-
#[cfg(test)]
mod test {
@@ -407,11 +407,12 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
#[cfg(test)]
mod test {
- use gpui::TestAppContext;
+ use gpui::{KeyBinding, TestAppContext};
use indoc::indoc;
use settings::SettingsStore;
use crate::{
+ motion,
state::Mode::{self},
test::{NeovimBackedTestContext, VimTestContext},
VimSettings,
@@ -1045,4 +1046,73 @@ mod test {
cx.simulate_shared_keystrokes(["4", "$"]).await;
cx.assert_shared_state("aa\nbb\ncˇc").await;
}
+
+ #[gpui::test]
+ async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+ cx.update(|cx| {
+ cx.bind_keys(vec![
+ KeyBinding::new(
+ "w",
+ motion::NextSubwordStart {
+ ignore_punctuation: false,
+ },
+ Some("Editor && VimControl && !VimWaiting && !menu"),
+ ),
+ KeyBinding::new(
+ "b",
+ motion::PreviousSubwordStart {
+ ignore_punctuation: false,
+ },
+ Some("Editor && VimControl && !VimWaiting && !menu"),
+ ),
+ KeyBinding::new(
+ "e",
+ motion::NextSubwordEnd {
+ ignore_punctuation: false,
+ },
+ Some("Editor && VimControl && !VimWaiting && !menu"),
+ ),
+ KeyBinding::new(
+ "g e",
+ motion::PreviousSubwordEnd {
+ ignore_punctuation: false,
+ },
+ Some("Editor && VimControl && !VimWaiting && !menu"),
+ ),
+ ]);
+ });
+
+ cx.assert_binding_normal(
+ ["w"],
+ indoc! {"ˇassert_binding"},
+ indoc! {"assert_ˇbinding"},
+ );
+ // Special case: In 'cw', 'w' acts like 'e'
+ cx.assert_binding(
+ ["c", "w"],
+ indoc! {"ˇassert_binding"},
+ Mode::Normal,
+ indoc! {"ˇ_binding"},
+ Mode::Insert,
+ );
+
+ cx.assert_binding_normal(
+ ["e"],
+ indoc! {"ˇassert_binding"},
+ indoc! {"asserˇt_binding"},
+ );
+
+ cx.assert_binding_normal(
+ ["b"],
+ indoc! {"assert_ˇbinding"},
+ indoc! {"ˇassert_binding"},
+ );
+
+ cx.assert_binding_normal(
+ ["g", "e"],
+ indoc! {"assert_bindinˇg"},
+ indoc! {"asserˇt_binding"},
+ );
+ }
}
@@ -1,15 +1,12 @@
use crate::{
- motion::Motion,
+ motion::{self, Motion},
object::Object,
state::Mode,
- utils::{coerce_punctuation, copy_selections_content},
+ utils::copy_selections_content,
Vim,
};
use editor::{
- display_map::DisplaySnapshot,
- movement::{self, FindRange, TextLayoutDetails},
- scroll::Autoscroll,
- DisplayPoint,
+ display_map::DisplaySnapshot, movement::TextLayoutDetails, scroll::Autoscroll, DisplayPoint,
};
use gpui::WindowContext;
use language::{char_kind, CharKind, Selection};
@@ -39,6 +36,16 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
times,
ignore_punctuation,
&text_layout_details,
+ false,
+ )
+ } else if let Motion::NextSubwordStart { ignore_punctuation } = motion {
+ expand_changed_word_selection(
+ map,
+ selection,
+ times,
+ ignore_punctuation,
+ &text_layout_details,
+ true,
)
} else {
motion.expand_selection(map, selection, times, false, &text_layout_details)
@@ -94,6 +101,7 @@ fn expand_changed_word_selection(
times: Option<usize>,
ignore_punctuation: bool,
text_layout_details: &TextLayoutDetails,
+ use_subword: bool,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
let scope = map
@@ -106,32 +114,30 @@ fn expand_changed_word_selection(
.unwrap_or_default();
if in_word {
- selection.end =
- movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
- let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
- let right_kind =
- coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
-
- left_kind != right_kind && left_kind != CharKind::Whitespace
- });
+ if !use_subword {
+ selection.end =
+ motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
+ } else {
+ selection.end =
+ motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
+ }
+ selection.end = motion::next_char(map, selection.end, false);
true
} else {
- Motion::NextWordStart { ignore_punctuation }.expand_selection(
- map,
- selection,
- None,
- false,
- &text_layout_details,
- )
+ let motion = if use_subword {
+ Motion::NextSubwordStart { ignore_punctuation }
+ } else {
+ Motion::NextWordStart { ignore_punctuation }
+ };
+ motion.expand_selection(map, selection, None, false, &text_layout_details)
}
} else {
- Motion::NextWordStart { ignore_punctuation }.expand_selection(
- map,
- selection,
- times,
- false,
- &text_layout_details,
- )
+ let motion = if use_subword {
+ Motion::NextSubwordStart { ignore_punctuation }
+ } else {
+ Motion::NextWordStart { ignore_punctuation }
+ };
+ motion.expand_selection(map, selection, times, false, &text_layout_details)
}
}