Skip to main content

limit_cli/tui/input/
editor.rs

1//! Input text editor for TUI
2//!
3//! Manages input text buffer with cursor position, editing operations, and history navigation.
4
5use crate::tui::input::history::InputHistory;
6use crate::tui::MAX_PASTE_SIZE;
7use std::path::PathBuf;
8
9/// Maximum length for displaying pasted text before using placeholder
10const MAX_DISPLAY_LENGTH: usize = 100;
11
12/// Content from a large paste operation
13struct PastedContent {
14    /// Full text content for submission
15    full_text: String,
16    /// Display placeholder text for UI
17    display_text: String,
18    /// Length of text that existed before the paste (for display positioning)
19    text_before_len: usize,
20}
21
22impl PastedContent {
23    /// Create a new PastedContent from pasted text
24    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    /// Create PastedContent with existing text before the paste
34    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
45/// Format pasted text as a display placeholder
46fn 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/// Compute display text zones for cursor navigation
57/// Returns (text_before_end, placeholder_start, placeholder_end, total_len)
58#[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    // Display structure: "{before} {placeholder} {after}"
67    // But if before is empty: "{placeholder} {after}"
68    // If after is empty: "{before} {placeholder}"
69    // If both empty: "{placeholder}"
70
71    let (text_before_end, placeholder_start, placeholder_end) = if text_before_len > 0 {
72        (
73            text_before_len,                       // end of text_before
74            text_before_len + 1,                   // start of placeholder (after space)
75            text_before_len + 1 + placeholder_len, // end of placeholder
76        )
77    } else {
78        (0, 0, placeholder_len) // no text before, placeholder starts at 0
79    };
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
99/// Input text editor with cursor management and history
100pub struct InputEditor {
101    /// Text buffer (for normal typing or appended content after paste)
102    text: String,
103    /// Cursor position (byte offset)
104    cursor: usize,
105    /// Input history
106    history: InputHistory,
107    /// Large pasted content (displayed as placeholder)
108    pasted_content: Option<PastedContent>,
109    /// Cursor position in display text when pasted content exists
110    /// This allows navigation through the combined display
111    display_cursor: usize,
112}
113
114impl InputEditor {
115    /// Create a new empty editor
116    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    /// Create editor with history loaded from file
127    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    /// Get the current text (for submission)
139    /// Returns full pasted content + any typed text after it
140    #[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                // No text after paste - just return full_text
147                pasted.full_text.clone()
148            } else {
149                // Text after paste - append it to full_text
150                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    /// Get text for display in UI
159    /// Returns placeholder for pasted content (use display_text_combined() for full display)
160    #[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    /// Get display text with appended typed text
170    /// Shows: "{text before} {placeholder} {text after}" depending on context
171    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                // Split text into before and after paste
177                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                // Build display: "{before} {placeholder} {after}"
185                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    /// Get a mutable reference to the text buffer
205    /// Note: This only affects the typed text, not pasted content
206    #[inline]
207    pub fn text_mut(&mut self) -> &mut String {
208        &mut self.text
209    }
210
211    /// Get cursor position
212    /// For pasted content, returns display_cursor position
213    #[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    /// Calculate display text zones for cursor navigation
223    /// Returns (text_before_end, placeholder_start, placeholder_end, total_len)
224    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    /// Set cursor position (clamped to valid range)
237    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            // Ensure cursor is at char boundary
244            while self.cursor > 0 && !self.text.is_char_boundary(self.cursor) {
245                self.cursor -= 1;
246            }
247        }
248    }
249
250    /// Check if text is empty
251    #[inline]
252    pub fn is_empty(&self) -> bool {
253        self.pasted_content.is_none() && self.text.is_empty()
254    }
255
256    /// Check if we have large pasted content (displayed as placeholder)
257    #[inline]
258    pub fn has_pasted_content(&self) -> bool {
259        self.pasted_content.is_some()
260    }
261
262    /// Check if cursor is at start
263    #[inline]
264    pub fn is_cursor_at_start(&self) -> bool {
265        self.cursor == 0
266    }
267
268    /// Check if cursor is at end
269    #[inline]
270    pub fn is_cursor_at_end(&self) -> bool {
271        self.cursor == self.text.len()
272    }
273
274    /// Insert a character at cursor position
275    /// Note: If there's pasted content, position depends on cursor zone
276    #[inline]
277    pub fn insert_char(&mut self, c: char) {
278        // Reset history navigation when user types
279        if self.history.is_navigating() {
280            self.history.reset_navigation();
281        }
282
283        // If we have pasted content, handle based on cursor position
284        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 cursor is in text_before zone, insert there
292            if self.display_cursor < text_before_end {
293                let offset_in_before = self.display_cursor;
294                self.text.insert(offset_in_before, c);
295                // Need to update pasted content too
296                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            // Otherwise, append to text_after
305            self.text.push(c);
306            // Update display_cursor to end of combined text
307            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    /// Insert a string at cursor position
323    /// Note: If there's pasted content, position depends on cursor zone
324    #[inline]
325    pub fn insert_str(&mut self, s: &str) {
326        // Reset history navigation when user types
327        if self.history.is_navigating() {
328            self.history.reset_navigation();
329        }
330
331        // If we have pasted content, handle based on cursor position
332        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 cursor is in text_before zone, insert there
340            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                // Need to update pasted content too
344                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            // Otherwise, append to text_after
353            self.text.push_str(s);
354            // Update display_cursor to end of combined text
355            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    /// Insert paste with size limit
371    /// Returns true if truncated
372    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        // Normalize newlines with pre-allocated capacity
383        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 we already have pasted content, append to it
400        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 we have existing typed text, handle paste separately
408        // to keep existing text visible
409        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 paste is large, create placeholder for paste only
418            // Keep existing text in self.text for visibility
419            if normalized.len() > MAX_DISPLAY_LENGTH {
420                tracing::trace!(
421                    "insert_paste: paste is large, creating placeholder for paste only"
422                );
423                // Use with_prefix to track the text that came before the paste
424                let original_text = self.text.clone();
425                self.pasted_content = Some(PastedContent::with_prefix(&original_text, &normalized));
426                // Keep original text in self.text so it's displayed before the placeholder
427                // self.text already contains the original text, don't change it
428                // Set display_cursor to end of combined text
429                self.display_cursor = self.display_zones().3;
430                return truncated;
431            }
432
433            // Small paste - append normally
434            self.text.push_str(&normalized);
435            tracing::trace!(
436                "insert_paste: small paste appended, total len={}",
437                self.text.len()
438            );
439
440            // If combined text is now large, convert to placeholder
441            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        // No existing content - if paste is large, use placeholder display
456        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        // Small paste with no existing content - insert normally
464        tracing::trace!("insert_paste: small paste, inserting normally");
465        self.text = normalized;
466        self.cursor = self.text.len();
467        truncated
468    }
469
470    /// Delete character before cursor (backspace)
471    pub fn delete_char_before(&mut self) -> bool {
472        // If we have pasted content, handle based on cursor position in display
473        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; // +1 for space
479
480            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            // Case 1: Cursor is in text_after zone - delete char from text_after
491            if self.display_cursor > text_after_start && text_after_len > 0 {
492                // Find the char before cursor in text_after
493                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                    // Delete the char at offset_in_after - 1
498                    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                        // Recalculate zones after deletion
510                        let new_text_after_len = self.text.len().saturating_sub(text_before_len);
511                        if new_text_after_len == 0 {
512                            // No more text_after, cursor goes to placeholder_end
513                            self.display_cursor = placeholder_end;
514                        } else {
515                            // Update cursor position
516                            self.display_cursor = text_after_start + delete_start;
517                        }
518                        return true;
519                    }
520                }
521            }
522
523            // Case 2: Cursor is at start of text_after (right after space) - jump to placeholder end
524            if self.display_cursor == text_after_start && text_after_len > 0 {
525                self.display_cursor = placeholder_end;
526                return true;
527            }
528
529            // Case 3: Cursor is right after placeholder (at placeholder_end) - delete the placeholder
530            // This DISCARDS the pasted content, keeping only typed text
531            if self.display_cursor == placeholder_end {
532                // Keep only text_before and text_after (typed text), discard pasted content
533                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                // Clear pasted content (discard it)
545                self.pasted_content = None;
546
547                // Set text to: text_before + text_after (no pasted_text)
548                self.text = format!("{}{}", text_before, text_after);
549
550                // Set cursor to end of text_before
551                self.cursor = text_before.len();
552                self.display_cursor = 0;
553                return true;
554            }
555
556            // Case 4: Cursor is inside placeholder - jump to start
557            if self.display_cursor > placeholder_start && self.display_cursor < placeholder_end {
558                self.display_cursor = placeholder_start;
559                return true;
560            }
561
562            // Case 5: Cursor is at placeholder start (right after space or at beginning)
563            if self.display_cursor == placeholder_start {
564                if text_before_len > 0 {
565                    // Jump to end of text_before
566                    self.display_cursor = text_before_end;
567                    return true;
568                } else {
569                    // No text before - discard the placeholder and all pasted content
570                    self.pasted_content = None;
571                    self.text.clear();
572                    self.cursor = 0;
573                    self.display_cursor = 0;
574                    return true;
575                }
576            }
577
578            // Case 6: Cursor is in the space between text_before and placeholder
579            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            // Case 7: Cursor is in text_before zone - delete char
585            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                        // Update pasted content
602                        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            // Case 8: Cursor is at start (0) - nothing to delete
623            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    /// Delete character at cursor (delete key)
641    pub fn delete_char_at(&mut self) -> bool {
642        // If we have pasted content, handle based on cursor position in display
643        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            // Case 1: Cursor is in text_after zone - delete char at cursor
661            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                    // Keep cursor position (or adjust if needed)
668                    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            // Case 2: Cursor is at placeholder_end (before space to text_after) - delete placeholder
677            // This DISCARDS the pasted content, keeping only typed text
678            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                // Discard pasted content
691                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            // Case 3: Cursor is inside placeholder - delete placeholder
699            // This DISCARDS the pasted content, keeping only typed text
700            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                // Discard pasted content
713                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            // Case 4: Cursor is at placeholder_start - delete placeholder
721            // This DISCARDS the pasted content, keeping only typed text
722            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                // Discard pasted content
735                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            // Case 5: Cursor is in text_before zone - delete char after cursor
743            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    /// Move cursor left one character
775    /// For pasted content, navigates through display text zones
776    #[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            // Determine which zone we're in and handle movement
797            if self.display_cursor > placeholder_end {
798                // In text_after zone - move left one char
799                let text_after_start = placeholder_end + 1; // +1 for space
800                if self.display_cursor > text_after_start {
801                    // Find char boundary in text_after
802                    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                        // Find previous char boundary
807                        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                // Inside placeholder - jump to start of placeholder
827                self.display_cursor = placeholder_start;
828            } else if self.display_cursor > text_before_end {
829                // In the space between text_before and placeholder
830                self.display_cursor = text_before_end;
831            } else if self.display_cursor > 0 {
832                // In text_before zone - move left one char
833                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    /// Move cursor right one character
859    /// For pasted content, navigates through display text zones
860    #[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            // Determine which zone we're in and handle movement
881            if self.display_cursor >= placeholder_end {
882                // At or after placeholder end - move in text_after zone
883                let text_after_start = placeholder_end + 1; // +1 for space
884                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                    // Find next char boundary
891                    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                    // At placeholder end with text_after - jump to start of text_after
905                    self.display_cursor = text_after_start;
906                }
907            } else if self.display_cursor >= placeholder_start {
908                // Inside or at start of placeholder - jump to end
909                self.display_cursor = placeholder_end;
910            } else if self.display_cursor >= text_before_end {
911                // In the space between text_before and placeholder - jump to placeholder end
912                self.display_cursor = placeholder_end;
913            } else {
914                // In text_before zone - move right one char
915                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                    // At end of text_before - jump to placeholder end (skip space and placeholder)
930                    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    /// Move cursor to start
942    #[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    /// Move cursor to end
952    #[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    /// Clear all text
962    #[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    /// Set text content (replaces all existing text)
971    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    /// Get trimmed text and clear
979    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    /// Replace text in a range (used for autocomplete)
988    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    /// Delete from start to cursor
995    #[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    /// Get text before cursor
1004    #[inline]
1005    pub fn text_before_cursor(&self) -> &str {
1006        &self.text[..self.cursor]
1007    }
1008
1009    /// Get text after cursor
1010    #[inline]
1011    pub fn text_after_cursor(&self) -> &str {
1012        &self.text[self.cursor..]
1013    }
1014
1015    /// Get character at cursor (if any)
1016    #[inline]
1017    pub fn char_at_cursor(&self) -> Option<char> {
1018        self.text[self.cursor..].chars().next()
1019    }
1020
1021    /// Get character before cursor (if any)
1022    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    /// Navigate to previous (older) history entry
1031    /// Returns true if navigation happened
1032    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    /// Navigate to next (newer) history entry
1047    /// Returns true if navigation happened (false if at newest)
1048    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            // Restoring draft - handled by saved_draft()
1055            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    /// Check if currently navigating history
1068    pub fn is_navigating_history(&self) -> bool {
1069        self.history.is_navigating()
1070    }
1071
1072    /// Add current text to history and clear
1073    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    /// Save history to file
1086    pub fn save_history(&self, path: &PathBuf) -> Result<(), String> {
1087        self.history.save(path)
1088    }
1089
1090    /// Get reference to history (for debugging)
1091    pub fn history(&self) -> &InputHistory {
1092        &self.history
1093    }
1094
1095    /// Get mutable reference to history (for testing)
1096    pub fn history_mut(&mut self) -> &mut InputHistory {
1097        &mut self.history
1098    }
1099
1100    /// Find previous char boundary
1101    #[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    /// Find next char boundary
1114    #[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/// Truncate paste to max size (freestanding function for reuse)
1128#[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        // Create text larger than MAX_PASTE_SIZE (300KB)
1257        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        // Text should be truncated to MAX_PASTE_SIZE
1262        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        // Display should show placeholder
1269        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    // History navigation tests
1380
1381    #[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        // Navigate up to newest
1408        editor.navigate_history_up();
1409        assert_eq!(editor.text(), "newest".to_string());
1410
1411        // Navigate up to oldest
1412        editor.navigate_history_up();
1413        assert_eq!(editor.text(), "oldest".to_string());
1414
1415        // Navigate down to newest
1416        editor.navigate_history_down();
1417        assert_eq!(editor.text(), "newest".to_string());
1418
1419        // Navigate down to draft (empty)
1420        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        // Restore draft
1435        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    // Paste placeholder tests
1472
1473    #[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        // Create multiline text with > 100 chars to trigger placeholder
1495        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        // Type characters - should be stored separately and shown after placeholder
1524        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        // Display should show placeholder + space + typed text
1532        assert_eq!(
1533            editor.display_text_combined(),
1534            "[Pasted text + 150 chars] ! test"
1535        );
1536
1537        // Full text should be paste + typed text
1538        assert_eq!(editor.text(), format!("{}! test", large_text));
1539
1540        // Should still have pasted_content
1541        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        // Should show placeholder
1583        assert!(editor.display_text().starts_with("[Pasted text +"));
1584        // Full text should be "Hello " + paste
1585        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        // Should still show placeholder
1600        assert!(editor.display_text().starts_with("[Pasted text +"));
1601        // Full text should be both pastes combined
1602        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        // Cursor navigation should now work
1616        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        // Move cursor left to be at placeholder end position
1637        editor.move_left();
1638
1639        // Backspace at placeholder position should delete the placeholder
1640        // and DISCARD the pasted content
1641        let deleted = editor.delete_char_before();
1642        assert!(deleted);
1643        // After deleting placeholder, pasted content is removed
1644        assert!(editor.pasted_content.is_none());
1645        // Text should be empty since there was no typed text before/after
1646        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        // Move cursor to start of placeholder
1660        editor.move_to_start();
1661
1662        // Delete key at placeholder should delete the placeholder
1663        // and DISCARD the pasted content
1664        let deleted = editor.delete_char_at();
1665        assert!(deleted);
1666        // After deleting placeholder, pasted content is removed
1667        assert!(editor.pasted_content.is_none());
1668        // Text should be empty since there was no typed text before/after
1669        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        // Type text first
1678        editor.insert_str("hello ");
1679
1680        // Paste large content
1681        let large_text = "x".repeat(150);
1682        editor.insert_paste(&large_text);
1683
1684        // Display: "hello [Pasted text + 150 chars]"
1685        assert!(editor.has_pasted_content());
1686
1687        // Move cursor to end of display (after placeholder)
1688        editor.move_to_end();
1689
1690        // Backspace to delete the placeholder
1691        let deleted = editor.delete_char_before();
1692        assert!(deleted);
1693
1694        // Pasted content should be discarded, but "hello " should remain
1695        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        // Cursor should be at end of display text (placeholder)
1707        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        // Two small pastes that together exceed 100 chars
1716        let paste1 = "x".repeat(60);
1717        let paste2 = "y".repeat(60);
1718
1719        editor.insert_paste(&paste1);
1720        assert_eq!(editor.display_text(), &paste1); // No placeholder yet
1721
1722        editor.insert_paste(&paste2);
1723        // Now combined > 100, should show placeholder
1724        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        // Type some text first
1733        editor.insert_str("Hello ");
1734
1735        // Paste large content
1736        let large_text = "x".repeat(150);
1737        editor.insert_paste(&large_text);
1738
1739        // Type more text after paste
1740        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        // The combined display text is: "[Pasted text + N chars] ! world"
1749        let combined = editor.display_text_combined();
1750        let combined_len = combined.len();
1751        let cursor_pos = editor.cursor();
1752
1753        // BUG: cursor() should be at the end of combined text for proper rendering
1754        // Currently cursor() returns pasted.display_text.len() which is shorter
1755        // than the combined text when there's additional typed content
1756        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        // Type text before paste
1768        editor.insert_str("ola");
1769
1770        // Paste large content
1771        let large_text = "x".repeat(150);
1772        editor.insert_paste(&large_text);
1773
1774        // Type text after paste
1775        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        // Display should show: "ola [Pasted text + 150 chars] mundo"
1783        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        // Full text should be: "ola" + paste + " mundo"
1796        let full_text = editor.text();
1797        assert!(full_text.starts_with("ola"));
1798        assert!(full_text.ends_with(" mundo"));
1799
1800        // Cursor should be at end of display
1801        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}