Skip to main content

sequoia_buffer/
cursor.rs

1use unicode_segmentation::GraphemeCursor;
2
3use crate::piece::TextBuffer;
4
5#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
6pub struct Cursor {
7    pub offset: usize,
8
9    pub anchor: Option<usize>,
10}
11
12#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13pub struct Selection {
14    pub start: usize,
15    pub end: usize,
16}
17
18impl Cursor {
19    pub fn new(offset: usize) -> Self {
20        Self {
21            offset,
22            anchor: None,
23        }
24    }
25
26    pub fn selection(&self) -> Option<Selection> {
27        self.anchor.map(|a| Selection {
28            start: a.min(self.offset),
29            end: a.max(self.offset),
30        })
31    }
32
33    pub fn is_collapsed(&self) -> bool {
34        self.anchor.is_none_or(|a| a == self.offset)
35    }
36
37    pub fn move_to(&mut self, offset: usize, extend: bool) {
38        if extend {
39            let anchor = self.anchor.unwrap_or(self.offset);
40            self.anchor = Some(anchor);
41            self.offset = offset;
42            if self.anchor == Some(self.offset) {
43                self.anchor = None;
44            }
45        } else {
46            self.offset = offset;
47            self.anchor = None;
48        }
49    }
50}
51
52fn is_word_char(c: char) -> bool {
53    c.is_alphanumeric() || c == '_'
54}
55
56struct Table<'a> {
57    chunks: Vec<(usize, &'a str)>,
58    len: usize,
59}
60
61fn table(buf: &TextBuffer) -> Table<'_> {
62    Table {
63        chunks: buf.chunk_offsets(),
64        len: buf.len_bytes(),
65    }
66}
67
68fn chunk_at<'a>(t: &Table<'a>, offset: usize) -> Option<(usize, usize, &'a str)> {
69    if t.chunks.is_empty() {
70        return None;
71    }
72    let mut idx = t.chunks.partition_point(|&(s, _)| s <= offset);
73    if idx == t.chunks.len() || t.chunks[idx].0 > offset {
74        idx = idx.saturating_sub(1);
75    }
76    let (start, chunk) = t.chunks[idx];
77    if offset > start + chunk.len() {
78        return None;
79    }
80    Some((idx, start, chunk))
81}
82
83fn prev_in_table(t: &Table<'_>, offset: usize) -> usize {
84    if offset == 0 || t.len == 0 {
85        return 0;
86    }
87    let mut cursor = GraphemeCursor::new(offset, t.len, true);
88    let Some((mut idx, _, _)) = chunk_at(t, offset.saturating_sub(1)) else {
89        return 0;
90    };
91
92    if offset > 0 {
93        while idx + 1 < t.chunks.len() && t.chunks[idx + 1].0 < offset {
94            idx += 1;
95        }
96    }
97    loop {
98        let (start, chunk) = t.chunks[idx];
99        match cursor.prev_boundary(chunk, start) {
100            Ok(Some(b)) => return b,
101            Ok(None) => return 0,
102            Err(unicode_segmentation::GraphemeIncomplete::PrevChunk) => {
103                if idx == 0 {
104                    return 0;
105                }
106                idx -= 1;
107            }
108            Err(_) => return offset,
109        }
110    }
111}
112
113fn next_in_table(t: &Table<'_>, offset: usize) -> usize {
114    if offset >= t.len || t.len == 0 {
115        return t.len;
116    }
117    let mut cursor = GraphemeCursor::new(offset, t.len, true);
118    let Some((mut idx, _, _)) = chunk_at(t, offset) else {
119        return t.len;
120    };
121    loop {
122        let (start, chunk) = t.chunks[idx];
123        match cursor.next_boundary(chunk, start) {
124            Ok(Some(b)) => return b,
125            Ok(None) => return t.len,
126            Err(unicode_segmentation::GraphemeIncomplete::NextChunk) => {
127                if idx + 1 >= t.chunks.len() {
128                    return t.len;
129                }
130                idx += 1;
131            }
132            Err(_) => return offset,
133        }
134    }
135}
136
137fn grapheme_first_char(t: &Table<'_>, pos: usize) -> char {
138    if pos >= t.len {
139        return '\0';
140    }
141    match chunk_at(t, pos) {
142        Some((_, start, chunk)) => chunk[pos - start..].chars().next().unwrap_or('\0'),
143        None => '\0',
144    }
145}
146
147fn is_word_span(t: &Table<'_>, from: usize, to: usize) -> bool {
148    from < to && is_word_char(grapheme_first_char(t, from))
149}
150
151pub fn prev_grapheme(buf: &TextBuffer, offset: usize) -> usize {
152    let t = table(buf);
153    prev_in_table(&t, buf.snap(offset))
154}
155
156pub fn next_grapheme(buf: &TextBuffer, offset: usize) -> usize {
157    let t = table(buf);
158    next_in_table(&t, buf.snap(offset))
159}
160
161pub fn word_start_before(buf: &TextBuffer, offset: usize) -> usize {
162    let t = table(buf);
163    let mut i = buf.snap(offset).min(t.len);
164    while i > 0 {
165        let p = prev_in_table(&t, i);
166        if p == i || is_word_span(&t, p, i) {
167            break;
168        }
169        i = p;
170    }
171    while i > 0 {
172        let p = prev_in_table(&t, i);
173        if p == i || !is_word_span(&t, p, i) {
174            break;
175        }
176        i = p;
177    }
178    i
179}
180
181pub fn word_end_after(buf: &TextBuffer, offset: usize) -> usize {
182    let t = table(buf);
183    let mut i = buf.snap(offset).min(t.len);
184    while i < t.len {
185        let n = next_in_table(&t, i);
186        if n == i || is_word_span(&t, i, n) {
187            break;
188        }
189        i = n;
190    }
191    while i < t.len {
192        let n = next_in_table(&t, i);
193        if n == i || !is_word_span(&t, i, n) {
194            break;
195        }
196        i = n;
197    }
198    i
199}
200
201pub fn line_home(buf: &TextBuffer, offset: usize) -> usize {
202    let lines = buf.lines();
203    lines.line_start(lines.line_of(offset.min(buf.len_bytes())))
204}
205
206pub fn line_end(buf: &TextBuffer, offset: usize) -> usize {
207    let lines = buf.lines();
208    let (_, end) = lines.line_range(lines.line_of(offset.min(buf.len_bytes())));
209    end
210}
211
212pub fn move_lines(
213    buf: &TextBuffer,
214    offset: usize,
215    delta: isize,
216    goal_col: Option<usize>,
217) -> (usize, usize) {
218    let lines = buf.lines();
219    let len = buf.len_bytes();
220    let offset = buf.snap(offset).min(len);
221    let (line, _) = lines.line_col(offset);
222    let col = match goal_col {
223        Some(c) => c,
224        None => {
225            let (start, _) = lines.line_range(line);
226            buf.substring(start..offset).chars().count()
227        }
228    };
229    let target = (line as isize + delta)
230        .max(0)
231        .min(lines.line_count().saturating_sub(1) as isize) as usize;
232    let (tstart, tend) = lines.line_range(target);
233    let mut pos = tstart;
234    let mut actual = 0usize;
235    for _ in 0..col {
236        if pos >= tend {
237            break;
238        }
239        let t = table(buf);
240        let n = next_in_table(&t, pos);
241        if n == pos || n > tend {
242            break;
243        }
244        pos = n;
245        actual += 1;
246    }
247    (pos, actual)
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::piece::TextBuffer;
254
255    #[test]
256    fn cursor_selection_semantics() {
257        let mut c = Cursor::new(5);
258        assert_eq!(c.selection(), None);
259        assert!(c.is_collapsed());
260        c.move_to(8, true);
261        assert_eq!(c.selection(), Some(Selection { start: 5, end: 8 }));
262
263        c.move_to(5, true);
264        assert_eq!(c.selection(), None);
265        assert!(c.is_collapsed());
266
267        c.move_to(10, false);
268        c.move_to(3, true);
269        assert_eq!(c.selection(), Some(Selection { start: 3, end: 10 }));
270        c.move_to(0, false);
271        assert_eq!(c.selection(), None);
272    }
273
274    #[test]
275    fn graphemes_never_split() {
276        let buf = TextBuffer::from("a👨‍👩‍👧🇵🇱é\r\nz");
277        let len = buf.len_bytes();
278
279        let mut pos = 0usize;
280        let mut count = 0;
281        while pos < len {
282            let n = next_grapheme(&buf, pos);
283            assert!(n > pos, "must advance at {pos}");
284            assert!(buf.snap(n) == n, "boundary at {n}");
285            pos = n;
286            count += 1;
287            assert!(count < 100, "no infinite loop");
288        }
289        assert_eq!(pos, len);
290
291        let mut back = len;
292        let mut count = 0;
293        while back > 0 {
294            back = prev_grapheme(&buf, back);
295            count += 1;
296            assert!(count < 100, "no infinite loop");
297        }
298        assert_eq!(back, 0);
299
300        assert_eq!(next_grapheme(&buf, 1), 1 + "👨‍👩‍👧".len());
301        assert_eq!(prev_grapheme(&buf, 1 + "👨‍👩‍👧".len()), 1);
302    }
303
304    #[test]
305    fn words_mirror_widget_behavior() {
306        let buf = TextBuffer::from("foo bar_baz2  qux");
307
308        assert_eq!(word_start_before(&buf, 11), 4);
309        assert_eq!(word_start_before(&buf, 4), 0);
310        assert_eq!(word_start_before(&buf, 3), 0);
311        assert_eq!(word_end_after(&buf, 0), 3);
312        assert_eq!(word_end_after(&buf, 3), 12);
313        assert_eq!(word_end_after(&buf, 11), 12);
314
315        let buf = TextBuffer::from("ab👨‍👩‍👧cd");
316        assert_eq!(buf.len_bytes(), 22);
317        assert_eq!(word_start_before(&buf, buf.len_bytes()), 20);
318    }
319
320    #[test]
321    fn line_home_end() {
322        let buf = TextBuffer::from("ab\ncde\n\nf");
323        assert_eq!(line_home(&buf, 5), 3);
324        assert_eq!(line_end(&buf, 5), 6);
325        assert_eq!(line_home(&buf, 0), 0);
326        assert_eq!(line_end(&buf, 8), 9);
327    }
328
329    #[test]
330    fn vertical_moves_clamp_to_short_lines() {
331        let buf = TextBuffer::from("abcdef\nxy\nabcdef");
332
333        let (pos, col) = move_lines(&buf, 4, 1, None);
334        assert_eq!(col, 2);
335        assert_eq!(pos, 7 + 2);
336
337        let (pos2, col2) = move_lines(&buf, pos, 1, Some(4));
338        assert_eq!((col2, pos2), (4, 10 + 4));
339
340        let (pos3, _) = move_lines(&buf, 2, -5, None);
341        assert_eq!(pos3, 2);
342    }
343}