binocular/editor/
line_editor.rs1pub fn char_count(s: &str) -> usize {
2 s.chars().count()
3}
4
5pub fn char_to_byte_idx(s: &str, char_idx: usize) -> usize {
6 s.char_indices()
7 .nth(char_idx)
8 .map(|(idx, _)| idx)
9 .unwrap_or(s.len())
10}
11
12pub fn clamp_cursor_insert(cursor: &mut usize, text: &str) {
13 *cursor = (*cursor).min(char_count(text));
14}
15
16pub fn clamp_cursor_normal(cursor: &mut usize, text: &str) {
17 let len = char_count(text);
18 if len == 0 {
19 *cursor = 0;
20 } else if *cursor >= len {
21 *cursor = len - 1;
22 }
23}
24
25pub fn move_right_insert(cursor: &mut usize, text: &str) {
26 let len = char_count(text);
27 if *cursor < len {
28 *cursor += 1;
29 }
30}
31
32pub fn move_right_normal(cursor: &mut usize, text: &str) {
33 let len = char_count(text);
34 if *cursor + 1 < len {
35 *cursor += 1;
36 }
37}
38
39pub fn move_word_forward(cursor: &mut usize, text: &str) {
40 *cursor = find_next_word_start(text, *cursor);
41}
42
43pub fn move_word_end_forward(cursor: &mut usize, text: &str) {
44 *cursor = find_word_end(text, *cursor);
45}
46
47pub fn move_word_backward(cursor: &mut usize, text: &str) {
48 *cursor = find_prev_word_start(text, *cursor);
49}
50
51pub fn move_big_word_forward(cursor: &mut usize, text: &str) {
52 *cursor = find_next_big_word_start(text, *cursor);
53}
54
55pub fn move_big_word_backward(cursor: &mut usize, text: &str) {
56 *cursor = find_prev_big_word_start(text, *cursor);
57}
58
59pub fn move_start_of_line(cursor: &mut usize) {
60 *cursor = 0;
61}
62
63pub fn move_end_of_line_normal(cursor: &mut usize, text: &str) {
64 let len = char_count(text);
65 *cursor = len.saturating_sub(1);
66}
67
68pub fn move_end_of_line_insert(cursor: &mut usize, text: &str) {
69 *cursor = char_count(text);
70}
71
72pub fn move_first_non_blank(cursor: &mut usize, text: &str) {
73 *cursor = text.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
74}
75
76pub fn insert_char(text: &mut String, cursor: &mut usize, c: char) {
77 let byte_idx = char_to_byte_idx(text, *cursor);
78 text.insert(byte_idx, c);
79 *cursor += 1;
80}
81
82pub fn backspace(text: &mut String, cursor: &mut usize) -> bool {
83 if *cursor == 0 {
84 return false;
85 }
86
87 let byte_idx = char_to_byte_idx(text, *cursor - 1);
88 text.remove(byte_idx);
89 *cursor -= 1;
90 true
91}
92
93pub fn delete_char_at_cursor(text: &mut String, cursor: usize) -> bool {
94 let len = char_count(text);
95 if cursor >= len {
96 return false;
97 }
98 let byte_idx = char_to_byte_idx(text, cursor);
99 text.remove(byte_idx);
100 true
101}
102
103pub fn truncate_from_cursor(text: &mut String, cursor: usize) -> bool {
104 let len = char_count(text);
105 if cursor >= len {
106 return false;
107 }
108 let byte_idx = char_to_byte_idx(text, cursor);
109 text.truncate(byte_idx);
110 true
111}
112
113pub fn replace_char_range(text: &mut String, start_char: usize, end_char: usize) -> bool {
114 if start_char >= end_char {
115 return false;
116 }
117 let start_byte = char_to_byte_idx(text, start_char);
118 let end_byte = char_to_byte_idx(text, end_char);
119 text.replace_range(start_byte..end_byte, "");
120 true
121}
122
123#[inline]
124fn char_at(chars: &[char], i: usize) -> char {
125 chars[i]
126}
127
128#[inline]
129fn byte_at(bytes: &[u8], i: usize) -> char {
130 bytes[i] as char
131}
132
133enum CharSlice<'a> {
135 Ascii(&'a [u8]),
136 Unicode(Vec<char>),
137}
138
139impl<'a> CharSlice<'a> {
140 fn new(s: &'a str) -> Self {
141 if s.is_ascii() {
142 CharSlice::Ascii(s.as_bytes())
143 } else {
144 CharSlice::Unicode(s.chars().collect())
145 }
146 }
147
148 fn len(&self) -> usize {
149 match self {
150 CharSlice::Ascii(b) => b.len(),
151 CharSlice::Unicode(v) => v.len(),
152 }
153 }
154
155 fn get(&self, i: usize) -> char {
156 match self {
157 CharSlice::Ascii(b) => byte_at(b, i),
158 CharSlice::Unicode(v) => char_at(v, i),
159 }
160 }
161}
162
163pub fn find_next_word_start(text: &str, cursor: usize) -> usize {
164 let chars = CharSlice::new(text);
165 let len = chars.len();
166 if cursor >= len {
167 return cursor;
168 }
169
170 let is_word = |c: char| c.is_alphanumeric() || c == '_';
171 let char_type = |c: char| {
172 if is_word(c) {
173 1u8
174 } else if c.is_whitespace() {
175 0
176 } else {
177 2
178 }
179 };
180
181 let mut i = cursor;
182 let start_type = char_type(chars.get(i));
183 while i < len && char_type(chars.get(i)) == start_type {
184 i += 1;
185 }
186 while i < len && chars.get(i).is_whitespace() {
187 i += 1;
188 }
189
190 i.min(len.saturating_sub(1))
191}
192
193pub fn find_word_end(text: &str, cursor: usize) -> usize {
194 let chars = CharSlice::new(text);
195 let len = chars.len();
196 if cursor >= len {
197 return cursor;
198 }
199
200 let is_word = |c: char| c.is_alphanumeric() || c == '_';
201 let mut i = cursor + 1;
202
203 while i < len && chars.get(i).is_whitespace() {
204 i += 1;
205 }
206 if i >= len {
207 return len.saturating_sub(1);
208 }
209
210 let word_type = is_word(chars.get(i));
211 while i + 1 < len {
212 let next = chars.get(i + 1);
213 if is_word(next) != word_type || next.is_whitespace() {
214 break;
215 }
216 i += 1;
217 }
218
219 i.min(len.saturating_sub(1))
220}
221
222pub fn find_prev_word_start(text: &str, cursor: usize) -> usize {
223 let chars = CharSlice::new(text);
224 if cursor == 0 {
225 return 0;
226 }
227
228 let is_word = |c: char| c.is_alphanumeric() || c == '_';
229 let mut i = cursor.saturating_sub(1);
230
231 while i > 0 && chars.get(i).is_whitespace() {
232 i -= 1;
233 }
234 if i == 0 {
235 return 0;
236 }
237
238 let word_type = is_word(chars.get(i));
239 while i > 0 {
240 let prev = chars.get(i - 1);
241 if is_word(prev) != word_type || prev.is_whitespace() {
242 break;
243 }
244 i -= 1;
245 }
246
247 i
248}
249
250pub fn find_next_big_word_start(text: &str, cursor: usize) -> usize {
251 let chars = CharSlice::new(text);
252 let len = chars.len();
253 if cursor >= len {
254 return cursor;
255 }
256
257 let mut i = cursor;
258 while i < len && !chars.get(i).is_whitespace() {
259 i += 1;
260 }
261 while i < len && chars.get(i).is_whitespace() {
262 i += 1;
263 }
264
265 i.min(len.saturating_sub(1))
266}
267
268pub fn find_prev_big_word_start(text: &str, cursor: usize) -> usize {
269 let chars = CharSlice::new(text);
270 if cursor == 0 {
271 return 0;
272 }
273
274 let mut i = cursor.saturating_sub(1);
275 while i > 0 && chars.get(i).is_whitespace() {
276 i -= 1;
277 }
278 while i > 0 && !chars.get(i - 1).is_whitespace() {
279 i -= 1;
280 }
281
282 i
283}
284
285pub fn find_word_bounds(
286 text: &str,
287 cursor: usize,
288 include_whitespace: bool,
289) -> Option<(usize, usize)> {
290 let chars = CharSlice::new(text);
291 let len = chars.len();
292 if cursor >= len {
293 return None;
294 }
295
296 let is_word = |c: char| c.is_alphanumeric() || c == '_';
297 let cur = chars.get(cursor);
298
299 if cur.is_whitespace() {
300 let mut start = cursor;
301 let mut end = cursor;
302 while start > 0 && chars.get(start - 1).is_whitespace() {
303 start -= 1;
304 }
305 while end < len && chars.get(end).is_whitespace() {
306 end += 1;
307 }
308 return Some((start, end));
309 }
310
311 let cursor_is_word = is_word(cur);
312 let mut start = cursor;
313 while start > 0 {
314 let prev = chars.get(start - 1);
315 if is_word(prev) != cursor_is_word || prev.is_whitespace() {
316 break;
317 }
318 start -= 1;
319 }
320
321 let mut end = cursor;
322 while end < len {
323 let c = chars.get(end);
324 if is_word(c) != cursor_is_word || c.is_whitespace() {
325 break;
326 }
327 end += 1;
328 }
329
330 if include_whitespace {
331 while end < len && chars.get(end).is_whitespace() {
332 end += 1;
333 }
334 }
335
336 Some((start, end))
337}
338
339pub fn find_big_word_bounds(
340 text: &str,
341 cursor: usize,
342 include_whitespace: bool,
343) -> Option<(usize, usize)> {
344 let chars = CharSlice::new(text);
345 let len = chars.len();
346 if cursor >= len {
347 return None;
348 }
349
350 if chars.get(cursor).is_whitespace() {
351 let mut start = cursor;
352 let mut end = cursor;
353 while start > 0 && chars.get(start - 1).is_whitespace() {
354 start -= 1;
355 }
356 while end < len && chars.get(end).is_whitespace() {
357 end += 1;
358 }
359 return Some((start, end));
360 }
361
362 let mut start = cursor;
363 while start > 0 && !chars.get(start - 1).is_whitespace() {
364 start -= 1;
365 }
366
367 let mut end = cursor;
368 while end < len && !chars.get(end).is_whitespace() {
369 end += 1;
370 }
371
372 if include_whitespace {
373 while end < len && chars.get(end).is_whitespace() {
374 end += 1;
375 }
376 }
377
378 Some((start, end))
379}