@@ -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()); |