@@ -0,0 +1,319 @@
3+use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
4+use crate::movement::{self, Direction};
8+fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
9+ this_word_bound_pos(slice, pos, Direction::Forward)
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 {
18+ Direction::Forward => slice.chars_at(pos + 1),
19+ Direction::Backward => {
20+ let mut iter = slice.chars_at(pos);
26+ match categorize_char(slice.char(pos)) {
27+ CharCategory::Eol | CharCategory::Whitespace => pos,
30+ let curr_category = categorize_char(peek);
31+ if curr_category != category
32+ || curr_category == CharCategory::Eol
33+ || curr_category == CharCategory::Whitespace
37+ pos = match direction {
38+ Direction::Forward => pos + 1,
39+ Direction::Backward => pos.saturating_sub(1),
47+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
53+// count doesn't do anything yet
54+pub fn textobject_word(
57+ textobject: TextObject,
60+ let this_word_start = this_word_start_pos(slice, range.head);
61+ let this_word_end = this_word_end_pos(slice, range.head);
65+ TextObject::Inside => {
66+ anchor = this_word_start;
67+ head = this_word_end;
69+ TextObject::Around => {
71+ .get_char(this_word_end + 1)
72+ .map_or(true, char_is_line_ending)
74+ head = this_word_end;
76+ .get_char(this_word_start.saturating_sub(1))
77+ .map_or(true, char_is_line_ending)
79+ // single word on a line
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)
100+pub fn textobject_surround(
103+ textobject: TextObject,
107+ surround::find_nth_pairs_pos(slice, ch, range.head, count)
108+ .map(|(anchor, head)| match textobject {
109+ TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
110+ TextObject::Around => Range::new(anchor, head),
117+ use super::TextObject::*;
124+ fn test_textobject_word() {
125+ // (text, [(cursor position, textobject, final range), ...])
128+ "cursor at beginning of doc",
129+ vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
132+ "cursor at middle of word",
134+ (13, Inside, (10, 15)),
135+ (10, Inside, (10, 15)),
136+ (15, Inside, (10, 15)),
137+ (13, Around, (10, 16)),
138+ (10, Around, (10, 16)),
139+ (15, Around, (10, 16)),
143+ "cursor between word whitespace",
144+ vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
147+ "cursor on word before newline\n",
149+ (22, Inside, (22, 28)),
150+ (28, Inside, (22, 28)),
151+ (25, Inside, (22, 28)),
152+ (22, Around, (21, 28)),
153+ (28, Around, (21, 28)),
154+ (25, Around, (21, 28)),
158+ "cursor on newline\nnext line",
159+ vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
162+ "cursor on word after newline\nnext line",
164+ (29, Inside, (29, 32)),
165+ (30, Inside, (29, 32)),
166+ (32, Inside, (29, 32)),
167+ (29, Around, (29, 33)),
168+ (30, Around, (29, 33)),
169+ (32, Around, (29, 33)),
173+ "cursor on #$%:;* punctuation",
175+ (13, Inside, (10, 15)),
176+ (10, Inside, (10, 15)),
177+ (15, Inside, (10, 15)),
178+ (13, Around, (10, 16)),
179+ (10, Around, (10, 16)),
180+ (15, Around, (10, 16)),
184+ "cursor on punc%^#$:;.tuation",
186+ (14, Inside, (14, 20)),
187+ (20, Inside, (14, 20)),
188+ (17, Inside, (14, 20)),
189+ (14, Around, (14, 20)),
191+ // (20, Around, (14, 20)),
192+ (17, Around, (14, 20)),
196+ "cursor in extra whitespace",
198+ (9, Inside, (9, 9)),
199+ (10, Inside, (10, 10)),
200+ (11, Inside, (11, 11)),
201+ (9, Around, (9, 16)),
202+ (10, Around, (9, 16)),
203+ (11, Around, (9, 16)),
207+ "cursor at end of doc",
208+ vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
212+ for (sample, scenario) in tests {
213+ let doc = Rope::from(*sample);
214+ let slice = doc.slice(..);
215+ for &case in scenario {
216+ let (pos, objtype, expected_range) = case;
217+ let result = textobject_word(slice, Range::point(pos), objtype, 1);
220+ expected_range.into(),
221+ "\nCase failed: {:?} - {:?}",
230+ fn test_textobject_surround() {
231+ // (text, [(cursor position, textobject, final range, count), ...])
234+ "simple (single) surround pairs",
236+ (3, Inside, (3, 3), '(', 1),
237+ (7, Inside, (8, 13), ')', 1),
238+ (10, Inside, (8, 13), '(', 1),
239+ (14, Inside, (8, 13), ')', 1),
240+ (3, Around, (3, 3), '(', 1),
241+ (7, Around, (7, 14), ')', 1),
242+ (10, Around, (7, 14), '(', 1),
243+ (14, Around, (7, 14), ')', 1),
247+ "samexx 'single' surround pairs",
249+ (3, Inside, (3, 3), '\'', 1),
250+ // FIXME: surround doesn't work when *on* same chars pair
251+ // (7, Inner, (8, 13), '\'', 1),
252+ (10, Inside, (8, 13), '\'', 1),
253+ // (14, Inner, (8, 13), '\'', 1),
254+ (3, Around, (3, 3), '\'', 1),
255+ // (7, Around, (7, 14), '\'', 1),
256+ (10, Around, (7, 14), '\'', 1),
257+ // (14, Around, (7, 14), '\'', 1),
261+ "(nested (surround (pairs)) 3 levels)",
263+ (0, Inside, (1, 34), '(', 1),
264+ (6, Inside, (1, 34), ')', 1),
265+ (8, Inside, (9, 24), '(', 1),
266+ (8, Inside, (9, 34), ')', 2),
267+ (20, Inside, (9, 24), '(', 2),
268+ (20, Inside, (1, 34), ')', 3),
269+ (0, Around, (0, 35), '(', 1),
270+ (6, Around, (0, 35), ')', 1),
271+ (8, Around, (8, 25), '(', 1),
272+ (8, Around, (8, 35), ')', 2),
273+ (20, Around, (8, 25), '(', 2),
274+ (20, Around, (0, 35), ')', 3),
278+ "(mixed {surround [pair] same} line)",
280+ (2, Inside, (1, 33), '(', 1),
281+ (9, Inside, (8, 27), '{', 1),
282+ (18, Inside, (18, 21), '[', 1),
283+ (2, Around, (0, 34), '(', 1),
284+ (9, Around, (7, 28), '{', 1),
285+ (18, Around, (17, 22), '[', 1),
289+ "(stepped (surround) pairs (should) skip)",
290+ vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
293+ "[surround pairs{\non different]\nlines}",
295+ (7, Inside, (1, 28), '[', 1),
296+ (15, Inside, (16, 35), '{', 1),
297+ (7, Around, (0, 29), '[', 1),
298+ (15, Around, (15, 36), '{', 1),
303+ for (sample, scenario) in tests {
304+ let doc = Rope::from(*sample);
305+ let slice = doc.slice(..);
306+ for &case in scenario {
307+ let (pos, objtype, expected_range, ch, count) = case;
308+ let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
311+ expected_range.into(),
312+ "\nCase failed: {:?} - {:?}",