aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/textobject.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core/src/textobject.rs')
-rw-r--r--helix-core/src/textobject.rs261
1 files changed, 128 insertions, 133 deletions
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index fbf66256..b06bca5d 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -1,21 +1,16 @@
use ropey::RopeSlice;
-use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
-use crate::movement::{self, Direction};
+use crate::chars::{categorize_char, CharCategory};
+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
+use crate::movement::Direction;
use crate::surround;
use crate::Range;
-fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
- this_word_bound_pos(slice, pos, Direction::Forward)
-}
-
-fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
- this_word_bound_pos(slice, pos, Direction::Backward)
-}
+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
+ use CharCategory::{Eol, Whitespace};
-fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
let iter = match direction {
- Direction::Forward => slice.chars_at(pos + 1),
+ Direction::Forward => slice.chars_at(pos),
Direction::Backward => {
let mut iter = slice.chars_at(pos);
iter.reverse();
@@ -23,25 +18,31 @@ fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -
}
};
- match categorize_char(slice.char(pos)) {
- CharCategory::Eol | CharCategory::Whitespace => pos,
- category => {
- for peek in iter {
- let curr_category = categorize_char(peek);
- if curr_category != category
- || curr_category == CharCategory::Eol
- || curr_category == CharCategory::Whitespace
- {
+ let mut prev_category = match direction {
+ Direction::Forward if pos == 0 => Whitespace,
+ Direction::Forward => categorize_char(slice.char(pos - 1)),
+ Direction::Backward if pos == slice.len_chars() => Whitespace,
+ Direction::Backward => categorize_char(slice.char(pos)),
+ };
+
+ for ch in iter {
+ match categorize_char(ch) {
+ Eol | Whitespace => return pos,
+ category => {
+ if category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos;
- }
- pos = match direction {
- Direction::Forward => pos + 1,
- Direction::Backward => pos.saturating_sub(1),
+ } else {
+ match direction {
+ Direction::Forward => pos += 1,
+ Direction::Backward => pos = pos.saturating_sub(1),
+ }
+ prev_category = category;
}
}
- pos
}
}
+
+ pos
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -55,46 +56,37 @@ pub fn textobject_word(
slice: RopeSlice,
range: Range,
textobject: TextObject,
- count: usize,
+ _count: usize,
) -> Range {
- let this_word_start = this_word_start_pos(slice, range.head);
- let this_word_end = this_word_end_pos(slice, range.head);
+ let pos = range.cursor(slice);
+
+ let word_start = find_word_boundary(slice, pos, Direction::Backward);
+ let word_end = match slice.get_char(pos).map(categorize_char) {
+ None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
+ _ => find_word_boundary(slice, pos + 1, Direction::Forward),
+ };
+
+ // Special case.
+ if word_start == word_end {
+ return Range::new(word_start, word_end);
+ }
- let (anchor, head);
match textobject {
- TextObject::Inside => {
- anchor = this_word_start;
- head = this_word_end;
- }
- TextObject::Around => {
- if slice
- .get_char(this_word_end + 1)
- .map_or(true, char_is_line_ending)
+ TextObject::Inside => Range::new(word_start, word_end),
+ TextObject::Around => Range::new(
+ match slice
+ .get_char(word_start.saturating_sub(1))
+ .map(categorize_char)
{
- head = this_word_end;
- if slice
- .get_char(this_word_start.saturating_sub(1))
- .map_or(true, char_is_line_ending)
- {
- // single word on a line
- anchor = this_word_start;
- } else {
- // last word on a line, select the whitespace before it too
- anchor = movement::move_prev_word_end(slice, range, count).head;
- }
- } else if char_is_whitespace(slice.char(range.head)) {
- // select whole whitespace and next word
- head = movement::move_next_word_end(slice, range, count).head;
- anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
- .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
- .unwrap_or(0);
- } else {
- head = movement::move_next_word_start(slice, range, count).head;
- anchor = this_word_start;
- }
- }
- };
- Range::new(anchor, head)
+ None | Some(CharCategory::Eol) => word_start,
+ _ => prev_grapheme_boundary(slice, word_start),
+ },
+ match slice.get_char(word_end).map(categorize_char) {
+ None | Some(CharCategory::Eol) => word_end,
+ _ => next_grapheme_boundary(slice, word_end),
+ },
+ ),
+ }
}
pub fn textobject_surround(
@@ -106,7 +98,10 @@ pub fn textobject_surround(
) -> Range {
surround::find_nth_pairs_pos(slice, ch, range.head, count)
.map(|(anchor, head)| match textobject {
- TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
+ TextObject::Inside => Range::new(
+ next_grapheme_boundary(slice, anchor),
+ prev_grapheme_boundary(slice, head),
+ ),
TextObject::Around => Range::new(anchor, head),
})
.unwrap_or(range)
@@ -126,70 +121,70 @@ mod test {
let tests = &[
(
"cursor at beginning of doc",
- vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
+ vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
),
(
"cursor at middle of word",
vec![
- (13, Inside, (10, 15)),
- (10, Inside, (10, 15)),
- (15, Inside, (10, 15)),
- (13, Around, (10, 16)),
- (10, Around, (10, 16)),
- (15, Around, (10, 16)),
+ (13, Inside, (10, 16)),
+ (10, Inside, (10, 16)),
+ (15, Inside, (10, 16)),
+ (13, Around, (9, 17)),
+ (10, Around, (9, 17)),
+ (15, Around, (9, 17)),
],
),
(
"cursor between word whitespace",
- vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
+ vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
),
(
"cursor on word before newline\n",
vec![
- (22, Inside, (22, 28)),
- (28, Inside, (22, 28)),
- (25, Inside, (22, 28)),
- (22, Around, (21, 28)),
- (28, Around, (21, 28)),
- (25, Around, (21, 28)),
+ (22, Inside, (22, 29)),
+ (28, Inside, (22, 29)),
+ (25, Inside, (22, 29)),
+ (22, Around, (21, 29)),
+ (28, Around, (21, 29)),
+ (25, Around, (21, 29)),
],
),
(
"cursor on newline\nnext line",
- vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
+ vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
),
(
"cursor on word after newline\nnext line",
vec![
- (29, Inside, (29, 32)),
- (30, Inside, (29, 32)),
- (32, Inside, (29, 32)),
- (29, Around, (29, 33)),
- (30, Around, (29, 33)),
- (32, Around, (29, 33)),
+ (29, Inside, (29, 33)),
+ (30, Inside, (29, 33)),
+ (32, Inside, (29, 33)),
+ (29, Around, (29, 34)),
+ (30, Around, (29, 34)),
+ (32, Around, (29, 34)),
],
),
(
"cursor on #$%:;* punctuation",
vec![
- (13, Inside, (10, 15)),
- (10, Inside, (10, 15)),
- (15, Inside, (10, 15)),
- (13, Around, (10, 16)),
- (10, Around, (10, 16)),
- (15, Around, (10, 16)),
+ (13, Inside, (10, 16)),
+ (10, Inside, (10, 16)),
+ (15, Inside, (10, 16)),
+ (13, Around, (9, 17)),
+ (10, Around, (9, 17)),
+ (15, Around, (9, 17)),
],
),
(
"cursor on punc%^#$:;.tuation",
vec![
- (14, Inside, (14, 20)),
- (20, Inside, (14, 20)),
- (17, Inside, (14, 20)),
- (14, Around, (14, 20)),
+ (14, Inside, (14, 21)),
+ (20, Inside, (14, 21)),
+ (17, Inside, (14, 21)),
+ (14, Around, (13, 22)),
// FIXME: edge case
// (20, Around, (14, 20)),
- (17, Around, (14, 20)),
+ (17, Around, (13, 22)),
],
),
(
@@ -198,14 +193,14 @@ mod test {
(9, Inside, (9, 9)),
(10, Inside, (10, 10)),
(11, Inside, (11, 11)),
- (9, Around, (9, 16)),
- (10, Around, (9, 16)),
- (11, Around, (9, 16)),
+ (9, Around, (9, 9)),
+ (10, Around, (10, 10)),
+ (11, Around, (11, 11)),
],
),
(
"cursor at end of doc",
- vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
+ vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
),
];
@@ -234,67 +229,67 @@ mod test {
"simple (single) surround pairs",
vec![
(3, Inside, (3, 3), '(', 1),
- (7, Inside, (8, 13), ')', 1),
- (10, Inside, (8, 13), '(', 1),
- (14, Inside, (8, 13), ')', 1),
+ (7, Inside, (8, 14), ')', 1),
+ (10, Inside, (8, 14), '(', 1),
+ (14, Inside, (8, 14), ')', 1),
(3, Around, (3, 3), '(', 1),
- (7, Around, (7, 14), ')', 1),
- (10, Around, (7, 14), '(', 1),
- (14, Around, (7, 14), ')', 1),
+ (7, Around, (7, 15), ')', 1),
+ (10, Around, (7, 15), '(', 1),
+ (14, Around, (7, 15), ')', 1),
],
),
(
"samexx 'single' surround pairs",
vec![
(3, Inside, (3, 3), '\'', 1),
- (7, Inside, (8, 13), '\'', 1),
- (10, Inside, (8, 13), '\'', 1),
- (14, Inside, (8, 13), '\'', 1),
+ (7, Inside, (7, 7), '\'', 1),
+ (10, Inside, (8, 14), '\'', 1),
+ (14, Inside, (14, 14), '\'', 1),
(3, Around, (3, 3), '\'', 1),
- (7, Around, (7, 14), '\'', 1),
- (10, Around, (7, 14), '\'', 1),
- (14, Around, (7, 14), '\'', 1),
+ (7, Around, (7, 7), '\'', 1),
+ (10, Around, (7, 15), '\'', 1),
+ (14, Around, (14, 14), '\'', 1),
],
),
(
"(nested (surround (pairs)) 3 levels)",
vec![
- (0, Inside, (1, 34), '(', 1),
- (6, Inside, (1, 34), ')', 1),
- (8, Inside, (9, 24), '(', 1),
- (8, Inside, (9, 34), ')', 2),
- (20, Inside, (9, 24), '(', 2),
- (20, Inside, (1, 34), ')', 3),
- (0, Around, (0, 35), '(', 1),
- (6, Around, (0, 35), ')', 1),
- (8, Around, (8, 25), '(', 1),
- (8, Around, (8, 35), ')', 2),
- (20, Around, (8, 25), '(', 2),
- (20, Around, (0, 35), ')', 3),
+ (0, Inside, (1, 35), '(', 1),
+ (6, Inside, (1, 35), ')', 1),
+ (8, Inside, (9, 25), '(', 1),
+ (8, Inside, (9, 35), ')', 2),
+ (20, Inside, (9, 25), '(', 2),
+ (20, Inside, (1, 35), ')', 3),
+ (0, Around, (0, 36), '(', 1),
+ (6, Around, (0, 36), ')', 1),
+ (8, Around, (8, 26), '(', 1),
+ (8, Around, (8, 36), ')', 2),
+ (20, Around, (8, 26), '(', 2),
+ (20, Around, (0, 36), ')', 3),
],
),
(
"(mixed {surround [pair] same} line)",
vec![
- (2, Inside, (1, 33), '(', 1),
- (9, Inside, (8, 27), '{', 1),
- (18, Inside, (18, 21), '[', 1),
- (2, Around, (0, 34), '(', 1),
- (9, Around, (7, 28), '{', 1),
- (18, Around, (17, 22), '[', 1),
+ (2, Inside, (1, 34), '(', 1),
+ (9, Inside, (8, 28), '{', 1),
+ (18, Inside, (18, 22), '[', 1),
+ (2, Around, (0, 35), '(', 1),
+ (9, Around, (7, 29), '{', 1),
+ (18, Around, (17, 23), '[', 1),
],
),
(
"(stepped (surround) pairs (should) skip)",
- vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
+ vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
),
(
"[surround pairs{\non different]\nlines}",
vec![
- (7, Inside, (1, 28), '[', 1),
- (15, Inside, (16, 35), '{', 1),
- (7, Around, (0, 29), '[', 1),
- (15, Around, (15, 36), '{', 1),
+ (7, Inside, (1, 29), '[', 1),
+ (15, Inside, (16, 36), '{', 1),
+ (7, Around, (0, 30), '[', 1),
+ (15, Around, (15, 37), '{', 1),
],
),
];