use std::{borrow::Cow, collections::HashMap, iter}; use anyhow::Result; use helix_core::NATIVE_LINE_ENDING; use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType}, document::SCRATCH_BUFFER_NAME, Editor, }; /// A key-value store for saving sets of values. /// /// Each register corresponds to a `char`. Most chars can be used to store any set of /// values but a few chars are "special registers". Special registers have unique /// behaviors when read or written to: /// /// * Black hole (`_`): all values read and written are discarded /// * Selection indices (`#`): index number of each selection starting at 1 /// * Selection contents (`.`) /// * Document path (`%`): filename of the current buffer /// * System clipboard (`*`) /// * Primary clipboard (`+`) #[derive(Debug)] pub struct Registers { /// The mapping of register to values. /// Values are stored in reverse order when inserted with `Registers::write`. /// The order is reversed again in `Registers::read`. This allows us to /// efficiently prepend new values in `Registers::push`. inner: HashMap>, clipboard_provider: Box, } impl Default for Registers { fn default() -> Self { Self { inner: Default::default(), clipboard_provider: get_clipboard_provider(), } } } impl Registers { pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option> { match name { '_' => Some(RegisterValues::new(iter::empty())), '#' => { let (view, doc) = current_ref!(editor); let selections = doc.selection(view.id).len(); // ExactSizeIterator is implemented for Range but // not RangeInclusive. Some(RegisterValues::new( (0..selections).map(|i| (i + 1).to_string().into()), )) } '.' => { let (view, doc) = current_ref!(editor); let text = doc.text().slice(..); Some(RegisterValues::new(doc.selection(view.id).fragments(text))) } '%' => { let doc = doc!(editor); let path = doc .path() .as_ref() .map(|p| p.to_string_lossy()) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); 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) .map(|values| RegisterValues::new(values.iter().map(Cow::from).rev())), } } pub fn write(&mut self, name: char, mut values: Vec) -> Result<()> { 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!(), }, )?; values.reverse(); self.inner.insert(name, values); Ok(()) } _ => { values.reverse(); self.inner.insert(name, values); Ok(()) } } } pub fn push(&mut self, name: char, mut value: String) -> Result<()> { 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.clone()); if !contents.is_empty() { value.push_str(NATIVE_LINE_ENDING.as_str()); } value.push_str(&contents); self.clipboard_provider .set_contents(value, clipboard_type)?; Ok(()) } _ => { self.inner.entry(name).or_insert_with(Vec::new).push(value); Ok(()) } } } pub fn first<'a>(&'a self, name: char, editor: &'a Editor) -> Option> { self.read(name, editor).and_then(|mut values| values.next()) } pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option> { self.read(name, editor).and_then(|values| values.last()) } pub fn iter_preview(&self) -> impl Iterator { self.inner .iter() .filter(|(name, _)| !matches!(name, '*' | '+')) .map(|(name, values)| { let preview = values .last() .and_then(|s| s.lines().next()) .unwrap_or(""); (*name, preview) }) .chain( [ ('_', ""), ('#', ""), ('.', ""), ('%', ""), ('*', ""), ('+', ""), ] .iter() .copied(), ) } 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>, 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).rev()) } 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().rev(); 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, // and can return either owned or borrowed values. Regular registers can // return borrowed values while some special registers need to return owned // values. pub struct RegisterValues<'a> { iter: Box> + 'a>, } impl<'a> RegisterValues<'a> { fn new( iter: impl DoubleEndedIterator> + ExactSizeIterator> + 'a, ) -> Self { Self { iter: Box::new(iter), } } } impl<'a> Iterator for RegisterValues<'a> { type Item = Cow<'a, str>; fn next(&mut self) -> Option { self.iter.next() } fn size_hint(&self) -> (usize, Option) { self.iter.size_hint() } } impl<'a> DoubleEndedIterator for RegisterValues<'a> { fn next_back(&mut self) -> Option { self.iter.next_back() } } impl<'a> ExactSizeIterator for RegisterValues<'a> { fn len(&self) -> usize { self.iter.len() } } // Each RegisterValues iterator is both double ended and exact size. We can't // type RegisterValues as `Box` // because only one non-auto trait is allowed in trait objects. So we need to // create a new trait that covers both. `RegisterValues` wraps that type so that // trait only needs to live in this module and not be imported for all register // callsites. trait DoubleEndedExactSizeIterator: DoubleEndedIterator + ExactSizeIterator {} impl DoubleEndedExactSizeIterator for I {}