on master help sign in
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>

commit0f19f282cfa49d441f58a8e2540a6b24efe1b769 authorMichael Davis <mcarsondavis@gmail.com> dateMon 10 Jul 2023, 17:17 (3 years ago) parent32d071a
1 file changed+146−2
helix-view/src/register.rs+146−2
helix-view/src/register.rs+146−2
@@ -1,8 +1,13 @@
11use std::{borrow::Cow, collections::HashMap, iter};
22
33use anyhow::Result;
4+use helix_core::NATIVE_LINE_ENDING;
45
5−use crate::{document::SCRATCH_BUFFER_NAME, Editor};
6+use crate::{
7+ clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
8+ document::SCRATCH_BUFFER_NAME,
9+ Editor,
10+};
611
712/// A key-value store for saving sets of values.
813///
@@ -14,9 +19,21 @@ use crate::{document::SCRATCH_BUFFER_NAME, Editor};
1419/// * Selection indices (`#`): index number of each selection starting at 1
1520/// * Selection contents (`.`)
1621/// * Document path (`%`): filename of the current buffer
17−#[derive(Debug, Default)]
22+/// * System clipboard (`*`)
23+/// * Primary clipboard (`+`)
24+#[derive(Debug)]
1825pub struct Registers {
1926 inner: HashMap<char, Vec<String>>,
27+ clipboard_provider: Box<dyn ClipboardProvider>,
28+}
29+
30+impl Default for Registers {
31+ fn default() -> Self {
32+ Self {
33+ inner: Default::default(),
34+ clipboard_provider: get_clipboard_provider(),
35+ }
36+ }
2037}
2138
2239impl Registers {
@@ -48,6 +65,15 @@ impl Registers {
4865
4966 Some(RegisterValues::new(iter::once(path)))
5067 }
68+ '*' | '+' => Some(read_from_clipboard(
69+ self.clipboard_provider.as_ref(),
70+ self.inner.get(&name),
71+ match name {
72+ '*' => ClipboardType::Clipboard,
73+ '+' => ClipboardType::Selection,
74+ _ => unreachable!(),
75+ },
76+ )),
5177 _ => self
5278 .inner
5379 .get(&name)
@@ -59,6 +85,18 @@ impl Registers {
5985 match name {
6086 '_' => Ok(()),
6187 '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
88+ '*' | '+' => {
89+ self.clipboard_provider.set_contents(
90+ values.join(NATIVE_LINE_ENDING.as_str()),
91+ match name {
92+ '*' => ClipboardType::Clipboard,
93+ '+' => ClipboardType::Selection,
94+ _ => unreachable!(),
95+ },
96+ )?;
97+ self.inner.insert(name, values);
98+ Ok(())
99+ }
62100 _ => {
63101 self.inner.insert(name, values);
64102 Ok(())
@@ -70,6 +108,27 @@ impl Registers {
70108 match name {
71109 '_' => Ok(()),
72110 '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")),
111+ '*' | '+' => {
112+ let clipboard_type = match name {
113+ '*' => ClipboardType::Clipboard,
114+ '+' => ClipboardType::Selection,
115+ _ => unreachable!(),
116+ };
117+ let contents = self.clipboard_provider.get_contents(clipboard_type)?;
118+ let saved_values = self.inner.entry(name).or_insert_with(Vec::new);
119+
120+ if !contents_are_saved(saved_values, &contents) {
121+ anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents");
122+ }
123+
124+ saved_values.push(value);
125+ self.clipboard_provider.set_contents(
126+ saved_values.join(NATIVE_LINE_ENDING.as_str()),
127+ clipboard_type,
128+ )?;
129+
130+ Ok(())
131+ }
73132 _ => {
74133 self.inner.entry(name).or_insert_with(Vec::new).push(value);
75134 Ok(())
@@ -88,6 +147,7 @@ impl Registers {
88147 pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
89148 self.inner
90149 .iter()
150+ .filter(|(name, _)| !matches!(name, '*' | '+'))
91151 .map(|(name, values)| {
92152 let preview = values
93153 .first()
@@ -102,6 +162,8 @@ impl Registers {
102162 ('#', "<selection indices>"),
103163 ('.', "<selection contents>"),
104164 ('%', "<document path>"),
165+ ('*', "<system clipboard>"),
166+ ('+', "<primary clipboard>"),
105167 ]
106168 .iter()
107169 .copied(),
@@ -109,15 +171,97 @@ impl Registers {
109171 }
110172
111173 pub fn clear(&mut self) {
174+ self.clear_clipboard(ClipboardType::Clipboard);
175+ self.clear_clipboard(ClipboardType::Selection);
112176 self.inner.clear()
113177 }
114178
115179 pub fn remove(&mut self, name: char) -> bool {
116180 match name {
181+ '*' | '+' => {
182+ self.clear_clipboard(match name {
183+ '*' => ClipboardType::Clipboard,
184+ '+' => ClipboardType::Selection,
185+ _ => unreachable!(),
186+ });
187+ self.inner.remove(&name);
188+
189+ true
190+ }
117191 '_' | '#' | '.' | '%' => false,
118192 _ => self.inner.remove(&name).is_some(),
119193 }
120194 }
195+
196+ fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
197+ if let Err(err) = self
198+ .clipboard_provider
199+ .set_contents("".into(), clipboard_type)
200+ {
201+ log::error!(
202+ "Failed to clear {} clipboard: {err}",
203+ match clipboard_type {
204+ ClipboardType::Clipboard => "system",
205+ ClipboardType::Selection => "primary",
206+ }
207+ )
208+ }
209+ }
210+}
211+
212+fn read_from_clipboard<'a>(
213+ provider: &dyn ClipboardProvider,
214+ saved_values: Option<&'a Vec<String>>,
215+ clipboard_type: ClipboardType,
216+) -> RegisterValues<'a> {
217+ match provider.get_contents(clipboard_type) {
218+ Ok(contents) => {
219+ // If we're pasting the same values that we just yanked, re-use
220+ // the saved values. This allows pasting multiple selections
221+ // even when yanked to a clipboard.
222+ let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) };
223+
224+ if contents_are_saved(values, &contents) {
225+ RegisterValues::new(values.iter().map(Cow::from))
226+ } else {
227+ RegisterValues::new(iter::once(contents.into()))
228+ }
229+ }
230+ Err(err) => {
231+ log::error!(
232+ "Failed to read {} clipboard: {err}",
233+ match clipboard_type {
234+ ClipboardType::Clipboard => "system",
235+ ClipboardType::Selection => "primary",
236+ }
237+ );
238+
239+ RegisterValues::new(iter::empty())
240+ }
241+ }
242+}
243+
244+fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool {
245+ let line_ending = NATIVE_LINE_ENDING.as_str();
246+ let mut values = saved_values.iter();
247+
248+ match values.next() {
249+ Some(first) if contents.starts_with(first) => {
250+ contents = &contents[first.len()..];
251+ }
252+ None if contents.is_empty() => return true,
253+ _ => return false,
254+ }
255+
256+ for value in values {
257+ if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) {
258+ contents = &contents[line_ending.len() + value.len()..];
259+ } else {
260+ return false;
261+ }
262+ }
263+
264+ true
121265}
122266
123267// This is a wrapper of an iterator that is both double ended and exact size,