@@ -822,6 +822,7 @@ impl Editor { |
| 822 | 822 | Event::Key(key_event) if key_event.kind != KeyEventKind::Release => { |
| 823 | 823 | self.process_key(key_event)? |
| 824 | 824 | } |
| 825 | + Event::Paste(text) => self.process_paste(&text)?, |
| 825 | 826 | Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?, |
| 826 | 827 | Event::Resize(cols, rows) => { |
| 827 | 828 | self.screen.cols = cols; |
@@ -839,6 +840,7 @@ impl Editor { |
| 839 | 840 | Event::Key(key_event) if key_event.kind != KeyEventKind::Release => { |
| 840 | 841 | self.process_key(key_event)? |
| 841 | 842 | } |
| 843 | + Event::Paste(text) => self.process_paste(&text)?, |
| 842 | 844 | Event::Mouse(mouse_event) => self.process_mouse(mouse_event)?, |
| 843 | 845 | Event::Resize(cols, rows) => { |
| 844 | 846 | self.screen.cols = cols; |
@@ -1725,29 +1727,33 @@ impl Editor { |
| 1725 | 1727 | let timeout = Duration::from_millis(self.escape_time); |
| 1726 | 1728 | |
| 1727 | 1729 | 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 | + } |
| 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) |
| 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 | + _ => {} |
| 1751 | 1757 | } |
| 1752 | 1758 | } |
| 1753 | 1759 | // No key followed - it's a real Escape |
@@ -1759,6 +1765,45 @@ impl Editor { |
| 1759 | 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 | 1807 | /// Process a mouse event |
| 1763 | 1808 | fn process_mouse(&mut self, mouse_event: MouseEvent) -> Result<()> { |
| 1764 | 1809 | if let Some(mouse) = Mouse::from_crossterm(mouse_event) { |
@@ -3510,7 +3555,12 @@ impl Editor { |
| 3510 | 3555 | } |
| 3511 | 3556 | |
| 3512 | 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 | 3566 | fn insert_char(&mut self, c: char) { |
@@ -4567,6 +4617,31 @@ impl Editor { |
| 4567 | 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 | 4645 | /// Get clipboard text (system if available, internal fallback) |
| 4571 | 4646 | fn get_clipboard(&mut self) -> String { |
| 4572 | 4647 | if let Some(ref mut cb) = self.clipboard { |
@@ -4641,7 +4716,7 @@ impl Editor { |
| 4641 | 4716 | } |
| 4642 | 4717 | |
| 4643 | 4718 | fn paste(&mut self) { |
| 4644 | | - let text = self.get_clipboard(); |
| 4719 | + let text = Self::normalize_line_endings(&self.get_clipboard()); |
| 4645 | 4720 | if !text.is_empty() { |
| 4646 | 4721 | self.insert_text(&text); |
| 4647 | 4722 | self.message = Some("Pasted".to_string()); |