summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Davis2023-07-10 22:17:04 +0000
committerBlaž Hrastnik2023-07-31 06:05:38 +0000
commit0f19f282cfa49d441f58a8e2540a6b24efe1b769 (patch)
treed0c94154c79d54ddcfb54ba49727845df535ec45
parent32d071a39207b9f1c1753692e93aff45fcdf12c0 (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>
-rw-r--r--helix-view/src/register.rs148
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,