@@ -1,21 +1,16 @@
1use ropey::RopeSlice;
1use ropey::RopeSlice;
3−use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
3+use crate::chars::{categorize_char, CharCategory};
4+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
4−use crate::movement::{self, Direction};
5+use crate::movement::Direction;
5use crate::surround;
6use crate::surround;
6use crate::Range;
7use crate::Range;
8−fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
9+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
9− this_word_bound_pos(slice, pos, Direction::Forward)
10+ use CharCategory::{Eol, Whitespace};
12−fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
13− this_word_bound_pos(slice, pos, Direction::Backward)
16−fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
17 let iter = match direction {
12 let iter = match direction {
18− Direction::Forward => slice.chars_at(pos + 1),
13+ Direction::Forward => slice.chars_at(pos),
19 Direction::Backward => {
14 Direction::Backward => {
20 let mut iter = slice.chars_at(pos);
15 let mut iter = slice.chars_at(pos);
21 iter.reverse();
16 iter.reverse();
@@ -23,25 +18,32 @@ fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -
26− match categorize_char(slice.char(pos)) {
21+ let mut prev_category = match direction {
27− CharCategory::Eol | CharCategory::Whitespace => pos,
22+ Direction::Forward if pos == 0 => Whitespace,
28− category => {
23+ Direction::Forward => categorize_char(slice.char(pos - 1)),
29− for peek in iter {
24+ Direction::Backward if pos == slice.len_chars() => Whitespace,
30− let curr_category = categorize_char(peek);
25+ Direction::Backward => categorize_char(slice.char(pos)),
31− if curr_category != category
26+ };
32− || curr_category == CharCategory::Eol
27+
33− || curr_category == CharCategory::Whitespace
28+ for ch in iter {
34− {
29+ match categorize_char(ch) {
30+ Eol | Whitespace => return pos,
32+ if category != prev_category && pos != 0 && pos != slice.len_chars() {
35 return pos;
33 return pos;
37− pos = match direction {
38− Direction::Forward => pos + 1,
35+ if direction == Direction::Forward {
39− Direction::Backward => pos.saturating_sub(1),
38+ pos = pos.saturating_sub(1);
40+ prev_category = category;
47#[derive(Copy, Clone, PartialEq, Eq, Debug)]
49#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -55,46 +57,42 @@ pub fn textobject_word(
55 slice: RopeSlice,
57 slice: RopeSlice,
56 range: Range,
58 range: Range,
57 textobject: TextObject,
59 textobject: TextObject,
58− count: usize,
60+ _count: usize,
59) -> Range {
61) -> Range {
60− let this_word_start = this_word_start_pos(slice, range.head);
62+ // For 1-width cursor semantics.
61− let this_word_end = this_word_end_pos(slice, range.head);
63+ let head = if range.head > range.anchor {
64+ prev_grapheme_boundary(slice, range.head)
69+ let word_start = find_word_boundary(slice, head, Direction::Backward);
70+ let word_end = match slice.get_char(head).map(categorize_char) {
71+ None | Some(CharCategory::Whitespace | CharCategory::Eol) => head,
72+ _ => find_word_boundary(slice, head + 1, Direction::Forward),
76+ if word_start == word_end {
77+ return Range::new(word_start, word_end);
64 match textobject {
80 match textobject {
65− TextObject::Inside => {
81+ TextObject::Inside => Range::new(word_start, word_end),
66− anchor = this_word_start;
67− head = this_word_end;
69− TextObject::Around => {
82+ TextObject::Around => Range::new(
70− if slice
83+ match slice
71− .get_char(this_word_end + 1)
84+ .get_char(word_start.saturating_sub(1))
72− .map_or(true, char_is_line_ending)
85+ .map(categorize_char)
74− head = this_word_end;
87+ None | Some(CharCategory::Eol) => word_start,
75− if slice
88+ _ => prev_grapheme_boundary(slice, word_start),
76− .get_char(this_word_start.saturating_sub(1))
89+ },
77− .map_or(true, char_is_line_ending)
90+ match slice.get_char(word_end).map(categorize_char) {
78− {
91+ None | Some(CharCategory::Eol) => word_end,
79− // single word on a line
92+ _ => next_grapheme_boundary(slice, word_end),
80− anchor = this_word_start;
82− // last word on a line, select the whitespace before it too
83− anchor = movement::move_prev_word_end(slice, range, count).head;
85− } else if char_is_whitespace(slice.char(range.head)) {
86− // select whole whitespace and next word
87− head = movement::move_next_word_end(slice, range, count).head;
88− anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
89− .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
92− head = movement::move_next_word_start(slice, range, count).head;
93− anchor = this_word_start;
97− Range::new(anchor, head)
100pub fn textobject_surround(
98pub fn textobject_surround(
@@ -106,7 +104,10 @@ pub fn textobject_surround(
106) -> Range {
104) -> Range {
107 surround::find_nth_pairs_pos(slice, ch, range.head, count)
105 surround::find_nth_pairs_pos(slice, ch, range.head, count)
108 .map(|(anchor, head)| match textobject {
106 .map(|(anchor, head)| match textobject {
109− TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
107+ TextObject::Inside => Range::new(
108+ next_grapheme_boundary(slice, anchor),
109+ prev_grapheme_boundary(slice, head),
110 TextObject::Around => Range::new(anchor, head),
111 TextObject::Around => Range::new(anchor, head),
112 .unwrap_or(range)
113 .unwrap_or(range)
@@ -126,70 +127,70 @@ mod test {
126 let tests = &[
127 let tests = &[
128 "cursor at beginning of doc",
129 "cursor at beginning of doc",
129− vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
130+ vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
132 "cursor at middle of word",
133 "cursor at middle of word",
134− (13, Inside, (10, 15)),
135+ (13, Inside, (10, 16)),
135− (10, Inside, (10, 15)),
136+ (10, Inside, (10, 16)),
136− (15, Inside, (10, 15)),
137+ (15, Inside, (10, 16)),
137− (13, Around, (10, 16)),
138+ (13, Around, (9, 17)),
138− (10, Around, (10, 16)),
139+ (10, Around, (9, 17)),
139− (15, Around, (10, 16)),
140+ (15, Around, (9, 17)),
143 "cursor between word whitespace",
144 "cursor between word whitespace",
144− vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
145+ vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
147 "cursor on word before newline\n",
148 "cursor on word before newline\n",
149− (22, Inside, (22, 28)),
150+ (22, Inside, (22, 29)),
150− (28, Inside, (22, 28)),
151+ (28, Inside, (22, 29)),
151− (25, Inside, (22, 28)),
152+ (25, Inside, (22, 29)),
152− (22, Around, (21, 28)),
153+ (22, Around, (21, 29)),
153− (28, Around, (21, 28)),
154+ (28, Around, (21, 29)),
154− (25, Around, (21, 28)),
155+ (25, Around, (21, 29)),
158 "cursor on newline\nnext line",
159 "cursor on newline\nnext line",
159− vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
160+ vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
162 "cursor on word after newline\nnext line",
163 "cursor on word after newline\nnext line",
164− (29, Inside, (29, 32)),
165+ (29, Inside, (29, 33)),
165− (30, Inside, (29, 32)),
166+ (30, Inside, (29, 33)),
166− (32, Inside, (29, 32)),
167+ (32, Inside, (29, 33)),
167− (29, Around, (29, 33)),
168+ (29, Around, (29, 34)),
168− (30, Around, (29, 33)),
169+ (30, Around, (29, 34)),
169− (32, Around, (29, 33)),
170+ (32, Around, (29, 34)),
173 "cursor on #$%:;* punctuation",
174 "cursor on #$%:;* punctuation",
175− (13, Inside, (10, 15)),
176+ (13, Inside, (10, 16)),
176− (10, Inside, (10, 15)),
177+ (10, Inside, (10, 16)),
177− (15, Inside, (10, 15)),
178+ (15, Inside, (10, 16)),
178− (13, Around, (10, 16)),
179+ (13, Around, (9, 17)),
179− (10, Around, (10, 16)),
180+ (10, Around, (9, 17)),
180− (15, Around, (10, 16)),
181+ (15, Around, (9, 17)),
184 "cursor on punc%^#$:;.tuation",
185 "cursor on punc%^#$:;.tuation",
186− (14, Inside, (14, 20)),
187+ (14, Inside, (14, 21)),
187− (20, Inside, (14, 20)),
188+ (20, Inside, (14, 21)),
188− (17, Inside, (14, 20)),
189+ (17, Inside, (14, 21)),
189− (14, Around, (14, 20)),
190+ (14, Around, (13, 22)),
190 // FIXME: edge case
191 // FIXME: edge case
191 // (20, Around, (14, 20)),
192 // (20, Around, (14, 20)),
192− (17, Around, (14, 20)),
193+ (17, Around, (13, 22)),
@@ -198,14 +199,14 @@ mod test {
198 (9, Inside, (9, 9)),
199 (9, Inside, (9, 9)),
199 (10, Inside, (10, 10)),
200 (10, Inside, (10, 10)),
200 (11, Inside, (11, 11)),
201 (11, Inside, (11, 11)),
201− (9, Around, (9, 16)),
202+ (9, Around, (9, 9)),
202− (10, Around, (9, 16)),
203+ (10, Around, (10, 10)),
203− (11, Around, (9, 16)),
204+ (11, Around, (11, 11)),
207 "cursor at end of doc",
208 "cursor at end of doc",
208− vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
209+ vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
@@ -234,67 +235,67 @@ mod test {
234 "simple (single) surround pairs",
235 "simple (single) surround pairs",
236 (3, Inside, (3, 3), '(', 1),
237 (3, Inside, (3, 3), '(', 1),
237− (7, Inside, (8, 13), ')', 1),
238+ (7, Inside, (8, 14), ')', 1),
238− (10, Inside, (8, 13), '(', 1),
239+ (10, Inside, (8, 14), '(', 1),
239− (14, Inside, (8, 13), ')', 1),
240+ (14, Inside, (8, 14), ')', 1),
240 (3, Around, (3, 3), '(', 1),
241 (3, Around, (3, 3), '(', 1),
241− (7, Around, (7, 14), ')', 1),
242+ (7, Around, (7, 15), ')', 1),
242− (10, Around, (7, 14), '(', 1),
243+ (10, Around, (7, 15), '(', 1),
243− (14, Around, (7, 14), ')', 1),
244+ (14, Around, (7, 15), ')', 1),
247 "samexx 'single' surround pairs",
248 "samexx 'single' surround pairs",
249 (3, Inside, (3, 3), '\'', 1),
250 (3, Inside, (3, 3), '\'', 1),
250− (7, Inside, (8, 13), '\'', 1),
251+ (7, Inside, (7, 7), '\'', 1),
251− (10, Inside, (8, 13), '\'', 1),
252+ (10, Inside, (8, 14), '\'', 1),
252− (14, Inside, (8, 13), '\'', 1),
253+ (14, Inside, (14, 14), '\'', 1),
253 (3, Around, (3, 3), '\'', 1),
254 (3, Around, (3, 3), '\'', 1),
254− (7, Around, (7, 14), '\'', 1),
255+ (7, Around, (7, 7), '\'', 1),
255− (10, Around, (7, 14), '\'', 1),
256+ (10, Around, (7, 15), '\'', 1),
256− (14, Around, (7, 14), '\'', 1),
257+ (14, Around, (14, 14), '\'', 1),
260 "(nested (surround (pairs)) 3 levels)",
261 "(nested (surround (pairs)) 3 levels)",
262− (0, Inside, (1, 34), '(', 1),
263+ (0, Inside, (1, 35), '(', 1),
263− (6, Inside, (1, 34), ')', 1),
264+ (6, Inside, (1, 35), ')', 1),
264− (8, Inside, (9, 24), '(', 1),
265+ (8, Inside, (9, 25), '(', 1),
265− (8, Inside, (9, 34), ')', 2),
266+ (8, Inside, (9, 35), ')', 2),
266− (20, Inside, (9, 24), '(', 2),
267+ (20, Inside, (9, 25), '(', 2),
267− (20, Inside, (1, 34), ')', 3),
268+ (20, Inside, (1, 35), ')', 3),
268− (0, Around, (0, 35), '(', 1),
269+ (0, Around, (0, 36), '(', 1),
269− (6, Around, (0, 35), ')', 1),
270+ (6, Around, (0, 36), ')', 1),
270− (8, Around, (8, 25), '(', 1),
271+ (8, Around, (8, 26), '(', 1),
271− (8, Around, (8, 35), ')', 2),
272+ (8, Around, (8, 36), ')', 2),
272− (20, Around, (8, 25), '(', 2),
273+ (20, Around, (8, 26), '(', 2),
273− (20, Around, (0, 35), ')', 3),
274+ (20, Around, (0, 36), ')', 3),
277 "(mixed {surround [pair] same} line)",
278 "(mixed {surround [pair] same} line)",
279− (2, Inside, (1, 33), '(', 1),
280+ (2, Inside, (1, 34), '(', 1),
280− (9, Inside, (8, 27), '{', 1),
281+ (9, Inside, (8, 28), '{', 1),
281− (18, Inside, (18, 21), '[', 1),
282+ (18, Inside, (18, 22), '[', 1),
282− (2, Around, (0, 34), '(', 1),
283+ (2, Around, (0, 35), '(', 1),
283− (9, Around, (7, 28), '{', 1),
284+ (9, Around, (7, 29), '{', 1),
284− (18, Around, (17, 22), '[', 1),
285+ (18, Around, (17, 23), '[', 1),
288 "(stepped (surround) pairs (should) skip)",
289 "(stepped (surround) pairs (should) skip)",
289− vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
290+ vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
292 "[surround pairs{\non different]\nlines}",
293 "[surround pairs{\non different]\nlines}",
294− (7, Inside, (1, 28), '[', 1),
295+ (7, Inside, (1, 29), '[', 1),
295− (15, Inside, (16, 35), '{', 1),
296+ (15, Inside, (16, 36), '{', 1),
296− (7, Around, (0, 29), '[', 1),
297+ (7, Around, (0, 30), '[', 1),
297− (15, Around, (15, 36), '{', 1),
298+ (15, Around, (15, 37), '{', 1),