diff options
author | Michael Davis | 2023-07-10 22:17:04 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2023-07-31 06:05:38 +0000 |
commit | 0f19f282cfa49d441f58a8e2540a6b24efe1b769 (patch) | |
tree | d0c94154c79d54ddcfb54ba49727845df535ec45 /helix-view | |
parent | 32d071a39207b9f1c1753692e93aff45fcdf12c0 (diff) |
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 <pascal.kuthe@semimod.de>
Diffstat (limited to 'helix-view')
-rw-r--r-- | helix-view/src/register.rs | 148 |
1 files changed, 146 insertions, 2 deletions
diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index 14fd3f7f..e82051dd 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<char, Vec<String>>, + clipboard_provider: Box<dyn ClipboardProvider>, +} + +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<Item = (char, &str)> { self.inner .iter() + .filter(|(name, _)| !matches!(name, '*' | '+')) .map(|(name, values)| { let preview = values .first() @@ -102,6 +162,8 @@ impl Registers { ('#', "<selection indices>"), ('.', "<selection contents>"), ('%', "<document path>"), + ('*', "<system clipboard>"), + ('+', "<primary clipboard>"), ] .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<String>>, + 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, |