1use unicode_segmentation::UnicodeSegmentation;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9enum Class {
10 Whitespace,
11 Word,
12 Punctuation,
13}
14
15fn class(grapheme: &str) -> Class {
19 let Some(c) = grapheme.chars().next() else {
20 return Class::Whitespace;
21 };
22 if c.is_whitespace() {
23 Class::Whitespace
24 } else if c.is_alphanumeric() || c == '_' {
25 Class::Word
26 } else {
27 Class::Punctuation
28 }
29}
30
31fn classes(line: &str) -> Vec<Class> {
32 line.graphemes(true).map(class).collect()
33}
34
35pub fn next_word_boundary(line: &str, col: usize) -> usize {
38 let classes = classes(line);
39 let len = classes.len();
40 let mut i = col.min(len);
41
42 while i < len && classes[i] == Class::Whitespace {
43 i += 1;
44 }
45 if i >= len {
46 return len;
47 }
48 let run = classes[i];
49 while i < len && classes[i] == run {
50 i += 1;
51 }
52 i
53}
54
55pub fn previous_word_boundary(line: &str, col: usize) -> usize {
57 let classes = classes(line);
58 let mut i = col.min(classes.len());
59
60 while i > 0 && classes[i - 1] == Class::Whitespace {
61 i -= 1;
62 }
63 if i == 0 {
64 return 0;
65 }
66 let run = classes[i - 1];
67 while i > 0 && classes[i - 1] == run {
68 i -= 1;
69 }
70 i
71}
72
73pub fn word_at(line: &str, col: usize) -> Option<(usize, usize)> {
78 let classes = classes(line);
79 let len = classes.len();
80 if len == 0 {
81 return None;
82 }
83 let probe = if col < len { col } else { len - 1 };
84 let target = classes[probe];
85 if target == Class::Whitespace {
86 return None;
87 }
88
89 let mut start = probe;
90 while start > 0 && classes[start - 1] == target {
91 start -= 1;
92 }
93 let mut end = probe;
94 while end < len && classes[end] == target {
95 end += 1;
96 }
97 Some((start, end))
98}