From c5d91f301edefb63711f66a81835bf258686fb8b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 10 Jun 2025 19:49:27 +0200 Subject: [PATCH] Large clipboard handling overhaul (#405) * Make each paste its own undo step. * Add a `Paste` input type, allowing us to... * Fill the internal clipboard with bracketed paste contents. * Abstract away clipboard handling into its own struct, so we can move the cut/copy/paste logic into `TextBuffer`, allowing us to... * Implement smart line-wise copy/paste via Ctrl+C/Ctrl+V. Closes #286 Closes #305 --- benches/lib.rs | 8 ++-- src/bin/edit/draw_menubar.rs | 8 ++-- src/bin/edit/main.rs | 60 +++++++++++++---------- src/bin/edit/state.rs | 6 +-- src/buffer/mod.rs | 72 ++++++++++++++++++++++++---- src/clipboard.rs | 53 +++++++++++++++++++++ src/input.rs | 30 ++++++------ src/lib.rs | 1 + src/tui.rs | 92 +++++++++++++----------------------- 9 files changed, 210 insertions(+), 120 deletions(-) create mode 100644 src/clipboard.rs diff --git a/benches/lib.rs b/benches/lib.rs index 738dbc0..006977f 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -37,7 +37,7 @@ fn bench_buffer(c: &mut Criterion) { { let mut tb = buffer::TextBuffer::new(false).unwrap(); tb.set_crlf(false); - tb.write(data.start_content.as_bytes(), true); + tb.write_raw(data.start_content.as_bytes()); for t in &data.txns { for p in &t.patches { @@ -46,7 +46,7 @@ fn bench_buffer(c: &mut Criterion) { tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType); - tb.write(p.2.as_bytes(), true); + tb.write_raw(p.2.as_bytes()); patches_with_coords.push((beg, p.1 as CoordType, p.2.clone())); } } @@ -72,12 +72,12 @@ fn bench_buffer(c: &mut Criterion) { let bench_text_buffer = || { let mut tb = buffer::TextBuffer::new(false).unwrap(); tb.set_crlf(false); - tb.write(data.start_content.as_bytes(), true); + tb.write_raw(data.start_content.as_bytes()); for p in &patches_with_coords { tb.cursor_move_to_logical(p.0); tb.delete(buffer::CursorMovement::Grapheme, p.1); - tb.write(p.2.as_bytes(), true); + tb.write_raw(p.2.as_bytes()); } tb diff --git a/src/bin/edit/draw_menubar.rs b/src/bin/edit/draw_menubar.rs index 560a766..eb0eeb6 100644 --- a/src/bin/edit/draw_menubar.rs +++ b/src/bin/edit/draw_menubar.rs @@ -74,13 +74,15 @@ fn draw_menu_edit(ctx: &mut Context, state: &mut State) { ctx.needs_rerender(); } if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) { - ctx.set_clipboard(tb.extract_selection(true)); + tb.cut(ctx.clipboard_mut()); + ctx.needs_rerender(); } if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) { - ctx.set_clipboard(tb.extract_selection(false)); + tb.copy(ctx.clipboard_mut()); + ctx.needs_rerender(); } if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) { - tb.write(ctx.clipboard(), true); + tb.paste(ctx.clipboard_ref()); ctx.needs_rerender(); } if state.wants_search.kind != StateSearchKind::Disabled { diff --git a/src/bin/edit/main.rs b/src/bin/edit/main.rs index 84a5379..b73ef9e 100644 --- a/src/bin/edit/main.rs +++ b/src/bin/edit/main.rs @@ -176,8 +176,8 @@ fn run() -> apperr::Result<()> { } } - if state.osc_clipboard_send_generation == tui.clipboard_generation() { - write_osc_clipboard(&mut output, &mut state, &tui); + if state.osc_clipboard_sync { + write_osc_clipboard(&mut tui, &mut state, &mut output); } #[cfg(feature = "debug-latency")] @@ -317,7 +317,7 @@ fn draw(ctx: &mut Context, state: &mut State) { if state.wants_about { draw_dialog_about(ctx, state); } - if state.osc_clipboard_seen_generation != ctx.clipboard_generation() { + if ctx.clipboard_ref().wants_host_sync() { draw_handle_clipboard_change(ctx, state); } if state.error_log_count != 0 { @@ -389,18 +389,19 @@ fn write_terminal_title(output: &mut ArenaString, filename: &str) { output.push_str("edit\x1b\\"); } -const LARGE_CLIPBOARD_THRESHOLD: usize = 4 * KIBI; +const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI; fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { - let generation = ctx.clipboard_generation(); + let data_len = ctx.clipboard_ref().read().len(); - if state.osc_clipboard_always_send || ctx.clipboard().len() < LARGE_CLIPBOARD_THRESHOLD { - state.osc_clipboard_seen_generation = generation; - state.osc_clipboard_send_generation = generation; + if state.osc_clipboard_always_send || data_len < LARGE_CLIPBOARD_THRESHOLD { + ctx.clipboard_mut().mark_as_synchronized(); + state.osc_clipboard_sync = true; return; } - let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4; + let over_limit = data_len >= SCRATCH_ARENA_CAPACITY / 4; + let mut done = None; ctx.modal_begin("warning", loc(LocId::WarningDialogTitle)); { @@ -415,7 +416,7 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { } else { let label2 = { let template = loc(LocId::LargeClipboardWarningLine2); - let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len())); + let size = arena_format!(ctx.arena(), "{}", MetricFormatter(data_len)); let mut label = ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena()); @@ -444,28 +445,26 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { if over_limit { if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) { - state.osc_clipboard_seen_generation = generation; + done = Some(true); } ctx.inherit_focus(); } else { if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) { state.osc_clipboard_always_send = true; - state.osc_clipboard_seen_generation = generation; - state.osc_clipboard_send_generation = generation; + done = Some(true); } if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) { - state.osc_clipboard_seen_generation = generation; - state.osc_clipboard_send_generation = generation; + done = Some(true); } - if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD { + if data_len < 10 * LARGE_CLIPBOARD_THRESHOLD { ctx.inherit_focus(); } if ctx.button("no", loc(LocId::No), ButtonStyle::default()) { - state.osc_clipboard_seen_generation = generation; + done = Some(false); } - if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD { + if data_len >= 10 * LARGE_CLIPBOARD_THRESHOLD { ctx.inherit_focus(); } } @@ -473,24 +472,33 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { ctx.table_end(); } if ctx.modal_end() { - state.osc_clipboard_seen_generation = generation; + done = Some(false); + } + + if let Some(sync) = done { + state.osc_clipboard_sync = sync; + ctx.clipboard_mut().mark_as_synchronized(); + ctx.needs_rerender(); } } #[cold] -fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) { - let clipboard = tui.clipboard(); - if !clipboard.is_empty() { +fn write_osc_clipboard(tui: &mut Tui, state: &mut State, output: &mut ArenaString) { + let clipboard = tui.clipboard_mut(); + let data = clipboard.read(); + + if !data.is_empty() { // Rust doubles the size of a string when it needs to grow it. - // If `clipboard` is *really* large, this may then double + // If `data` is *really* large, this may then double // the size of the `output` from e.g. 100MB to 200MB. Not good. // We can avoid that by reserving the needed size in advance. - output.reserve_exact(base64::encode_len(clipboard.len()) + 16); + output.reserve_exact(base64::encode_len(data.len()) + 16); output.push_str("\x1b]52;c;"); - base64::encode(output, clipboard); + base64::encode(output, data); output.push_str("\x1b\\"); } - state.osc_clipboard_send_generation = tui.clipboard_generation().wrapping_sub(1); + + state.osc_clipboard_sync = false; } struct RestoreModes; diff --git a/src/bin/edit/state.rs b/src/bin/edit/state.rs index 774f22a..dfdc03f 100644 --- a/src/bin/edit/state.rs +++ b/src/bin/edit/state.rs @@ -162,8 +162,7 @@ pub struct State { pub goto_invalid: bool, pub osc_title_filename: String, - pub osc_clipboard_seen_generation: u32, - pub osc_clipboard_send_generation: u32, + pub osc_clipboard_sync: bool, pub osc_clipboard_always_send: bool, pub exit: bool, } @@ -211,8 +210,7 @@ impl State { goto_invalid: false, osc_title_filename: Default::default(), - osc_clipboard_seen_generation: 0, - osc_clipboard_send_generation: 0, + osc_clipboard_sync: false, osc_clipboard_always_send: false, exit: false, }) diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 776f824..79d25be 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -38,6 +38,7 @@ pub use gap_buffer::GapBuffer; use crate::arena::{ArenaString, scratch_arena}; use crate::cell::SemiRefCell; +use crate::clipboard::Clipboard; use crate::document::{ReadableDocument, WriteableDocument}; use crate::framebuffer::{Framebuffer, IndexedColor}; use crate::helpers::*; @@ -1083,7 +1084,7 @@ impl TextBuffer { if let (Some(search), Some(..)) = (&mut self.search, &self.selection) { let search = search.get_mut(); if search.selection_generation == self.selection_generation { - self.write(replacement.as_bytes(), true); + self.write(replacement.as_bytes(), self.cursor, true); } } @@ -1106,7 +1107,7 @@ impl TextBuffer { if !self.has_selection() { break; } - self.write(replacement, true); + self.write(replacement, self.cursor, true); offset = self.cursor.offset; } @@ -1822,15 +1823,60 @@ impl TextBuffer { Some(RenderResult { visual_pos_x_max }) } - /// Inserts `text` at the current cursor position. - /// - /// If there's a current selection, it will be replaced. - /// The selection is cleared after the call. - pub fn write(&mut self, text: &[u8], raw: bool) { + pub fn cut(&mut self, clipboard: &mut Clipboard) { + self.cut_copy(clipboard, true); + } + + pub fn copy(&mut self, clipboard: &mut Clipboard) { + self.cut_copy(clipboard, false); + } + + fn cut_copy(&mut self, clipboard: &mut Clipboard, cut: bool) { + let line_copy = !self.has_selection(); + let selection = self.extract_selection(cut); + clipboard.write(selection); + clipboard.write_was_line_copy(line_copy); + } + + pub fn paste(&mut self, clipboard: &Clipboard) { + let data = clipboard.read(); + if data.is_empty() { + return; + } + + let pos = self.cursor_logical_pos(); + let at = if clipboard.is_line_copy() { + self.goto_line_start(self.cursor, pos.y) + } else { + self.cursor + }; + + self.write(data, at, true); + + if clipboard.is_line_copy() { + self.cursor_move_to_logical(Point { x: pos.x, y: pos.y + 1 }); + } + } + + /// Inserts the user input `text` at the current cursor position. + /// Replaces tabs with whitespace if needed, etc. + pub fn write_canon(&mut self, text: &[u8]) { + self.write(text, self.cursor, false); + } + + /// Inserts `text` as-is at the current cursor position. + /// The only transformation applied is that newlines are normalized. + pub fn write_raw(&mut self, text: &[u8]) { + self.write(text, self.cursor, true); + } + + fn write(&mut self, text: &[u8], at: Cursor, raw: bool) { + let history_type = if raw { HistoryType::Other } else { HistoryType::Write }; + // If we have an active selection, writing an empty `text` // will still delete the selection. As such, we check this first. if let Some((beg, end)) = self.selection_range_internal(false) { - self.edit_begin(HistoryType::Write, beg); + self.edit_begin(history_type, beg); self.edit_delete(end); self.set_selection(None); } @@ -1846,7 +1892,7 @@ impl TextBuffer { } if self.active_edit_depth <= 0 { - self.edit_begin(HistoryType::Write, self.cursor); + self.edit_begin(history_type, at); } let mut offset = 0; @@ -2125,7 +2171,8 @@ impl TextBuffer { /// Extracts the contents of the current selection. /// May optionally delete it, if requested. This is meant to be used for Ctrl+X. - pub fn extract_selection(&mut self, delete: bool) -> Vec { + fn extract_selection(&mut self, delete: bool) -> Vec { + let line_copy = !self.has_selection(); let Some((beg, end)) = self.selection_range_internal(true) else { return Vec::new(); }; @@ -2140,6 +2187,11 @@ impl TextBuffer { self.set_selection(None); } + // Line copies (= Ctrl+C when there's no selection) always end with a newline. + if line_copy && !out.ends_with(b"\n") { + out.replace_range(out.len().., if self.newlines_are_crlf { b"\r\n" } else { b"\n" }); + } + out } diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..413de71 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,53 @@ +//! Clipboard facilities for the editor. + +/// The builtin, internal clipboard of the editor. +/// +/// This is useful particularly when the terminal doesn't support +/// OSC 52 or when the clipboard contents are huge (e.g. 1GiB). +#[derive(Default)] +pub struct Clipboard { + data: Vec, + line_copy: bool, + wants_host_sync: bool, +} + +impl Clipboard { + /// If true, we should emit a OSC 52 sequence to sync the clipboard + /// with the hosting terminal. + pub fn wants_host_sync(&self) -> bool { + self.wants_host_sync + } + + /// Call this once the clipboard has been synchronized with the host. + pub fn mark_as_synchronized(&mut self) { + self.wants_host_sync = false; + } + + /// The editor has a special behavior when you have no selection and press + /// Ctrl+C: It copies the current line to the clipboard. Then, when you + /// paste it, it inserts the line at *the start* of the current line. + /// This effectively prepends the current line with the copied line. + /// `clipboard_line_start` is true in that case. + pub fn is_line_copy(&self) -> bool { + self.line_copy + } + + /// Returns the current contents of the clipboard. + pub fn read(&self) -> &[u8] { + &self.data + } + + /// Fill the clipboard with the given data. + pub fn write(&mut self, data: Vec) { + if !data.is_empty() { + self.data = data; + self.line_copy = false; + self.wants_host_sync = true; + } + } + + /// See [`Clipboard::is_line_copy`]. + pub fn write_was_line_copy(&mut self, line_copy: bool) { + self.line_copy = line_copy; + } +} diff --git a/src/input.rs b/src/input.rs index 81558b5..8808e14 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,6 +6,8 @@ //! In the future this allows us to take apart the application and //! support input schemes that aren't VT, such as UEFI, or GUI. +use std::mem; + use crate::helpers::{CoordType, Point, Size}; use crate::vt; @@ -217,16 +219,6 @@ pub mod kbmod { pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000); } -/// Text input. -/// -/// "Keyboard" input is also "text" input and vice versa. -/// It differs in that text input can also be Unicode. -#[derive(Clone, Copy)] -pub struct InputText<'a> { - pub text: &'a str, - pub bracketed: bool, -} - /// Mouse input state. Up/Down, Left/Right, etc. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum InputMouseState { @@ -261,9 +253,10 @@ pub enum Input<'input> { /// Window resize event. Resize(Size), /// Text input. - /// /// Note that [`Input::Keyboard`] events can also be text. - Text(InputText<'input>), + Text(&'input str), + /// A clipboard paste. + Paste(Vec), /// Keyboard input. Keyboard(InputKey), /// Mouse input. @@ -273,6 +266,7 @@ pub enum Input<'input> { /// Parses VT sequences into input events. pub struct Parser { bracketed_paste: bool, + bracketed_paste_buf: Vec, x10_mouse_want: bool, x10_mouse_buf: [u8; 3], x10_mouse_len: usize, @@ -285,6 +279,7 @@ impl Parser { pub fn new() -> Self { Self { bracketed_paste: false, + bracketed_paste_buf: Vec::new(), x10_mouse_want: false, x10_mouse_buf: [0; 3], x10_mouse_len: 0, @@ -333,7 +328,7 @@ impl<'input> Iterator for Stream<'_, '_, 'input> { match self.stream.next()? { vt::Token::Text(text) => { - return Some(Input::Text(InputText { text, bracketed: false })); + return Some(Input::Text(text)); } vt::Token::Ctrl(ch) => match ch { '\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))), @@ -519,8 +514,13 @@ impl<'input> Stream<'_, '_, 'input> { } if end != beg { - let input = self.stream.input(); - Some(Input::Text(InputText { text: &input[beg..end], bracketed: true })) + self.parser + .bracketed_paste_buf + .extend_from_slice(&self.stream.input().as_bytes()[beg..end]); + } + + if !self.parser.bracketed_paste { + Some(Input::Paste(mem::take(&mut self.parser.bracketed_paste_buf))) } else { None } diff --git a/src/lib.rs b/src/lib.rs index db7790d..d6e64d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub mod apperr; pub mod base64; pub mod buffer; pub mod cell; +pub mod clipboard; pub mod document; pub mod framebuffer; pub mod fuzzy; diff --git a/src/tui.rs b/src/tui.rs index c4eab1e..36e1d9f 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -152,6 +152,7 @@ use std::{iter, mem, ptr, time}; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell}; use crate::cell::*; +use crate::clipboard::Clipboard; use crate::document::WriteableDocument; use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor}; use crate::hash::*; @@ -167,7 +168,6 @@ const KBMOD_FOR_WORD_NAV: InputKeyMod = type Input<'input> = input::Input<'input>; type InputKey = input::InputKey; type InputMouseState = input::InputMouseState; -type InputText<'input> = input::InputText<'input>; /// Since [`TextBuffer`] creation and management is expensive, /// we cache instances of them for reuse between frames. @@ -363,10 +363,7 @@ pub struct Tui { cached_text_buffers: Vec, /// The clipboard contents. - clipboard: Vec, - /// A counter that is incremented every time the clipboard changes. - /// Allows for tracking clipboard changes without comparing contents. - clipboard_generation: u32, + clipboard: Clipboard, settling_have: i32, settling_want: i32, @@ -416,8 +413,7 @@ impl Tui { cached_text_buffers: Vec::with_capacity(16), - clipboard: Vec::new(), - clipboard_generation: 0, + clipboard: Default::default(), settling_have: 0, settling_want: 0, @@ -490,16 +486,14 @@ impl Tui { self.framebuffer.contrasted(color) } - /// Returns the current clipboard contents. - pub fn clipboard(&self) -> &[u8] { + /// Returns the clipboard. + pub fn clipboard_ref(&self) -> &Clipboard { &self.clipboard } - /// Returns the current clipboard generation. - /// The generation changes every time the clipboard contents change. - /// This allows you to track clipboard changes. - pub fn clipboard_generation(&self) -> u32 { - self.clipboard_generation + /// Returns the clipboard (mutable). + pub fn clipboard_mut(&mut self) -> &mut Clipboard { + &mut self.clipboard } /// Starts a new frame and returns a [`Context`] for it. @@ -553,11 +547,17 @@ impl Tui { // This causes us to ignore the keyboard input here. We need a way to inform the caller over // how much of the input text we actually processed in a single frame. Or perhaps we could use // the needs_settling logic? - if !text.bracketed && text.text.len() == 1 { - let ch = text.text.as_bytes()[0]; + if text.len() == 1 { + let ch = text.as_bytes()[0]; input_keyboard = InputKey::from_ascii(ch as char) } } + Some(Input::Paste(paste)) => { + let clipboard = self.clipboard_mut(); + clipboard.write(paste); + clipboard.mark_as_synchronized(); + input_keyboard = Some(kbmod::CTRL | vk::V); + } Some(Input::Keyboard(keyboard)) => { input_keyboard = Some(keyboard); } @@ -1314,7 +1314,7 @@ pub struct Context<'a, 'input> { tui: &'a mut Tui, /// Current text input, if any. - input_text: Option>, + input_text: Option<&'input str>, /// Current keyboard input, if any. input_keyboard: Option, input_mouse_modifiers: InputKeyMod, @@ -1376,25 +1376,14 @@ impl<'a> Context<'a, '_> { self.tui.framebuffer.contrasted(color) } - /// Returns the current clipboard contents. - pub fn clipboard(&self) -> &[u8] { - self.tui.clipboard() + /// Returns the clipboard. + pub fn clipboard_ref(&self) -> &Clipboard { + &self.tui.clipboard } - /// Returns the current clipboard generation. - /// The generation changes every time the clipboard contents change. - /// This allows you to track clipboard changes. - pub fn clipboard_generation(&self) -> u32 { - self.tui.clipboard_generation() - } - - /// Sets the clipboard contents. - pub fn set_clipboard(&mut self, data: Vec) { - if !data.is_empty() { - self.tui.clipboard = data; - self.tui.clipboard_generation = self.tui.clipboard_generation.wrapping_add(1); - self.needs_rerender(); - } + /// Returns the clipboard (mutable). + pub fn clipboard_mut(&mut self) -> &mut Clipboard { + &mut self.tui.clipboard } /// Tell the UI framework that your state changed and you need another layout pass. @@ -2052,11 +2041,7 @@ impl<'a> Context<'a, '_> { /// Creates a text input field. /// Returns true if the text contents changed. - pub fn editline<'s, 'b: 's>( - &'s mut self, - classname: &'static str, - text: &'b mut dyn WriteableDocument, - ) -> bool { + pub fn editline(&mut self, classname: &'static str, text: &mut dyn WriteableDocument) -> bool { self.textarea_internal(classname, TextBufferPayload::Editline(text)) } @@ -2322,14 +2307,10 @@ impl<'a> Context<'a, '_> { return false; } - let mut write: &[u8] = b""; - let mut write_raw = false; + let mut write: &[u8] = &[]; if let Some(input) = &self.input_text { - write = input.text.as_bytes(); - write_raw = input.bracketed; - tc.preferred_column = tb.cursor_visual_pos().x; - make_cursor_visible = true; + write = input.as_bytes(); } else if let Some(input) = &self.input_keyboard { let key = input.key(); let modifiers = input.modifiers(); @@ -2643,15 +2624,12 @@ impl<'a> Context<'a, '_> { } } vk::INSERT => match modifiers { - kbmod::SHIFT => { - write = &self.tui.clipboard; - write_raw = true; - } - kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)), + kbmod::SHIFT => tb.paste(self.clipboard_ref()), + kbmod::CTRL => tb.copy(self.clipboard_mut()), _ => tb.set_overtype(!tb.is_overtype()), }, vk::DELETE => match modifiers { - kbmod::SHIFT => self.set_clipboard(tb.extract_selection(true)), + kbmod::SHIFT => tb.cut(self.clipboard_mut()), kbmod::CTRL => tb.delete(CursorMovement::Word, 1), _ => tb.delete(CursorMovement::Grapheme, 1), }, @@ -2680,18 +2658,15 @@ impl<'a> Context<'a, '_> { _ => return false, }, vk::X => match modifiers { - kbmod::CTRL => self.set_clipboard(tb.extract_selection(true)), + kbmod::CTRL => tb.cut(self.clipboard_mut()), _ => return false, }, vk::C => match modifiers { - kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)), + kbmod::CTRL => tb.copy(self.clipboard_mut()), _ => return false, }, vk::V => match modifiers { - kbmod::CTRL => { - write = &self.tui.clipboard; - write_raw = true; - } + kbmod::CTRL => tb.paste(self.clipboard_ref()), _ => return false, }, vk::Y => match modifiers { @@ -2717,8 +2692,9 @@ impl<'a> Context<'a, '_> { write = unicode::strip_newline(&write[..end]); } if !write.is_empty() { - tb.write(write, write_raw); + tb.write_canon(write); change_preferred_column = true; + make_cursor_visible = true; } if change_preferred_column {