diff --git a/src/editor.rs b/src/editor.rs index af4433a..192a14c 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -2,7 +2,10 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyCode, KeyEvent}; +use edtui::actions::{ + Composed, Execute, SwitchMode, cpaste::Paste, insert::InsertChar, motion::MoveBackward, +}; use edtui::{EditorEventHandler, EditorMode, EditorState, Index2, Lines}; /// A file-backed edtui buffer: vim editing plus load/save and a modified flag. @@ -59,12 +62,23 @@ impl EditorBuffer { } pub fn handle_key(&mut self, key: KeyEvent) { - self.handler.on_key_event(key, &mut self.state); + if self.state.mode == EditorMode::Normal + && key.code == KeyCode::Char('p') + && key.modifiers.is_empty() + { + self.paste_yanked_at_cursor(); + } else { + self.handler.on_key_event(key, &mut self.state); + } self.refresh_modified(); } pub fn handle_paste(&mut self, text: String) { - self.handler.on_paste_event(text, &mut self.state); + if self.state.mode == EditorMode::Normal { + self.paste_text_at_cursor(text); + } else { + self.handler.on_paste_event(text, &mut self.state); + } self.refresh_modified(); } @@ -102,6 +116,45 @@ impl EditorBuffer { self.handle_paste(text.to_string()); } + /// Paste the yank register at the cursor, not after it (edtui's default `p`). + fn paste_yanked_at_cursor(&mut self) { + // edtui's `Paste` inserts after the cursor; in insert mode, stepping back + // one column first lands the insertion at the normal-mode cursor. + if self.state.cursor.col == 0 && self.cursor_line_has_text() { + self.paste_text_at_cursor(self.clipboard_text()); + return; + } + let mut action = Composed::new(SwitchMode(EditorMode::Insert)); + if self.state.cursor.col > 0 { + action = action.chain(MoveBackward(1)); + } + action = action.chain(Paste).chain(SwitchMode(EditorMode::Normal)); + action.execute(&mut self.state); + } + + fn cursor_line_has_text(&self) -> bool { + self.state + .lines + .len_col(self.state.cursor.row) + .is_some_and(|len| len > 0) + } + + fn clipboard_text(&self) -> String { + arboard::Clipboard::new() + .and_then(|mut clip| clip.get_text()) + .unwrap_or_default() + } + + /// Paste arbitrary text at the cursor in normal mode (bracketed paste). + fn paste_text_at_cursor(&mut self, text: String) { + let restore = self.state.mode; + self.state.execute(SwitchMode(EditorMode::Insert)); + for ch in text.chars() { + self.state.execute(InsertChar(ch)); + } + self.state.execute(SwitchMode(restore)); + } + fn refresh_modified(&mut self) { // .http files are small; hashing the buffer per keystroke is cheap // and beats trying to track dirtiness through edtui's undo stack. @@ -117,3 +170,43 @@ fn hash_str(s: &str) -> u64 { s.hash(&mut h); h.finish() } + +#[cfg(test)] +mod tests { + use super::*; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + + fn test_buffer(text: &str) -> EditorBuffer { + let path = + std::env::temp_dir().join(format!("req-editor-test-{}.http", std::process::id())); + std::fs::write(&path, text).unwrap(); + EditorBuffer::open(path).unwrap() + } + + fn set_system_clipboard(text: &str) { + arboard::Clipboard::new() + .and_then(|mut clip| clip.set_text(text.to_string())) + .unwrap(); + } + + #[test] + fn normal_p_pastes_at_cursor_not_after() { + let mut buf = test_buffer("Hello World"); + buf.state.cursor = Index2::new(0, 5); + set_system_clipboard("X"); + + buf.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)); + + assert_eq!(buf.text(), "HelloX World"); + } + + #[test] + fn normal_bracketed_paste_inserts_at_cursor() { + let mut buf = test_buffer("Hello World"); + buf.state.cursor = Index2::new(0, 5); + + buf.handle_paste("X".to_string()); + + assert_eq!(buf.text(), "HelloX World"); + } +}