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}