From 0f19f282cfa49d441f58a8e2540a6b24efe1b769 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 10 Jul 2023 17:17:04 -0500 Subject: [PATCH] Add system & primary clipboards as special registers These special registers join and copy the values to the clipboards with '*' corresponding to the system clipboard and '+' to the primary as they are in Vim. This also uses the trick from PR6889 to save the values in the register and re-use them without joining into one value when pasting a value which was yanked and not changed. These registers are not implemented in Kakoune but Kakoune also does not have a built-in clipboard integration. Co-authored-by: CcydtN <51289140+CcydtN@users.noreply.github.com> Co-authored-by: Pascal Kuthe --- helix-view/src/register.rs | 148 ++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 14fd3f7fa..e82051dd5 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -1,8 +1,13 @@ use std::{borrow::Cow, collections::HashMap, iter}; use anyhow::Result; +use helix_core::NATIVE_LINE_ENDING; -use crate::{document::SCRATCH_BUFFER_NAME, Editor}; +use crate::{ + clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType}, + document::SCRATCH_BUFFER_NAME, + Editor, +}; /// A key-value store for saving sets of values. /// @@ -14,9 +19,21 @@ use crate::{document::SCRATCH_BUFFER_NAME, Editor}; /// * Selection indices (`#`): index number of each selection starting at 1 /// * Selection contents (`.`) /// * Document path (`%`): filename of the current buffer -#[derive(Debug, Default)] +/// * System clipboard (`*`) +/// * Primary clipboard (`+`) +#[derive(Debug)] pub struct Registers { inner: HashMap>, + clipboard_provider: Box, +} + +impl Default for Registers { + fn default() -> Self { + Self { + inner: Default::default(), + clipboard_provider: get_clipboard_provider(), + } + } } impl Registers { @@ -48,6 +65,15 @@ impl Registers { Some(RegisterValues::new(iter::once(path))) } + '*' | '+' => Some(read_from_clipboard( + self.clipboard_provider.as_ref(), + self.inner.get(&name), + match name { + '*' => ClipboardType::Clipboard, + '+' => ClipboardType::Selection, + _ => unreachable!(), + }, + )), _ => self .inner .get(&name) @@ -59,6 +85,18 @@ impl Registers { match name { '_' => Ok(()), '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")), + '*' | '+' => { + self.clipboard_provider.set_contents( + values.join(NATIVE_LINE_ENDING.as_str()), + match name { + '*' => ClipboardType::Clipboard, + '+' => ClipboardType::Selection, + _ => unreachable!(), + }, + )?; + self.inner.insert(name, values); + Ok(()) + } _ => { self.inner.insert(name, values); Ok(()) @@ -70,6 +108,27 @@ impl Registers { match name { '_' => Ok(()), '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")), + '*' | '+' => { + let clipboard_type = match name { + '*' => ClipboardType::Clipboard, + '+' => ClipboardType::Selection, + _ => unreachable!(), + }; + let contents = self.clipboard_provider.get_contents(clipboard_type)?; + let saved_values = self.inner.entry(name).or_insert_with(Vec::new); + + if !contents_are_saved(saved_values, &contents) { + anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents"); + } + + saved_values.push(value); + self.clipboard_provider.set_contents( + saved_values.join(NATIVE_LINE_ENDING.as_str()), + clipboard_type, + )?; + + Ok(()) + } _ => { self.inner.entry(name).or_insert_with(Vec::new).push(value); Ok(()) @@ -88,6 +147,7 @@ impl Registers { pub fn iter_preview(&self) -> impl Iterator { self.inner .iter() + .filter(|(name, _)| !matches!(name, '*' | '+')) .map(|(name, values)| { let preview = values .first() @@ -102,6 +162,8 @@ impl Registers { ('#', ""), ('.', ""), ('%', ""), + ('*', ""), + ('+', ""), ] .iter() .copied(), @@ -109,15 +171,97 @@ impl Registers { } pub fn clear(&mut self) { + self.clear_clipboard(ClipboardType::Clipboard); + self.clear_clipboard(ClipboardType::Selection); self.inner.clear() } pub fn remove(&mut self, name: char) -> bool { match name { + '*' | '+' => { + self.clear_clipboard(match name { + '*' => ClipboardType::Clipboard, + '+' => ClipboardType::Selection, + _ => unreachable!(), + }); + self.inner.remove(&name); + + true + } '_' | '#' | '.' | '%' => false, _ => self.inner.remove(&name).is_some(), } } + + fn clear_clipboard(&mut self, clipboard_type: ClipboardType) { + if let Err(err) = self + .clipboard_provider + .set_contents("".into(), clipboard_type) + { + log::error!( + "Failed to clear {} clipboard: {err}", + match clipboard_type { + ClipboardType::Clipboard => "system", + ClipboardType::Selection => "primary", + } + ) + } + } +} + +fn read_from_clipboard<'a>( + provider: &dyn ClipboardProvider, + saved_values: Option<&'a Vec>, + clipboard_type: ClipboardType, +) -> RegisterValues<'a> { + match provider.get_contents(clipboard_type) { + Ok(contents) => { + // If we're pasting the same values that we just yanked, re-use + // the saved values. This allows pasting multiple selections + // even when yanked to a clipboard. + let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) }; + + if contents_are_saved(values, &contents) { + RegisterValues::new(values.iter().map(Cow::from)) + } else { + RegisterValues::new(iter::once(contents.into())) + } + } + Err(err) => { + log::error!( + "Failed to read {} clipboard: {err}", + match clipboard_type { + ClipboardType::Clipboard => "system", + ClipboardType::Selection => "primary", + } + ); + + RegisterValues::new(iter::empty()) + } + } +} + +fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool { + let line_ending = NATIVE_LINE_ENDING.as_str(); + let mut values = saved_values.iter(); + + match values.next() { + Some(first) if contents.starts_with(first) => { + contents = &contents[first.len()..]; + } + None if contents.is_empty() => return true, + _ => return false, + } + + for value in values { + if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) { + contents = &contents[line_ending.len() + value.len()..]; + } else { + return false; + } + } + + true } // This is a wrapper of an iterator that is both double ended and exact size,