limit_cli/tui/input/
editor.rs1use crate::tui::MAX_PASTE_SIZE;
6
7pub struct InputEditor {
9 text: String,
11 cursor: usize,
13}
14
15impl InputEditor {
16 pub fn new() -> Self {
18 Self {
19 text: String::with_capacity(256),
20 cursor: 0,
21 }
22 }
23
24 #[inline]
26 pub fn text(&self) -> &str {
27 &self.text
28 }
29
30 #[inline]
32 pub fn text_mut(&mut self) -> &mut String {
33 &mut self.text
34 }
35
36 #[inline]
38 pub fn cursor(&self) -> usize {
39 self.cursor
40 }
41
42 pub fn set_cursor(&mut self, pos: usize) {
44 self.cursor = pos.min(self.text.len());
45 while self.cursor > 0 && !self.text.is_char_boundary(self.cursor) {
47 self.cursor -= 1;
48 }
49 }
50
51 #[inline]
53 pub fn is_empty(&self) -> bool {
54 self.text.is_empty()
55 }
56
57 #[inline]
59 pub fn is_cursor_at_start(&self) -> bool {
60 self.cursor == 0
61 }
62
63 #[inline]
65 pub fn is_cursor_at_end(&self) -> bool {
66 self.cursor == self.text.len()
67 }
68
69 #[inline]
71 pub fn insert_char(&mut self, c: char) {
72 self.text.insert(self.cursor, c);
73 self.cursor += c.len_utf8();
74 }
75
76 #[inline]
78 pub fn insert_str(&mut self, s: &str) {
79 self.text.insert_str(self.cursor, s);
80 self.cursor += s.len();
81 }
82
83 pub fn insert_paste(&mut self, text: &str) -> bool {
86 let (text, truncated) = truncate_paste(text);
87
88 let normalized = if text.contains('\r') {
90 let mut normalized = String::with_capacity(text.len());
91 for c in text.chars() {
92 normalized.push(if c == '\r' { '\n' } else { c });
93 }
94 normalized
95 } else {
96 return {
97 self.insert_str(text);
98 truncated
99 };
100 };
101
102 self.insert_str(&normalized);
103 truncated
104 }
105
106 pub fn delete_char_before(&mut self) -> bool {
108 if self.cursor == 0 {
109 return false;
110 }
111
112 let prev_pos = self.prev_char_pos();
113 self.text.drain(prev_pos..self.cursor);
114 self.cursor = prev_pos;
115 true
116 }
117
118 pub fn delete_char_at(&mut self) -> bool {
120 if self.cursor >= self.text.len() {
121 return false;
122 }
123
124 let next_pos = self.next_char_pos();
125 self.text.drain(self.cursor..next_pos);
126 true
127 }
128
129 #[inline]
131 pub fn move_left(&mut self) {
132 if self.cursor > 0 {
133 self.cursor = self.prev_char_pos();
134 }
135 }
136
137 #[inline]
139 pub fn move_right(&mut self) {
140 if self.cursor < self.text.len() {
141 self.cursor = self.next_char_pos();
142 }
143 }
144
145 #[inline]
147 pub fn move_to_start(&mut self) {
148 self.cursor = 0;
149 }
150
151 #[inline]
153 pub fn move_to_end(&mut self) {
154 self.cursor = self.text.len();
155 }
156
157 #[inline]
159 pub fn clear(&mut self) {
160 self.text.clear();
161 self.cursor = 0;
162 }
163
164 pub fn take_trimmed(&mut self) -> String {
166 let trimmed = self.text.trim();
167 let result = String::from(trimmed);
168 self.clear();
169 result
170 }
171
172 pub fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
174 self.text.drain(start..end);
175 self.text.insert_str(start, replacement);
176 self.cursor = start + replacement.len();
177 }
178
179 #[inline]
181 pub fn delete_range_to_cursor(&mut self, start: usize) {
182 if start < self.cursor {
183 self.text.drain(start..self.cursor);
184 self.cursor = start;
185 }
186 }
187
188 #[inline]
190 pub fn text_before_cursor(&self) -> &str {
191 &self.text[..self.cursor]
192 }
193
194 #[inline]
196 pub fn text_after_cursor(&self) -> &str {
197 &self.text[self.cursor..]
198 }
199
200 #[inline]
202 pub fn char_at_cursor(&self) -> Option<char> {
203 self.text[self.cursor..].chars().next()
204 }
205
206 pub fn char_before_cursor(&self) -> Option<char> {
208 if self.cursor == 0 {
209 return None;
210 }
211 let prev_pos = self.prev_char_pos();
212 self.text[prev_pos..self.cursor].chars().next()
213 }
214
215 #[inline]
217 fn prev_char_pos(&self) -> usize {
218 if self.cursor == 0 {
219 return 0;
220 }
221 let mut pos = self.cursor - 1;
222 while pos > 0 && !self.text.is_char_boundary(pos) {
223 pos -= 1;
224 }
225 pos
226 }
227
228 #[inline]
230 fn next_char_pos(&self) -> usize {
231 if self.cursor >= self.text.len() {
232 return self.text.len();
233 }
234 let mut pos = self.cursor + 1;
235 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
236 pos += 1;
237 }
238 pos
239 }
240}
241
242#[inline]
244fn truncate_paste(text: &str) -> (&str, bool) {
245 if text.len() <= MAX_PASTE_SIZE {
246 return (text, false);
247 }
248
249 let truncated = &text[..text
250 .char_indices()
251 .nth(MAX_PASTE_SIZE)
252 .map(|(i, _)| i)
253 .unwrap_or(text.len())];
254 (truncated, true)
255}
256
257impl Default for InputEditor {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_editor_creation() {
269 let editor = InputEditor::new();
270 assert!(editor.is_empty());
271 assert_eq!(editor.cursor(), 0);
272 }
273
274 #[test]
275 fn test_insert_char() {
276 let mut editor = InputEditor::new();
277 editor.insert_char('h');
278 editor.insert_char('i');
279 assert_eq!(editor.text(), "hi");
280 assert_eq!(editor.cursor(), 2);
281 }
282
283 #[test]
284 fn test_delete_char_before() {
285 let mut editor = InputEditor::new();
286 editor.insert_str("hello");
287 editor.set_cursor(3);
288
289 assert!(editor.delete_char_before());
290 assert_eq!(editor.text(), "helo");
291 assert_eq!(editor.cursor(), 2);
292 }
293
294 #[test]
295 fn test_navigation() {
296 let mut editor = InputEditor::new();
297 editor.insert_str("hello");
298
299 editor.move_left();
300 assert_eq!(editor.cursor(), 4);
301
302 editor.move_to_start();
303 assert_eq!(editor.cursor(), 0);
304
305 editor.move_to_end();
306 assert_eq!(editor.cursor(), 5);
307 }
308
309 #[test]
310 fn test_utf8() {
311 let mut editor = InputEditor::new();
312 editor.insert_str("hΓ©llo");
313
314 let pos = editor.text().char_indices().nth(2).map(|(i, _)| i).unwrap();
315 editor.set_cursor(pos);
316
317 assert_eq!(editor.cursor(), pos);
318 assert_eq!(editor.char_before_cursor(), Some('Γ©'));
319 }
320
321 #[test]
322 fn test_take_trimmed() {
323 let mut editor = InputEditor::new();
324 editor.insert_str(" hello ");
325 let text = editor.take_trimmed();
326 assert_eq!(text, "hello");
327 assert!(editor.is_empty());
328 }
329
330 #[test]
331 fn test_replace_range() {
332 let mut editor = InputEditor::new();
333 editor.insert_str("hello world");
334 editor.replace_range(6, 11, "universe");
335 assert_eq!(editor.text(), "hello universe");
336 }
337
338 #[test]
339 fn test_utf8_emojis() {
340 let mut editor = InputEditor::new();
341
342 editor.insert_str("Hello π World π");
343 assert_eq!(editor.text(), "Hello π World π");
344
345 editor.move_to_start();
346 editor.move_right();
347 editor.move_right();
348
349 editor.insert_char('π');
350 assert_eq!(editor.text(), "Heπllo π World π");
351 }
352
353 #[test]
354 fn test_utf8_multibyte_chars() {
355 let mut editor = InputEditor::new();
356
357 editor.insert_str("ζ₯ζ¬θͺ");
358 assert_eq!(editor.text(), "ζ₯ζ¬θͺ");
359 assert_eq!(editor.cursor(), 9);
360
361 editor.set_cursor(6);
362 assert!(editor.delete_char_before());
363 assert_eq!(editor.text(), "ζ₯θͺ");
364 assert_eq!(editor.cursor(), 3);
365 }
366
367 #[test]
368 fn test_paste_size_limit() {
369 let mut editor = InputEditor::new();
370
371 let large_text = "x".repeat(150 * 1024);
372 let truncated = editor.insert_paste(&large_text);
373
374 assert!(truncated, "Should indicate paste was truncated");
375 assert!(editor.text().len() <= MAX_PASTE_SIZE);
376 }
377
378 #[test]
379 fn test_paste_normal_size() {
380 let mut editor = InputEditor::new();
381
382 let text = "normal text";
383 let truncated = editor.insert_paste(text);
384
385 assert!(!truncated, "Should not truncate normal-sized paste");
386 assert_eq!(editor.text(), text);
387 }
388
389 #[test]
390 fn test_paste_newline_normalization() {
391 let mut editor = InputEditor::new();
392
393 editor.insert_paste("line1\r\nline2\r\n");
394 assert_eq!(editor.text(), "line1\n\nline2\n\n");
395 }
396
397 #[test]
398 fn test_navigation_empty_text() {
399 let mut editor = InputEditor::new();
400
401 editor.move_left();
402 assert_eq!(editor.cursor(), 0);
403
404 editor.move_right();
405 assert_eq!(editor.cursor(), 0);
406
407 editor.move_to_start();
408 assert_eq!(editor.cursor(), 0);
409
410 editor.move_to_end();
411 assert_eq!(editor.cursor(), 0);
412
413 assert!(!editor.delete_char_before());
414 assert!(!editor.delete_char_at());
415 }
416
417 #[test]
418 fn test_replace_range_invalid() {
419 let mut editor = InputEditor::new();
420 editor.insert_str("hello");
421
422 editor.replace_range(5, 5, " world");
423 assert_eq!(editor.text(), "hello world");
424
425 editor.replace_range(6, 11, "universe");
426 assert_eq!(editor.text(), "hello universe");
427 }
428
429 #[test]
430 fn test_replace_range_multibyte() {
431 let mut editor = InputEditor::new();
432 editor.insert_str("hello δΈη");
433
434 let world_start = editor.text().char_indices().nth(6).map(|(i, _)| i).unwrap();
435 editor.replace_range(world_start, editor.text().len(), "π");
436 assert_eq!(editor.text(), "hello π");
437 }
438
439 #[test]
440 fn test_cursor_boundary_safety() {
441 let mut editor = InputEditor::new();
442 editor.insert_str("hΓ©llo");
443
444 editor.set_cursor(2);
445 assert_ne!(editor.cursor(), 2, "Cursor should not be in middle of char");
446 assert!(editor.text().is_char_boundary(editor.cursor()));
447 }
448
449 #[test]
450 fn test_char_at_cursor() {
451 let mut editor = InputEditor::new();
452 editor.insert_str("hello");
453
454 editor.set_cursor(0);
455 assert_eq!(editor.char_at_cursor(), Some('h'));
456
457 editor.set_cursor(5);
458 assert_eq!(editor.char_at_cursor(), None);
459
460 editor.clear();
461 assert_eq!(editor.char_at_cursor(), None);
462 }
463
464 #[test]
465 fn test_text_before_after_cursor() {
466 let mut editor = InputEditor::new();
467 editor.insert_str("hello world");
468 editor.set_cursor(5);
469
470 assert_eq!(editor.text_before_cursor(), "hello");
471 assert_eq!(editor.text_after_cursor(), " world");
472 }
473
474 #[test]
475 fn test_delete_range_to_cursor() {
476 let mut editor = InputEditor::new();
477 editor.insert_str("hello world");
478 editor.set_cursor(11);
479
480 editor.delete_range_to_cursor(6);
481 assert_eq!(editor.text(), "hello ");
482 assert_eq!(editor.cursor(), 6);
483 }
484}