Skip to main content

binocular/editor/
line_editor.rs

1pub 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
133/// Thin abstraction over a char sequence that avoids allocation for ASCII.
134enum 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}