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>
This commit is contained in:
Michael Davis 2023-07-10 17:17:04 -05:00 committed by Blaž Hrastnik
parent 32d071a392
commit 0f19f282cf

View File

@ -1,8 +1,13 @@
use std::{borrow::Cow, collections::HashMap, iter}; use std::{borrow::Cow, collections::HashMap, iter};
use anyhow::Result; 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. /// A key-value store for saving sets of values.
/// ///
@ -14,9 +19,21 @@
/// * Selection indices (`#`): index number of each selection starting at 1 /// * Selection indices (`#`): index number of each selection starting at 1
/// * Selection contents (`.`) /// * Selection contents (`.`)
/// * Document path (`%`): filename of the current buffer /// * Document path (`%`): filename of the current buffer
#[derive(Debug, Default)] /// * System clipboard (`*`)
/// * Primary clipboard (`+`)
#[derive(Debug)]
pub struct Registers { pub struct Registers {
inner: HashMap<char, Vec<String>>, 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 { impl Registers {
@ -48,6 +65,15 @@ pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValu
Some(RegisterValues::new(iter::once(path))) 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 _ => self
.inner .inner
.get(&name) .get(&name)
@ -59,6 +85,18 @@ pub fn write(&mut self, name: char, values: Vec<String>) -> Result<()> {
match name { match name {
'_' => Ok(()), '_' => Ok(()),
'#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")), '#' | '.' | '%' => 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); self.inner.insert(name, values);
Ok(()) Ok(())
@ -70,6 +108,27 @@ pub fn push(&mut self, name: char, value: String) -> Result<()> {
match name { match name {
'_' => Ok(()), '_' => Ok(()),
'#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")), '#' | '.' | '%' => 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); self.inner.entry(name).or_insert_with(Vec::new).push(value);
Ok(()) Ok(())
@ -88,6 +147,7 @@ pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>
pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> { pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
self.inner self.inner
.iter() .iter()
.filter(|(name, _)| !matches!(name, '*' | '+'))
.map(|(name, values)| { .map(|(name, values)| {
let preview = values let preview = values
.first() .first()
@ -102,6 +162,8 @@ pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
('#', "<selection indices>"), ('#', "<selection indices>"),
('.', "<selection contents>"), ('.', "<selection contents>"),
('%', "<document path>"), ('%', "<document path>"),
('*', "<system clipboard>"),
('+', "<primary clipboard>"),
] ]
.iter() .iter()
.copied(), .copied(),
@ -109,15 +171,97 @@ pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.clear_clipboard(ClipboardType::Clipboard);
self.clear_clipboard(ClipboardType::Selection);
self.inner.clear() self.inner.clear()
} }
pub fn remove(&mut self, name: char) -> bool { pub fn remove(&mut self, name: char) -> bool {
match name { match name {
'*' | '+' => {
self.clear_clipboard(match name {
'*' => ClipboardType::Clipboard,
'+' => ClipboardType::Selection,
_ => unreachable!(),
});
self.inner.remove(&name);
true
}
'_' | '#' | '.' | '%' => false, '_' | '#' | '.' | '%' => false,
_ => self.inner.remove(&name).is_some(), _ => 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, // This is a wrapper of an iterator that is both double ended and exact size,