@@ -20,6 +20,7 @@ use serde::Deserialize;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
pub enum Object {
Word { ignore_punctuation: bool },
+ Subword { ignore_punctuation: bool },
Sentence,
Paragraph,
Quotes,
@@ -46,6 +47,12 @@ struct Word {
ignore_punctuation: bool,
}
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Subword {
+ #[serde(default)]
+ ignore_punctuation: bool,
+}
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
struct IndentObj {
@@ -53,7 +60,7 @@ struct IndentObj {
include_below: bool,
}
-impl_actions!(vim, [Word, IndentObj]);
+impl_actions!(vim, [Word, Subword, IndentObj]);
actions!(
vim,
@@ -85,6 +92,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
vim.object(Object::Word { ignore_punctuation }, cx)
},
);
+ Vim::action(
+ editor,
+ cx,
+ |vim, &Subword { ignore_punctuation }: &Subword, cx| {
+ vim.object(Object::Subword { ignore_punctuation }, cx)
+ },
+ );
Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
Vim::action(editor, cx, |vim, _: &Sentence, cx| {
vim.object(Object::Sentence, cx)
@@ -159,6 +173,7 @@ impl Object {
pub fn is_multiline(self) -> bool {
match self {
Object::Word { .. }
+ | Object::Subword { .. }
| Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
@@ -182,6 +197,7 @@ impl Object {
pub fn always_expands_both_ways(self) -> bool {
match self {
Object::Word { .. }
+ | Object::Subword { .. }
| Object::Sentence
| Object::Paragraph
| Object::Argument
@@ -205,6 +221,7 @@ impl Object {
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
match self {
Object::Word { .. }
+ | Object::Subword { .. }
| Object::Sentence
| Object::Quotes
| Object::AnyQuotes
@@ -251,6 +268,13 @@ impl Object {
in_word(map, relative_to, ignore_punctuation)
}
}
+ Object::Subword { ignore_punctuation } => {
+ if around {
+ around_subword(map, relative_to, ignore_punctuation)
+ } else {
+ in_subword(map, relative_to, ignore_punctuation)
+ }
+ }
Object::Sentence => sentence(map, relative_to, around),
Object::Paragraph => paragraph(map, relative_to, around),
Object::Quotes => {
@@ -387,6 +411,63 @@ fn in_word(
Some(start..end)
}
+fn in_subword(
+ map: &DisplaySnapshot,
+ relative_to: DisplayPoint,
+ ignore_punctuation: bool,
+) -> Option<Range<DisplayPoint>> {
+ let offset = relative_to.to_offset(map, Bias::Left);
+ // Use motion::right so that we consider the character under the cursor when looking for the start
+ let classifier = map
+ .buffer_snapshot
+ .char_classifier_at(relative_to.to_point(map))
+ .ignore_punctuation(ignore_punctuation);
+ let in_subword = map
+ .buffer_chars_at(offset)
+ .next()
+ .map(|(c, _)| {
+ if classifier.is_word('-') {
+ !classifier.is_whitespace(c) && c != '_' && c != '-'
+ } else {
+ !classifier.is_whitespace(c) && c != '_'
+ }
+ })
+ .unwrap_or(false);
+
+ let start = if in_subword {
+ movement::find_preceding_boundary_display_point(
+ map,
+ right(map, relative_to, 1),
+ movement::FindRange::SingleLine,
+ |left, right| {
+ let is_word_start = classifier.kind(left) != classifier.kind(right);
+ let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
+ || left == '_' && right != '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_start || is_subword_start
+ },
+ )
+ } else {
+ movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
+ let is_word_start = classifier.kind(left) != classifier.kind(right);
+ let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
+ || left == '_' && right != '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_start || is_subword_start
+ })
+ };
+
+ let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
+ let is_word_end = classifier.kind(left) != classifier.kind(right);
+ let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
+ || left != '_' && right == '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_end || is_subword_end
+ });
+
+ Some(start..end)
+}
+
pub fn surrounding_html_tag(
map: &DisplaySnapshot,
head: DisplayPoint,
@@ -498,6 +579,40 @@ fn around_word(
}
}
+fn around_subword(
+ map: &DisplaySnapshot,
+ relative_to: DisplayPoint,
+ ignore_punctuation: bool,
+) -> Option<Range<DisplayPoint>> {
+ // Use motion::right so that we consider the character under the cursor when looking for the start
+ let classifier = map
+ .buffer_snapshot
+ .char_classifier_at(relative_to.to_point(map))
+ .ignore_punctuation(ignore_punctuation);
+ let start = movement::find_preceding_boundary_display_point(
+ map,
+ right(map, relative_to, 1),
+ movement::FindRange::SingleLine,
+ |left, right| {
+ let is_word_start = classifier.kind(left) != classifier.kind(right);
+ let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
+ || left != '_' && right == '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_start || is_subword_start
+ },
+ );
+
+ let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
+ let is_word_end = classifier.kind(left) != classifier.kind(right);
+ let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
+ || left != '_' && right == '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_end || is_subword_end
+ });
+
+ Some(start..end)
+}
+
fn around_containing_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,