mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-24 18:36:18 +04:00
Merge 4b4740a322
into b8313da5a8
This commit is contained in:
commit
c9a9b48026
@ -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]
|
||||||
|
@ -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 == '_') {
|
||||||
|
@ -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(),
|
||||||
|
@ -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),
|
||||||
|
@ -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()
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user