You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helix/helix-view/src/register.rs

334 lines
11 KiB
Rust

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<char, Vec<String>>,
clipboard_provider: Box<dyn ClipboardProvider>,
pub last_search_register: char,
}
impl Default for Registers {
fn default() -> Self {
Self {
inner: Default::default(),
clipboard_provider: get_clipboard_provider(),
last_search_register: '/',
}
}
}
impl Registers {
pub fn read<'a>(&'a self, name: char, editor: &'a Editor) -> Option<RegisterValues<'a>> {
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<usize> but
// not RangeInclusive<usize>.
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<String>) -> 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<Cow<'a, str>> {
self.read(name, editor).and_then(|mut values| values.next())
}
pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> {
self.read(name, editor).and_then(|values| values.last())
}
pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
self.inner
.iter()
.filter(|(name, _)| !matches!(name, '*' | '+'))
.map(|(name, values)| {
let preview = values
.last()
.and_then(|s| s.lines().next())
.unwrap_or("<empty>");
(*name, preview)
})
.chain(
[
('_', "<empty>"),
('#', "<selection indices>"),
('.', "<selection contents>"),
('%', "<document path>"),
('+', "<system clipboard>"),
('*', "<primary clipboard>"),
]
.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",
}
)
}
}
pub fn clipboard_provider_name(&self) -> Cow<str> {
self.clipboard_provider.name()
}
}
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).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<dyn DoubleEndedExactSizeIterator<Item = Cow<'a, str>> + 'a>,
}
impl<'a> RegisterValues<'a> {
fn new(
iter: impl DoubleEndedIterator<Item = Cow<'a, str>>
+ ExactSizeIterator<Item = Cow<'a, str>>
+ 'a,
) -> Self {
Self {
iter: Box::new(iter),
}
}
}
impl<'a> Iterator for RegisterValues<'a> {
type Item = Cow<'a, str>;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
impl<'a> DoubleEndedIterator for RegisterValues<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
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<dyn DoubleEndedIterator + ExactSizeIterator>`
// 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<I: DoubleEndedIterator + ExactSizeIterator> DoubleEndedExactSizeIterator for I {}