1use crate::tui::input::history::InputHistory;
6use crate::tui::MAX_PASTE_SIZE;
7use std::path::PathBuf;
8
9const MAX_DISPLAY_LENGTH: usize = 100;
11
12struct PastedContent {
14 full_text: String,
16 display_text: String,
18 text_before_len: usize,
20}
21
22impl PastedContent {
23 fn new(text: String) -> Self {
25 let display_text = format_paste_placeholder(&text);
26 Self {
27 full_text: text,
28 display_text,
29 text_before_len: 0,
30 }
31 }
32
33 fn with_prefix(prefix: &str, pasted: &str) -> Self {
35 let full_text = format!("{}{}", prefix, pasted);
36 let display_text = format_paste_placeholder(pasted);
37 Self {
38 full_text,
39 display_text,
40 text_before_len: prefix.len(),
41 }
42 }
43}
44
45fn format_paste_placeholder(text: &str) -> String {
47 let lines = text.lines().count();
48
49 if lines > 1 {
50 format!("[Pasted text + {} lines]", lines)
51 } else {
52 format!("[Pasted text + {} chars]", text.len())
53 }
54}
55
56#[inline]
59fn compute_display_zones(
60 text_before_len: usize,
61 placeholder_len: usize,
62 total_text_len: usize,
63) -> (usize, usize, usize, usize) {
64 let text_after_len = total_text_len.saturating_sub(text_before_len);
65
66 let (text_before_end, placeholder_start, placeholder_end) = if text_before_len > 0 {
72 (
73 text_before_len, text_before_len + 1, text_before_len + 1 + placeholder_len, )
77 } else {
78 (0, 0, placeholder_len) };
80
81 let total_len = if text_before_len > 0 && text_after_len > 0 {
82 text_before_len + 1 + placeholder_len + 1 + text_after_len
83 } else if text_before_len > 0 {
84 text_before_len + 1 + placeholder_len
85 } else if text_after_len > 0 {
86 placeholder_len + 1 + text_after_len
87 } else {
88 placeholder_len
89 };
90
91 (
92 text_before_end,
93 placeholder_start,
94 placeholder_end,
95 total_len,
96 )
97}
98
99pub struct InputEditor {
101 text: String,
103 cursor: usize,
105 history: InputHistory,
107 pasted_content: Option<PastedContent>,
109 display_cursor: usize,
112}
113
114impl InputEditor {
115 pub fn new() -> Self {
117 Self {
118 text: String::with_capacity(256),
119 cursor: 0,
120 history: InputHistory::new(),
121 pasted_content: None,
122 display_cursor: 0,
123 }
124 }
125
126 pub fn with_history(history_path: &PathBuf) -> Result<Self, String> {
128 let history = InputHistory::load(history_path)?;
129 Ok(Self {
130 text: String::with_capacity(256),
131 cursor: 0,
132 history,
133 pasted_content: None,
134 display_cursor: 0,
135 })
136 }
137
138 #[inline]
141 pub fn text(&self) -> String {
142 if let Some(ref pasted) = self.pasted_content {
143 let text_before_len = pasted.text_before_len;
144
145 if self.text.len() <= text_before_len {
146 pasted.full_text.clone()
148 } else {
149 let text_after = &self.text[text_before_len..];
151 format!("{}{}", pasted.full_text, text_after)
152 }
153 } else {
154 self.text.clone()
155 }
156 }
157
158 #[inline]
161 pub fn display_text(&self) -> &str {
162 if let Some(ref pasted) = self.pasted_content {
163 &pasted.display_text
164 } else {
165 &self.text
166 }
167 }
168
169 pub fn display_text_combined(&self) -> std::borrow::Cow<'_, str> {
172 if let Some(ref pasted) = self.pasted_content {
173 let text_before_len = pasted.text_before_len;
174
175 if text_before_len > 0 || self.text.len() > text_before_len {
176 let text_before = &self.text[..text_before_len.min(self.text.len())];
178 let text_after = if self.text.len() > text_before_len {
179 &self.text[text_before_len..]
180 } else {
181 ""
182 };
183
184 let mut result = String::new();
186 if !text_before.is_empty() {
187 result.push_str(text_before);
188 result.push(' ');
189 }
190 result.push_str(&pasted.display_text);
191 if !text_after.is_empty() {
192 result.push(' ');
193 result.push_str(text_after);
194 }
195 std::borrow::Cow::Owned(result)
196 } else {
197 std::borrow::Cow::Borrowed(&pasted.display_text)
198 }
199 } else {
200 std::borrow::Cow::Borrowed(&self.text)
201 }
202 }
203
204 #[inline]
207 pub fn text_mut(&mut self) -> &mut String {
208 &mut self.text
209 }
210
211 #[inline]
214 pub fn cursor(&self) -> usize {
215 if self.pasted_content.is_some() {
216 self.display_cursor
217 } else {
218 self.cursor
219 }
220 }
221
222 fn display_zones(&self) -> (usize, usize, usize, usize) {
225 if let Some(ref pasted) = self.pasted_content {
226 compute_display_zones(
227 pasted.text_before_len,
228 pasted.display_text.len(),
229 self.text.len(),
230 )
231 } else {
232 (0, 0, 0, self.text.len())
233 }
234 }
235
236 pub fn set_cursor(&mut self, pos: usize) {
238 if self.pasted_content.is_some() {
239 let (_, _, _, total_len) = self.display_zones();
240 self.display_cursor = pos.min(total_len);
241 } else {
242 self.cursor = pos.min(self.text.len());
243 while self.cursor > 0 && !self.text.is_char_boundary(self.cursor) {
245 self.cursor -= 1;
246 }
247 }
248 }
249
250 #[inline]
252 pub fn is_empty(&self) -> bool {
253 self.pasted_content.is_none() && self.text.is_empty()
254 }
255
256 #[inline]
258 pub fn has_pasted_content(&self) -> bool {
259 self.pasted_content.is_some()
260 }
261
262 #[inline]
264 pub fn is_cursor_at_start(&self) -> bool {
265 self.cursor == 0
266 }
267
268 #[inline]
270 pub fn is_cursor_at_end(&self) -> bool {
271 self.cursor == self.text.len()
272 }
273
274 #[inline]
277 pub fn insert_char(&mut self, c: char) {
278 if self.history.is_navigating() {
280 self.history.reset_navigation();
281 }
282
283 if let Some(ref pasted) = self.pasted_content {
285 let (text_before_end, _, _, _) = compute_display_zones(
286 pasted.text_before_len,
287 pasted.display_text.len(),
288 self.text.len(),
289 );
290
291 if self.display_cursor < text_before_end {
293 let offset_in_before = self.display_cursor;
294 self.text.insert(offset_in_before, c);
295 if let Some(ref mut pasted) = self.pasted_content {
297 pasted.text_before_len += 1;
298 pasted.full_text.insert(offset_in_before, c);
299 }
300 self.display_cursor += c.len_utf8();
301 return;
302 }
303
304 self.text.push(c);
306 if let Some(ref pasted) = self.pasted_content {
308 let (_, _, _, total_len) = compute_display_zones(
309 pasted.text_before_len,
310 pasted.display_text.len(),
311 self.text.len(),
312 );
313 self.display_cursor = total_len;
314 }
315 return;
316 }
317
318 self.text.insert(self.cursor, c);
319 self.cursor += c.len_utf8();
320 }
321
322 #[inline]
325 pub fn insert_str(&mut self, s: &str) {
326 if self.history.is_navigating() {
328 self.history.reset_navigation();
329 }
330
331 if let Some(ref pasted) = self.pasted_content {
333 let (text_before_end, _, _, _) = compute_display_zones(
334 pasted.text_before_len,
335 pasted.display_text.len(),
336 self.text.len(),
337 );
338
339 if self.display_cursor < text_before_end {
341 let offset_in_before = self.display_cursor;
342 self.text.insert_str(offset_in_before, s);
343 if let Some(ref mut pasted) = self.pasted_content {
345 pasted.text_before_len += s.len();
346 pasted.full_text.insert_str(offset_in_before, s);
347 }
348 self.display_cursor += s.len();
349 return;
350 }
351
352 self.text.push_str(s);
354 if let Some(ref pasted) = self.pasted_content {
356 let (_, _, _, total_len) = compute_display_zones(
357 pasted.text_before_len,
358 pasted.display_text.len(),
359 self.text.len(),
360 );
361 self.display_cursor = total_len;
362 }
363 return;
364 }
365
366 self.text.insert_str(self.cursor, s);
367 self.cursor += s.len();
368 }
369
370 pub fn insert_paste(&mut self, text: &str) -> bool {
373 tracing::trace!(
374 "insert_paste: input len={}, existing text len={}, has_pasted_content={}",
375 text.len(),
376 self.text.len(),
377 self.pasted_content.is_some()
378 );
379
380 let (text, truncated) = truncate_paste(text);
381
382 let normalized = if text.contains('\r') {
384 let mut normalized = String::with_capacity(text.len());
385 for c in text.chars() {
386 normalized.push(if c == '\r' { '\n' } else { c });
387 }
388 normalized
389 } else {
390 text.to_string()
391 };
392
393 tracing::trace!(
394 "insert_paste: normalized len={}, will_create_placeholder={}",
395 normalized.len(),
396 normalized.len() > MAX_DISPLAY_LENGTH
397 );
398
399 if let Some(ref mut pasted) = self.pasted_content {
401 tracing::trace!("insert_paste: appending to existing pasted content");
402 pasted.full_text.push_str(&normalized);
403 pasted.display_text = format_paste_placeholder(&pasted.full_text);
404 return truncated;
405 }
406
407 if !self.text.is_empty() {
410 tracing::trace!(
411 "insert_paste: existing text '{}' (len={}), paste len={}",
412 &self.text,
413 self.text.len(),
414 normalized.len()
415 );
416
417 if normalized.len() > MAX_DISPLAY_LENGTH {
420 tracing::trace!(
421 "insert_paste: paste is large, creating placeholder for paste only"
422 );
423 let original_text = self.text.clone();
425 self.pasted_content = Some(PastedContent::with_prefix(&original_text, &normalized));
426 self.display_cursor = self.display_zones().3;
430 return truncated;
431 }
432
433 self.text.push_str(&normalized);
435 tracing::trace!(
436 "insert_paste: small paste appended, total len={}",
437 self.text.len()
438 );
439
440 if self.text.len() > MAX_DISPLAY_LENGTH {
442 tracing::trace!(
443 "insert_paste: combined text len={} > {}, converting to pasted content",
444 self.text.len(),
445 MAX_DISPLAY_LENGTH
446 );
447 let full_text = std::mem::take(&mut self.text);
448 self.pasted_content = Some(PastedContent::new(full_text));
449 self.display_cursor = self.display_zones().3;
450 }
451
452 return truncated;
453 }
454
455 if normalized.len() > MAX_DISPLAY_LENGTH {
457 tracing::trace!("insert_paste: no existing content, creating placeholder");
458 self.pasted_content = Some(PastedContent::new(normalized));
459 self.display_cursor = self.display_zones().3;
460 return truncated;
461 }
462
463 tracing::trace!("insert_paste: small paste, inserting normally");
465 self.text = normalized;
466 self.cursor = self.text.len();
467 truncated
468 }
469
470 pub fn delete_char_before(&mut self) -> bool {
472 if let Some(ref pasted) = self.pasted_content {
474 let text_before_len = pasted.text_before_len;
475 let text_after_len = self.text.len().saturating_sub(text_before_len);
476 let (text_before_end, placeholder_start, placeholder_end, total_len) =
477 compute_display_zones(text_before_len, pasted.display_text.len(), self.text.len());
478 let text_after_start = placeholder_end + 1; tracing::trace!(
481 "delete_char_before: display_cursor={}, zones=({},{},{},{}), text_after_len={}",
482 self.display_cursor,
483 text_before_end,
484 placeholder_start,
485 placeholder_end,
486 total_len,
487 text_after_len
488 );
489
490 if self.display_cursor > text_after_start && text_after_len > 0 {
492 let text_after = &self.text[text_before_len..];
494 let offset_in_after = self.display_cursor - text_after_start;
495
496 if offset_in_after > 0 {
497 let char_offset = text_after.chars().take(offset_in_after).count();
499 if char_offset > 0 {
500 let delete_char_offset = char_offset - 1;
501 let delete_start = text_after
502 .char_indices()
503 .nth(delete_char_offset)
504 .map(|(i, _)| i)
505 .unwrap_or(0);
506
507 self.text.remove(text_before_len + delete_start);
508
509 let new_text_after_len = self.text.len().saturating_sub(text_before_len);
511 if new_text_after_len == 0 {
512 self.display_cursor = placeholder_end;
514 } else {
515 self.display_cursor = text_after_start + delete_start;
517 }
518 return true;
519 }
520 }
521 }
522
523 if self.display_cursor == text_after_start && text_after_len > 0 {
525 self.display_cursor = placeholder_end;
526 return true;
527 }
528
529 if self.display_cursor == placeholder_end {
532 let text_before = if text_before_len > 0 {
534 self.text[..text_before_len].to_string()
535 } else {
536 String::new()
537 };
538 let text_after = if text_after_len > 0 {
539 self.text[text_before_len..].to_string()
540 } else {
541 String::new()
542 };
543
544 self.pasted_content = None;
546
547 self.text = format!("{}{}", text_before, text_after);
549
550 self.cursor = text_before.len();
552 self.display_cursor = 0;
553 return true;
554 }
555
556 if self.display_cursor > placeholder_start && self.display_cursor < placeholder_end {
558 self.display_cursor = placeholder_start;
559 return true;
560 }
561
562 if self.display_cursor == placeholder_start {
564 if text_before_len > 0 {
565 self.display_cursor = text_before_end;
567 return true;
568 } else {
569 self.pasted_content = None;
571 self.text.clear();
572 self.cursor = 0;
573 self.display_cursor = 0;
574 return true;
575 }
576 }
577
578 if self.display_cursor > text_before_end && self.display_cursor <= placeholder_start {
580 self.display_cursor = text_before_end;
581 return true;
582 }
583
584 if self.display_cursor > 0 && self.display_cursor <= text_before_end {
586 let text_before = &self.text[..text_before_len];
587 let offset_in_before = self.display_cursor;
588
589 if offset_in_before > 0 {
590 let char_offset = text_before.chars().take(offset_in_before).count();
591 if char_offset > 0 {
592 let delete_char_offset = char_offset - 1;
593 let delete_start = text_before
594 .char_indices()
595 .nth(delete_char_offset)
596 .map(|(i, _)| i)
597 .unwrap_or(0);
598
599 self.text.remove(delete_start);
600
601 if let Some(ref mut pasted) = self.pasted_content {
603 pasted.text_before_len -= 1;
604 let paste_portion = if pasted.full_text.len() > text_before_len {
605 pasted.full_text[text_before_len..].to_string()
606 } else {
607 String::new()
608 };
609 pasted.full_text = format!(
610 "{}{}",
611 &self.text[..pasted.text_before_len],
612 paste_portion
613 );
614 }
615
616 self.display_cursor = delete_start;
617 return true;
618 }
619 }
620 }
621
622 if self.display_cursor == 0 {
624 return false;
625 }
626
627 return false;
628 }
629
630 if self.cursor == 0 {
631 return false;
632 }
633
634 let prev_pos = self.prev_char_pos();
635 self.text.drain(prev_pos..self.cursor);
636 self.cursor = prev_pos;
637 true
638 }
639
640 pub fn delete_char_at(&mut self) -> bool {
642 if let Some(ref pasted) = self.pasted_content {
644 let text_before_len = pasted.text_before_len;
645 let text_after_len = self.text.len().saturating_sub(text_before_len);
646 let (text_before_end, placeholder_start, placeholder_end, total_len) =
647 compute_display_zones(text_before_len, pasted.display_text.len(), self.text.len());
648 let text_after_start = placeholder_end + 1;
649
650 tracing::trace!(
651 "delete_char_at: display_cursor={}, zones=({},{},{},{}), text_after_len={}",
652 self.display_cursor,
653 text_before_end,
654 placeholder_start,
655 placeholder_end,
656 total_len,
657 text_after_len
658 );
659
660 if self.display_cursor >= text_after_start && text_after_len > 0 {
662 let text_after = &self.text[text_before_len..];
663 let offset_in_after = self.display_cursor - text_after_start;
664
665 if offset_in_after < text_after.len() {
666 self.text.remove(text_before_len + offset_in_after);
667 let new_total = self.display_zones().3;
669 if self.display_cursor > new_total {
670 self.display_cursor = new_total;
671 }
672 return true;
673 }
674 }
675
676 if self.display_cursor == placeholder_end {
679 let text_before = if text_before_len > 0 {
680 self.text[..text_before_len].to_string()
681 } else {
682 String::new()
683 };
684 let text_after = if text_after_len > 0 {
685 self.text[text_before_len..].to_string()
686 } else {
687 String::new()
688 };
689
690 self.pasted_content = None;
692 self.text = format!("{}{}", text_before, text_after);
693 self.cursor = text_before.len();
694 self.display_cursor = 0;
695 return true;
696 }
697
698 if self.display_cursor >= placeholder_start && self.display_cursor < placeholder_end {
701 let text_before = if text_before_len > 0 {
702 self.text[..text_before_len].to_string()
703 } else {
704 String::new()
705 };
706 let text_after = if text_after_len > 0 {
707 self.text[text_before_len..].to_string()
708 } else {
709 String::new()
710 };
711
712 self.pasted_content = None;
714 self.text = format!("{}{}", text_before, text_after);
715 self.cursor = text_before.len();
716 self.display_cursor = 0;
717 return true;
718 }
719
720 if self.display_cursor == placeholder_start {
723 let text_before = if text_before_len > 0 {
724 self.text[..text_before_len].to_string()
725 } else {
726 String::new()
727 };
728 let text_after = if text_after_len > 0 {
729 self.text[text_before_len..].to_string()
730 } else {
731 String::new()
732 };
733
734 self.pasted_content = None;
736 self.text = format!("{}{}", text_before, text_after);
737 self.cursor = text_before.len();
738 self.display_cursor = 0;
739 return true;
740 }
741
742 if self.display_cursor < text_before_end && text_before_len > 0 {
744 let offset_in_before = self.display_cursor;
745 if offset_in_before < text_before_len {
746 self.text.remove(offset_in_before);
747
748 if let Some(ref mut pasted) = self.pasted_content {
749 pasted.text_before_len -= 1;
750 let paste_portion = if pasted.full_text.len() > text_before_len {
751 pasted.full_text[text_before_len..].to_string()
752 } else {
753 String::new()
754 };
755 pasted.full_text =
756 format!("{}{}", &self.text[..pasted.text_before_len], paste_portion);
757 }
758 return true;
759 }
760 }
761
762 return false;
763 }
764
765 if self.cursor >= self.text.len() {
766 return false;
767 }
768
769 let next_pos = self.next_char_pos();
770 self.text.drain(self.cursor..next_pos);
771 true
772 }
773
774 #[inline]
777 pub fn move_left(&mut self) {
778 if let Some(ref pasted) = self.pasted_content {
779 let (text_before_end, placeholder_start, placeholder_end, total_len) =
780 self.display_zones();
781
782 tracing::trace!(
783 "move_left: display_cursor={}, zones=({},{},{},{}), text_before_len={}",
784 self.display_cursor,
785 text_before_end,
786 placeholder_start,
787 placeholder_end,
788 total_len,
789 pasted.text_before_len
790 );
791
792 if self.display_cursor == 0 {
793 return;
794 }
795
796 if self.display_cursor > placeholder_end {
798 let text_after_start = placeholder_end + 1; if self.display_cursor > text_after_start {
801 let _text_after_len = self.text.len().saturating_sub(pasted.text_before_len);
803 let offset_in_after = self.display_cursor - text_after_start;
804 if offset_in_after > 0 {
805 let text_after = &self.text[pasted.text_before_len..];
806 let char_offset = text_after.chars().take(offset_in_after).count();
808 if char_offset > 0 {
809 let new_char_offset = char_offset - 1;
810 let new_byte_offset = text_after
811 .char_indices()
812 .nth(new_char_offset)
813 .map(|(i, _)| i)
814 .unwrap_or(0);
815 self.display_cursor = text_after_start + new_byte_offset;
816 } else {
817 self.display_cursor = text_after_start;
818 }
819 } else {
820 self.display_cursor = placeholder_end;
821 }
822 } else {
823 self.display_cursor = placeholder_end;
824 }
825 } else if self.display_cursor > placeholder_start {
826 self.display_cursor = placeholder_start;
828 } else if self.display_cursor > text_before_end {
829 self.display_cursor = text_before_end;
831 } else if self.display_cursor > 0 {
832 let text_before = &self.text[..pasted.text_before_len];
834 let offset_in_before = self.display_cursor;
835 if offset_in_before > 0 {
836 let char_offset = text_before.chars().take(offset_in_before).count();
837 if char_offset > 0 {
838 let new_char_offset = char_offset - 1;
839 let new_byte_offset = text_before
840 .char_indices()
841 .nth(new_char_offset)
842 .map(|(i, _)| i)
843 .unwrap_or(0);
844 self.display_cursor = new_byte_offset;
845 } else {
846 self.display_cursor = 0;
847 }
848 }
849 }
850 return;
851 }
852
853 if self.cursor > 0 {
854 self.cursor = self.prev_char_pos();
855 }
856 }
857
858 #[inline]
861 pub fn move_right(&mut self) {
862 if let Some(ref pasted) = self.pasted_content {
863 let (text_before_end, placeholder_start, placeholder_end, total_len) =
864 self.display_zones();
865
866 tracing::trace!(
867 "move_right: display_cursor={}, zones=({},{},{},{}), text_before_len={}",
868 self.display_cursor,
869 text_before_end,
870 placeholder_start,
871 placeholder_end,
872 total_len,
873 pasted.text_before_len
874 );
875
876 if self.display_cursor >= total_len {
877 return;
878 }
879
880 if self.display_cursor >= placeholder_end {
882 let text_after_start = placeholder_end + 1; let text_after_len = self.text.len().saturating_sub(pasted.text_before_len);
885
886 if text_after_len > 0 && self.display_cursor >= text_after_start {
887 let text_after = &self.text[pasted.text_before_len..];
888 let offset_in_after = self.display_cursor - text_after_start;
889
890 let char_offset = text_after.chars().take(offset_in_after).count();
892 let total_chars = text_after.chars().count();
893
894 if char_offset < total_chars {
895 let new_char_offset = char_offset + 1;
896 let new_byte_offset = text_after
897 .char_indices()
898 .nth(new_char_offset)
899 .map(|(i, _)| i)
900 .unwrap_or(text_after.len());
901 self.display_cursor = (text_after_start + new_byte_offset).min(total_len);
902 }
903 } else if self.display_cursor == placeholder_end && text_after_len > 0 {
904 self.display_cursor = text_after_start;
906 }
907 } else if self.display_cursor >= placeholder_start {
908 self.display_cursor = placeholder_end;
910 } else if self.display_cursor >= text_before_end {
911 self.display_cursor = placeholder_end;
913 } else {
914 let text_before = &self.text[..pasted.text_before_len];
916 let offset_in_before = self.display_cursor;
917 let char_offset = text_before.chars().take(offset_in_before).count();
918 let total_chars = text_before.chars().count();
919
920 if char_offset < total_chars {
921 let new_char_offset = char_offset + 1;
922 let new_byte_offset = text_before
923 .char_indices()
924 .nth(new_char_offset)
925 .map(|(i, _)| i)
926 .unwrap_or(text_before.len());
927 self.display_cursor = new_byte_offset;
928 } else {
929 self.display_cursor = placeholder_end;
931 }
932 }
933 return;
934 }
935
936 if self.cursor < self.text.len() {
937 self.cursor = self.next_char_pos();
938 }
939 }
940
941 #[inline]
943 pub fn move_to_start(&mut self) {
944 if self.pasted_content.is_some() {
945 self.display_cursor = 0;
946 } else {
947 self.cursor = 0;
948 }
949 }
950
951 #[inline]
953 pub fn move_to_end(&mut self) {
954 if self.pasted_content.is_some() {
955 self.display_cursor = self.display_zones().3;
956 } else {
957 self.cursor = self.text.len();
958 }
959 }
960
961 #[inline]
963 pub fn clear(&mut self) {
964 self.text.clear();
965 self.cursor = 0;
966 self.pasted_content = None;
967 self.display_cursor = 0;
968 }
969
970 pub fn set_text(&mut self, text: &str) {
972 self.text = text.to_string();
973 self.cursor = self.text.len();
974 self.pasted_content = None;
975 self.display_cursor = self.cursor;
976 }
977
978 pub fn take_trimmed(&mut self) -> String {
980 let full_text = self.text();
981 let trimmed = full_text.trim();
982 let result = String::from(trimmed);
983 self.clear();
984 result
985 }
986
987 pub fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
989 self.text.drain(start..end);
990 self.text.insert_str(start, replacement);
991 self.cursor = start + replacement.len();
992 }
993
994 #[inline]
996 pub fn delete_range_to_cursor(&mut self, start: usize) {
997 if start < self.cursor {
998 self.text.drain(start..self.cursor);
999 self.cursor = start;
1000 }
1001 }
1002
1003 #[inline]
1005 pub fn text_before_cursor(&self) -> &str {
1006 &self.text[..self.cursor]
1007 }
1008
1009 #[inline]
1011 pub fn text_after_cursor(&self) -> &str {
1012 &self.text[self.cursor..]
1013 }
1014
1015 #[inline]
1017 pub fn char_at_cursor(&self) -> Option<char> {
1018 self.text[self.cursor..].chars().next()
1019 }
1020
1021 pub fn char_before_cursor(&self) -> Option<char> {
1023 if self.cursor == 0 {
1024 return None;
1025 }
1026 let prev_pos = self.prev_char_pos();
1027 self.text[prev_pos..self.cursor].chars().next()
1028 }
1029
1030 pub fn navigate_history_up(&mut self) -> bool {
1033 let current = self.text();
1034 let current = current.trim();
1035
1036 if let Some(entry) = self.history.navigate_up(current) {
1037 self.text = entry.to_string();
1038 self.cursor = self.text.len();
1039 self.pasted_content = None;
1040 return true;
1041 }
1042
1043 false
1044 }
1045
1046 pub fn navigate_history_down(&mut self) -> bool {
1049 if let Some(entry) = self.history.navigate_down() {
1050 self.text = entry.to_string();
1051 self.cursor = self.text.len();
1052 return true;
1053 } else if !self.history.is_navigating() {
1054 if let Some(draft) = self.history.saved_draft() {
1056 self.text = draft.to_string();
1057 self.cursor = self.text.len();
1058 } else {
1059 self.clear();
1060 }
1061 return true;
1062 }
1063
1064 false
1065 }
1066
1067 pub fn is_navigating_history(&self) -> bool {
1069 self.history.is_navigating()
1070 }
1071
1072 pub fn take_and_add_to_history(&mut self) -> String {
1074 let full_text = self.text();
1075 let text = full_text.trim().to_string();
1076
1077 if !text.is_empty() {
1078 self.history.add(&text);
1079 }
1080
1081 self.clear();
1082 text
1083 }
1084
1085 pub fn save_history(&self, path: &PathBuf) -> Result<(), String> {
1087 self.history.save(path)
1088 }
1089
1090 pub fn history(&self) -> &InputHistory {
1092 &self.history
1093 }
1094
1095 pub fn history_mut(&mut self) -> &mut InputHistory {
1097 &mut self.history
1098 }
1099
1100 #[inline]
1102 fn prev_char_pos(&self) -> usize {
1103 if self.cursor == 0 {
1104 return 0;
1105 }
1106 let mut pos = self.cursor - 1;
1107 while pos > 0 && !self.text.is_char_boundary(pos) {
1108 pos -= 1;
1109 }
1110 pos
1111 }
1112
1113 #[inline]
1115 fn next_char_pos(&self) -> usize {
1116 if self.cursor >= self.text.len() {
1117 return self.text.len();
1118 }
1119 let mut pos = self.cursor + 1;
1120 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
1121 pos += 1;
1122 }
1123 pos
1124 }
1125}
1126
1127#[inline]
1129fn truncate_paste(text: &str) -> (&str, bool) {
1130 if text.len() <= MAX_PASTE_SIZE {
1131 return (text, false);
1132 }
1133
1134 let truncated = &text[..text
1135 .char_indices()
1136 .nth(MAX_PASTE_SIZE)
1137 .map(|(i, _)| i)
1138 .unwrap_or(text.len())];
1139 (truncated, true)
1140}
1141
1142impl Default for InputEditor {
1143 fn default() -> Self {
1144 Self::new()
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 #[test]
1153 fn test_editor_creation() {
1154 let editor = InputEditor::new();
1155 assert!(editor.is_empty());
1156 assert_eq!(editor.cursor(), 0);
1157 }
1158
1159 #[test]
1160 fn test_insert_char() {
1161 let mut editor = InputEditor::new();
1162 editor.insert_char('h');
1163 editor.insert_char('i');
1164 assert_eq!(editor.text(), "hi".to_string());
1165 assert_eq!(editor.cursor(), 2);
1166 }
1167
1168 #[test]
1169 fn test_delete_char_before() {
1170 let mut editor = InputEditor::new();
1171 editor.insert_str("hello");
1172 editor.set_cursor(3);
1173
1174 assert!(editor.delete_char_before());
1175 assert_eq!(editor.text(), "helo".to_string());
1176 assert_eq!(editor.cursor(), 2);
1177 }
1178
1179 #[test]
1180 fn test_navigation() {
1181 let mut editor = InputEditor::new();
1182 editor.insert_str("hello");
1183
1184 editor.move_left();
1185 assert_eq!(editor.cursor(), 4);
1186
1187 editor.move_to_start();
1188 assert_eq!(editor.cursor(), 0);
1189
1190 editor.move_to_end();
1191 assert_eq!(editor.cursor(), 5);
1192 }
1193
1194 #[test]
1195 fn test_utf8() {
1196 let mut editor = InputEditor::new();
1197 editor.insert_str("hΓ©llo");
1198
1199 let pos = editor.text().char_indices().nth(2).map(|(i, _)| i).unwrap();
1200 editor.set_cursor(pos);
1201
1202 assert_eq!(editor.cursor(), pos);
1203 assert_eq!(editor.char_before_cursor(), Some('Γ©'));
1204 }
1205
1206 #[test]
1207 fn test_take_trimmed() {
1208 let mut editor = InputEditor::new();
1209 editor.insert_str(" hello ");
1210 let text = editor.take_trimmed();
1211 assert_eq!(text, "hello");
1212 assert!(editor.is_empty());
1213 }
1214
1215 #[test]
1216 fn test_replace_range() {
1217 let mut editor = InputEditor::new();
1218 editor.insert_str("hello world");
1219 editor.replace_range(6, 11, "universe");
1220 assert_eq!(editor.text(), "hello universe".to_string());
1221 }
1222
1223 #[test]
1224 fn test_utf8_emojis() {
1225 let mut editor = InputEditor::new();
1226
1227 editor.insert_str("Hello π World π");
1228 assert_eq!(editor.text(), "Hello π World π".to_string());
1229
1230 editor.move_to_start();
1231 editor.move_right();
1232 editor.move_right();
1233
1234 editor.insert_char('π');
1235 assert_eq!(editor.text(), "Heπllo π World π".to_string());
1236 }
1237
1238 #[test]
1239 fn test_utf8_multibyte_chars() {
1240 let mut editor = InputEditor::new();
1241
1242 editor.insert_str("ζ₯ζ¬θͺ");
1243 assert_eq!(editor.text(), "ζ₯ζ¬θͺ".to_string());
1244 assert_eq!(editor.cursor(), 9);
1245
1246 editor.set_cursor(6);
1247 assert!(editor.delete_char_before());
1248 assert_eq!(editor.text(), "ζ₯θͺ".to_string());
1249 assert_eq!(editor.cursor(), 3);
1250 }
1251
1252 #[test]
1253 fn test_paste_size_limit() {
1254 let mut editor = InputEditor::new();
1255
1256 let large_text = "x".repeat(350 * 1024);
1258 let truncated = editor.insert_paste(&large_text);
1259
1260 assert!(truncated, "Should indicate paste was truncated");
1261 assert!(
1263 editor.text().len() <= MAX_PASTE_SIZE,
1264 "Text length {} should be <= MAX_PASTE_SIZE {}",
1265 editor.text().len(),
1266 MAX_PASTE_SIZE
1267 );
1268 assert!(editor.display_text().starts_with("[Pasted text +"));
1270 }
1271
1272 #[test]
1273 fn test_paste_normal_size() {
1274 let mut editor = InputEditor::new();
1275
1276 let text = "normal text";
1277 let truncated = editor.insert_paste(text);
1278
1279 assert!(!truncated, "Should not truncate normal-sized paste");
1280 assert_eq!(editor.text(), text);
1281 }
1282
1283 #[test]
1284 fn test_paste_newline_normalization() {
1285 let mut editor = InputEditor::new();
1286
1287 editor.insert_paste("line1\r\nline2\r\n");
1288 assert_eq!(editor.text(), "line1\n\nline2\n\n".to_string());
1289 }
1290
1291 #[test]
1292 fn test_navigation_empty_text() {
1293 let mut editor = InputEditor::new();
1294
1295 editor.move_left();
1296 assert_eq!(editor.cursor(), 0);
1297
1298 editor.move_right();
1299 assert_eq!(editor.cursor(), 0);
1300
1301 editor.move_to_start();
1302 assert_eq!(editor.cursor(), 0);
1303
1304 editor.move_to_end();
1305 assert_eq!(editor.cursor(), 0);
1306
1307 assert!(!editor.delete_char_before());
1308 assert!(!editor.delete_char_at());
1309 }
1310
1311 #[test]
1312 fn test_replace_range_invalid() {
1313 let mut editor = InputEditor::new();
1314 editor.insert_str("hello");
1315
1316 editor.replace_range(5, 5, " world");
1317 assert_eq!(editor.text(), "hello world".to_string());
1318
1319 editor.replace_range(6, 11, "universe");
1320 assert_eq!(editor.text(), "hello universe".to_string());
1321 }
1322
1323 #[test]
1324 fn test_replace_range_multibyte() {
1325 let mut editor = InputEditor::new();
1326 editor.insert_str("hello δΈη");
1327
1328 let world_start = editor.text().char_indices().nth(6).map(|(i, _)| i).unwrap();
1329 editor.replace_range(world_start, editor.text().len(), "π");
1330 assert_eq!(editor.text(), "hello π".to_string());
1331 }
1332
1333 #[test]
1334 fn test_cursor_boundary_safety() {
1335 let mut editor = InputEditor::new();
1336 editor.insert_str("hΓ©llo");
1337
1338 editor.set_cursor(2);
1339 assert_ne!(editor.cursor(), 2, "Cursor should not be in middle of char");
1340 assert!(editor.text().is_char_boundary(editor.cursor()));
1341 }
1342
1343 #[test]
1344 fn test_char_at_cursor() {
1345 let mut editor = InputEditor::new();
1346 editor.insert_str("hello");
1347
1348 editor.set_cursor(0);
1349 assert_eq!(editor.char_at_cursor(), Some('h'));
1350
1351 editor.set_cursor(5);
1352 assert_eq!(editor.char_at_cursor(), None);
1353
1354 editor.clear();
1355 assert_eq!(editor.char_at_cursor(), None);
1356 }
1357
1358 #[test]
1359 fn test_text_before_after_cursor() {
1360 let mut editor = InputEditor::new();
1361 editor.insert_str("hello world");
1362 editor.set_cursor(5);
1363
1364 assert_eq!(editor.text_before_cursor(), "hello");
1365 assert_eq!(editor.text_after_cursor(), " world");
1366 }
1367
1368 #[test]
1369 fn test_delete_range_to_cursor() {
1370 let mut editor = InputEditor::new();
1371 editor.insert_str("hello world");
1372 editor.set_cursor(11);
1373
1374 editor.delete_range_to_cursor(6);
1375 assert_eq!(editor.text(), "hello ".to_string());
1376 assert_eq!(editor.cursor(), 6);
1377 }
1378
1379 #[test]
1382 fn test_navigate_history_up_empty_history() {
1383 let mut editor = InputEditor::new();
1384
1385 let navigated = editor.navigate_history_up();
1386 assert!(!navigated);
1387 assert!(editor.is_empty());
1388 }
1389
1390 #[test]
1391 fn test_navigate_history_up_with_entries() {
1392 let mut editor = InputEditor::new();
1393 editor.history_mut().add("previous");
1394
1395 let navigated = editor.navigate_history_up();
1396 assert!(navigated);
1397 assert_eq!(editor.text(), "previous".to_string());
1398 assert!(editor.is_navigating_history());
1399 }
1400
1401 #[test]
1402 fn test_navigate_history_cycle() {
1403 let mut editor = InputEditor::new();
1404 editor.history_mut().add("oldest");
1405 editor.history_mut().add("newest");
1406
1407 editor.navigate_history_up();
1409 assert_eq!(editor.text(), "newest".to_string());
1410
1411 editor.navigate_history_up();
1413 assert_eq!(editor.text(), "oldest".to_string());
1414
1415 editor.navigate_history_down();
1417 assert_eq!(editor.text(), "newest".to_string());
1418
1419 editor.navigate_history_down();
1421 assert!(editor.is_empty());
1422 assert!(!editor.is_navigating_history());
1423 }
1424
1425 #[test]
1426 fn test_navigate_history_saves_draft() {
1427 let mut editor = InputEditor::new();
1428 editor.insert_str("my draft");
1429 editor.history_mut().add("previous");
1430
1431 editor.navigate_history_up();
1432 assert_eq!(editor.text(), "previous".to_string());
1433
1434 editor.navigate_history_down();
1436 assert_eq!(editor.text(), "my draft".to_string());
1437 }
1438
1439 #[test]
1440 fn test_take_and_add_to_history() {
1441 let mut editor = InputEditor::new();
1442 editor.insert_str(" hello world ");
1443
1444 let text = editor.take_and_add_to_history();
1445 assert_eq!(text, "hello world");
1446 assert!(editor.is_empty());
1447 assert_eq!(editor.history().len(), 1);
1448 }
1449
1450 #[test]
1451 fn test_take_and_add_to_history_empty() {
1452 let mut editor = InputEditor::new();
1453
1454 let text = editor.take_and_add_to_history();
1455 assert!(text.is_empty());
1456 assert_eq!(editor.history().len(), 0);
1457 }
1458
1459 #[test]
1460 fn test_insert_char_resets_navigation() {
1461 let mut editor = InputEditor::new();
1462 editor.history_mut().add("previous");
1463
1464 editor.navigate_history_up();
1465 assert!(editor.is_navigating_history());
1466
1467 editor.insert_char('a');
1468 assert!(!editor.is_navigating_history());
1469 }
1470
1471 #[test]
1474 fn test_large_paste_creates_placeholder() {
1475 let mut editor = InputEditor::new();
1476
1477 let large_text = "x".repeat(150);
1478 let truncated = editor.insert_paste(&large_text);
1479
1480 assert!(!truncated, "Should not truncate text under MAX_PASTE_SIZE");
1481 assert_eq!(
1482 editor.text(),
1483 large_text,
1484 "text() should return full content"
1485 );
1486 assert_eq!(editor.display_text(), "[Pasted text + 150 chars]");
1487 assert!(!editor.is_empty());
1488 }
1489
1490 #[test]
1491 fn test_multiline_paste_shows_lines() {
1492 let mut editor = InputEditor::new();
1493
1494 let multiline = "line number one with some text\nline number two with more text\nline number three here\nline number four with content\nline number five has text";
1496 assert!(multiline.len() > 100, "Test text should be > 100 chars");
1497 editor.insert_paste(multiline);
1498
1499 assert_eq!(editor.display_text(), "[Pasted text + 5 lines]");
1500 assert_eq!(editor.text(), multiline);
1501 }
1502
1503 #[test]
1504 fn test_small_paste_no_placeholder() {
1505 let mut editor = InputEditor::new();
1506
1507 let small_text = "hello world";
1508 editor.insert_paste(small_text);
1509
1510 assert_eq!(editor.display_text(), small_text);
1511 assert_eq!(editor.text(), small_text);
1512 }
1513
1514 #[test]
1515 fn test_typing_after_paste_appends() {
1516 let mut editor = InputEditor::new();
1517
1518 let large_text = "x".repeat(150);
1519 editor.insert_paste(&large_text);
1520
1521 assert_eq!(editor.display_text(), "[Pasted text + 150 chars]");
1522
1523 editor.insert_char('!');
1525 editor.insert_char(' ');
1526 editor.insert_char('t');
1527 editor.insert_char('e');
1528 editor.insert_char('s');
1529 editor.insert_char('t');
1530
1531 assert_eq!(
1533 editor.display_text_combined(),
1534 "[Pasted text + 150 chars] ! test"
1535 );
1536
1537 assert_eq!(editor.text(), format!("{}! test", large_text));
1539
1540 assert!(editor.has_pasted_content());
1542 }
1543
1544 #[test]
1545 fn test_clear_removes_pasted_content() {
1546 let mut editor = InputEditor::new();
1547
1548 let large_text = "x".repeat(150);
1549 editor.insert_paste(&large_text);
1550
1551 assert!(!editor.is_empty());
1552
1553 editor.clear();
1554
1555 assert!(editor.is_empty());
1556 assert!(editor.pasted_content.is_none());
1557 }
1558
1559 #[test]
1560 fn test_take_and_add_to_history_with_pasted_content() {
1561 let mut editor = InputEditor::new();
1562
1563 let large_text = "x".repeat(150);
1564 editor.insert_paste(&large_text);
1565
1566 let text = editor.take_and_add_to_history();
1567
1568 assert_eq!(text, large_text);
1569 assert!(editor.is_empty());
1570 assert_eq!(editor.history().len(), 1);
1571 }
1572
1573 #[test]
1574 fn test_paste_appends_to_typed_text() {
1575 let mut editor = InputEditor::new();
1576
1577 editor.insert_str("Hello ");
1578
1579 let paste_text = "x".repeat(150);
1580 editor.insert_paste(&paste_text);
1581
1582 assert!(editor.display_text().starts_with("[Pasted text +"));
1584 assert!(editor.text().starts_with("Hello "));
1586 assert!(editor.text().ends_with(&paste_text));
1587 }
1588
1589 #[test]
1590 fn test_paste_appends_to_existing_paste() {
1591 let mut editor = InputEditor::new();
1592
1593 let paste1 = "x".repeat(150);
1594 editor.insert_paste(&paste1);
1595
1596 let paste2 = "y".repeat(50);
1597 editor.insert_paste(&paste2);
1598
1599 assert!(editor.display_text().starts_with("[Pasted text +"));
1601 assert!(editor.text().starts_with(&paste1));
1603 assert!(editor.text().ends_with(&paste2));
1604 }
1605
1606 #[test]
1607 fn test_cursor_navigation_enabled_with_paste() {
1608 let mut editor = InputEditor::new();
1609
1610 let large_text = "x".repeat(150);
1611 editor.insert_paste(&large_text);
1612
1613 let initial_cursor = editor.cursor();
1614
1615 editor.move_left();
1617 assert!(editor.cursor() < initial_cursor, "Cursor should move left");
1618
1619 editor.move_right();
1620 assert_eq!(
1621 editor.cursor(),
1622 initial_cursor,
1623 "Cursor should return to end"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_backspace_deletes_placeholder_discards_content() {
1629 let mut editor = InputEditor::new();
1630
1631 let large_text = "x".repeat(150);
1632 editor.insert_paste(&large_text);
1633
1634 assert!(!editor.is_empty());
1635
1636 editor.move_left();
1638
1639 let deleted = editor.delete_char_before();
1642 assert!(deleted);
1643 assert!(editor.pasted_content.is_none());
1645 assert!(editor.is_empty());
1647 assert_eq!(editor.text(), "");
1648 }
1649
1650 #[test]
1651 fn test_delete_key_deletes_placeholder_discards_content() {
1652 let mut editor = InputEditor::new();
1653
1654 let large_text = "x".repeat(150);
1655 editor.insert_paste(&large_text);
1656
1657 assert!(!editor.is_empty());
1658
1659 editor.move_to_start();
1661
1662 let deleted = editor.delete_char_at();
1665 assert!(deleted);
1666 assert!(editor.pasted_content.is_none());
1668 assert!(editor.is_empty());
1670 assert_eq!(editor.text(), "");
1671 }
1672
1673 #[test]
1674 fn test_backspace_placeholder_keeps_typed_text() {
1675 let mut editor = InputEditor::new();
1676
1677 editor.insert_str("hello ");
1679
1680 let large_text = "x".repeat(150);
1682 editor.insert_paste(&large_text);
1683
1684 assert!(editor.has_pasted_content());
1686
1687 editor.move_to_end();
1689
1690 let deleted = editor.delete_char_before();
1692 assert!(deleted);
1693
1694 assert!(editor.pasted_content.is_none());
1696 assert_eq!(editor.text(), "hello ");
1697 }
1698
1699 #[test]
1700 fn test_cursor_at_end_of_placeholder() {
1701 let mut editor = InputEditor::new();
1702
1703 let large_text = "x".repeat(150);
1704 editor.insert_paste(&large_text);
1705
1706 let display = editor.display_text();
1708 assert_eq!(editor.cursor(), display.len());
1709 }
1710
1711 #[test]
1712 fn test_small_paste_converts_to_pasted_content_when_combined() {
1713 let mut editor = InputEditor::new();
1714
1715 let paste1 = "x".repeat(60);
1717 let paste2 = "y".repeat(60);
1718
1719 editor.insert_paste(&paste1);
1720 assert_eq!(editor.display_text(), &paste1); editor.insert_paste(&paste2);
1723 assert!(editor.display_text().starts_with("[Pasted text +"));
1725 assert_eq!(editor.text(), format!("{}{}", paste1, paste2));
1726 }
1727
1728 #[test]
1729 fn test_cursor_position_after_paste_with_typed_text() {
1730 let mut editor = InputEditor::new();
1731
1732 editor.insert_str("Hello ");
1734
1735 let large_text = "x".repeat(150);
1737 editor.insert_paste(&large_text);
1738
1739 editor.insert_char('!');
1741 editor.insert_char(' ');
1742 editor.insert_char('w');
1743 editor.insert_char('o');
1744 editor.insert_char('r');
1745 editor.insert_char('l');
1746 editor.insert_char('d');
1747
1748 let combined = editor.display_text_combined();
1750 let combined_len = combined.len();
1751 let cursor_pos = editor.cursor();
1752
1753 assert_eq!(
1757 cursor_pos, combined_len,
1758 "Cursor should be at end of combined display text! cursor={}, combined_len={}",
1759 cursor_pos, combined_len
1760 );
1761 }
1762
1763 #[test]
1764 fn test_text_before_and_after_paste() {
1765 let mut editor = InputEditor::new();
1766
1767 editor.insert_str("ola");
1769
1770 let large_text = "x".repeat(150);
1772 editor.insert_paste(&large_text);
1773
1774 editor.insert_char(' ');
1776 editor.insert_char('m');
1777 editor.insert_char('u');
1778 editor.insert_char('n');
1779 editor.insert_char('d');
1780 editor.insert_char('o');
1781
1782 let display = editor.display_text_combined();
1784 assert!(
1785 display.starts_with("ola [Pasted text +"),
1786 "Display should start with 'ola [Pasted text +', got: '{}'",
1787 display
1788 );
1789 assert!(
1790 display.ends_with(" mundo"),
1791 "Display should end with ' mundo', got: '{}'",
1792 display
1793 );
1794
1795 let full_text = editor.text();
1797 assert!(full_text.starts_with("ola"));
1798 assert!(full_text.ends_with(" mundo"));
1799
1800 let cursor_pos = editor.cursor();
1802 let display_len = display.len();
1803 assert_eq!(
1804 cursor_pos, display_len,
1805 "Cursor should be at end of display: cursor={}, display_len={}",
1806 cursor_pos, display_len
1807 );
1808 }
1809}