mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 17:36:19 +04:00
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:
parent
32d071a392
commit
0f19f282cf
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user