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::debug!(
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::debug!(
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::debug!("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::debug!(
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::debug!(
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::debug!(
436 "insert_paste: small paste appended, total len={}",
437 self.text.len()
438 );
439
440 if self.text.len() > MAX_DISPLAY_LENGTH {
442 tracing::debug!(
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::debug!("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::debug!("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::debug!(
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::debug!(
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::debug!(
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::debug!(
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 take_trimmed(&mut self) -> String {
972 let full_text = self.text();
973 let trimmed = full_text.trim();
974 let result = String::from(trimmed);
975 self.clear();
976 result
977 }
978
979 pub fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
981 self.text.drain(start..end);
982 self.text.insert_str(start, replacement);
983 self.cursor = start + replacement.len();
984 }
985
986 #[inline]
988 pub fn delete_range_to_cursor(&mut self, start: usize) {
989 if start < self.cursor {
990 self.text.drain(start..self.cursor);
991 self.cursor = start;
992 }
993 }
994
995 #[inline]
997 pub fn text_before_cursor(&self) -> &str {
998 &self.text[..self.cursor]
999 }
1000
1001 #[inline]
1003 pub fn text_after_cursor(&self) -> &str {
1004 &self.text[self.cursor..]
1005 }
1006
1007 #[inline]
1009 pub fn char_at_cursor(&self) -> Option<char> {
1010 self.text[self.cursor..].chars().next()
1011 }
1012
1013 pub fn char_before_cursor(&self) -> Option<char> {
1015 if self.cursor == 0 {
1016 return None;
1017 }
1018 let prev_pos = self.prev_char_pos();
1019 self.text[prev_pos..self.cursor].chars().next()
1020 }
1021
1022 pub fn navigate_history_up(&mut self) -> bool {
1025 let current = self.text();
1026 let current = current.trim();
1027
1028 if let Some(entry) = self.history.navigate_up(current) {
1029 self.text = entry.to_string();
1030 self.cursor = self.text.len();
1031 self.pasted_content = None;
1032 return true;
1033 }
1034
1035 false
1036 }
1037
1038 pub fn navigate_history_down(&mut self) -> bool {
1041 if let Some(entry) = self.history.navigate_down() {
1042 self.text = entry.to_string();
1043 self.cursor = self.text.len();
1044 return true;
1045 } else if !self.history.is_navigating() {
1046 if let Some(draft) = self.history.saved_draft() {
1048 self.text = draft.to_string();
1049 self.cursor = self.text.len();
1050 } else {
1051 self.clear();
1052 }
1053 return true;
1054 }
1055
1056 false
1057 }
1058
1059 pub fn is_navigating_history(&self) -> bool {
1061 self.history.is_navigating()
1062 }
1063
1064 pub fn take_and_add_to_history(&mut self) -> String {
1066 let full_text = self.text();
1067 let text = full_text.trim().to_string();
1068
1069 if !text.is_empty() {
1070 self.history.add(&text);
1071 }
1072
1073 self.clear();
1074 text
1075 }
1076
1077 pub fn save_history(&self, path: &PathBuf) -> Result<(), String> {
1079 self.history.save(path)
1080 }
1081
1082 pub fn history(&self) -> &InputHistory {
1084 &self.history
1085 }
1086
1087 pub fn history_mut(&mut self) -> &mut InputHistory {
1089 &mut self.history
1090 }
1091
1092 #[inline]
1094 fn prev_char_pos(&self) -> usize {
1095 if self.cursor == 0 {
1096 return 0;
1097 }
1098 let mut pos = self.cursor - 1;
1099 while pos > 0 && !self.text.is_char_boundary(pos) {
1100 pos -= 1;
1101 }
1102 pos
1103 }
1104
1105 #[inline]
1107 fn next_char_pos(&self) -> usize {
1108 if self.cursor >= self.text.len() {
1109 return self.text.len();
1110 }
1111 let mut pos = self.cursor + 1;
1112 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
1113 pos += 1;
1114 }
1115 pos
1116 }
1117}
1118
1119#[inline]
1121fn truncate_paste(text: &str) -> (&str, bool) {
1122 if text.len() <= MAX_PASTE_SIZE {
1123 return (text, false);
1124 }
1125
1126 let truncated = &text[..text
1127 .char_indices()
1128 .nth(MAX_PASTE_SIZE)
1129 .map(|(i, _)| i)
1130 .unwrap_or(text.len())];
1131 (truncated, true)
1132}
1133
1134impl Default for InputEditor {
1135 fn default() -> Self {
1136 Self::new()
1137 }
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use super::*;
1143
1144 #[test]
1145 fn test_editor_creation() {
1146 let editor = InputEditor::new();
1147 assert!(editor.is_empty());
1148 assert_eq!(editor.cursor(), 0);
1149 }
1150
1151 #[test]
1152 fn test_insert_char() {
1153 let mut editor = InputEditor::new();
1154 editor.insert_char('h');
1155 editor.insert_char('i');
1156 assert_eq!(editor.text(), "hi".to_string());
1157 assert_eq!(editor.cursor(), 2);
1158 }
1159
1160 #[test]
1161 fn test_delete_char_before() {
1162 let mut editor = InputEditor::new();
1163 editor.insert_str("hello");
1164 editor.set_cursor(3);
1165
1166 assert!(editor.delete_char_before());
1167 assert_eq!(editor.text(), "helo".to_string());
1168 assert_eq!(editor.cursor(), 2);
1169 }
1170
1171 #[test]
1172 fn test_navigation() {
1173 let mut editor = InputEditor::new();
1174 editor.insert_str("hello");
1175
1176 editor.move_left();
1177 assert_eq!(editor.cursor(), 4);
1178
1179 editor.move_to_start();
1180 assert_eq!(editor.cursor(), 0);
1181
1182 editor.move_to_end();
1183 assert_eq!(editor.cursor(), 5);
1184 }
1185
1186 #[test]
1187 fn test_utf8() {
1188 let mut editor = InputEditor::new();
1189 editor.insert_str("hΓ©llo");
1190
1191 let pos = editor.text().char_indices().nth(2).map(|(i, _)| i).unwrap();
1192 editor.set_cursor(pos);
1193
1194 assert_eq!(editor.cursor(), pos);
1195 assert_eq!(editor.char_before_cursor(), Some('Γ©'));
1196 }
1197
1198 #[test]
1199 fn test_take_trimmed() {
1200 let mut editor = InputEditor::new();
1201 editor.insert_str(" hello ");
1202 let text = editor.take_trimmed();
1203 assert_eq!(text, "hello");
1204 assert!(editor.is_empty());
1205 }
1206
1207 #[test]
1208 fn test_replace_range() {
1209 let mut editor = InputEditor::new();
1210 editor.insert_str("hello world");
1211 editor.replace_range(6, 11, "universe");
1212 assert_eq!(editor.text(), "hello universe".to_string());
1213 }
1214
1215 #[test]
1216 fn test_utf8_emojis() {
1217 let mut editor = InputEditor::new();
1218
1219 editor.insert_str("Hello π World π");
1220 assert_eq!(editor.text(), "Hello π World π".to_string());
1221
1222 editor.move_to_start();
1223 editor.move_right();
1224 editor.move_right();
1225
1226 editor.insert_char('π');
1227 assert_eq!(editor.text(), "Heπllo π World π".to_string());
1228 }
1229
1230 #[test]
1231 fn test_utf8_multibyte_chars() {
1232 let mut editor = InputEditor::new();
1233
1234 editor.insert_str("ζ₯ζ¬θͺ");
1235 assert_eq!(editor.text(), "ζ₯ζ¬θͺ".to_string());
1236 assert_eq!(editor.cursor(), 9);
1237
1238 editor.set_cursor(6);
1239 assert!(editor.delete_char_before());
1240 assert_eq!(editor.text(), "ζ₯θͺ".to_string());
1241 assert_eq!(editor.cursor(), 3);
1242 }
1243
1244 #[test]
1245 fn test_paste_size_limit() {
1246 let mut editor = InputEditor::new();
1247
1248 let large_text = "x".repeat(350 * 1024);
1250 let truncated = editor.insert_paste(&large_text);
1251
1252 assert!(truncated, "Should indicate paste was truncated");
1253 assert!(
1255 editor.text().len() <= MAX_PASTE_SIZE,
1256 "Text length {} should be <= MAX_PASTE_SIZE {}",
1257 editor.text().len(),
1258 MAX_PASTE_SIZE
1259 );
1260 assert!(editor.display_text().starts_with("[Pasted text +"));
1262 }
1263
1264 #[test]
1265 fn test_paste_normal_size() {
1266 let mut editor = InputEditor::new();
1267
1268 let text = "normal text";
1269 let truncated = editor.insert_paste(text);
1270
1271 assert!(!truncated, "Should not truncate normal-sized paste");
1272 assert_eq!(editor.text(), text);
1273 }
1274
1275 #[test]
1276 fn test_paste_newline_normalization() {
1277 let mut editor = InputEditor::new();
1278
1279 editor.insert_paste("line1\r\nline2\r\n");
1280 assert_eq!(editor.text(), "line1\n\nline2\n\n".to_string());
1281 }
1282
1283 #[test]
1284 fn test_navigation_empty_text() {
1285 let mut editor = InputEditor::new();
1286
1287 editor.move_left();
1288 assert_eq!(editor.cursor(), 0);
1289
1290 editor.move_right();
1291 assert_eq!(editor.cursor(), 0);
1292
1293 editor.move_to_start();
1294 assert_eq!(editor.cursor(), 0);
1295
1296 editor.move_to_end();
1297 assert_eq!(editor.cursor(), 0);
1298
1299 assert!(!editor.delete_char_before());
1300 assert!(!editor.delete_char_at());
1301 }
1302
1303 #[test]
1304 fn test_replace_range_invalid() {
1305 let mut editor = InputEditor::new();
1306 editor.insert_str("hello");
1307
1308 editor.replace_range(5, 5, " world");
1309 assert_eq!(editor.text(), "hello world".to_string());
1310
1311 editor.replace_range(6, 11, "universe");
1312 assert_eq!(editor.text(), "hello universe".to_string());
1313 }
1314
1315 #[test]
1316 fn test_replace_range_multibyte() {
1317 let mut editor = InputEditor::new();
1318 editor.insert_str("hello δΈη");
1319
1320 let world_start = editor.text().char_indices().nth(6).map(|(i, _)| i).unwrap();
1321 editor.replace_range(world_start, editor.text().len(), "π");
1322 assert_eq!(editor.text(), "hello π".to_string());
1323 }
1324
1325 #[test]
1326 fn test_cursor_boundary_safety() {
1327 let mut editor = InputEditor::new();
1328 editor.insert_str("hΓ©llo");
1329
1330 editor.set_cursor(2);
1331 assert_ne!(editor.cursor(), 2, "Cursor should not be in middle of char");
1332 assert!(editor.text().is_char_boundary(editor.cursor()));
1333 }
1334
1335 #[test]
1336 fn test_char_at_cursor() {
1337 let mut editor = InputEditor::new();
1338 editor.insert_str("hello");
1339
1340 editor.set_cursor(0);
1341 assert_eq!(editor.char_at_cursor(), Some('h'));
1342
1343 editor.set_cursor(5);
1344 assert_eq!(editor.char_at_cursor(), None);
1345
1346 editor.clear();
1347 assert_eq!(editor.char_at_cursor(), None);
1348 }
1349
1350 #[test]
1351 fn test_text_before_after_cursor() {
1352 let mut editor = InputEditor::new();
1353 editor.insert_str("hello world");
1354 editor.set_cursor(5);
1355
1356 assert_eq!(editor.text_before_cursor(), "hello");
1357 assert_eq!(editor.text_after_cursor(), " world");
1358 }
1359
1360 #[test]
1361 fn test_delete_range_to_cursor() {
1362 let mut editor = InputEditor::new();
1363 editor.insert_str("hello world");
1364 editor.set_cursor(11);
1365
1366 editor.delete_range_to_cursor(6);
1367 assert_eq!(editor.text(), "hello ".to_string());
1368 assert_eq!(editor.cursor(), 6);
1369 }
1370
1371 #[test]
1374 fn test_navigate_history_up_empty_history() {
1375 let mut editor = InputEditor::new();
1376
1377 let navigated = editor.navigate_history_up();
1378 assert!(!navigated);
1379 assert!(editor.is_empty());
1380 }
1381
1382 #[test]
1383 fn test_navigate_history_up_with_entries() {
1384 let mut editor = InputEditor::new();
1385 editor.history_mut().add("previous");
1386
1387 let navigated = editor.navigate_history_up();
1388 assert!(navigated);
1389 assert_eq!(editor.text(), "previous".to_string());
1390 assert!(editor.is_navigating_history());
1391 }
1392
1393 #[test]
1394 fn test_navigate_history_cycle() {
1395 let mut editor = InputEditor::new();
1396 editor.history_mut().add("oldest");
1397 editor.history_mut().add("newest");
1398
1399 editor.navigate_history_up();
1401 assert_eq!(editor.text(), "newest".to_string());
1402
1403 editor.navigate_history_up();
1405 assert_eq!(editor.text(), "oldest".to_string());
1406
1407 editor.navigate_history_down();
1409 assert_eq!(editor.text(), "newest".to_string());
1410
1411 editor.navigate_history_down();
1413 assert!(editor.is_empty());
1414 assert!(!editor.is_navigating_history());
1415 }
1416
1417 #[test]
1418 fn test_navigate_history_saves_draft() {
1419 let mut editor = InputEditor::new();
1420 editor.insert_str("my draft");
1421 editor.history_mut().add("previous");
1422
1423 editor.navigate_history_up();
1424 assert_eq!(editor.text(), "previous".to_string());
1425
1426 editor.navigate_history_down();
1428 assert_eq!(editor.text(), "my draft".to_string());
1429 }
1430
1431 #[test]
1432 fn test_take_and_add_to_history() {
1433 let mut editor = InputEditor::new();
1434 editor.insert_str(" hello world ");
1435
1436 let text = editor.take_and_add_to_history();
1437 assert_eq!(text, "hello world");
1438 assert!(editor.is_empty());
1439 assert_eq!(editor.history().len(), 1);
1440 }
1441
1442 #[test]
1443 fn test_take_and_add_to_history_empty() {
1444 let mut editor = InputEditor::new();
1445
1446 let text = editor.take_and_add_to_history();
1447 assert!(text.is_empty());
1448 assert_eq!(editor.history().len(), 0);
1449 }
1450
1451 #[test]
1452 fn test_insert_char_resets_navigation() {
1453 let mut editor = InputEditor::new();
1454 editor.history_mut().add("previous");
1455
1456 editor.navigate_history_up();
1457 assert!(editor.is_navigating_history());
1458
1459 editor.insert_char('a');
1460 assert!(!editor.is_navigating_history());
1461 }
1462
1463 #[test]
1466 fn test_large_paste_creates_placeholder() {
1467 let mut editor = InputEditor::new();
1468
1469 let large_text = "x".repeat(150);
1470 let truncated = editor.insert_paste(&large_text);
1471
1472 assert!(!truncated, "Should not truncate text under MAX_PASTE_SIZE");
1473 assert_eq!(
1474 editor.text(),
1475 large_text,
1476 "text() should return full content"
1477 );
1478 assert_eq!(editor.display_text(), "[Pasted text + 150 chars]");
1479 assert!(!editor.is_empty());
1480 }
1481
1482 #[test]
1483 fn test_multiline_paste_shows_lines() {
1484 let mut editor = InputEditor::new();
1485
1486 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";
1488 assert!(multiline.len() > 100, "Test text should be > 100 chars");
1489 editor.insert_paste(multiline);
1490
1491 assert_eq!(editor.display_text(), "[Pasted text + 5 lines]");
1492 assert_eq!(editor.text(), multiline);
1493 }
1494
1495 #[test]
1496 fn test_small_paste_no_placeholder() {
1497 let mut editor = InputEditor::new();
1498
1499 let small_text = "hello world";
1500 editor.insert_paste(small_text);
1501
1502 assert_eq!(editor.display_text(), small_text);
1503 assert_eq!(editor.text(), small_text);
1504 }
1505
1506 #[test]
1507 fn test_typing_after_paste_appends() {
1508 let mut editor = InputEditor::new();
1509
1510 let large_text = "x".repeat(150);
1511 editor.insert_paste(&large_text);
1512
1513 assert_eq!(editor.display_text(), "[Pasted text + 150 chars]");
1514
1515 editor.insert_char('!');
1517 editor.insert_char(' ');
1518 editor.insert_char('t');
1519 editor.insert_char('e');
1520 editor.insert_char('s');
1521 editor.insert_char('t');
1522
1523 assert_eq!(
1525 editor.display_text_combined(),
1526 "[Pasted text + 150 chars] ! test"
1527 );
1528
1529 assert_eq!(editor.text(), format!("{}! test", large_text));
1531
1532 assert!(editor.has_pasted_content());
1534 }
1535
1536 #[test]
1537 fn test_clear_removes_pasted_content() {
1538 let mut editor = InputEditor::new();
1539
1540 let large_text = "x".repeat(150);
1541 editor.insert_paste(&large_text);
1542
1543 assert!(!editor.is_empty());
1544
1545 editor.clear();
1546
1547 assert!(editor.is_empty());
1548 assert!(editor.pasted_content.is_none());
1549 }
1550
1551 #[test]
1552 fn test_take_and_add_to_history_with_pasted_content() {
1553 let mut editor = InputEditor::new();
1554
1555 let large_text = "x".repeat(150);
1556 editor.insert_paste(&large_text);
1557
1558 let text = editor.take_and_add_to_history();
1559
1560 assert_eq!(text, large_text);
1561 assert!(editor.is_empty());
1562 assert_eq!(editor.history().len(), 1);
1563 }
1564
1565 #[test]
1566 fn test_paste_appends_to_typed_text() {
1567 let mut editor = InputEditor::new();
1568
1569 editor.insert_str("Hello ");
1570
1571 let paste_text = "x".repeat(150);
1572 editor.insert_paste(&paste_text);
1573
1574 assert!(editor.display_text().starts_with("[Pasted text +"));
1576 assert!(editor.text().starts_with("Hello "));
1578 assert!(editor.text().ends_with(&paste_text));
1579 }
1580
1581 #[test]
1582 fn test_paste_appends_to_existing_paste() {
1583 let mut editor = InputEditor::new();
1584
1585 let paste1 = "x".repeat(150);
1586 editor.insert_paste(&paste1);
1587
1588 let paste2 = "y".repeat(50);
1589 editor.insert_paste(&paste2);
1590
1591 assert!(editor.display_text().starts_with("[Pasted text +"));
1593 assert!(editor.text().starts_with(&paste1));
1595 assert!(editor.text().ends_with(&paste2));
1596 }
1597
1598 #[test]
1599 fn test_cursor_navigation_enabled_with_paste() {
1600 let mut editor = InputEditor::new();
1601
1602 let large_text = "x".repeat(150);
1603 editor.insert_paste(&large_text);
1604
1605 let initial_cursor = editor.cursor();
1606
1607 editor.move_left();
1609 assert!(editor.cursor() < initial_cursor, "Cursor should move left");
1610
1611 editor.move_right();
1612 assert_eq!(
1613 editor.cursor(),
1614 initial_cursor,
1615 "Cursor should return to end"
1616 );
1617 }
1618
1619 #[test]
1620 fn test_backspace_deletes_placeholder_discards_content() {
1621 let mut editor = InputEditor::new();
1622
1623 let large_text = "x".repeat(150);
1624 editor.insert_paste(&large_text);
1625
1626 assert!(!editor.is_empty());
1627
1628 editor.move_left();
1630
1631 let deleted = editor.delete_char_before();
1634 assert!(deleted);
1635 assert!(editor.pasted_content.is_none());
1637 assert!(editor.is_empty());
1639 assert_eq!(editor.text(), "");
1640 }
1641
1642 #[test]
1643 fn test_delete_key_deletes_placeholder_discards_content() {
1644 let mut editor = InputEditor::new();
1645
1646 let large_text = "x".repeat(150);
1647 editor.insert_paste(&large_text);
1648
1649 assert!(!editor.is_empty());
1650
1651 editor.move_to_start();
1653
1654 let deleted = editor.delete_char_at();
1657 assert!(deleted);
1658 assert!(editor.pasted_content.is_none());
1660 assert!(editor.is_empty());
1662 assert_eq!(editor.text(), "");
1663 }
1664
1665 #[test]
1666 fn test_backspace_placeholder_keeps_typed_text() {
1667 let mut editor = InputEditor::new();
1668
1669 editor.insert_str("hello ");
1671
1672 let large_text = "x".repeat(150);
1674 editor.insert_paste(&large_text);
1675
1676 assert!(editor.has_pasted_content());
1678
1679 editor.move_to_end();
1681
1682 let deleted = editor.delete_char_before();
1684 assert!(deleted);
1685
1686 assert!(editor.pasted_content.is_none());
1688 assert_eq!(editor.text(), "hello ");
1689 }
1690
1691 #[test]
1692 fn test_cursor_at_end_of_placeholder() {
1693 let mut editor = InputEditor::new();
1694
1695 let large_text = "x".repeat(150);
1696 editor.insert_paste(&large_text);
1697
1698 let display = editor.display_text();
1700 assert_eq!(editor.cursor(), display.len());
1701 }
1702
1703 #[test]
1704 fn test_small_paste_converts_to_pasted_content_when_combined() {
1705 let mut editor = InputEditor::new();
1706
1707 let paste1 = "x".repeat(60);
1709 let paste2 = "y".repeat(60);
1710
1711 editor.insert_paste(&paste1);
1712 assert_eq!(editor.display_text(), &paste1); editor.insert_paste(&paste2);
1715 assert!(editor.display_text().starts_with("[Pasted text +"));
1717 assert_eq!(editor.text(), format!("{}{}", paste1, paste2));
1718 }
1719
1720 #[test]
1721 fn test_cursor_position_after_paste_with_typed_text() {
1722 let mut editor = InputEditor::new();
1723
1724 editor.insert_str("Hello ");
1726
1727 let large_text = "x".repeat(150);
1729 editor.insert_paste(&large_text);
1730
1731 editor.insert_char('!');
1733 editor.insert_char(' ');
1734 editor.insert_char('w');
1735 editor.insert_char('o');
1736 editor.insert_char('r');
1737 editor.insert_char('l');
1738 editor.insert_char('d');
1739
1740 let combined = editor.display_text_combined();
1742 let combined_len = combined.len();
1743 let cursor_pos = editor.cursor();
1744
1745 assert_eq!(
1749 cursor_pos, combined_len,
1750 "Cursor should be at end of combined display text! cursor={}, combined_len={}",
1751 cursor_pos, combined_len
1752 );
1753 }
1754
1755 #[test]
1756 fn test_text_before_and_after_paste() {
1757 let mut editor = InputEditor::new();
1758
1759 editor.insert_str("ola");
1761
1762 let large_text = "x".repeat(150);
1764 editor.insert_paste(&large_text);
1765
1766 editor.insert_char(' ');
1768 editor.insert_char('m');
1769 editor.insert_char('u');
1770 editor.insert_char('n');
1771 editor.insert_char('d');
1772 editor.insert_char('o');
1773
1774 let display = editor.display_text_combined();
1776 assert!(
1777 display.starts_with("ola [Pasted text +"),
1778 "Display should start with 'ola [Pasted text +', got: '{}'",
1779 display
1780 );
1781 assert!(
1782 display.ends_with(" mundo"),
1783 "Display should end with ' mundo', got: '{}'",
1784 display
1785 );
1786
1787 let full_text = editor.text();
1789 assert!(full_text.starts_with("ola"));
1790 assert!(full_text.ends_with(" mundo"));
1791
1792 let cursor_pos = editor.cursor();
1794 let display_len = display.len();
1795 assert_eq!(
1796 cursor_pos, display_len,
1797 "Cursor should be at end of display: cursor={}, display_len={}",
1798 cursor_pos, display_len
1799 );
1800 }
1801}