tenseleyflow/fackr / 50fd38a

Browse files

fix multiline paste handling

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
50fd38a63ef99bc305dad7e7e840939ffb622a62
Parents
a275a53
Tree
9c16b25

3 changed files

StatusFile+-
M src/editor/state.rs 97 22
M src/input/key.rs 40 0
M src/render/screen.rs 15 3
src/editor/state.rsmodified
@@ -822,6 +822,7 @@ impl Editor {
822822
                     Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
823823
                         self.process_key(key_event)?
824824
                     }
825
+                    Event::Paste(text) => self.process_paste(&text)?,
825826
                     Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
826827
                     Event::Resize(cols, rows) => {
827828
                         self.screen.cols = cols;
@@ -839,6 +840,7 @@ impl Editor {
839840
                         Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
840841
                             self.process_key(key_event)?
841842
                         }
843
+                        Event::Paste(text) => self.process_paste(&text)?,
842844
                         Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
843845
                         Event::Resize(cols, rows) => {
844846
                             self.screen.cols = cols;
@@ -1725,29 +1727,33 @@ impl Editor {
17251727
             let timeout = Duration::from_millis(self.escape_time);
17261728
 
17271729
             if event::poll(timeout)? {
1728
-                if let Event::Key(next_event) = event::read()? {
1729
-                    // Check for CSI sequences (ESC [ ...) which are arrow keys etc.
1730
-                    if next_event.code == KeyCode::Char('[') {
1731
-                        // CSI sequence - read the rest
1732
-                        if event::poll(timeout)? {
1733
-                            if let Event::Key(csi_event) = event::read()? {
1734
-                                let mods = Modifiers { alt: true, ..Default::default() };
1735
-                                return match csi_event.code {
1736
-                                    KeyCode::Char('A') => self.handle_key_with_mods(Key::Up, mods),
1737
-                                    KeyCode::Char('B') => self.handle_key_with_mods(Key::Down, mods),
1738
-                                    KeyCode::Char('C') => self.handle_key_with_mods(Key::Right, mods),
1739
-                                    KeyCode::Char('D') => self.handle_key_with_mods(Key::Left, mods),
1740
-                                    _ => Ok(()), // Unknown CSI sequence
1741
-                                };
1730
+                match event::read()? {
1731
+                    Event::Key(next_event) => {
1732
+                        // Check for CSI sequences (ESC [ ...) which are arrow keys etc.
1733
+                        if next_event.code == KeyCode::Char('[') {
1734
+                            // CSI sequence - read the rest
1735
+                            if event::poll(timeout)? {
1736
+                                if let Event::Key(csi_event) = event::read()? {
1737
+                                    let mods = Modifiers { alt: true, ..Default::default() };
1738
+                                    return match csi_event.code {
1739
+                                        KeyCode::Char('A') => self.handle_key_with_mods(Key::Up, mods),
1740
+                                        KeyCode::Char('B') => self.handle_key_with_mods(Key::Down, mods),
1741
+                                        KeyCode::Char('C') => self.handle_key_with_mods(Key::Right, mods),
1742
+                                        KeyCode::Char('D') => self.handle_key_with_mods(Key::Left, mods),
1743
+                                        _ => Ok(()), // Unknown CSI sequence
1744
+                                    };
1745
+                                }
17421746
                             }
1747
+                            return Ok(()); // Incomplete CSI
17431748
                         }
1744
-                        return Ok(()); // Incomplete CSI
1745
-                    }
17461749
 
1747
-                    // Regular Alt+key (ESC followed by a normal key)
1748
-                    let (key, mut mods) = Key::from_crossterm(next_event);
1749
-                    mods.alt = true;
1750
-                    return self.handle_key_with_mods(key, mods);
1750
+                        // Regular Alt+key (ESC followed by a normal key)
1751
+                        let (key, mut mods) = Key::from_crossterm(next_event);
1752
+                        mods.alt = true;
1753
+                        return self.handle_key_with_mods(key, mods);
1754
+                    }
1755
+                    Event::Paste(text) => return self.process_paste(&text),
1756
+                    _ => {}
17511757
                 }
17521758
             }
17531759
             // No key followed - it's a real Escape
@@ -1759,6 +1765,45 @@ impl Editor {
17591765
         self.handle_key_with_mods(key, mods)
17601766
     }
17611767
 
1768
+    /// Process a paste event from the terminal.
1769
+    /// Handles multiline text and normalizes CRLF/CR line endings.
1770
+    fn process_paste(&mut self, text: &str) -> Result<()> {
1771
+        let text = Self::normalize_line_endings(text);
1772
+        if text.is_empty() {
1773
+            return Ok(());
1774
+        }
1775
+
1776
+        // Route paste to terminal when terminal has focus.
1777
+        if self.focus == Focus::Terminal && self.terminal.visible {
1778
+            self.terminal.send_input(text.as_bytes())?;
1779
+            return Ok(());
1780
+        }
1781
+
1782
+        // Fast path for editor: insert as one operation to preserve undo grouping.
1783
+        if self.prompt == PromptState::None && self.focus == Focus::Editor {
1784
+            self.history_mut().maybe_break_group();
1785
+            self.insert_text(&text);
1786
+            self.dismiss_ghost_text();
1787
+            self.message = Some("Pasted".to_string());
1788
+            self.history_mut().maybe_break_group();
1789
+            self.on_buffer_edit();
1790
+            self.scroll_to_cursor();
1791
+            return Ok(());
1792
+        }
1793
+
1794
+        // For prompts, server manager, or fuss mode, replay as plain key input.
1795
+        for c in text.chars() {
1796
+            let key = match c {
1797
+                '\n' => Key::Enter,
1798
+                '\t' => Key::Tab,
1799
+                _ => Key::Char(c),
1800
+            };
1801
+            self.handle_key_with_mods(key, Modifiers::default())?;
1802
+        }
1803
+
1804
+        Ok(())
1805
+    }
1806
+
17621807
     /// Process a mouse event
17631808
     fn process_mouse(&mut self, mouse_event: MouseEvent) -> Result<()> {
17641809
         if let Some(mouse) = Mouse::from_crossterm(mouse_event) {
@@ -3510,7 +3555,12 @@ impl Editor {
35103555
     }
35113556
 
35123557
     fn insert_text(&mut self, text: &str) {
3513
-        self.insert_text_multi(text);
3558
+        if text.contains('\r') || text.contains('\u{2028}') || text.contains('\u{2029}') {
3559
+            let normalized = Self::normalize_line_endings(text);
3560
+            self.insert_text_multi(&normalized);
3561
+        } else {
3562
+            self.insert_text_multi(text);
3563
+        }
35143564
     }
35153565
 
35163566
     fn insert_char(&mut self, c: char) {
@@ -4567,6 +4617,31 @@ impl Editor {
45674617
         self.internal_clipboard = text;
45684618
     }
45694619
 
4620
+    /// Normalize CRLF/CR to LF for internal editing operations.
4621
+    fn normalize_line_endings(text: &str) -> String {
4622
+        if !text.contains('\r') && !text.contains('\u{2028}') && !text.contains('\u{2029}') {
4623
+            return text.to_string();
4624
+        }
4625
+
4626
+        let mut out = String::with_capacity(text.len());
4627
+        let mut chars = text.chars().peekable();
4628
+        while let Some(ch) = chars.next() {
4629
+            match ch {
4630
+                '\r' => {
4631
+                    // Collapse CRLF into a single LF.
4632
+                    if chars.peek() == Some(&'\n') {
4633
+                        chars.next();
4634
+                    }
4635
+                    out.push('\n');
4636
+                }
4637
+                // Unicode line and paragraph separators
4638
+                '\u{2028}' | '\u{2029}' => out.push('\n'),
4639
+                _ => out.push(ch),
4640
+            }
4641
+        }
4642
+        out
4643
+    }
4644
+
45704645
     /// Get clipboard text (system if available, internal fallback)
45714646
     fn get_clipboard(&mut self) -> String {
45724647
         if let Some(ref mut cb) = self.clipboard {
@@ -4641,7 +4716,7 @@ impl Editor {
46414716
     }
46424717
 
46434718
     fn paste(&mut self) {
4644
-        let text = self.get_clipboard();
4719
+        let text = Self::normalize_line_endings(&self.get_clipboard());
46454720
         if !text.is_empty() {
46464721
             self.insert_text(&text);
46474722
             self.message = Some("Pasted".to_string());
src/input/key.rsmodified
@@ -45,6 +45,13 @@ impl Key {
4545
         let modifiers = Modifiers::from(event.modifiers);
4646
         let key = match event.code {
4747
             KeyCode::Char(c) => {
48
+                // Some terminals report Enter/newline as Char('\r') or Char('\n')
49
+                // during paste or raw key input. Normalize those to Enter so
50
+                // multiline paste preserves line breaks.
51
+                if c == '\r' || c == '\n' {
52
+                    return (Key::Enter, modifiers);
53
+                }
54
+
4855
                 // If shift is pressed and character is lowercase alphabetic,
4956
                 // uppercase it. This handles terminals that don't support
5057
                 // REPORT_ALTERNATE_KEYS properly (which would report 'A' directly).
@@ -76,3 +83,36 @@ impl Key {
7683
         (key, modifiers)
7784
     }
7885
 }
86
+
87
+#[cfg(test)]
88
+mod tests {
89
+    use super::*;
90
+    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
91
+
92
+    #[test]
93
+    fn char_carriage_return_maps_to_enter() {
94
+        let (key, mods) = Key::from_crossterm(KeyEvent::new(
95
+            KeyCode::Char('\r'),
96
+            KeyModifiers::NONE,
97
+        ));
98
+        assert_eq!(key, Key::Enter);
99
+        assert_eq!(mods, Modifiers::default());
100
+    }
101
+
102
+    #[test]
103
+    fn char_newline_maps_to_enter() {
104
+        let (key, mods) = Key::from_crossterm(KeyEvent::new(
105
+            KeyCode::Char('\n'),
106
+            KeyModifiers::SHIFT,
107
+        ));
108
+        assert_eq!(key, Key::Enter);
109
+        assert_eq!(
110
+            mods,
111
+            Modifiers {
112
+                ctrl: false,
113
+                alt: false,
114
+                shift: true,
115
+            }
116
+        );
117
+    }
118
+}
src/render/screen.rsmodified
@@ -2,7 +2,7 @@ use anyhow::Result;
22
 use crossterm::{
33
     cursor::{Hide, MoveTo, Show},
44
     event::{
5
-        DisableMouseCapture, EnableMouseCapture,
5
+        DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
66
         KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
77
     },
88
     execute, queue,
@@ -113,7 +113,13 @@ impl Screen {
113113
 
114114
     pub fn enter_raw_mode(&mut self) -> Result<()> {
115115
         terminal::enable_raw_mode()?;
116
-        execute!(self.stdout, EnterAlternateScreen, Hide, EnableMouseCapture)?;
116
+        execute!(
117
+            self.stdout,
118
+            EnterAlternateScreen,
119
+            Hide,
120
+            EnableMouseCapture,
121
+            EnableBracketedPaste
122
+        )?;
117123
 
118124
         // Try to enable keyboard enhancement for better modifier key detection
119125
         // This enables the kitty keyboard protocol on supporting terminals.
@@ -139,7 +145,13 @@ impl Screen {
139145
         if self.keyboard_enhanced {
140146
             let _ = execute!(self.stdout, PopKeyboardEnhancementFlags);
141147
         }
142
-        execute!(self.stdout, Show, DisableMouseCapture, LeaveAlternateScreen)?;
148
+        execute!(
149
+            self.stdout,
150
+            Show,
151
+            DisableMouseCapture,
152
+            DisableBracketedPaste,
153
+            LeaveAlternateScreen
154
+        )?;
143155
         terminal::disable_raw_mode()?;
144156
         Ok(())
145157
     }