Skip to main content

claude_code_rust/app/
input.rs

1// Claude Code Rust - A native Rust terminal interface for Claude Code
2// Copyright (C) 2025  Simon Peter Rothgang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as
6// published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use tui_textarea::{CursorMove, TextArea};
18
19#[derive(Debug)]
20pub struct InputState {
21    pub lines: Vec<String>,
22    pub cursor_row: usize,
23    pub cursor_col: usize,
24    /// Monotonically increasing version counter. Bumped on every content or cursor change
25    /// so that downstream caches (e.g. wrap result) can detect staleness cheaply.
26    pub version: u64,
27    /// Stored paste blocks: each entry holds the full text of a large paste (>10 lines).
28    /// A placeholder line `[Pasted Text N]` is inserted into `lines` at the paste point.
29    /// On `text()`, placeholders are expanded back to the original pasted content.
30    pub paste_blocks: Vec<String>,
31    editor: TextArea<'static>,
32}
33
34/// Prefix/suffix used to identify paste placeholder lines in the input buffer.
35const PASTE_PREFIX: &str = "[Pasted Text ";
36const PASTE_SUFFIX: &str = "]";
37/// Line threshold above which pasted content is collapsed to a placeholder.
38pub const PASTE_PLACEHOLDER_LINE_THRESHOLD: usize = 10;
39
40impl InputState {
41    pub fn new() -> Self {
42        Self {
43            lines: vec![String::new()],
44            cursor_row: 0,
45            cursor_col: 0,
46            version: 0,
47            paste_blocks: Vec::new(),
48            editor: TextArea::default(),
49        }
50    }
51
52    #[must_use]
53    pub fn text(&self) -> String {
54        if self.paste_blocks.is_empty() {
55            return self.lines.join("\n");
56        }
57        // Expand paste placeholders back to their full content
58        let mut result = String::new();
59        for (i, line) in self.lines.iter().enumerate() {
60            if i > 0 {
61                result.push('\n');
62            }
63            if let Some((idx, suffix_end)) = parse_paste_placeholder_with_suffix(line) {
64                if let Some(content) = self.paste_blocks.get(idx) {
65                    result.push_str(content);
66                    if suffix_end < line.len() {
67                        result.push_str(&line[suffix_end..]);
68                    }
69                } else {
70                    result.push_str(line);
71                }
72            } else {
73                result.push_str(line);
74            }
75        }
76        result
77    }
78
79    #[must_use]
80    pub fn is_empty(&self) -> bool {
81        self.lines.len() == 1 && self.lines[0].is_empty()
82    }
83
84    pub fn clear(&mut self) {
85        self.lines = vec![String::new()];
86        self.cursor_row = 0;
87        self.cursor_col = 0;
88        self.paste_blocks.clear();
89        self.version += 1;
90        self.rebuild_editor_from_snapshot();
91    }
92
93    /// Replace the input with the given text, placing the cursor at the end.
94    pub fn set_text(&mut self, text: &str) {
95        self.lines = text.split('\n').map(String::from).collect();
96        if self.lines.is_empty() {
97            self.lines.push(String::new());
98        }
99        self.cursor_row = self.lines.len() - 1;
100        self.cursor_col = self.lines[self.cursor_row].chars().count();
101        self.paste_blocks.clear();
102        self.version += 1;
103        self.rebuild_editor_from_snapshot();
104    }
105
106    pub fn insert_char(&mut self, c: char) {
107        let line = &mut self.lines[self.cursor_row];
108        let byte_idx = char_to_byte_index(line, self.cursor_col);
109        line.insert(byte_idx, c);
110        self.cursor_col += 1;
111        self.version += 1;
112    }
113
114    fn as_textarea(&self) -> TextArea<'static> {
115        let mut textarea = TextArea::from(self.lines.clone());
116        textarea.move_cursor(CursorMove::Jump(
117            u16::try_from(self.cursor_row).unwrap_or(u16::MAX),
118            u16::try_from(self.cursor_col).unwrap_or(u16::MAX),
119        ));
120        textarea
121    }
122
123    fn sync_snapshot_from_editor(&mut self) -> bool {
124        let (row, col) = self.editor.cursor();
125        let lines = self.editor.lines().to_vec();
126        if self.lines == lines && self.cursor_row == row && self.cursor_col == col {
127            return false;
128        }
129        self.lines = lines;
130        self.cursor_row = row;
131        self.cursor_col = col;
132        self.version += 1;
133        true
134    }
135
136    fn rebuild_editor_from_snapshot(&mut self) {
137        self.editor = self.as_textarea();
138    }
139
140    pub fn sync_textarea_engine(&mut self) {
141        self.rebuild_editor_from_snapshot();
142    }
143
144    fn ensure_editor_synced_from_snapshot(&mut self) {
145        if self.editor.cursor() != (self.cursor_row, self.cursor_col)
146            || self.editor.lines() != self.lines.as_slice()
147        {
148            self.rebuild_editor_from_snapshot();
149        }
150    }
151
152    fn apply_textarea_edit(&mut self, edit: impl FnOnce(&mut TextArea<'_>)) -> bool {
153        self.ensure_editor_synced_from_snapshot();
154        edit(&mut self.editor);
155        self.sync_snapshot_from_editor()
156    }
157
158    pub fn textarea_insert_char(&mut self, c: char) -> bool {
159        self.apply_textarea_edit(|textarea| {
160            textarea.insert_char(c);
161        })
162    }
163
164    pub fn textarea_insert_newline(&mut self) -> bool {
165        self.apply_textarea_edit(|textarea| {
166            textarea.insert_newline();
167        })
168    }
169
170    pub fn textarea_delete_char_before(&mut self) -> bool {
171        self.apply_textarea_edit(|textarea| {
172            let _ = textarea.delete_char();
173        })
174    }
175
176    pub fn textarea_delete_char_after(&mut self) -> bool {
177        self.apply_textarea_edit(|textarea| {
178            let _ = textarea.delete_next_char();
179        })
180    }
181
182    pub fn textarea_move_left(&mut self) -> bool {
183        self.apply_textarea_edit(|textarea| {
184            textarea.move_cursor(CursorMove::Back);
185        })
186    }
187
188    pub fn textarea_move_right(&mut self) -> bool {
189        self.apply_textarea_edit(|textarea| {
190            textarea.move_cursor(CursorMove::Forward);
191        })
192    }
193
194    pub fn textarea_move_up(&mut self) -> bool {
195        self.apply_textarea_edit(|textarea| {
196            textarea.move_cursor(CursorMove::Up);
197        })
198    }
199
200    pub fn textarea_move_down(&mut self) -> bool {
201        self.apply_textarea_edit(|textarea| {
202            textarea.move_cursor(CursorMove::Down);
203        })
204    }
205
206    pub fn textarea_move_home(&mut self) -> bool {
207        self.apply_textarea_edit(|textarea| {
208            textarea.move_cursor(CursorMove::Head);
209        })
210    }
211
212    pub fn textarea_move_end(&mut self) -> bool {
213        self.apply_textarea_edit(|textarea| {
214            textarea.move_cursor(CursorMove::End);
215        })
216    }
217
218    pub fn textarea_undo(&mut self) -> bool {
219        self.apply_textarea_edit(|textarea| {
220            let _ = textarea.undo();
221        })
222    }
223
224    pub fn textarea_redo(&mut self) -> bool {
225        self.apply_textarea_edit(|textarea| {
226            let _ = textarea.redo();
227        })
228    }
229
230    pub fn textarea_move_word_left(&mut self) -> bool {
231        self.apply_textarea_edit(|textarea| {
232            textarea.move_cursor(CursorMove::WordBack);
233        })
234    }
235
236    pub fn textarea_move_word_right(&mut self) -> bool {
237        self.apply_textarea_edit(|textarea| {
238            textarea.move_cursor(CursorMove::WordForward);
239        })
240    }
241
242    pub fn textarea_delete_word_before(&mut self) -> bool {
243        self.apply_textarea_edit(|textarea| {
244            let _ = textarea.delete_word();
245        })
246    }
247
248    pub fn textarea_delete_word_after(&mut self) -> bool {
249        self.apply_textarea_edit(|textarea| {
250            let _ = textarea.delete_next_word();
251        })
252    }
253
254    pub fn insert_newline(&mut self) {
255        let line = &mut self.lines[self.cursor_row];
256        let byte_idx = char_to_byte_index(line, self.cursor_col);
257        let rest = line[byte_idx..].to_string();
258        line.truncate(byte_idx);
259        self.cursor_row += 1;
260        self.lines.insert(self.cursor_row, rest);
261        self.cursor_col = 0;
262        self.version += 1;
263    }
264
265    pub fn insert_str(&mut self, s: &str) {
266        for c in s.chars() {
267            if c == '\n' || c == '\r' {
268                self.insert_newline();
269            } else {
270                self.insert_char(c);
271            }
272        }
273    }
274
275    /// Insert a large paste as a compact placeholder line.
276    /// The full text is stored in `paste_blocks` and expanded by `text()` on submit.
277    /// Returns the placeholder label for display purposes.
278    pub fn insert_paste_block(&mut self, text: &str) -> String {
279        let idx = self.paste_blocks.len();
280        let placeholder = paste_placeholder_label(idx, count_text_lines(text));
281        self.paste_blocks.push(text.to_owned());
282
283        // Insert the placeholder at the current cursor position.
284        // If cursor is mid-line, split the current line around the placeholder.
285        let current_line = &mut self.lines[self.cursor_row];
286        let byte_idx = char_to_byte_index(current_line, self.cursor_col);
287        let after = current_line[byte_idx..].to_string();
288        current_line.truncate(byte_idx);
289        let before_empty = current_line.is_empty();
290
291        if before_empty {
292            // Replace current empty/start-of-line with placeholder
293            current_line.clone_from(&placeholder);
294            if !after.is_empty() {
295                self.lines.insert(self.cursor_row + 1, after);
296            }
297            self.cursor_col = placeholder.chars().count();
298        } else {
299            // Insert placeholder on a new line after the current content
300            self.cursor_row += 1;
301            self.lines.insert(self.cursor_row, placeholder.clone());
302            if !after.is_empty() {
303                self.lines.insert(self.cursor_row + 1, after);
304            }
305            self.cursor_col = placeholder.chars().count();
306        }
307
308        self.version += 1;
309        placeholder
310    }
311
312    /// Append text to the paste block under the cursor if the cursor currently
313    /// sits at the end of a standalone placeholder line.
314    ///
315    /// This handles terminals that deliver one clipboard paste as multiple
316    /// `Event::Paste` chunks.
317    pub fn append_to_active_paste_block(&mut self, text: &str) -> bool {
318        let Some(current_line) = self.lines.get(self.cursor_row) else {
319            return false;
320        };
321        let Some(idx) = parse_paste_placeholder(current_line) else {
322            return false;
323        };
324        // Only merge chunks when cursor is at the end of that placeholder line.
325        if self.cursor_col != current_line.chars().count() {
326            return false;
327        }
328
329        let Some(block) = self.paste_blocks.get_mut(idx) else {
330            return false;
331        };
332        block.push_str(text);
333
334        self.lines[self.cursor_row] = paste_placeholder_label(idx, count_text_lines(block));
335        self.cursor_col = self.lines[self.cursor_row].chars().count();
336        self.version += 1;
337        true
338    }
339
340    pub fn delete_char_before(&mut self) {
341        if self.cursor_col > 0 {
342            let line = &mut self.lines[self.cursor_row];
343            self.cursor_col -= 1;
344            let byte_idx = char_to_byte_index(line, self.cursor_col);
345            line.remove(byte_idx);
346            self.version += 1;
347        } else if self.cursor_row > 0 {
348            let removed = self.lines.remove(self.cursor_row);
349            self.cursor_row -= 1;
350            self.cursor_col = self.lines[self.cursor_row].chars().count();
351            self.lines[self.cursor_row].push_str(&removed);
352            self.version += 1;
353        }
354    }
355
356    pub fn delete_char_after(&mut self) {
357        let line_len = self.lines[self.cursor_row].chars().count();
358        if self.cursor_col < line_len {
359            let line = &mut self.lines[self.cursor_row];
360            let byte_idx = char_to_byte_index(line, self.cursor_col);
361            line.remove(byte_idx);
362            self.version += 1;
363        } else if self.cursor_row + 1 < self.lines.len() {
364            let next = self.lines.remove(self.cursor_row + 1);
365            self.lines[self.cursor_row].push_str(&next);
366            self.version += 1;
367        }
368    }
369
370    pub fn move_left(&mut self) {
371        if self.cursor_col > 0 {
372            self.cursor_col -= 1;
373            self.version += 1;
374        } else if self.cursor_row > 0 {
375            self.cursor_row -= 1;
376            self.cursor_col = self.lines[self.cursor_row].chars().count();
377            self.version += 1;
378        }
379    }
380
381    pub fn move_right(&mut self) {
382        let line_len = self.lines[self.cursor_row].chars().count();
383        if self.cursor_col < line_len {
384            self.cursor_col += 1;
385            self.version += 1;
386        } else if self.cursor_row + 1 < self.lines.len() {
387            self.cursor_row += 1;
388            self.cursor_col = 0;
389            self.version += 1;
390        }
391    }
392
393    pub fn move_up(&mut self) {
394        if self.cursor_row > 0 {
395            self.cursor_row -= 1;
396            let line_len = self.lines[self.cursor_row].chars().count();
397            self.cursor_col = self.cursor_col.min(line_len);
398            self.version += 1;
399        }
400    }
401
402    pub fn move_down(&mut self) {
403        if self.cursor_row + 1 < self.lines.len() {
404            self.cursor_row += 1;
405            let line_len = self.lines[self.cursor_row].chars().count();
406            self.cursor_col = self.cursor_col.min(line_len);
407            self.version += 1;
408        }
409    }
410
411    pub fn move_home(&mut self) {
412        self.cursor_col = 0;
413        self.version += 1;
414    }
415
416    pub fn move_end(&mut self) {
417        self.cursor_col = self.lines[self.cursor_row].chars().count();
418        self.version += 1;
419    }
420
421    #[must_use]
422    pub fn line_count(&self) -> u16 {
423        u16::try_from(self.lines.len()).unwrap_or(u16::MAX)
424    }
425}
426
427impl Default for InputState {
428    fn default() -> Self {
429        Self::new()
430    }
431}
432
433/// Convert a character index to a byte index within a string.
434fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
435    s.char_indices().nth(char_idx).map_or(s.len(), |(i, _)| i)
436}
437
438/// Count logical lines for text containing mixed `\n`, `\r`, and `\r\n` endings.
439#[must_use]
440pub fn count_text_lines(text: &str) -> usize {
441    // Count universal newlines (\n, \r, and \r\n as a single break).
442    let mut lines = 1;
443    let bytes = text.as_bytes();
444    let mut i = 0;
445    while i < bytes.len() {
446        match bytes[i] {
447            b'\n' => lines += 1,
448            b'\r' => {
449                lines += 1;
450                if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
451                    i += 1;
452                }
453            }
454            _ => {}
455        }
456        i += 1;
457    }
458    lines
459}
460
461/// Trim trailing line-break characters (`\n` and `\r`) from text.
462#[must_use]
463pub fn trim_trailing_line_breaks(mut text: &str) -> &str {
464    while let Some(stripped) = text.strip_suffix('\n').or_else(|| text.strip_suffix('\r')) {
465        text = stripped;
466    }
467    text
468}
469
470fn paste_placeholder_label(idx: usize, line_count: usize) -> String {
471    format!("{PASTE_PREFIX}{} - {line_count} lines{PASTE_SUFFIX}", idx + 1)
472}
473
474/// Parse a placeholder at the start of a line.
475///
476/// Returns `(paste_block_index, placeholder_end_byte_index)`.
477pub fn parse_paste_placeholder_with_suffix(line: &str) -> Option<(usize, usize)> {
478    let rest = line.strip_prefix(PASTE_PREFIX)?;
479    let close_rel = rest.find(PASTE_SUFFIX)?;
480    let rest = &rest[..close_rel];
481    let num_str = rest.split(" - ").next()?;
482    let n: usize = num_str.parse().ok()?;
483    if n == 0 {
484        return None;
485    }
486    let end = PASTE_PREFIX.len() + close_rel + PASTE_SUFFIX.len();
487    Some((n - 1, end))
488}
489
490/// Check if a line is a paste placeholder. Returns the 0-based paste block index if so.
491pub fn parse_paste_placeholder(line: &str) -> Option<usize> {
492    let (idx, suffix_end) = parse_paste_placeholder_with_suffix(line)?;
493    if suffix_end == line.len() { Some(idx) } else { None }
494}
495
496#[cfg(test)]
497mod tests {
498    // =====
499    // TESTS: 83
500    // =====
501
502    use super::*;
503    use pretty_assertions::assert_eq;
504
505    // char_to_byte_index
506
507    #[test]
508    fn char_to_byte_index_ascii() {
509        assert_eq!(char_to_byte_index("hello", 0), 0);
510        assert_eq!(char_to_byte_index("hello", 2), 2);
511        assert_eq!(char_to_byte_index("hello", 5), 5); // past end
512    }
513
514    #[test]
515    fn char_to_byte_index_multibyte_utf8() {
516        // 'e' with accent: 2 bytes in UTF-8
517        let s = "cafe\u{0301}"; // "cafe" + combining accent = 5 chars, but accent is 2 bytes
518        assert_eq!(char_to_byte_index(s, 4), 4); // the combining char starts at byte 4
519    }
520
521    #[test]
522    fn char_to_byte_index_emoji() {
523        let s = "\u{1F600}hello"; // grinning face (4 bytes) + "hello"
524        assert_eq!(char_to_byte_index(s, 0), 0);
525        assert_eq!(char_to_byte_index(s, 1), 4); // after emoji
526    }
527
528    #[test]
529    fn char_to_byte_index_beyond_string() {
530        assert_eq!(char_to_byte_index("ab", 10), 2); // returns s.len()
531    }
532
533    #[test]
534    fn char_to_byte_index_empty_string() {
535        assert_eq!(char_to_byte_index("", 0), 0);
536        assert_eq!(char_to_byte_index("", 5), 0);
537    }
538
539    // InputState::new / Default
540
541    #[test]
542    fn new_creates_empty_state() {
543        let input = InputState::new();
544        assert_eq!(input.lines, vec![String::new()]);
545        assert_eq!(input.cursor_row, 0);
546        assert_eq!(input.cursor_col, 0);
547        assert_eq!(input.version, 0);
548    }
549
550    #[test]
551    fn default_equals_new() {
552        let a = InputState::new();
553        let b = InputState::default();
554        assert_eq!(a.lines, b.lines);
555        assert_eq!(a.cursor_row, b.cursor_row);
556        assert_eq!(a.cursor_col, b.cursor_col);
557        assert_eq!(a.version, b.version);
558    }
559
560    // text()
561
562    #[test]
563    fn text_single_empty_line() {
564        let input = InputState::new();
565        assert_eq!(input.text(), "");
566    }
567
568    #[test]
569    fn text_joins_lines_with_newline() {
570        let mut input = InputState::new();
571        input.insert_str("line1\nline2\nline3");
572        assert_eq!(input.text(), "line1\nline2\nline3");
573    }
574
575    // is_empty()
576
577    #[test]
578    fn is_empty_true_for_new() {
579        assert!(InputState::new().is_empty());
580    }
581
582    #[test]
583    fn is_empty_false_after_insert() {
584        let mut input = InputState::new();
585        input.insert_char('a');
586        assert!(!input.is_empty());
587    }
588
589    #[test]
590    fn is_empty_false_for_empty_multiline() {
591        // Two empty lines: not considered "empty" by the method
592        let mut input = InputState::new();
593        input.insert_newline();
594        assert!(!input.is_empty());
595    }
596
597    // clear()
598
599    #[test]
600    fn clear_resets_to_empty() {
601        let mut input = InputState::new();
602        input.insert_str("hello\nworld");
603        let v_before = input.version;
604        input.clear();
605        assert!(input.is_empty());
606        assert_eq!(input.cursor_row, 0);
607        assert_eq!(input.cursor_col, 0);
608        assert!(input.version > v_before);
609    }
610
611    // insert_char
612
613    #[test]
614    fn insert_char_ascii() {
615        let mut input = InputState::new();
616        input.insert_char('h');
617        input.insert_char('i');
618        assert_eq!(input.lines[0], "hi");
619        assert_eq!(input.cursor_col, 2);
620    }
621
622    #[test]
623    fn insert_char_unicode_emoji() {
624        let mut input = InputState::new();
625        input.insert_char('\u{1F600}'); // grinning face
626        assert_eq!(input.cursor_col, 1);
627        assert_eq!(input.lines[0], "\u{1F600}");
628    }
629
630    #[test]
631    fn insert_char_cjk() {
632        let mut input = InputState::new();
633        input.insert_char('\u{4F60}'); // Chinese "ni"
634        input.insert_char('\u{597D}'); // Chinese "hao"
635        assert_eq!(input.lines[0], "\u{4F60}\u{597D}");
636        assert_eq!(input.cursor_col, 2);
637    }
638
639    #[test]
640    fn insert_char_mid_line() {
641        let mut input = InputState::new();
642        input.insert_str("ac");
643        input.move_left(); // cursor at col 1
644        input.insert_char('b');
645        assert_eq!(input.lines[0], "abc");
646        assert_eq!(input.cursor_col, 2);
647    }
648
649    #[test]
650    fn insert_char_bumps_version() {
651        let mut input = InputState::new();
652        let v = input.version;
653        input.insert_char('x');
654        assert!(input.version > v);
655    }
656
657    // insert_newline
658
659    #[test]
660    fn insert_newline_at_end() {
661        let mut input = InputState::new();
662        input.insert_str("hello");
663        input.insert_newline();
664        assert_eq!(input.lines, vec!["hello", ""]);
665        assert_eq!(input.cursor_row, 1);
666        assert_eq!(input.cursor_col, 0);
667    }
668
669    #[test]
670    fn insert_newline_mid_line() {
671        let mut input = InputState::new();
672        input.insert_str("helloworld");
673        // Move cursor to position 5
674        input.cursor_col = 5;
675        input.insert_newline();
676        assert_eq!(input.lines, vec!["hello", "world"]);
677        assert_eq!(input.cursor_row, 1);
678        assert_eq!(input.cursor_col, 0);
679    }
680
681    #[test]
682    fn insert_newline_at_start() {
683        let mut input = InputState::new();
684        input.insert_str("hello");
685        input.move_home();
686        input.insert_newline();
687        assert_eq!(input.lines, vec!["", "hello"]);
688    }
689
690    // insert_str
691
692    #[test]
693    fn insert_str_multiline() {
694        let mut input = InputState::new();
695        input.insert_str("line1\nline2\nline3");
696        assert_eq!(input.lines, vec!["line1", "line2", "line3"]);
697        assert_eq!(input.cursor_row, 2);
698        assert_eq!(input.cursor_col, 5);
699    }
700
701    #[test]
702    fn insert_str_with_carriage_returns() {
703        let mut input = InputState::new();
704        input.insert_str("a\rb\rc");
705        // \r treated same as \n
706        assert_eq!(input.lines, vec!["a", "b", "c"]);
707    }
708
709    #[test]
710    fn insert_str_empty() {
711        let mut input = InputState::new();
712        let v = input.version;
713        input.insert_str("");
714        assert_eq!(input.version, v); // no mutation
715    }
716
717    // delete_char_before (backspace)
718
719    #[test]
720    fn backspace_mid_line() {
721        let mut input = InputState::new();
722        input.insert_str("abc");
723        input.delete_char_before();
724        assert_eq!(input.lines[0], "ab");
725        assert_eq!(input.cursor_col, 2);
726    }
727
728    #[test]
729    fn backspace_start_of_line_joins() {
730        let mut input = InputState::new();
731        input.insert_str("hello\nworld");
732        // cursor at row 1, col 5. Move to start of row 1.
733        input.move_home();
734        input.delete_char_before();
735        assert_eq!(input.lines, vec!["helloworld"]);
736        assert_eq!(input.cursor_row, 0);
737        assert_eq!(input.cursor_col, 5); // at the join point
738    }
739
740    #[test]
741    fn backspace_start_of_buffer_noop() {
742        let mut input = InputState::new();
743        input.insert_str("hi");
744        input.move_home();
745        let v = input.version;
746        input.delete_char_before(); // should do nothing
747        assert_eq!(input.lines[0], "hi");
748        assert_eq!(input.version, v); // no version bump
749    }
750
751    #[test]
752    fn backspace_unicode() {
753        let mut input = InputState::new();
754        input.insert_char('\u{1F600}');
755        input.insert_char('x');
756        input.delete_char_before();
757        assert_eq!(input.lines[0], "\u{1F600}");
758    }
759
760    // delete_char_after (delete key)
761
762    #[test]
763    fn delete_mid_line() {
764        let mut input = InputState::new();
765        input.insert_str("abc");
766        input.move_home();
767        input.delete_char_after();
768        assert_eq!(input.lines[0], "bc");
769        assert_eq!(input.cursor_col, 0);
770    }
771
772    #[test]
773    fn delete_end_of_line_joins_next() {
774        let mut input = InputState::new();
775        input.insert_str("hello\nworld");
776        input.cursor_row = 0;
777        input.cursor_col = 5; // end of "hello"
778        input.delete_char_after();
779        assert_eq!(input.lines, vec!["helloworld"]);
780    }
781
782    #[test]
783    fn delete_end_of_buffer_noop() {
784        let mut input = InputState::new();
785        input.insert_str("hi");
786        // cursor at end of last line
787        let v = input.version;
788        input.delete_char_after();
789        assert_eq!(input.lines[0], "hi");
790        assert_eq!(input.version, v);
791    }
792
793    // Navigation: move_left, move_right
794
795    #[test]
796    fn move_left_within_line() {
797        let mut input = InputState::new();
798        input.insert_str("abc");
799        input.move_left();
800        assert_eq!(input.cursor_col, 2);
801    }
802
803    #[test]
804    fn move_left_wraps_to_previous_line() {
805        let mut input = InputState::new();
806        input.insert_str("ab\ncd");
807        input.move_home(); // at col 0, row 1
808        input.move_left();
809        assert_eq!(input.cursor_row, 0);
810        assert_eq!(input.cursor_col, 2); // end of "ab"
811    }
812
813    #[test]
814    fn move_left_at_origin_noop() {
815        let mut input = InputState::new();
816        input.insert_char('a');
817        input.move_home();
818        let v = input.version;
819        input.move_left();
820        assert_eq!(input.cursor_col, 0);
821        assert_eq!(input.cursor_row, 0);
822        assert_eq!(input.version, v); // no change
823    }
824
825    #[test]
826    fn move_right_within_line() {
827        let mut input = InputState::new();
828        input.insert_str("abc");
829        input.move_home();
830        input.move_right();
831        assert_eq!(input.cursor_col, 1);
832    }
833
834    #[test]
835    fn move_right_wraps_to_next_line() {
836        let mut input = InputState::new();
837        input.insert_str("ab\ncd");
838        input.cursor_row = 0;
839        input.cursor_col = 2; // end of "ab"
840        input.move_right();
841        assert_eq!(input.cursor_row, 1);
842        assert_eq!(input.cursor_col, 0);
843    }
844
845    #[test]
846    fn move_right_at_end_noop() {
847        let mut input = InputState::new();
848        input.insert_str("ab");
849        let v = input.version;
850        input.move_right(); // already at end
851        assert_eq!(input.version, v);
852    }
853
854    // Navigation: move_up, move_down
855
856    #[test]
857    fn move_up_clamps_col() {
858        let mut input = InputState::new();
859        input.insert_str("ab\nhello");
860        // cursor at row 1, col 5 ("hello" end)
861        input.move_up();
862        assert_eq!(input.cursor_row, 0);
863        assert_eq!(input.cursor_col, 2); // clamped to "ab" length
864    }
865
866    #[test]
867    fn move_up_at_top_noop() {
868        let mut input = InputState::new();
869        input.insert_str("hello");
870        let v = input.version;
871        input.move_up();
872        assert_eq!(input.cursor_row, 0);
873        assert_eq!(input.version, v);
874    }
875
876    #[test]
877    fn move_down_clamps_col() {
878        let mut input = InputState::new();
879        input.insert_str("hello\nab");
880        input.cursor_row = 0;
881        input.cursor_col = 5;
882        input.move_down();
883        assert_eq!(input.cursor_row, 1);
884        assert_eq!(input.cursor_col, 2); // clamped to "ab" length
885    }
886
887    #[test]
888    fn move_down_at_bottom_noop() {
889        let mut input = InputState::new();
890        input.insert_str("hello");
891        let v = input.version;
892        input.move_down();
893        assert_eq!(input.version, v);
894    }
895
896    // Navigation: move_home, move_end
897
898    #[test]
899    fn move_home_sets_col_zero() {
900        let mut input = InputState::new();
901        input.insert_str("hello");
902        input.move_home();
903        assert_eq!(input.cursor_col, 0);
904    }
905
906    #[test]
907    fn move_end_sets_col_to_line_len() {
908        let mut input = InputState::new();
909        input.insert_str("hello");
910        input.move_home();
911        input.move_end();
912        assert_eq!(input.cursor_col, 5);
913    }
914
915    #[test]
916    fn move_home_always_bumps_version() {
917        let mut input = InputState::new();
918        input.insert_str("hello");
919        input.move_home(); // col was 5, now 0
920        let v = input.version;
921        input.move_home(); // col already 0, but still bumps
922        assert!(input.version > v);
923    }
924
925    // line_count
926
927    #[test]
928    fn line_count_single() {
929        assert_eq!(InputState::new().line_count(), 1);
930    }
931
932    #[test]
933    fn line_count_multi() {
934        let mut input = InputState::new();
935        input.insert_str("a\nb\nc");
936        assert_eq!(input.line_count(), 3);
937    }
938
939    // version counter
940
941    #[test]
942    fn version_increments_on_every_mutation() {
943        let mut input = InputState::new();
944        let mut v = input.version;
945
946        input.insert_char('a');
947        assert!(input.version > v);
948        v = input.version;
949
950        input.insert_newline();
951        assert!(input.version > v);
952        v = input.version;
953
954        input.delete_char_before();
955        assert!(input.version > v);
956        v = input.version;
957
958        input.move_left();
959        assert!(input.version > v);
960        v = input.version;
961
962        input.clear();
963        assert!(input.version > v);
964    }
965
966    #[test]
967    fn rapid_insert_delete_cycle() {
968        let mut input = InputState::new();
969        for _ in 0..100 {
970            input.insert_char('x');
971        }
972        assert_eq!(input.lines[0].len(), 100);
973        for _ in 0..100 {
974            input.delete_char_before();
975        }
976        assert!(input.is_empty());
977    }
978
979    #[test]
980    fn mixed_unicode_operations() {
981        let mut input = InputState::new();
982        // Insert mixed: ASCII, emoji, CJK
983        input.insert_str("hi\u{1F600}\u{4F60}");
984        assert_eq!(input.cursor_col, 4); // h, i, emoji, CJK
985        input.move_home();
986        input.move_right(); // past 'h'
987        input.move_right(); // past 'i'
988        input.delete_char_after(); // delete emoji
989        assert_eq!(input.lines[0], "hi\u{4F60}");
990    }
991
992    #[test]
993    fn multiline_editing_stress() {
994        let mut input = InputState::new();
995        // Create 10 lines
996        for i in 0..10 {
997            input.insert_str(&format!("line{i}"));
998            if i < 9 {
999                input.insert_newline();
1000            }
1001        }
1002        assert_eq!(input.lines.len(), 10);
1003
1004        // Navigate to middle and delete lines by joining
1005        input.cursor_row = 5;
1006        input.cursor_col = 0;
1007        input.delete_char_before(); // join line 5 with line 4
1008        assert_eq!(input.lines.len(), 9);
1009
1010        // Text should be coherent
1011        let text = input.text();
1012        assert!(text.contains("line4line5"));
1013    }
1014
1015    #[test]
1016    fn insert_str_with_only_newlines() {
1017        let mut input = InputState::new();
1018        input.insert_str("\n\n\n");
1019        assert_eq!(input.lines, vec!["", "", "", ""]);
1020        assert_eq!(input.cursor_row, 3);
1021        assert_eq!(input.cursor_col, 0);
1022    }
1023
1024    #[test]
1025    fn cursor_clamping_on_vertical_nav() {
1026        let mut input = InputState::new();
1027        input.insert_str("long line here\nab\nmedium line");
1028        // cursor at row 2, col 11 (end of "medium line")
1029        input.move_up(); // to row 1 "ab", col clamped to 2
1030        assert_eq!(input.cursor_col, 2);
1031        input.move_up(); // to row 0 "long line here", col stays 2
1032        assert_eq!(input.cursor_col, 2);
1033        input.move_end(); // col = 14
1034        input.move_down(); // to row 1 "ab", col clamped to 2
1035        assert_eq!(input.cursor_col, 2);
1036    }
1037
1038    // weird inputs
1039
1040    #[test]
1041    fn insert_tab_character() {
1042        let mut input = InputState::new();
1043        input.insert_char('\t');
1044        assert_eq!(input.lines[0], "\t");
1045        assert_eq!(input.cursor_col, 1);
1046    }
1047
1048    #[test]
1049    fn insert_null_byte() {
1050        let mut input = InputState::new();
1051        input.insert_char('\0');
1052        assert_eq!(input.lines[0].len(), 1);
1053        assert_eq!(input.cursor_col, 1);
1054    }
1055
1056    #[test]
1057    fn insert_control_chars() {
1058        let mut input = InputState::new();
1059        // Bell, backspace-char (not the key), escape
1060        input.insert_char('\x07');
1061        input.insert_char('\x08');
1062        input.insert_char('\x1B');
1063        assert_eq!(input.cursor_col, 3);
1064        assert_eq!(input.lines[0].chars().count(), 3);
1065    }
1066
1067    #[test]
1068    fn windows_crlf_line_endings() {
1069        // \r\n should produce TWO newlines (each triggers insert_newline)
1070        let mut input = InputState::new();
1071        input.insert_str("a\r\nb");
1072        // \r -> newline, \n -> another newline
1073        assert_eq!(input.lines, vec!["a", "", "b"]);
1074    }
1075
1076    #[test]
1077    fn insert_zero_width_joiner_sequence() {
1078        // Family emoji: man + ZWJ + woman + ZWJ + girl
1079        let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
1080        let mut input = InputState::new();
1081        input.insert_str(family);
1082        // Each code point is a separate char as far as Rust is concerned
1083        assert_eq!(input.cursor_col, family.chars().count());
1084        assert_eq!(input.text(), family);
1085    }
1086
1087    #[test]
1088    fn insert_flag_emoji() {
1089        // Regional indicator symbols for US flag
1090        let flag = "\u{1F1FA}\u{1F1F8}";
1091        let mut input = InputState::new();
1092        input.insert_str(flag);
1093        assert_eq!(input.cursor_col, 2); // two chars
1094        assert_eq!(input.text(), flag);
1095    }
1096
1097    #[test]
1098    fn insert_combining_diacritical_marks() {
1099        // e + combining acute + combining cedilla
1100        let mut input = InputState::new();
1101        input.insert_char('e');
1102        input.insert_char('\u{0301}'); // combining acute
1103        input.insert_char('\u{0327}'); // combining cedilla
1104        assert_eq!(input.cursor_col, 3);
1105        // Delete the last combining mark
1106        input.delete_char_before();
1107        assert_eq!(input.cursor_col, 2);
1108        assert_eq!(input.lines[0], "e\u{0301}");
1109    }
1110
1111    #[test]
1112    fn insert_right_to_left_text() {
1113        // Arabic text
1114        let arabic = "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}";
1115        let mut input = InputState::new();
1116        input.insert_str(arabic);
1117        assert_eq!(input.cursor_col, 5);
1118        assert_eq!(input.text(), arabic);
1119        // Navigate and delete should still work
1120        input.move_home();
1121        input.delete_char_after();
1122        assert_eq!(input.cursor_col, 0);
1123        assert_eq!(input.lines[0].chars().count(), 4);
1124    }
1125
1126    #[test]
1127    fn insert_very_long_single_line() {
1128        let mut input = InputState::new();
1129        let long_str: String = "x".repeat(10_000);
1130        input.insert_str(&long_str);
1131        assert_eq!(input.cursor_col, 10_000);
1132        assert_eq!(input.lines[0].len(), 10_000);
1133        // Navigate to middle
1134        input.move_home();
1135        for _ in 0..5000 {
1136            input.move_right();
1137        }
1138        assert_eq!(input.cursor_col, 5000);
1139        // Insert in the middle
1140        input.insert_char('Y');
1141        assert_eq!(input.lines[0].len(), 10_001);
1142    }
1143
1144    #[test]
1145    fn insert_many_short_lines() {
1146        let mut input = InputState::new();
1147        for i in 0..500 {
1148            input.insert_str(&format!("{i}"));
1149            input.insert_newline();
1150        }
1151        assert_eq!(input.lines.len(), 501); // 500 newlines + 1 trailing empty
1152        assert_eq!(input.cursor_row, 500);
1153    }
1154
1155    // rapid key combinations
1156
1157    #[test]
1158    fn type_then_backspace_all_then_retype() {
1159        let mut input = InputState::new();
1160        input.insert_str("hello world");
1161        // Backspace everything
1162        for _ in 0..11 {
1163            input.delete_char_before();
1164        }
1165        assert!(input.is_empty());
1166        assert_eq!(input.cursor_col, 0);
1167        // Type again
1168        input.insert_str("new text");
1169        assert_eq!(input.text(), "new text");
1170    }
1171
1172    #[test]
1173    fn alternating_insert_and_navigate() {
1174        let mut input = InputState::new();
1175        // Simulate: type 'a', left, type 'b', left, type 'c' -> "cba"
1176        input.insert_char('a');
1177        input.move_left();
1178        input.insert_char('b');
1179        input.move_left();
1180        input.insert_char('c');
1181        assert_eq!(input.lines[0], "cba");
1182        assert_eq!(input.cursor_col, 1); // after 'c'
1183    }
1184
1185    #[test]
1186    fn home_end_rapid_cycle() {
1187        let mut input = InputState::new();
1188        input.insert_str("hello");
1189        for _ in 0..50 {
1190            input.move_home();
1191            assert_eq!(input.cursor_col, 0);
1192            input.move_end();
1193            assert_eq!(input.cursor_col, 5);
1194        }
1195    }
1196
1197    #[test]
1198    fn left_right_round_trip_preserves_position() {
1199        let mut input = InputState::new();
1200        input.insert_str("abcdef");
1201        input.move_home();
1202        input.move_right();
1203        input.move_right();
1204        input.move_right(); // at col 3
1205        let col = input.cursor_col;
1206        // Go left 2 then right 2 -- should be back at same spot
1207        input.move_left();
1208        input.move_left();
1209        input.move_right();
1210        input.move_right();
1211        assert_eq!(input.cursor_col, col);
1212    }
1213
1214    #[test]
1215    fn up_down_round_trip_with_short_line() {
1216        let mut input = InputState::new();
1217        input.insert_str("longline\na\nlongline");
1218        input.cursor_row = 0;
1219        input.cursor_col = 7; // end-ish of first line
1220        input.move_down(); // to "a" -- col clamped to 1
1221        assert_eq!(input.cursor_col, 1);
1222        input.move_down(); // to "longline" -- col stays at 1 (not restored to 7)
1223        assert_eq!(input.cursor_col, 1);
1224    }
1225
1226    #[test]
1227    fn newline_then_immediate_backspace() {
1228        let mut input = InputState::new();
1229        input.insert_str("hello");
1230        input.insert_newline();
1231        assert_eq!(input.lines.len(), 2);
1232        input.delete_char_before(); // should rejoin
1233        assert_eq!(input.lines.len(), 1);
1234        assert_eq!(input.lines[0], "hello");
1235        assert_eq!(input.cursor_col, 5);
1236    }
1237
1238    #[test]
1239    fn delete_forward_through_multiple_line_joins() {
1240        let mut input = InputState::new();
1241        input.insert_str("a\nb\nc\nd");
1242        assert_eq!(input.lines.len(), 4);
1243        // Go to very start
1244        input.cursor_row = 0;
1245        input.cursor_col = 0;
1246        // Move to col 1 (after 'a'), then delete forward repeatedly
1247        input.move_right(); // past 'a'
1248        input.delete_char_after(); // join "a" + "b" -> "ab"
1249        assert_eq!(input.lines[0], "ab");
1250        input.move_right(); // past 'b'
1251        input.delete_char_after(); // join "ab" + "c" -> "abc"
1252        assert_eq!(input.lines[0], "abc");
1253        input.move_right(); // past 'c'
1254        input.delete_char_after(); // join "abc" + "d" -> "abcd"
1255        assert_eq!(input.lines, vec!["abcd"]);
1256    }
1257
1258    #[test]
1259    fn backspace_collapses_all_lines_to_one() {
1260        let mut input = InputState::new();
1261        input.insert_str("a\nb\nc\nd\ne");
1262        assert_eq!(input.lines.len(), 5);
1263        // Cursor is at end of last line. Backspace everything.
1264        let total_chars = input.text().len(); // includes \n chars
1265        for _ in 0..total_chars {
1266            input.delete_char_before();
1267        }
1268        assert!(input.is_empty());
1269        assert_eq!(input.lines.len(), 1);
1270        assert_eq!(input.cursor_row, 0);
1271        assert_eq!(input.cursor_col, 0);
1272    }
1273
1274    // interleaved actions
1275
1276    #[test]
1277    fn type_on_multiple_lines_then_clear() {
1278        let mut input = InputState::new();
1279        input.insert_str("line1\nline2\nline3");
1280        input.move_up();
1281        input.move_home();
1282        input.insert_str("prefix_");
1283        assert_eq!(input.lines[1], "prefix_line2");
1284        input.clear();
1285        assert!(input.is_empty());
1286        assert_eq!(input.cursor_row, 0);
1287    }
1288
1289    #[test]
1290    fn insert_between_emoji() {
1291        let mut input = InputState::new();
1292        input.insert_char('\u{1F600}');
1293        input.insert_char('\u{1F601}');
1294        // cursor at col 2, after both emoji
1295        input.move_left(); // between the two emoji, col 1
1296        input.insert_char('X');
1297        assert_eq!(input.lines[0], "\u{1F600}X\u{1F601}");
1298        assert_eq!(input.cursor_col, 2);
1299    }
1300
1301    #[test]
1302    fn delete_char_after_on_multibyte_boundary() {
1303        let mut input = InputState::new();
1304        input.insert_str("\u{1F600}\u{1F601}\u{1F602}");
1305        input.move_home();
1306        input.move_right(); // after first emoji
1307        input.delete_char_after(); // delete second emoji
1308        assert_eq!(input.lines[0], "\u{1F600}\u{1F602}");
1309    }
1310
1311    #[test]
1312    fn text_consistent_after_every_operation() {
1313        let mut input = InputState::new();
1314
1315        input.insert_str("hello");
1316        assert_eq!(input.text(), "hello");
1317
1318        input.insert_newline();
1319        assert_eq!(input.text(), "hello\n");
1320
1321        input.insert_str("world");
1322        assert_eq!(input.text(), "hello\nworld");
1323
1324        input.move_up();
1325        input.move_end();
1326        input.insert_char('!');
1327        assert_eq!(input.text(), "hello!\nworld");
1328
1329        input.delete_char_before();
1330        assert_eq!(input.text(), "hello\nworld");
1331
1332        input.move_down();
1333        input.move_home();
1334        input.delete_char_before(); // join lines
1335        assert_eq!(input.text(), "helloworld");
1336
1337        input.clear();
1338        assert_eq!(input.text(), "");
1339    }
1340
1341    #[test]
1342    fn navigate_through_empty_lines() {
1343        let mut input = InputState::new();
1344        input.insert_str("\n\n\n");
1345        // 4 empty lines, cursor at row 3
1346        assert_eq!(input.cursor_row, 3);
1347        input.move_up();
1348        assert_eq!(input.cursor_row, 2);
1349        assert_eq!(input.cursor_col, 0);
1350        input.move_up();
1351        input.move_up();
1352        assert_eq!(input.cursor_row, 0);
1353        // Insert on the first empty line
1354        input.insert_char('x');
1355        assert_eq!(input.lines[0], "x");
1356        assert_eq!(input.lines.len(), 4);
1357    }
1358
1359    #[test]
1360    fn insert_str_into_middle_of_existing_content() {
1361        let mut input = InputState::new();
1362        input.insert_str("hd");
1363        input.move_left(); // between h and d
1364        input.insert_str("ello worl");
1365        assert_eq!(input.lines[0], "hello world");
1366    }
1367
1368    #[test]
1369    fn multiline_paste_into_middle_of_line() {
1370        let mut input = InputState::new();
1371        input.insert_str("start end");
1372        // Move cursor to col 6 (between "start " and "end")
1373        input.move_home();
1374        for _ in 0..6 {
1375            input.move_right();
1376        }
1377        input.insert_str("line1\nline2\nline3 ");
1378        assert_eq!(input.lines[0], "start line1");
1379        assert_eq!(input.lines[1], "line2");
1380        assert_eq!(input.lines[2], "line3 end");
1381        assert_eq!(input.cursor_row, 2);
1382    }
1383
1384    #[test]
1385    fn version_never_wraps_in_reasonable_use() {
1386        let mut input = InputState::new();
1387        // After 1000 operations version should be 1000
1388        for _ in 0..500 {
1389            input.insert_char('a');
1390            input.delete_char_before();
1391        }
1392        assert_eq!(input.version, 1000);
1393    }
1394
1395    #[test]
1396    fn mixed_cr_and_lf_in_paste() {
1397        let mut input = InputState::new();
1398        // Mix of \r, \n, and \r\n
1399        input.insert_str("a\rb\nc\r\nd");
1400        // \r -> newline, \n -> newline, \r -> newline, \n -> newline
1401        // So: "a", "", "b", "", "c", "", "", "d" -- no wait, let me think again
1402        // \r -> newline (line "a" done, new line), b -> char, \n -> newline,
1403        // c -> char, \r -> newline, \n -> newline, d -> char
1404        // lines: ["a", "b", "c", "", "d"]
1405        assert_eq!(input.lines[0], "a");
1406        assert_eq!(input.lines.last().unwrap(), "d");
1407        // The key point: it doesn't crash and 'd' ends up somewhere
1408        assert!(input.text().contains('d'));
1409    }
1410
1411    #[test]
1412    fn parse_placeholder_with_trailing_suffix_text() {
1413        let line = "[Pasted Text 2 - 42 lines]tail";
1414        let parsed = parse_paste_placeholder_with_suffix(line).unwrap();
1415        assert_eq!(parsed.0, 1);
1416        assert_eq!(&line[..parsed.1], "[Pasted Text 2 - 42 lines]");
1417    }
1418
1419    #[test]
1420    fn text_expands_placeholder_even_with_trailing_text() {
1421        let mut input = InputState::new();
1422        input.insert_paste_block("line1\nline2");
1423        input.lines[0].push_str(" + extra");
1424        input.cursor_col = input.lines[0].chars().count();
1425        assert_eq!(input.text(), "line1\nline2 + extra");
1426    }
1427
1428    #[test]
1429    fn append_to_active_paste_block_merges_chunks_and_updates_label() {
1430        let mut input = InputState::new();
1431        let original = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk";
1432        input.insert_paste_block(original);
1433        assert!(input.append_to_active_paste_block("\nl\nm"));
1434        assert_eq!(input.lines[0], "[Pasted Text 1 - 13 lines]");
1435        assert_eq!(input.text(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm");
1436    }
1437
1438    #[test]
1439    fn append_to_active_paste_block_rejects_dirty_placeholder_line() {
1440        let mut input = InputState::new();
1441        input.insert_paste_block("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk");
1442        input.lines[0].push_str("tail");
1443        input.cursor_col = input.lines[0].chars().count();
1444        assert!(!input.append_to_active_paste_block("x"));
1445    }
1446
1447    #[test]
1448    fn count_text_lines_handles_mixed_line_endings() {
1449        assert_eq!(count_text_lines("a\r\nb\nc\rd"), 4);
1450        assert_eq!(count_text_lines("single"), 1);
1451        assert_eq!(count_text_lines("x\r\n"), 2);
1452    }
1453
1454    #[test]
1455    fn trim_trailing_line_breaks_handles_crlf_and_lf() {
1456        assert_eq!(trim_trailing_line_breaks("a\r\n\r\n"), "a");
1457        assert_eq!(trim_trailing_line_breaks("a\n\n"), "a");
1458        assert_eq!(trim_trailing_line_breaks("a\r\r"), "a");
1459        assert_eq!(trim_trailing_line_breaks("a"), "a");
1460    }
1461}