1use std::ops::Range;
2
3use ropey::Rope;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum CharKind {
8 Whitespace,
10 Word,
12 Punctuation,
14}
15
16pub fn classify_char(c: char) -> CharKind {
18 if c.is_whitespace() {
19 CharKind::Whitespace
20 } else if c.is_alphanumeric() || c == '_' {
21 CharKind::Word
22 } else {
23 CharKind::Punctuation
24 }
25}
26
27pub fn find_prev_word_start(text: &Rope, cursor_byte: usize) -> usize {
36 let cursor_byte = cursor_byte.min(text.len_bytes());
37 if cursor_byte == 0 {
38 return 0;
39 }
40
41 let mut char_idx = text.byte_to_char(cursor_byte);
42 if char_idx == 0 {
43 return 0;
44 }
45
46 let prev_c = text.char(char_idx - 1);
47 if prev_c == '\n' {
48 if char_idx >= 2 && text.char(char_idx - 2) == '\r' {
49 return text.char_to_byte(char_idx - 2);
50 }
51 return text.char_to_byte(char_idx - 1);
52 }
53
54 while char_idx > 0 {
55 let c = text.char(char_idx - 1);
56 if c == '\n' || c == '\r' {
57 return text.char_to_byte(char_idx);
58 }
59 if classify_char(c) != CharKind::Whitespace {
60 break;
61 }
62 char_idx -= 1;
63 }
64
65 if char_idx == 0 {
66 return 0;
67 }
68
69 let target_kind = classify_char(text.char(char_idx - 1));
70
71 while char_idx > 0 {
72 let c = text.char(char_idx - 1);
73 if c == '\n' || c == '\r' || classify_char(c) != target_kind {
74 break;
75 }
76 char_idx -= 1;
77 }
78
79 text.char_to_byte(char_idx)
80}
81
82pub fn find_next_word_end(text: &Rope, cursor_byte: usize) -> usize {
91 let total_bytes = text.len_bytes();
92 let cursor_byte = cursor_byte.min(total_bytes);
93 if cursor_byte >= total_bytes {
94 return total_bytes;
95 }
96
97 let total_chars = text.len_chars();
98 let mut char_idx = text.byte_to_char(cursor_byte);
99 if char_idx >= total_chars {
100 return total_bytes;
101 }
102
103 let c = text.char(char_idx);
104 if c == '\r' {
105 if char_idx + 1 < total_chars && text.char(char_idx + 1) == '\n' {
106 return text.char_to_byte(char_idx + 2);
107 }
108 return text.char_to_byte(char_idx + 1);
109 }
110 if c == '\n' {
111 return text.char_to_byte(char_idx + 1);
112 }
113
114 while char_idx < total_chars {
115 let c = text.char(char_idx);
116 if c == '\n' || c == '\r' {
117 return text.char_to_byte(char_idx);
118 }
119 if classify_char(c) != CharKind::Whitespace {
120 break;
121 }
122 char_idx += 1;
123 }
124
125 if char_idx >= total_chars {
126 return total_bytes;
127 }
128
129 let target_kind = classify_char(text.char(char_idx));
130
131 while char_idx < total_chars {
132 let c = text.char(char_idx);
133 if c == '\n' || c == '\r' || classify_char(c) != target_kind {
134 break;
135 }
136 char_idx += 1;
137 }
138
139 text.char_to_byte(char_idx)
140}
141
142pub fn find_line_start(text: &Rope, cursor_byte: usize) -> usize {
144 let cursor_byte = cursor_byte.min(text.len_bytes());
145 let char_idx = text.byte_to_char(cursor_byte);
146 let line_idx = text.char_to_line(char_idx);
147 let line_start_char = text.line_to_char(line_idx);
148 text.char_to_byte(line_start_char)
149}
150
151pub fn find_line_end(text: &Rope, cursor_byte: usize) -> usize {
154 let cursor_byte = cursor_byte.min(text.len_bytes());
155 let char_idx = text.byte_to_char(cursor_byte);
156 let line_idx = text.char_to_line(char_idx);
157 let line = text.line(line_idx);
158 let mut line_len_chars = line.len_chars();
159
160 if line_len_chars > 0 && line.char(line_len_chars - 1) == '\n' {
161 line_len_chars -= 1;
162 if line_len_chars > 0 && line.char(line_len_chars - 1) == '\r' {
163 line_len_chars -= 1;
164 }
165 }
166
167 let line_start_char = text.line_to_char(line_idx);
168 text.char_to_byte(line_start_char + line_len_chars)
169}
170
171pub fn find_word_range_at(text: &Rope, cursor_byte: usize) -> Range<usize> {
178 let total_bytes = text.len_bytes();
179 let cursor_byte = cursor_byte.min(total_bytes);
180 if total_bytes == 0 {
181 return 0..0;
182 }
183
184 let total_chars = text.len_chars();
185 let char_idx = text.byte_to_char(cursor_byte);
186
187 let target_idx = if char_idx >= total_chars {
188 if char_idx > 0 {
189 let prev = text.char(char_idx - 1);
190 if prev != '\n' && prev != '\r' {
191 char_idx - 1
192 } else {
193 return cursor_byte..cursor_byte;
194 }
195 } else {
196 return cursor_byte..cursor_byte;
197 }
198 } else {
199 let curr = text.char(char_idx);
200 if curr == '\n' || curr == '\r' {
201 if char_idx > 0 {
202 let prev = text.char(char_idx - 1);
203 if prev != '\n' && prev != '\r' {
204 char_idx - 1
205 } else {
206 return cursor_byte..cursor_byte;
207 }
208 } else {
209 return cursor_byte..cursor_byte;
210 }
211 } else if classify_char(curr) == CharKind::Whitespace && char_idx > 0 {
212 let prev = text.char(char_idx - 1);
213 if prev != '\n' && prev != '\r' && classify_char(prev) != CharKind::Whitespace {
214 char_idx - 1
215 } else {
216 char_idx
217 }
218 } else {
219 char_idx
220 }
221 };
222
223 let target_char = text.char(target_idx);
224 let target_kind = classify_char(target_char);
225
226 let mut start_idx = target_idx;
227 while start_idx > 0 {
228 let prev = text.char(start_idx - 1);
229 if prev == '\n' || prev == '\r' || classify_char(prev) != target_kind {
230 break;
231 }
232 start_idx -= 1;
233 }
234
235 let mut end_idx = target_idx + 1;
236 while end_idx < total_chars {
237 let next = text.char(end_idx);
238 if next == '\n' || next == '\r' || classify_char(next) != target_kind {
239 break;
240 }
241 end_idx += 1;
242 }
243
244 let start_byte = text.char_to_byte(start_idx);
245 let end_byte = text.char_to_byte(end_idx);
246 start_byte..end_byte
247}
248
249pub fn find_line_range_at(text: &Rope, cursor_byte: usize) -> Range<usize> {
252 let total_bytes = text.len_bytes();
253 let cursor_byte = cursor_byte.min(total_bytes);
254 if total_bytes == 0 {
255 return 0..0;
256 }
257
258 let char_idx = text.byte_to_char(cursor_byte);
259 let line_idx = text.char_to_line(char_idx);
260 let line_start_char = text.line_to_char(line_idx);
261 let start = text.char_to_byte(line_start_char);
262
263 let end = if line_idx + 1 < text.len_lines() {
264 text.line_to_byte(line_idx + 1)
265 } else {
266 total_bytes
267 };
268
269 start..end
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_classify_char() {
278 assert_eq!(classify_char('a'), CharKind::Word);
279 assert_eq!(classify_char('Z'), CharKind::Word);
280 assert_eq!(classify_char('0'), CharKind::Word);
281 assert_eq!(classify_char('_'), CharKind::Word);
282 assert_eq!(classify_char(' '), CharKind::Whitespace);
283 assert_eq!(classify_char('\t'), CharKind::Whitespace);
284 assert_eq!(classify_char('\n'), CharKind::Whitespace);
285 assert_eq!(classify_char('.'), CharKind::Punctuation);
286 assert_eq!(classify_char('('), CharKind::Punctuation);
287 assert_eq!(classify_char(';'), CharKind::Punctuation);
288 }
289
290 #[test]
291 fn test_find_prev_word_start() {
292 let text = Rope::from_str("hello world, foo.bar();");
293
294 assert_eq!(find_prev_word_start(&text, 23), 20);
295 assert_eq!(find_prev_word_start(&text, 20), 17);
296 assert_eq!(find_prev_word_start(&text, 17), 16);
297 assert_eq!(find_prev_word_start(&text, 16), 13);
298 assert_eq!(find_prev_word_start(&text, 13), 11);
299 assert_eq!(find_prev_word_start(&text, 11), 6);
300 assert_eq!(find_prev_word_start(&text, 6), 0);
301 assert_eq!(find_prev_word_start(&text, 0), 0);
302 }
303
304 #[test]
305 fn test_find_prev_word_multiple_spaces() {
306 let text = Rope::from_str("hello world");
307 assert_eq!(find_prev_word_start(&text, 14), 9);
308 assert_eq!(find_prev_word_start(&text, 9), 0);
309 }
310
311 #[test]
312 fn test_find_prev_word_across_lines() {
313 let text = Rope::from_str("hello\nworld");
314 assert_eq!(find_prev_word_start(&text, 11), 6);
315 assert_eq!(find_prev_word_start(&text, 6), 5);
316 assert_eq!(find_prev_word_start(&text, 5), 0);
317 }
318
319 #[test]
320 fn test_find_next_word_end() {
321 let text = Rope::from_str("hello world, foo.bar();");
322
323 assert_eq!(find_next_word_end(&text, 0), 5);
324 assert_eq!(find_next_word_end(&text, 5), 11);
325 assert_eq!(find_next_word_end(&text, 11), 12);
326 assert_eq!(find_next_word_end(&text, 12), 16);
327 assert_eq!(find_next_word_end(&text, 16), 17);
328 assert_eq!(find_next_word_end(&text, 17), 20);
329 assert_eq!(find_next_word_end(&text, 20), 23);
330 assert_eq!(find_next_word_end(&text, 23), 23);
331 }
332
333 #[test]
334 fn test_find_next_word_multiple_spaces() {
335 let text = Rope::from_str("hello world");
336 assert_eq!(find_next_word_end(&text, 0), 5);
337 assert_eq!(find_next_word_end(&text, 5), 14);
338 }
339
340 #[test]
341 fn test_find_line_boundaries() {
342 let text = Rope::from_str("first line\nsecond line\nthird");
343
344 assert_eq!(find_line_start(&text, 5), 0);
345 assert_eq!(find_line_end(&text, 5), 10);
346
347 assert_eq!(find_line_start(&text, 15), 11);
348 assert_eq!(find_line_end(&text, 15), 22);
349
350 assert_eq!(find_line_start(&text, 25), 23);
351 assert_eq!(find_line_end(&text, 25), 28);
352 }
353
354 #[test]
355 fn test_find_word_range_at() {
356 let text = Rope::from_str("hello world, foo.bar();\nsecond line");
357
358 assert_eq!(find_word_range_at(&text, 0), 0..5);
359 assert_eq!(find_word_range_at(&text, 2), 0..5);
360 assert_eq!(find_word_range_at(&text, 5), 0..5);
361
362 assert_eq!(find_word_range_at(&text, 6), 6..11);
363 assert_eq!(find_word_range_at(&text, 10), 6..11);
364
365 assert_eq!(find_word_range_at(&text, 11), 11..12);
366
367 assert_eq!(find_word_range_at(&text, 12), 11..12);
368
369 assert_eq!(find_word_range_at(&text, 13), 13..16);
370 assert_eq!(find_word_range_at(&text, 16), 16..17);
371 assert_eq!(find_word_range_at(&text, 17), 17..20);
372 assert_eq!(find_word_range_at(&text, 20), 20..23);
373 assert_eq!(find_word_range_at(&text, 21), 20..23);
374 assert_eq!(find_word_range_at(&text, 22), 20..23);
375
376 assert_eq!(find_word_range_at(&text, 23), 20..23);
377
378 assert_eq!(find_word_range_at(&text, 24), 24..30);
379
380 let empty = Rope::from_str("");
381 assert_eq!(find_word_range_at(&empty, 0), 0..0);
382
383 let unicode = Rope::from_str("مرحبا بالعالم");
384 assert_eq!(find_word_range_at(&unicode, 0), 0..10);
385 }
386
387 #[test]
388 fn test_find_word_range_multiple_spaces() {
389 let text = Rope::from_str("hello world");
390 assert_eq!(find_word_range_at(&text, 5), 0..5);
391 assert_eq!(find_word_range_at(&text, 6), 5..8);
392 assert_eq!(find_word_range_at(&text, 7), 5..8);
393 assert_eq!(find_word_range_at(&text, 8), 8..13);
394 }
395
396 #[test]
397 fn test_find_line_range_at() {
398 let text = Rope::from_str("first line\nsecond line\nthird");
399
400 assert_eq!(find_line_range_at(&text, 0), 0..11);
401 assert_eq!(find_line_range_at(&text, 5), 0..11);
402 assert_eq!(find_line_range_at(&text, 10), 0..11);
403
404 assert_eq!(find_line_range_at(&text, 11), 11..23);
405 assert_eq!(find_line_range_at(&text, 15), 11..23);
406
407 assert_eq!(find_line_range_at(&text, 23), 23..28);
408 assert_eq!(find_line_range_at(&text, 27), 23..28);
409 assert_eq!(find_line_range_at(&text, 28), 23..28);
410
411 let crlf = Rope::from_str("first\r\nsecond\r\n");
412 assert_eq!(find_line_range_at(&crlf, 2), 0..7);
413 assert_eq!(find_line_range_at(&crlf, 8), 7..15);
414
415 let empty = Rope::from_str("");
416 assert_eq!(find_line_range_at(&empty, 0), 0..0);
417 }
418}