@@ -0,0 +1,319 @@
1+use ropey::RopeSlice;
2+
3+use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
4+use crate::movement::{self, Direction};
5+use crate::surround;
6+use crate::Range;
7+
8+fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
9+ this_word_bound_pos(slice, pos, Direction::Forward)
10+}
11+
12+fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
13+ this_word_bound_pos(slice, pos, Direction::Backward)
14+}
15+
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);
21+ iter.reverse();
22+ iter
23+ }
24+ };
25+
26+ match categorize_char(slice.char(pos)) {
27+ CharCategory::Eol | CharCategory::Whitespace => pos,
28+ category => {
29+ for peek in iter {
30+ let curr_category = categorize_char(peek);
31+ if curr_category != category
32+ || curr_category == CharCategory::Eol
33+ || curr_category == CharCategory::Whitespace
34+ {
35+ return pos;
36+ }
37+ pos = match direction {
38+ Direction::Forward => pos + 1,
39+ Direction::Backward => pos.saturating_sub(1),
40+ }
41+ }
42+ pos
43+ }
44+ }
45+}
46+
47+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
48+pub enum TextObject {
49+ Around,
50+ Inside,
51+}
52+
53+// count doesn't do anything yet
54+pub fn textobject_word(
55+ slice: RopeSlice,
56+ range: Range,
57+ textobject: TextObject,
58+ count: usize,
59+) -> Range {
60+ let this_word_start = this_word_start_pos(slice, range.head);
61+ let this_word_end = this_word_end_pos(slice, range.head);
62+
63+ let (anchor, head);
64+ match textobject {
65+ TextObject::Inside => {
66+ anchor = this_word_start;
67+ head = this_word_end;
68+ }
69+ TextObject::Around => {
70+ if slice
71+ .get_char(this_word_end + 1)
72+ .map_or(true, char_is_line_ending)
73+ {
74+ head = this_word_end;
75+ if slice
76+ .get_char(this_word_start.saturating_sub(1))
77+ .map_or(true, char_is_line_ending)
78+ {
79+ // single word on a line
80+ anchor = this_word_start;
81+ } else {
82+ // last word on a line, select the whitespace before it too
83+ anchor = movement::move_prev_word_end(slice, range, count).head;
84+ }
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
90+ .unwrap_or(0);
91+ } else {
92+ head = movement::move_next_word_start(slice, range, count).head;
93+ anchor = this_word_start;
94+ }
95+ }
96+ };
97+ Range::new(anchor, head)
98+}
99+
100+pub fn textobject_surround(
101+ slice: RopeSlice,
102+ range: Range,
103+ textobject: TextObject,
104+ ch: char,
105+ count: usize,
106+) -> Range {
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),
111+ })
112+ .unwrap_or(range)
113+}
114+
115+#[cfg(test)]
116+mod test {
117+ use super::TextObject::*;
118+ use super::*;
119+
120+ use crate::Range;
121+ use ropey::Rope;
122+
123+ #[test]
124+ fn test_textobject_word() {
125+ // (text, [(cursor position, textobject, final range), ...])
126+ let tests = &[
127+ (
128+ "cursor at beginning of doc",
129+ vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
130+ ),
131+ (
132+ "cursor at middle of word",
133+ vec![
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)),
140+ ],
141+ ),
142+ (
143+ "cursor between word whitespace",
144+ vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
145+ ),
146+ (
147+ "cursor on word before newline\n",
148+ vec![
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)),
155+ ],
156+ ),
157+ (
158+ "cursor on newline\nnext line",
159+ vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
160+ ),
161+ (
162+ "cursor on word after newline\nnext line",
163+ vec![
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)),
170+ ],
171+ ),
172+ (
173+ "cursor on #$%:;* punctuation",
174+ vec![
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)),
181+ ],
182+ ),
183+ (
184+ "cursor on punc%^#$:;.tuation",
185+ vec![
186+ (14, Inside, (14, 20)),
187+ (20, Inside, (14, 20)),
188+ (17, Inside, (14, 20)),
189+ (14, Around, (14, 20)),
190+ // FIXME: edge case
191+ // (20, Around, (14, 20)),
192+ (17, Around, (14, 20)),
193+ ],
194+ ),
195+ (
196+ "cursor in extra whitespace",
197+ vec![
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)),
204+ ],
205+ ),
206+ (
207+ "cursor at end of doc",
208+ vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
209+ ),
210+ ];
211+
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);
218+ assert_eq!(
219+ result,
220+ expected_range.into(),
221+ "\nCase failed: {:?} - {:?}",
222+ sample,
223+ case
224+ );
225+ }
226+ }
227+ }
228+
229+ #[test]
230+ fn test_textobject_surround() {
231+ // (text, [(cursor position, textobject, final range, count), ...])
232+ let tests = &[
233+ (
234+ "simple (single) surround pairs",
235+ vec![
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),
244+ ],
245+ ),
246+ (
247+ "samexx 'single' surround pairs",
248+ vec![
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),
258+ ],
259+ ),
260+ (
261+ "(nested (surround (pairs)) 3 levels)",
262+ vec![
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),
275+ ],
276+ ),
277+ (
278+ "(mixed {surround [pair] same} line)",
279+ vec![
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),
286+ ],
287+ ),
288+ (
289+ "(stepped (surround) pairs (should) skip)",
290+ vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
291+ ),
292+ (
293+ "[surround pairs{\non different]\nlines}",
294+ vec![
295+ (7, Inside, (1, 28), '[', 1),
296+ (15, Inside, (16, 35), '{', 1),
297+ (7, Around, (0, 29), '[', 1),
298+ (15, Around, (15, 36), '{', 1),
299+ ],
300+ ),
301+ ];
302+
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);
309+ assert_eq!(
310+ result,
311+ expected_range.into(),
312+ "\nCase failed: {:?} - {:?}",
313+ sample,
314+ case
315+ );
316+ }
317+ }
318+ }
319+}