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::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        // 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::debug!(
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::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 we have existing typed text, handle paste separately
408        // to keep existing text visible
409        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 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::debug!(
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::debug!(
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::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        // No existing content - if paste is large, use placeholder display
456        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        // Small paste with no existing content - insert normally
464        tracing::debug!("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::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            // 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::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            // 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::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            // 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::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            // 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    /// Get trimmed text and clear
971    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    /// Replace text in a range (used for autocomplete)
980    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    /// Delete from start to cursor
987    #[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    /// Get text before cursor
996    #[inline]
997    pub fn text_before_cursor(&self) -> &str {
998        &self.text[..self.cursor]
999    }
1000
1001    /// Get text after cursor
1002    #[inline]
1003    pub fn text_after_cursor(&self) -> &str {
1004        &self.text[self.cursor..]
1005    }
1006
1007    /// Get character at cursor (if any)
1008    #[inline]
1009    pub fn char_at_cursor(&self) -> Option<char> {
1010        self.text[self.cursor..].chars().next()
1011    }
1012
1013    /// Get character before cursor (if any)
1014    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    /// Navigate to previous (older) history entry
1023    /// Returns true if navigation happened
1024    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    /// Navigate to next (newer) history entry
1039    /// Returns true if navigation happened (false if at newest)
1040    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            // Restoring draft - handled by saved_draft()
1047            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    /// Check if currently navigating history
1060    pub fn is_navigating_history(&self) -> bool {
1061        self.history.is_navigating()
1062    }
1063
1064    /// Add current text to history and clear
1065    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    /// Save history to file
1078    pub fn save_history(&self, path: &PathBuf) -> Result<(), String> {
1079        self.history.save(path)
1080    }
1081
1082    /// Get reference to history (for debugging)
1083    pub fn history(&self) -> &InputHistory {
1084        &self.history
1085    }
1086
1087    /// Get mutable reference to history (for testing)
1088    pub fn history_mut(&mut self) -> &mut InputHistory {
1089        &mut self.history
1090    }
1091
1092    /// Find previous char boundary
1093    #[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    /// Find next char boundary
1106    #[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/// Truncate paste to max size (freestanding function for reuse)
1120#[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        // Create text larger than MAX_PASTE_SIZE (300KB)
1249        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        // Text should be truncated to MAX_PASTE_SIZE
1254        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        // Display should show placeholder
1261        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    // History navigation tests
1372
1373    #[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        // Navigate up to newest
1400        editor.navigate_history_up();
1401        assert_eq!(editor.text(), "newest".to_string());
1402
1403        // Navigate up to oldest
1404        editor.navigate_history_up();
1405        assert_eq!(editor.text(), "oldest".to_string());
1406
1407        // Navigate down to newest
1408        editor.navigate_history_down();
1409        assert_eq!(editor.text(), "newest".to_string());
1410
1411        // Navigate down to draft (empty)
1412        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        // Restore draft
1427        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    // Paste placeholder tests
1464
1465    #[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        // Create multiline text with > 100 chars to trigger placeholder
1487        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        // Type characters - should be stored separately and shown after placeholder
1516        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        // Display should show placeholder + space + typed text
1524        assert_eq!(
1525            editor.display_text_combined(),
1526            "[Pasted text + 150 chars] ! test"
1527        );
1528
1529        // Full text should be paste + typed text
1530        assert_eq!(editor.text(), format!("{}! test", large_text));
1531
1532        // Should still have pasted_content
1533        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        // Should show placeholder
1575        assert!(editor.display_text().starts_with("[Pasted text +"));
1576        // Full text should be "Hello " + paste
1577        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        // Should still show placeholder
1592        assert!(editor.display_text().starts_with("[Pasted text +"));
1593        // Full text should be both pastes combined
1594        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        // Cursor navigation should now work
1608        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        // Move cursor left to be at placeholder end position
1629        editor.move_left();
1630
1631        // Backspace at placeholder position should delete the placeholder
1632        // and DISCARD the pasted content
1633        let deleted = editor.delete_char_before();
1634        assert!(deleted);
1635        // After deleting placeholder, pasted content is removed
1636        assert!(editor.pasted_content.is_none());
1637        // Text should be empty since there was no typed text before/after
1638        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        // Move cursor to start of placeholder
1652        editor.move_to_start();
1653
1654        // Delete key at placeholder should delete the placeholder
1655        // and DISCARD the pasted content
1656        let deleted = editor.delete_char_at();
1657        assert!(deleted);
1658        // After deleting placeholder, pasted content is removed
1659        assert!(editor.pasted_content.is_none());
1660        // Text should be empty since there was no typed text before/after
1661        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        // Type text first
1670        editor.insert_str("hello ");
1671
1672        // Paste large content
1673        let large_text = "x".repeat(150);
1674        editor.insert_paste(&large_text);
1675
1676        // Display: "hello [Pasted text + 150 chars]"
1677        assert!(editor.has_pasted_content());
1678
1679        // Move cursor to end of display (after placeholder)
1680        editor.move_to_end();
1681
1682        // Backspace to delete the placeholder
1683        let deleted = editor.delete_char_before();
1684        assert!(deleted);
1685
1686        // Pasted content should be discarded, but "hello " should remain
1687        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        // Cursor should be at end of display text (placeholder)
1699        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        // Two small pastes that together exceed 100 chars
1708        let paste1 = "x".repeat(60);
1709        let paste2 = "y".repeat(60);
1710
1711        editor.insert_paste(&paste1);
1712        assert_eq!(editor.display_text(), &paste1); // No placeholder yet
1713
1714        editor.insert_paste(&paste2);
1715        // Now combined > 100, should show placeholder
1716        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        // Type some text first
1725        editor.insert_str("Hello ");
1726
1727        // Paste large content
1728        let large_text = "x".repeat(150);
1729        editor.insert_paste(&large_text);
1730
1731        // Type more text after paste
1732        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        // The combined display text is: "[Pasted text + N chars] ! world"
1741        let combined = editor.display_text_combined();
1742        let combined_len = combined.len();
1743        let cursor_pos = editor.cursor();
1744
1745        // BUG: cursor() should be at the end of combined text for proper rendering
1746        // Currently cursor() returns pasted.display_text.len() which is shorter
1747        // than the combined text when there's additional typed content
1748        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        // Type text before paste
1760        editor.insert_str("ola");
1761
1762        // Paste large content
1763        let large_text = "x".repeat(150);
1764        editor.insert_paste(&large_text);
1765
1766        // Type text after paste
1767        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        // Display should show: "ola [Pasted text + 150 chars] mundo"
1775        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        // Full text should be: "ola" + paste + " mundo"
1788        let full_text = editor.text();
1789        assert!(full_text.starts_with("ola"));
1790        assert!(full_text.ends_with(" mundo"));
1791
1792        // Cursor should be at end of display
1793        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}