refactor: move modal paste policy into editor input
This commit is contained in:
+101
-4
@@ -10,13 +10,14 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
const MAX_EFFECT_BYTES: usize = 4 * 1024 * 1024;
|
||||
const MAX_EDIT_BYTES: usize = 4 * 1024 * 1024;
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Input {
|
||||
Char(char),
|
||||
Ctrl(char),
|
||||
Paste(String),
|
||||
Escape,
|
||||
Enter,
|
||||
Backspace,
|
||||
@@ -323,6 +324,7 @@ impl Editor {
|
||||
let mut bytes = [0; 4];
|
||||
self.insert_text(ch.encode_utf8(&mut bytes));
|
||||
}
|
||||
Input::Paste(text) => self.paste(&text),
|
||||
Input::Enter => self.insert_text("\n"),
|
||||
Input::Backspace => self.backspace(),
|
||||
Input::Delete => self.delete_forward(),
|
||||
@@ -429,7 +431,7 @@ impl Editor {
|
||||
total = total
|
||||
.checked_add(text.len())
|
||||
.ok_or("Plugin effects exceed 4 MiB")?;
|
||||
if total > MAX_EFFECT_BYTES {
|
||||
if total > MAX_EDIT_BYTES {
|
||||
return Err("Plugin effects exceed 4 MiB".into());
|
||||
}
|
||||
validate_text(text)?;
|
||||
@@ -510,6 +512,27 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste(&mut self, text: &str) {
|
||||
let validation = if text.len() > MAX_EDIT_BYTES {
|
||||
Err("Paste exceeds 4 MiB".into())
|
||||
} else {
|
||||
validate_text(text)
|
||||
};
|
||||
if let Err(error) = validation {
|
||||
self.set_message(error);
|
||||
return;
|
||||
}
|
||||
// Preserve the separate typing / paste / typing undo groups.
|
||||
self.finish_insert();
|
||||
let before = (!text.is_empty()).then(|| self.snapshot());
|
||||
self.insert_text(&normalize_newlines(text));
|
||||
self.clamp_cursor();
|
||||
if let Some(before) = before {
|
||||
self.record(before);
|
||||
}
|
||||
self.insert_start = Some(self.snapshot());
|
||||
}
|
||||
|
||||
fn undo(&mut self) {
|
||||
if let Some(before) = self.undo.pop() {
|
||||
let current = self.snapshot();
|
||||
@@ -788,6 +811,80 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_is_ignored_in_normal_and_command_modes_without_dispatching() {
|
||||
let mut editor = editor();
|
||||
assert_eq!(editor.handle(Input::Paste(":q!\r\n".into())), None);
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert!(!editor.dirty());
|
||||
assert!(!editor.should_quit());
|
||||
|
||||
editor.handle(Input::Char(':'));
|
||||
type_text(&mut editor, "q");
|
||||
assert_eq!(editor.handle(Input::Paste("\rq!\n\0".into())), None);
|
||||
assert_eq!(editor.mode(), Mode::Command);
|
||||
assert_eq!(editor.command_line(), "q");
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert!(!editor.should_quit());
|
||||
editor.handle(Input::Enter);
|
||||
assert!(editor.should_quit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasted_unicode_and_newlines_are_literal_and_a_separate_undo_action() {
|
||||
let mut editor = editor();
|
||||
editor.handle(Input::Char('i'));
|
||||
type_text(&mut editor, "a");
|
||||
assert_eq!(
|
||||
editor.handle(Input::Paste("界e\u{301}\t\r\n:q!\r".into())),
|
||||
None
|
||||
);
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 2, column: 0 });
|
||||
type_text(&mut editor, "z");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "a界e\u{301}\t\n:q!\nz");
|
||||
assert!(!editor.should_quit());
|
||||
|
||||
for expected in ["a界e\u{301}\t\n:q!\n", "a", ""] {
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, expected);
|
||||
}
|
||||
for expected in ["a", "a界e\u{301}\t\n:q!\n", "a界e\u{301}\t\n:q!\nz"] {
|
||||
editor.handle(Input::Ctrl('r'));
|
||||
assert_eq!(editor.context().text, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_paste_preserves_text_cursor_mode_and_pending_insert_history() {
|
||||
for text in [
|
||||
"valid prefix\ninvalid\0".to_owned(),
|
||||
"\u{1b}[31m".to_owned(),
|
||||
"x".repeat(MAX_EDIT_BYTES + 1),
|
||||
] {
|
||||
let mut editor = editor();
|
||||
editor.handle(Input::Char('i'));
|
||||
type_text(&mut editor, "before");
|
||||
let cursor = editor.cursor();
|
||||
editor.handle(Input::Paste(text));
|
||||
assert_eq!(editor.context().text, "before");
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
assert!(
|
||||
editor.message().contains("Unsupported control character")
|
||||
|| editor.message().contains("Paste exceeds 4 MiB")
|
||||
);
|
||||
type_text(&mut editor, "after");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "");
|
||||
editor.handle(Input::Ctrl('r'));
|
||||
assert_eq!(editor.context().text, "beforeafter");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_sessions_and_open_lines_are_single_undo_actions() {
|
||||
let mut editor = editor();
|
||||
@@ -996,7 +1093,7 @@ mod tests {
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::Insert("a".repeat(MAX_EFFECT_BYTES)),
|
||||
Effect::Insert("a".repeat(MAX_EDIT_BYTES)),
|
||||
Effect::Insert("x".into())
|
||||
])
|
||||
.is_err()
|
||||
|
||||
+1
-10
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::{Editor, Effect, Input, Invocation},
|
||||
editor::{Editor, Input, Invocation},
|
||||
ruby::{Config, RubyPlugin, load_config},
|
||||
};
|
||||
|
||||
@@ -77,15 +77,6 @@ impl EditorSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies frontend effects, such as the TUI's existing Insert-mode paste.
|
||||
/// Plugin results are applied internally by dispatch, with owner retirement
|
||||
/// on rejection; frontend effects have no plugin owner to retire.
|
||||
pub fn apply(&mut self, effects: Vec<Effect>) {
|
||||
if let Err(error) = self.editor.apply(effects) {
|
||||
self.editor.set_message(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch(&mut self, invocation: Invocation) {
|
||||
let result = match self.plugins.get_mut(&invocation.plugin) {
|
||||
Some(plugin) => plugin
|
||||
|
||||
+2
-5
@@ -21,7 +21,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
editor::{Editor, Effect, Input, Mode},
|
||||
editor::{Editor, Input, Mode},
|
||||
session::EditorSession,
|
||||
};
|
||||
|
||||
@@ -75,10 +75,7 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
session.handle(input);
|
||||
}
|
||||
}
|
||||
// Bracketed paste must never execute pasted Ex commands.
|
||||
Event::Paste(text) if session.editor().mode() == Mode::Insert => {
|
||||
session.apply(vec![Effect::Insert(text)]);
|
||||
}
|
||||
Event::Paste(text) => session.handle(Input::Paste(text)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user