Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the OS clipboard only for explicit cut/copy/paste operations #761

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions src/core_editor/clip_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,23 @@ impl Clipboard for LocalClipboard {
}
}

/// Creates a local clipboard
pub fn get_local_clipboard() -> Box<dyn Clipboard> {
Box::new(LocalClipboard::new())
}

#[cfg(feature = "system_clipboard")]
pub use system_clipboard::SystemClipboard;

/// Creates a handle for the OS clipboard
#[cfg(feature = "system_clipboard")]
/// Helper to get a clipboard based on the `system_clipboard` feature flag:
///
/// Enabled -> [`SystemClipboard`], which talks to the system. If the system clipboard can't be
/// accessed, it will default to [`LocalClipboard`].
///
/// Disabled -> [`LocalClipboard`], which supports cutting and pasting limited to the [`crate::Reedline`] instance
pub fn get_default_clipboard() -> Box<dyn Clipboard> {
pub fn get_system_clipboard() -> Box<dyn Clipboard> {
SystemClipboard::new().map_or_else(
|_e| Box::new(LocalClipboard::new()) as Box<dyn Clipboard>,
|cb| Box::new(cb),
)
}

#[cfg(not(feature = "system_clipboard"))]
/// Helper to get a clipboard based on the `system_clipboard` feature flag:
///
/// Enabled -> `SystemClipboard`, which talks to the system. If the system clipboard can't be
/// accessed, it will default to [`LocalClipboard`].
///
/// Disabled -> [`LocalClipboard`], which supports cutting and pasting limited to the [`crate::Reedline`] instance
pub fn get_default_clipboard() -> Box<dyn Clipboard> {
Box::new(LocalClipboard::new())
}

#[cfg(feature = "system_clipboard")]
mod system_clipboard {
use super::*;
Expand Down Expand Up @@ -124,10 +113,31 @@ mod system_clipboard {

#[cfg(test)]
mod tests {
use super::{get_default_clipboard, ClipboardMode};
#[cfg(feature = "system_clipboard")]
use super::get_system_clipboard;
use super::{get_local_clipboard, ClipboardMode};
#[test]
fn reads_back_local() {
let mut cb = get_local_clipboard();
// If the system clipboard is used we want to persist it for the user
let previous_state = cb.get().0;

// Actual test
cb.set("test", ClipboardMode::Normal);
assert_eq!(cb.len(), 4);
assert_eq!(cb.get().0, "test".to_owned());
cb.clear();
assert_eq!(cb.get().0, String::new());

// Restore!

cb.set(&previous_state, ClipboardMode::Normal);
}

#[cfg(feature = "system_clipboard")]
#[test]
fn reads_back() {
let mut cb = get_default_clipboard();
fn reads_back_system() {
let mut cb = get_system_clipboard();
// If the system clipboard is used we want to persist it for the user
let previous_state = cb.get().0;

Expand Down
111 changes: 90 additions & 21 deletions src/core_editor/editor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use super::{edit_stack::EditStack, Clipboard, ClipboardMode, LineBuffer};
#[cfg(feature = "system_clipboard")]
use crate::core_editor::get_system_clipboard;
use crate::enums::{EditType, UndoBehavior};
use crate::{core_editor::get_default_clipboard, EditCommand};
use crate::{core_editor::get_local_clipboard, EditCommand};
use std::ops::DerefMut;

/// Stateful editor executing changes to the underlying [`LineBuffer`]
///
Expand All @@ -9,6 +12,8 @@ use crate::{core_editor::get_default_clipboard, EditCommand};
pub struct Editor {
line_buffer: LineBuffer,
cut_buffer: Box<dyn Clipboard>,
#[cfg(feature = "system_clipboard")]
system_clipboard: Box<dyn Clipboard>,
edit_stack: EditStack<LineBuffer>,
last_undo_behavior: UndoBehavior,
selection_anchor: Option<usize>,
Expand All @@ -18,7 +23,9 @@ impl Default for Editor {
fn default() -> Self {
Editor {
line_buffer: LineBuffer::new(),
cut_buffer: get_default_clipboard(),
cut_buffer: get_local_clipboard(),
#[cfg(feature = "system_clipboard")]
system_clipboard: get_system_clipboard(),
edit_stack: EditStack::new(),
last_undo_behavior: UndoBehavior::CreateUndoPoint,
selection_anchor: None,
Expand Down Expand Up @@ -110,8 +117,15 @@ impl Editor {
self.move_left_until_char(*c, true, true, *select)
}
EditCommand::SelectAll => self.select_all(),
EditCommand::CutSelection => self.cut_selection(),
EditCommand::CopySelection => self.copy_selection(),
EditCommand::CutSelection => self.cut_selection_to_cut_buffer(),
EditCommand::CopySelection => self.copy_selection_to_cut_buffer(),
EditCommand::Paste => self.paste_cut_buffer(),
#[cfg(feature = "system_clipboard")]
EditCommand::CutSelectionSystem => self.cut_selection_to_system(),
#[cfg(feature = "system_clipboard")]
EditCommand::CopySelectionSystem => self.copy_selection_to_system(),
#[cfg(feature = "system_clipboard")]
EditCommand::PasteSystem => self.paste_from_system(),
}
if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) {
self.selection_anchor = None;
Expand Down Expand Up @@ -390,21 +404,7 @@ impl Editor {

fn insert_cut_buffer_before(&mut self) {
self.delete_selection();
match self.cut_buffer.get() {
(content, ClipboardMode::Normal) => {
self.line_buffer.insert_str(&content);
}
(mut content, ClipboardMode::Lines) => {
// TODO: Simplify that?
self.line_buffer.move_to_line_start();
self.line_buffer.move_line_up();
if !content.ends_with('\n') {
// TODO: Make sure platform requirements are met
content.push('\n');
}
self.line_buffer.insert_str(&content);
}
}
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut())
}

fn insert_cut_buffer_after(&mut self) {
Expand Down Expand Up @@ -526,7 +526,17 @@ impl Editor {
self.line_buffer.move_to_end();
}

fn cut_selection(&mut self) {
#[cfg(feature = "system_clipboard")]
fn cut_selection_to_system(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.system_clipboard.set(cut_slice, ClipboardMode::Normal);
self.line_buffer.clear_range_safe(start, end);
self.selection_anchor = None;
}
}

fn cut_selection_to_cut_buffer(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.cut_buffer.set(cut_slice, ClipboardMode::Normal);
Expand All @@ -535,7 +545,15 @@ impl Editor {
}
}

fn copy_selection(&mut self) {
#[cfg(feature = "system_clipboard")]
fn copy_selection_to_system(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.system_clipboard.set(cut_slice, ClipboardMode::Normal);
}
}

fn copy_selection_to_cut_buffer(&mut self) {
if let Some((start, end)) = self.get_selection() {
let cut_slice = &self.line_buffer.get_buffer()[start..end];
self.cut_buffer.set(cut_slice, ClipboardMode::Normal);
Expand Down Expand Up @@ -619,6 +637,35 @@ impl Editor {
self.delete_selection();
self.line_buffer.insert_newline();
}

#[cfg(feature = "system_clipboard")]
fn paste_from_system(&mut self) {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.system_clipboard.deref_mut());
}

fn paste_cut_buffer(&mut self) {
self.delete_selection();
insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut());
}
}

fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) {
match clipboard.get() {
(content, ClipboardMode::Normal) => {
line_buffer.insert_str(&content);
}
(mut content, ClipboardMode::Lines) => {
// TODO: Simplify that?
line_buffer.move_to_line_start();
line_buffer.move_line_up();
if !content.ends_with('\n') {
// TODO: Make sure platform requirements are met
content.push('\n');
}
line_buffer.insert_str(&content);
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -829,4 +876,26 @@ mod test {
editor.run_edit_command(&EditCommand::Undo);
assert_eq!(editor.get_buffer(), "This \r\n is a test");
}
#[cfg(feature = "system_clipboard")]
mod without_system_clipboard {
use super::*;
#[test]
fn test_cut_selection_system() {
let mut editor = editor_with("This is a test!");
editor.selection_anchor = Some(editor.line_buffer.len());
editor.line_buffer.set_insertion_point(0);
editor.run_edit_command(&EditCommand::CutSelectionSystem);
assert!(editor.line_buffer.get_buffer().is_empty());
}
#[test]
fn test_copypaste_selection_system() {
let s = "This is a test!";
let mut editor = editor_with(s);
editor.selection_anchor = Some(editor.line_buffer.len());
editor.line_buffer.set_insertion_point(0);
editor.run_edit_command(&EditCommand::CopySelectionSystem);
editor.run_edit_command(&EditCommand::PasteSystem);
pretty_assertions::assert_eq!(editor.line_buffer.len(), s.len() * 2);
}
}
}
4 changes: 3 additions & 1 deletion src/core_editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod edit_stack;
mod editor;
mod line_buffer;

pub(crate) use clip_buffer::{get_default_clipboard, Clipboard, ClipboardMode};
#[cfg(feature = "system_clipboard")]
pub(crate) use clip_buffer::get_system_clipboard;
pub(crate) use clip_buffer::{get_local_clipboard, Clipboard, ClipboardMode};
pub use editor::Editor;
pub use line_buffer::LineBuffer;
9 changes: 6 additions & 3 deletions src/edit_mode/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,20 +214,23 @@ pub fn add_common_edit_bindings(kb: &mut Keybindings) {
// Base commands should not affect cut buffer
kb.add_binding(KM::CONTROL, KC::Char('h'), edit_bind(EC::Backspace));
kb.add_binding(KM::CONTROL, KC::Char('w'), edit_bind(EC::BackspaceWord));
#[cfg(feature = "system_clipboard")]
kb.add_binding(
KM::CONTROL | KM::SHIFT,
KC::Char('x'),
edit_bind(EC::CutSelection),
edit_bind(EC::CutSelectionSystem),
);
#[cfg(feature = "system_clipboard")]
kb.add_binding(
KM::CONTROL | KM::SHIFT,
KC::Char('c'),
edit_bind(EC::CopySelection),
edit_bind(EC::CopySelectionSystem),
);
#[cfg(feature = "system_clipboard")]
kb.add_binding(
KM::CONTROL | KM::SHIFT,
KC::Char('v'),
edit_bind(EC::PasteCutBufferBefore),
edit_bind(EC::PasteSystem),
);
}

Expand Down
36 changes: 32 additions & 4 deletions src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum Signal {
/// Editing actions which can be mapped to key bindings.
///
/// Executed by `Reedline::run_edit_commands()`
#[non_exhaustive]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, EnumIter)]
pub enum EditCommand {
/// Move to the start of the buffer
Expand Down Expand Up @@ -257,11 +258,26 @@ pub enum EditCommand {
/// Select whole input buffer
SelectAll,

/// Cut selection
/// Cut selection to local buffer
CutSelection,

/// Copy selection
/// Copy selection to local buffer
CopySelection,

/// Paste content from local buffer at the current cursor position
Paste,

/// Cut selection to system clipboard
#[cfg(feature = "system_clipboard")]
CutSelectionSystem,

/// Copy selection to system clipboard
#[cfg(feature = "system_clipboard")]
CopySelectionSystem,

/// Paste content from system clipboard at the current cursor position
#[cfg(feature = "system_clipboard")]
PasteSystem,
}

impl Display for EditCommand {
Expand Down Expand Up @@ -348,6 +364,13 @@ impl Display for EditCommand {
EditCommand::SelectAll => write!(f, "SelectAll"),
EditCommand::CutSelection => write!(f, "CutSelection"),
EditCommand::CopySelection => write!(f, "CopySelection"),
EditCommand::Paste => write!(f, "Paste"),
#[cfg(feature = "system_clipboard")]
EditCommand::CutSelectionSystem => write!(f, "CutSelectionSystem"),
#[cfg(feature = "system_clipboard")]
EditCommand::CopySelectionSystem => write!(f, "CopySelectionSystem"),
#[cfg(feature = "system_clipboard")]
EditCommand::PasteSystem => write!(f, "PasteSystem"),
}
}
}
Expand Down Expand Up @@ -380,7 +403,6 @@ impl EditCommand {
}

EditCommand::SelectAll => EditType::MoveCursor { select: true },

// Text edits
EditCommand::InsertChar(_)
| EditCommand::Backspace
Expand Down Expand Up @@ -418,11 +440,17 @@ impl EditCommand {
| EditCommand::CutRightBefore(_)
| EditCommand::CutLeftUntil(_)
| EditCommand::CutLeftBefore(_)
| EditCommand::CutSelection => EditType::EditText,
| EditCommand::CutSelection
| EditCommand::Paste => EditType::EditText,

#[cfg(feature = "system_clipboard")] // Sadly cfg attributes in patterns don't work
EditCommand::CutSelectionSystem | EditCommand::PasteSystem => EditType::EditText,

EditCommand::Undo | EditCommand::Redo => EditType::UndoRedo,

EditCommand::CopySelection => EditType::NoOp,
#[cfg(feature = "system_clipboard")]
EditCommand::CopySelectionSystem => EditType::NoOp,
}
}
}
Expand Down
Loading