Skip to main content

redox_core/buffer/text_buffer/
words.rs

1//! Word-motion helpers for `TextBuffer`.
2//!
3//! Vim-like lowercase word motions use three character classes:
4//! - keyword chars (`[A-Za-z0-9_]` for now)
5//! - whitespace
6//! - symbols (any non-whitespace, non-keyword char)
7//!
8//! This setup lets `w`/`b`/`e` visit punctuation such as `(`, `)`, `/`, `-`, etc.
9
10use super::super::util::is_word_char;
11use super::TextBuffer;
12use crate::buffer::Pos;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum CharClass {
16    Whitespace,
17    Keyword,
18    Symbol,
19}
20
21#[inline]
22fn classify(ch: char) -> CharClass {
23    if ch.is_whitespace() {
24        CharClass::Whitespace
25    } else if is_word_char(ch) {
26        CharClass::Keyword
27    } else {
28        CharClass::Symbol
29    }
30}
31
32impl TextBuffer {
33    /// Find the start of the previous/current word-like run (`b`-style).
34    pub fn word_start_before(&self, pos: Pos) -> Pos {
35        let mut c = self.pos_to_char(pos);
36        if c == 0 {
37            return Pos::zero();
38        }
39
40        while c > 0 && classify(self.rope.char(c - 1)) == CharClass::Whitespace {
41            c -= 1;
42        }
43
44        if c == 0 {
45            return Pos::zero();
46        }
47
48        let target = classify(self.rope.char(c - 1));
49        while c > 0 && classify(self.rope.char(c - 1)) == target {
50            c -= 1;
51        }
52
53        self.char_to_pos(c)
54    }
55
56    /// Find the end of the current/next word-like run (`e`-style).
57    pub fn word_end_after(&self, pos: Pos) -> Pos {
58        let mut c = self.pos_to_char(pos);
59        let maxc = self.len_chars();
60        if c >= maxc {
61            return self.char_to_pos(c);
62        }
63
64        let mut class = classify(self.rope.char(c));
65        if class == CharClass::Whitespace {
66            while c < maxc && classify(self.rope.char(c)) == CharClass::Whitespace {
67                c += 1;
68            }
69            if c >= maxc {
70                return self.char_to_pos(c.saturating_sub(1));
71            }
72            class = classify(self.rope.char(c));
73        } else {
74            let at_end = c + 1 >= maxc || classify(self.rope.char(c + 1)) != class;
75            if at_end {
76                c += 1;
77                while c < maxc && classify(self.rope.char(c)) == CharClass::Whitespace {
78                    c += 1;
79                }
80                if c >= maxc {
81                    return self.char_to_pos(c.saturating_sub(1));
82                }
83                class = classify(self.rope.char(c));
84            }
85        }
86
87        while c + 1 < maxc && classify(self.rope.char(c + 1)) == class {
88            c += 1;
89        }
90
91        self.char_to_pos(c)
92    }
93
94    /// Find the start of the next word-like run (`w`-style).
95    pub fn word_start_after(&self, pos: Pos) -> Pos {
96        let mut c = self.pos_to_char(pos);
97        let maxc = self.len_chars();
98        if c >= maxc {
99            return self.char_to_pos(c);
100        }
101
102        let class = classify(self.rope.char(c));
103        if class != CharClass::Whitespace {
104            while c < maxc && classify(self.rope.char(c)) == class {
105                c += 1;
106            }
107        }
108
109        while c < maxc && classify(self.rope.char(c)) == CharClass::Whitespace {
110            c += 1;
111        }
112
113        self.char_to_pos(c)
114    }
115}