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 {
822
                     Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
822
                     Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
823
                         self.process_key(key_event)?
823
                         self.process_key(key_event)?
824
                     }
824
                     }
825
+                    Event::Paste(text) => self.process_paste(&text)?,
825
                     Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
826
                     Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
826
                     Event::Resize(cols, rows) => {
827
                     Event::Resize(cols, rows) => {
827
                         self.screen.cols = cols;
828
                         self.screen.cols = cols;
@@ -839,6 +840,7 @@ impl Editor {
839
                         Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
840
                         Event::Key(key_event) if key_event.kind != KeyEventKind::Release => {
840
                             self.process_key(key_event)?
841
                             self.process_key(key_event)?
841
                         }
842
                         }
843
+                        Event::Paste(text) => self.process_paste(&text)?,
842
                         Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
844
                         Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?,
843
                         Event::Resize(cols, rows) => {
845
                         Event::Resize(cols, rows) => {
844
                             self.screen.cols = cols;
846
                             self.screen.cols = cols;
@@ -1725,29 +1727,33 @@ impl Editor {
1725
             let timeout = Duration::from_millis(self.escape_time);
1727
             let timeout = Duration::from_millis(self.escape_time);
1726
 
1728
 
1727
             if event::poll(timeout)? {
1729
             if event::poll(timeout)? {
1728
-                if let Event::Key(next_event) = event::read()? {
1730
+                match event::read()? {
1729
-                    // Check for CSI sequences (ESC [ ...) which are arrow keys etc.
1731
+                    Event::Key(next_event) => {
1730
-                    if next_event.code == KeyCode::Char('[') {
1732
+                        // Check for CSI sequences (ESC [ ...) which are arrow keys etc.
1731
-                        // CSI sequence - read the rest
1733
+                        if next_event.code == KeyCode::Char('[') {
1732
-                        if event::poll(timeout)? {
1734
+                            // CSI sequence - read the rest
1733
-                            if let Event::Key(csi_event) = event::read()? {
1735
+                            if event::poll(timeout)? {
1734
-                                let mods = Modifiers { alt: true, ..Default::default() };
1736
+                                if let Event::Key(csi_event) = event::read()? {
1735
-                                return match csi_event.code {
1737
+                                    let mods = Modifiers { alt: true, ..Default::default() };
1736
-                                    KeyCode::Char('A') => self.handle_key_with_mods(Key::Up, mods),
1738
+                                    return match csi_event.code {
1737
-                                    KeyCode::Char('B') => self.handle_key_with_mods(Key::Down, mods),
1739
+                                        KeyCode::Char('A') => self.handle_key_with_mods(Key::Up, mods),
1738
-                                    KeyCode::Char('C') => self.handle_key_with_mods(Key::Right, mods),
1740
+                                        KeyCode::Char('B') => self.handle_key_with_mods(Key::Down, mods),
1739
-                                    KeyCode::Char('D') => self.handle_key_with_mods(Key::Left, mods),
1741
+                                        KeyCode::Char('C') => self.handle_key_with_mods(Key::Right, mods),
1740
-                                    _ => Ok(()), // Unknown CSI sequence
1742
+                                        KeyCode::Char('D') => self.handle_key_with_mods(Key::Left, mods),
1741
-                                };
1743
+                                        _ => Ok(()), // Unknown CSI sequence
1744
+                                    };
1745
+                                }
1742
                             }
1746
                             }
1747
+                            return Ok(()); // Incomplete CSI
1743
                         }
1748
                         }
1744
-                        return Ok(()); // Incomplete CSI
1745
-                    }
1746
 
1749
 
1747
-                    // Regular Alt+key (ESC followed by a normal key)
1750
+                        // Regular Alt+key (ESC followed by a normal key)
1748
-                    let (key, mut mods) = Key::from_crossterm(next_event);
1751
+                        let (key, mut mods) = Key::from_crossterm(next_event);
1749
-                    mods.alt = true;
1752
+                        mods.alt = true;
1750
-                    return self.handle_key_with_mods(key, mods);
1753
+                        return self.handle_key_with_mods(key, mods);
1754
+                    }
1755
+                    Event::Paste(text) => return self.process_paste(&text),
1756
+                    _ => {}
1751
                 }
1757
                 }
1752
             }
1758
             }
1753
             // No key followed - it's a real Escape
1759
             // No key followed - it's a real Escape
@@ -1759,6 +1765,45 @@ impl Editor {
1759
         self.handle_key_with_mods(key, mods)
1765
         self.handle_key_with_mods(key, mods)
1760
     }
1766
     }
1761
 
1767
 
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
+
1762
     /// Process a mouse event
1807
     /// Process a mouse event
1763
     fn process_mouse(&mut self, mouse_event: MouseEvent) -> Result<()> {
1808
     fn process_mouse(&mut self, mouse_event: MouseEvent) -> Result<()> {
1764
         if let Some(mouse) = Mouse::from_crossterm(mouse_event) {
1809
         if let Some(mouse) = Mouse::from_crossterm(mouse_event) {
@@ -3510,7 +3555,12 @@ impl Editor {
3510
     }
3555
     }
3511
 
3556
 
3512
     fn insert_text(&mut self, text: &str) {
3557
     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
+        }
3514
     }
3564
     }
3515
 
3565
 
3516
     fn insert_char(&mut self, c: char) {
3566
     fn insert_char(&mut self, c: char) {
@@ -4567,6 +4617,31 @@ impl Editor {
4567
         self.internal_clipboard = text;
4617
         self.internal_clipboard = text;
4568
     }
4618
     }
4569
 
4619
 
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
+
4570
     /// Get clipboard text (system if available, internal fallback)
4645
     /// Get clipboard text (system if available, internal fallback)
4571
     fn get_clipboard(&mut self) -> String {
4646
     fn get_clipboard(&mut self) -> String {
4572
         if let Some(ref mut cb) = self.clipboard {
4647
         if let Some(ref mut cb) = self.clipboard {
@@ -4641,7 +4716,7 @@ impl Editor {
4641
     }
4716
     }
4642
 
4717
 
4643
     fn paste(&mut self) {
4718
     fn paste(&mut self) {
4644
-        let text = self.get_clipboard();
4719
+        let text = Self::normalize_line_endings(&self.get_clipboard());
4645
         if !text.is_empty() {
4720
         if !text.is_empty() {
4646
             self.insert_text(&text);
4721
             self.insert_text(&text);
4647
             self.message = Some("Pasted".to_string());
4722
             self.message = Some("Pasted".to_string());
src/input/key.rsmodified
@@ -45,6 +45,13 @@ impl Key {
45
         let modifiers = Modifiers::from(event.modifiers);
45
         let modifiers = Modifiers::from(event.modifiers);
46
         let key = match event.code {
46
         let key = match event.code {
47
             KeyCode::Char(c) => {
47
             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
+
48
                 // If shift is pressed and character is lowercase alphabetic,
55
                 // If shift is pressed and character is lowercase alphabetic,
49
                 // uppercase it. This handles terminals that don't support
56
                 // uppercase it. This handles terminals that don't support
50
                 // REPORT_ALTERNATE_KEYS properly (which would report 'A' directly).
57
                 // REPORT_ALTERNATE_KEYS properly (which would report 'A' directly).
@@ -76,3 +83,36 @@ impl Key {
76
         (key, modifiers)
83
         (key, modifiers)
77
     }
84
     }
78
 }
85
 }
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;
2
 use crossterm::{
2
 use crossterm::{
3
     cursor::{Hide, MoveTo, Show},
3
     cursor::{Hide, MoveTo, Show},
4
     event::{
4
     event::{
5
-        DisableMouseCapture, EnableMouseCapture,
5
+        DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
6
         KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
6
         KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
7
     },
7
     },
8
     execute, queue,
8
     execute, queue,
@@ -113,7 +113,13 @@ impl Screen {
113
 
113
 
114
     pub fn enter_raw_mode(&mut self) -> Result<()> {
114
     pub fn enter_raw_mode(&mut self) -> Result<()> {
115
         terminal::enable_raw_mode()?;
115
         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
+        )?;
117
 
123
 
118
         // Try to enable keyboard enhancement for better modifier key detection
124
         // Try to enable keyboard enhancement for better modifier key detection
119
         // This enables the kitty keyboard protocol on supporting terminals.
125
         // This enables the kitty keyboard protocol on supporting terminals.
@@ -139,7 +145,13 @@ impl Screen {
139
         if self.keyboard_enhanced {
145
         if self.keyboard_enhanced {
140
             let _ = execute!(self.stdout, PopKeyboardEnhancementFlags);
146
             let _ = execute!(self.stdout, PopKeyboardEnhancementFlags);
141
         }
147
         }
142
-        execute!(self.stdout, Show, DisableMouseCapture, LeaveAlternateScreen)?;
148
+        execute!(
149
+            self.stdout,
150
+            Show,
151
+            DisableMouseCapture,
152
+            DisableBracketedPaste,
153
+            LeaveAlternateScreen
154
+        )?;
143
         terminal::disable_raw_mode()?;
155
         terminal::disable_raw_mode()?;
144
         Ok(())
156
         Ok(())
145
     }
157
     }