1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Word boundary detection for text field navigation.
//!
//! This module provides Unicode-aware word boundary functions for
//! Ctrl+Left/Right navigation and double-click word selection.
//!
//! Used by:
//! - `handle_key_event_impl` for Ctrl+Arrow navigation
//! - `create_handler` for double-click word selection
/// Finds the position at the start of the previous word.
/// Used for Ctrl+Left navigation.
///
/// Uses Unicode-aware `char::is_alphanumeric()` for proper international text support.
pub fn find_word_start(text: &str, pos: usize) -> usize {
if pos == 0 || text.is_empty() {
return 0;
}
// Get char indices up to pos
let chars_before: Vec<(usize, char)> = text[..pos.min(text.len())].char_indices().collect();
if chars_before.is_empty() {
return 0;
}
let mut idx = chars_before.len();
// Skip any whitespace/punctuation before (moving left)
while idx > 0 {
let c = chars_before[idx - 1].1;
if c.is_alphanumeric() || c == '_' {
break;
}
idx -= 1;
}
// Now scan back through word chars
while idx > 0 {
let c = chars_before[idx - 1].1;
if !c.is_alphanumeric() && c != '_' {
break;
}
idx -= 1;
}
if idx == 0 {
0
} else {
chars_before[idx].0
}
}
/// Finds the position at the end of the next word.
/// Used for Ctrl+Right navigation.
///
/// Uses Unicode-aware `char::is_alphanumeric()` for proper international text support.
pub fn find_word_end(text: &str, pos: usize) -> usize {
let len = text.len();
if pos >= len || text.is_empty() {
return len;
}
// Get char indices from pos onwards
let chars_after: Vec<(usize, char)> = text[pos.min(len)..]
.char_indices()
.map(|(i, c)| (pos + i, c))
.collect();
if chars_after.is_empty() {
return len;
}
let mut idx = 0;
// Skip any whitespace/punctuation (moving right)
while idx < chars_after.len() {
let c = chars_after[idx].1;
if c.is_alphanumeric() || c == '_' {
break;
}
idx += 1;
}
// Now scan forward through word chars
while idx < chars_after.len() {
let c = chars_after[idx].1;
if !c.is_alphanumeric() && c != '_' {
break;
}
idx += 1;
}
if idx >= chars_after.len() {
len
} else {
chars_after[idx].0
}
}
/// Finds the word boundaries (start, end) around a byte position.
/// Used for double-click word selection.
///
/// Returns the byte offsets of the word start and end.
pub fn find_word_boundaries(text: &str, pos: usize) -> (usize, usize) {
if text.is_empty() {
return (0, 0);
}
let pos = pos.min(text.len());
// Find the character at pos
let char_at_pos = text[pos..].chars().next();
// If we're on whitespace/punctuation, just return the position
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
if char_at_pos.map(|c| !is_word_char(c)).unwrap_or(true) {
// Check char before pos instead
let char_before = text[..pos].chars().last();
if char_before.map(|c| !is_word_char(c)).unwrap_or(true) {
return (pos, pos);
}
}
// Find word start (scan backwards)
let mut start = pos;
for (i, c) in text[..pos].char_indices().rev() {
if !is_word_char(c) {
start = i + c.len_utf8();
break;
}
start = i;
}
// Find word end (scan forwards)
let mut end = pos;
for (i, c) in text[pos..].char_indices() {
if !is_word_char(c) {
end = pos + i;
break;
}
end = pos + i + c.len_utf8();
}
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_word_start() {
assert_eq!(find_word_start("hello world", 6), 0); // Before 'w', goes to 'h'
assert_eq!(find_word_start("hello world", 11), 6); // End, goes to 'w'
assert_eq!(find_word_start("hello", 0), 0);
}
#[test]
fn test_find_word_end() {
assert_eq!(find_word_end("hello world", 0), 5); // At 'h', goes to after 'o'
assert_eq!(find_word_end("hello world", 6), 11); // At 'w', goes to end
}
#[test]
fn test_find_word_boundaries() {
let (start, end) = find_word_boundaries("hello world", 2);
assert_eq!(start, 0);
assert_eq!(end, 5);
let (start, end) = find_word_boundaries("hello world", 8);
assert_eq!(start, 6);
assert_eq!(end, 11);
}
}