ad_editor/dot/
text_object.rs

1//! Vim style text objects
2use 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/// A vim-like text object which can be used to manipulate the current Dot in a Buffer
13#[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            // Can't move forward to the buffer start or move forward between delimiters
86            (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            // Can't move back to the buffer end or move back between delimiters
120            (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 we are searching forward and are not currently sat on whitespace then we could be
245        // on the end of a word which would cause us to stick in place, so we advance a single
246        // character and start the search from there.
247        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                // We're at the start of the current word so start.idx is correct
286                (Whitespace, Word | Punctuation) => (),
287
288                // We're in whitespace so advance until we hit a word or the end of the buffer
289                (_, 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                // We're in a word so search back to find the start
300                _ => 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                // We're at the end of a word so end.idx is correct
310                (Word | Punctuation, Whitespace) => (),
311
312                // We're within a word or in whitespace so advance to find the end of the word
313                _ => 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        // Should have advanced to the end of the buffer and selected that final space character
370        assert!(matches!(expanded, Dot::Cur { c: Cur { idx: 16 } }));
371        assert_eq!(content, " ");
372    }
373}