This commit is contained in:
Nikita Revenco 2024-11-21 10:53:56 +09:00 committed by GitHub
commit c9a9b48026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 16 deletions

View File

@ -11,6 +11,7 @@ pub enum CharCategory {
Unknown, Unknown,
} }
/// Determine whether a character is a line ending, whitespace, a "word" character, a punctuation or unknown
#[inline] #[inline]
pub fn categorize_char(ch: char) -> CharCategory { pub fn categorize_char(ch: char) -> CharCategory {
if char_is_line_ending(ch) { if char_is_line_ending(ch) {
@ -32,6 +33,12 @@ pub fn char_is_line_ending(ch: char) -> bool {
LineEnding::from_char(ch).is_some() LineEnding::from_char(ch).is_some()
} }
/// Determine whether a character is a subword text object delimiter.
#[inline]
pub fn char_is_subword_textobj_delimiter(ch: char) -> bool {
ch == '_' || ch == '-' || ch == '/'
}
/// Determine whether a character qualifies as (non-line-break) /// Determine whether a character qualifies as (non-line-break)
/// whitespace. /// whitespace.
#[inline] #[inline]

View File

@ -502,7 +502,7 @@ fn is_long_word_boundary(a: char, b: char) -> bool {
} }
} }
fn is_sub_word_boundary(a: char, b: char, dir: Direction) -> bool { pub fn is_sub_word_boundary(a: char, b: char, dir: Direction) -> bool {
match (categorize_char(a), categorize_char(b)) { match (categorize_char(a), categorize_char(b)) {
(CharCategory::Word, CharCategory::Word) => { (CharCategory::Word, CharCategory::Word) => {
if (a == '_') != (b == '_') { if (a == '_') != (b == '_') {

View File

@ -3,19 +3,33 @@
use ropey::RopeSlice; use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor}; use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::chars::{
categorize_char, char_is_subword_textobj_delimiter, char_is_whitespace, CharCategory,
};
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending; use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction; use crate::movement::{is_sub_word_boundary, Direction};
use crate::syntax::LanguageConfiguration; use crate::syntax::LanguageConfiguration;
use crate::Range; use crate::Range;
use crate::{surround, Syntax}; use crate::{surround, Syntax};
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { /// # Arguments
///
/// * `pos` - index of the character
/// * `long` - whether it's a word or a WORD
fn find_word_boundary(
slice: RopeSlice,
mut pos: usize,
direction: Direction,
long: bool,
is_subword: bool,
) -> usize {
use CharCategory::{Eol, Whitespace}; use CharCategory::{Eol, Whitespace};
let iter = match direction { let iter = match direction {
// create forward iterator
Direction::Forward => slice.chars_at(pos), Direction::Forward => slice.chars_at(pos),
// create reverse iterator, if we iterate over it we will be advancing in the opposite direction
Direction::Backward => { Direction::Backward => {
let mut iter = slice.chars_at(pos); let mut iter = slice.chars_at(pos);
iter.reverse(); iter.reverse();
@ -24,17 +38,44 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo
}; };
let mut prev_category = match direction { let mut prev_category = match direction {
// if we are at the beginning or end of the document
Direction::Forward if pos == 0 => Whitespace, Direction::Forward if pos == 0 => Whitespace,
Direction::Forward => categorize_char(slice.char(pos - 1)),
Direction::Backward if pos == slice.len_chars() => Whitespace, Direction::Backward if pos == slice.len_chars() => Whitespace,
Direction::Forward => categorize_char(slice.char(pos - 1)),
Direction::Backward => categorize_char(slice.char(pos)), Direction::Backward => categorize_char(slice.char(pos)),
}; };
let mut prev_ch = match direction {
// if we are at the beginning or end of the document
Direction::Forward if pos == 0 => ' ',
Direction::Backward if pos == slice.len_chars() => ' ',
Direction::Forward => slice.char(pos - 1),
Direction::Backward => slice.char(pos),
};
for ch in iter { for ch in iter {
match categorize_char(ch) { match categorize_char(ch) {
// When we find the first whitespace, that's going to be our position that we jump to
Eol | Whitespace => return pos, Eol | Whitespace => return pos,
// every character other than a whitespace
category => { category => {
if !long && category != prev_category && pos != 0 && pos != slice.len_chars() { let matches_short_word = !long
&& !is_subword
&& category != prev_category
&& pos != 0
&& pos != slice.len_chars();
let matches_subword = is_subword
&& match direction {
Direction::Forward => is_sub_word_boundary(prev_ch, ch, Direction::Forward),
Direction::Backward => {
is_sub_word_boundary(prev_ch, ch, Direction::Backward)
}
};
if matches_subword {
return pos;
} else if matches_short_word {
return pos; return pos;
} else { } else {
match direction { match direction {
@ -42,6 +83,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo
Direction::Backward => pos = pos.saturating_sub(1), Direction::Backward => pos = pos.saturating_sub(1),
} }
prev_category = category; prev_category = category;
prev_ch = ch;
} }
} }
} }
@ -75,13 +117,14 @@ pub fn textobject_word(
textobject: TextObject, textobject: TextObject,
_count: usize, _count: usize,
long: bool, long: bool,
is_subword: bool,
) -> Range { ) -> Range {
let pos = range.cursor(slice); let pos = range.cursor(slice);
let word_start = find_word_boundary(slice, pos, Direction::Backward, long); let word_start = find_word_boundary(slice, pos, Direction::Backward, long, is_subword);
let word_end = match slice.get_char(pos).map(categorize_char) { let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward, long), _ => find_word_boundary(slice, pos + 1, Direction::Forward, long, is_subword),
}; };
// Special case. // Special case.
@ -89,9 +132,28 @@ pub fn textobject_word(
return Range::new(word_start, word_end); return Range::new(word_start, word_end);
} }
match textobject { match (textobject, is_subword) {
TextObject::Inside => Range::new(word_start, word_end), (TextObject::Inside, true) => Range::new(word_start, word_end),
TextObject::Around => { (TextObject::Around, true) => {
let underscores_count_right = slice
.chars_at(word_end)
.take_while(|c| char_is_subword_textobj_delimiter(*c))
.count();
if underscores_count_right > 0 {
Range::new(word_start, word_end + underscores_count_right)
} else {
let underscore_count_left = {
let mut iter = slice.chars_at(word_start);
iter.reverse();
iter.take_while(|c| char_is_subword_textobj_delimiter(*c))
.count()
};
Range::new(word_start - underscore_count_left, word_end)
}
}
(TextObject::Inside, false) => Range::new(word_start, word_end),
(TextObject::Around, false) => {
let whitespace_count_right = slice let whitespace_count_right = slice
.chars_at(word_end) .chars_at(word_end)
.take_while(|c| char_is_whitespace(*c)) .take_while(|c| char_is_whitespace(*c))
@ -108,7 +170,8 @@ pub fn textobject_word(
Range::new(word_start - whitespace_count_left, word_end) Range::new(word_start - whitespace_count_left, word_end)
} }
} }
TextObject::Movement => unreachable!(), (TextObject::Movement, false) => unreachable!(),
(TextObject::Movement, true) => unreachable!(),
} }
} }
@ -401,7 +464,7 @@ fn test_textobject_word() {
let (pos, objtype, expected_range) = case; let (pos, objtype, expected_range) = case;
// cursor is a single width selection // cursor is a single width selection
let range = Range::new(pos, pos + 1); let range = Range::new(pos, pos + 1);
let result = textobject_word(slice, range, objtype, 1, false); let result = textobject_word(slice, range, objtype, 1, false, false);
assert_eq!( assert_eq!(
result, result,
expected_range.into(), expected_range.into(),

View File

@ -5590,8 +5590,15 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
match ch { match ch {
'w' => textobject::textobject_word(text, range, objtype, count, false), 'w' => {
'W' => textobject::textobject_word(text, range, objtype, count, true), textobject::textobject_word(text, range, objtype, count, false, false)
}
'W' => {
textobject::textobject_word(text, range, objtype, count, true, false)
}
's' => {
textobject::textobject_word(text, range, objtype, count, false, true)
}
't' => textobject_treesitter("class", range), 't' => textobject_treesitter("class", range),
'f' => textobject_treesitter("function", range), 'f' => textobject_treesitter("function", range),
'a' => textobject_treesitter("parameter", range), 'a' => textobject_treesitter("parameter", range),

View File

@ -1068,7 +1068,7 @@ fn get_prefill_from_word_boundary(editor: &Editor) -> String {
primary_selection primary_selection
} else { } else {
use helix_core::textobject::{textobject_word, TextObject}; use helix_core::textobject::{textobject_word, TextObject};
textobject_word(text, primary_selection, TextObject::Inside, 1, false) textobject_word(text, primary_selection, TextObject::Inside, 1, false, false)
} }
.fragment(text) .fragment(text)
.into() .into()

View File

@ -586,6 +586,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
textobject::TextObject::Inside, textobject::TextObject::Inside,
1, 1,
false, false,
false,
); );
let line = text.slice(range.from()..range.to()).to_string(); let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() { if !line.is_empty() {