Skip to content

Commit

Permalink
vim: Add Subword Textobject (#22387)
Browse files Browse the repository at this point in the history
Closes #22761

[Vim: subword text object?
#22280](#22280)

Release Notes:

- Added Vim SubWord TextObject

---------

Co-authored-by: Conrad Irwin <[email protected]>
  • Loading branch information
0x2CA and ConradIrwin authored Jan 14, 2025
1 parent 03c99e3 commit 26be440
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 1 deletion.
3 changes: 3 additions & 0 deletions assets/keymaps/vim.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
// Subword TextObject
// "w": "vim::Subword",
// "shift-w": ["vim::Subword", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",
Expand Down
117 changes: 116 additions & 1 deletion crates/vim/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -46,14 +47,20 @@ 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 {
#[serde(default)]
include_below: bool,
}

impl_actions!(vim, [Word, IndentObj]);
impl_actions!(vim, [Word, Subword, IndentObj]);

actions!(
vim,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -159,6 +173,7 @@ impl Object {
pub fn is_multiline(self) -> bool {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 26be440

Please sign in to comment.