1use crate::{
3 buffer::Buffer,
4 dot::{
5 Cur, Dot, Range,
6 find::{Find, find_backward_start, find_forward_end},
7 },
8 key::Arrow,
9};
10use std::cmp::min;
11
12#[allow(dead_code)]
14#[derive(Debug, Copy, Clone, PartialEq, Eq)]
15pub enum TextObject {
16 Arr(Arrow),
17 BufferEnd,
18 BufferStart,
19 Character,
20 FindChar(char),
21 Delimited(char, char),
22 Line,
23 LineEnd,
24 LineStart,
25 Paragraph,
26 Word,
27}
28
29impl TextObject {
30 pub fn as_dot(&self, b: &Buffer) -> Dot {
31 use TextObject::*;
32
33 match self {
34 Arr(arr) => b.dot.active_cur().arr(*arr, b).into(),
35 BufferEnd => Cur::buffer_end(b).into(),
36 BufferStart => Cur::buffer_start().into(),
37 Character => b.dot.active_cur().arr(Arrow::Right, b).into(),
38 FindChar(ch) => find_forward_end(ch, b.dot.active_cur(), b).into(),
39 Delimited(l, r) => FindDelimited::new(*l, *r).expand(b.dot, b),
40 LineEnd => b.dot.active_cur().move_to_line_end(b).into(),
41 LineStart => b.dot.active_cur().move_to_line_start(b).into(),
42 Line => Dot::from(
43 b.dot
44 .as_range()
45 .extend_to_line_start(b)
46 .extend_to_line_end(b),
47 )
48 .collapse_null_range(),
49 Paragraph => FindParagraph::Fwd.expand(b.dot, b),
50 Word => FindWord::Fwd.expand(b.dot, b),
51 }
52 }
53
54 pub fn set_dot(&self, b: &mut Buffer) {
55 b.dot = self.as_dot(b);
56 }
57
58 pub fn extend_dot_forward(&self, b: &mut Buffer) {
59 use TextObject::*;
60
61 let Range {
62 mut start,
63 mut end,
64 start_active,
65 } = b.dot.as_range();
66
67 (start, end) = match (self, start_active) {
68 (Arr(arr), _) => (start, end.arr(*arr, b)),
69 (BufferEnd, true) => (end, Cur::buffer_end(b)),
70 (BufferEnd, false) => (start, Cur::buffer_end(b)),
71 (Character, true) => (start.arr_w_count(Arrow::Right, 1, b), end),
72 (Character, false) => (start, end.arr_w_count(Arrow::Right, 1, b)),
73 (FindChar(ch), true) => (find_forward_end(ch, start, b), end),
74 (FindChar(ch), false) => (start, find_forward_end(ch, end, b)),
75 (Line, true) => (start.arr_w_count(Arrow::Down, 1, b), end),
76 (Line, false) => (start, end.arr_w_count(Arrow::Down, 1, b)),
77 (LineEnd, true) => (start.move_to_line_end(b), end),
78 (LineEnd, false) => (start, end.move_to_line_end(b)),
79 (LineStart, true) => (start.move_to_line_start(b), end),
80 (LineStart, false) => (start, end.move_to_line_start(b)),
81 (Paragraph, true) => (find_forward_end(&FindParagraph::Fwd, start, b), end),
82 (Paragraph, false) => (start, find_forward_end(&FindParagraph::Fwd, end, b)),
83 (Word, true) => (find_forward_end(&FindWord::Fwd, start, b), end),
84 (Word, false) => (start, find_forward_end(&FindWord::Fwd, end, b)),
85 (BufferStart | Delimited(_, _), _) => return,
87 };
88
89 b.dot = Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range();
90 }
91
92 pub fn extend_dot_backward(&self, b: &mut Buffer) {
93 use TextObject::*;
94
95 let Range {
96 mut start,
97 mut end,
98 start_active,
99 } = b.dot.as_range();
100
101 (start, end) = match (self, start_active) {
102 (Arr(arr), _) => (start.arr(arr.flip(), b), end),
103 (BufferStart, true) => (Cur::buffer_start(), end),
104 (BufferStart, false) => (Cur::buffer_start(), start),
105 (Character, true) => (start.arr_w_count(Arrow::Left, 1, b), end),
106 (Character, false) => (start, end.arr_w_count(Arrow::Left, 1, b)),
107 (FindChar(ch), true) => (find_backward_start(ch, start, b), end),
108 (FindChar(ch), false) => (start, find_backward_start(ch, end, b)),
109 (Line, true) => (start.arr_w_count(Arrow::Up, 1, b), end),
110 (Line, false) => (start, end.arr_w_count(Arrow::Up, 1, b)),
111 (LineEnd, true) => (start.move_to_line_end(b), end),
112 (LineEnd, false) => (start, end.move_to_line_end(b)),
113 (LineStart, true) => (start.move_to_line_start(b), end),
114 (LineStart, false) => (start, end.move_to_line_start(b)),
115 (Paragraph, true) => (find_backward_start(&FindParagraph::Fwd, start, b), end),
116 (Paragraph, false) => (start, find_backward_start(&FindParagraph::Fwd, end, b)),
117 (Word, true) => (find_backward_start(&FindWord::Fwd, start, b), end),
118 (Word, false) => (start, find_backward_start(&FindWord::Fwd, end, b)),
119 (BufferEnd | Delimited(_, _), _) => return,
121 };
122
123 b.dot = Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range();
124 }
125}
126
127pub struct FindDelimited {
128 l: String,
129 r: String,
130 rev: bool,
131}
132
133impl FindDelimited {
134 pub fn new(l: impl Into<String>, r: impl Into<String>) -> Self {
135 Self {
136 l: l.into(),
137 r: r.into(),
138 rev: false,
139 }
140 }
141}
142
143impl Find for FindDelimited {
144 type Reversed = FindDelimited;
145
146 fn reversed(&self) -> Self::Reversed {
147 Self {
148 l: self.l.clone(),
149 r: self.r.clone(),
150 rev: !self.rev,
151 }
152 }
153
154 fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
155 where
156 I: Iterator<Item = (usize, char)>,
157 {
158 let (target, other) = if self.rev {
159 (&self.l, &self.r)
160 } else {
161 (&self.r, &self.l)
162 };
163 let mut skips = 0;
164
165 for (i, ch) in it {
166 if other.contains(ch) && target != other {
167 skips += 1;
168 } else if skips == 0 && target.contains(ch) {
169 let ix = if self.rev { i + 1 } else { i - 1 };
170 return Some((ix, ix));
171 } else if target.contains(ch) {
172 skips -= 1;
173 }
174 }
175
176 None
177 }
178}
179
180enum FindParagraph {
181 Fwd,
182 Bck,
183}
184
185impl Find for FindParagraph {
186 type Reversed = FindParagraph;
187
188 fn reversed(&self) -> Self::Reversed {
189 match self {
190 Self::Fwd => Self::Bck,
191 Self::Bck => Self::Fwd,
192 }
193 }
194
195 fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
196 where
197 I: Iterator<Item = (usize, char)>,
198 {
199 let mut prev_was_newline = false;
200 let mut pos = 0;
201
202 for (i, ch) in it {
203 match ch {
204 '\n' if prev_was_newline => {
205 return match self {
206 Self::Fwd => Some((i, i)),
207 Self::Bck => Some((i + 1, i + 1)),
208 };
209 }
210 '\n' => prev_was_newline = true,
211 _ => prev_was_newline = false,
212 }
213 pos = i;
214 }
215
216 Some((pos, pos))
217 }
218}
219
220enum FindWord {
221 Fwd,
222 Bck,
223}
224
225impl Find for FindWord {
226 type Reversed = FindWord;
227
228 fn reversed(&self) -> Self::Reversed {
229 match self {
230 Self::Fwd => Self::Bck,
231 Self::Bck => Self::Fwd,
232 }
233 }
234
235 fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
236 where
237 I: Iterator<Item = (usize, char)>,
238 {
239 use CharKind::*;
240
241 let mut it = it.peekable();
242 let mut prev = CharKind::from(it.peek()?.1);
243
244 if matches!((prev, self), (Word | Punctuation, FindWord::Fwd)) {
248 it.next();
249 prev = CharKind::from(it.peek()?.1);
250 }
251
252 for (i, ch) in it {
253 let kind = CharKind::from(ch);
254 match (prev, kind) {
255 (Word, Punctuation) | (Punctuation, Word) | (Word | Punctuation, Whitespace) => {
256 return match self {
257 Self::Fwd => Some((i - 1, i - 1)),
258 Self::Bck => Some((i + 1, i + 1)),
259 };
260 }
261 _ => prev = kind,
262 }
263 }
264
265 None
266 }
267
268 fn expand(&self, dot: Dot, b: &Buffer) -> Dot {
269 use CharKind::*;
270
271 let Range {
272 mut start,
273 mut end,
274 start_active,
275 } = dot.as_range();
276 let max_idx = b.txt.len_chars() - 1;
277 start.idx = min(start.idx, max_idx);
278 end.idx = min(end.idx, max_idx);
279
280 if start.idx > 0 {
281 let current = CharKind::from(b.txt.char(start.idx));
282 let prev = CharKind::from(b.txt.char(start.idx - 1));
283
284 match (prev, current) {
285 (Whitespace, Word | Punctuation) => (),
287
288 (_, Whitespace) if start.idx < max_idx => {
290 while matches!(CharKind::from(b.txt.char(start.idx)), Whitespace) {
291 start.idx += 1;
292 if start.idx == max_idx {
293 end.idx = max_idx;
294 break;
295 }
296 }
297 }
298
299 _ => start = find_backward_start(self, start, b),
301 }
302 }
303
304 if end.idx < max_idx {
305 let current = CharKind::from(b.txt.char(end.idx));
306 let next = CharKind::from(b.txt.char(end.idx + 1));
307
308 match (current, next) {
309 (Word | Punctuation, Whitespace) => (),
311
312 _ => end = find_forward_end(self, end, b),
314 }
315 }
316
317 Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range()
318 }
319}
320
321#[derive(Debug, Clone, Copy)]
322enum CharKind {
323 Word,
324 Punctuation,
325 Whitespace,
326}
327
328impl From<char> for CharKind {
329 fn from(ch: char) -> Self {
330 if ch.is_alphanumeric() || ch == '_' {
331 CharKind::Word
332 } else if ch.is_whitespace() {
333 CharKind::Whitespace
334 } else {
335 CharKind::Punctuation
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use simple_test_case::test_case;
344
345 #[test_case(FindWord::Fwd, 0, "this"; "forward start of buffer")]
346 #[test_case(FindWord::Fwd, 3, "this"; "forward end of first word")]
347 #[test_case(FindWord::Fwd, 4, "is"; "forward after first word")]
348 #[test_case(FindWord::Fwd, 5, "is"; "forward start of second word")]
349 #[test_case(FindWord::Fwd, 6, "is"; "forward end of second word")]
350 #[test_case(FindWord::Fwd, 9, "test"; "forward before last word")]
351 #[test_case(FindWord::Fwd, 13, "test"; "forward end of buffer")]
352 #[test]
353 fn expand_word(fw: FindWord, idx: usize, expected: &str) {
354 let b = Buffer::new_virtual(0, "test", "this is a test", Default::default());
355 let dot = Dot::Cur { c: Cur { idx } };
356 let expanded = fw.expand(dot, &b);
357 let content = expanded.content(&b);
358
359 assert_eq!(content, expected);
360 }
361
362 #[test]
363 fn expand_word_for_buffer_with_trailing_spaces() {
364 let b = Buffer::new_virtual(0, "test", "this is a test ", Default::default());
365 let dot = Dot::Cur { c: Cur { idx: 14 } };
366 let expanded = FindWord::Fwd.expand(dot, &b);
367 let content = expanded.content(&b);
368
369 assert!(matches!(expanded, Dot::Cur { c: Cur { idx: 16 } }));
371 assert_eq!(content, " ");
372 }
373}