Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/register.rs')
| -rw-r--r-- | helix-view/src/register.rs | 343 |
1 files changed, 272 insertions, 71 deletions
diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index b2bb53ac..6960a8e9 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -1,14 +1,107 @@ -use std::{borrow::Cow, collections::HashMap, iter}; +use std::{ + borrow::Cow, + collections::{vec_deque, HashMap, VecDeque}, + iter, + num::NonZeroUsize, +}; use anyhow::Result; -use arc_swap::access::DynAccess; use helix_core::NATIVE_LINE_ENDING; use crate::{ - clipboard::{ClipboardError, ClipboardProvider, ClipboardType}, + clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType}, + document::SCRATCH_BUFFER_NAME, Editor, }; +/// Standard registers store up to this many yanks worth of history. +/// Once a register hits this many yanks, it discards the oldest values to +/// make space for new yanks. +const MAX_REGISTER_HISTORY_LEN: usize = 100; + +#[cfg_attr(test, derive(Clone))] +#[derive(Debug, Default)] +struct Register { + /// The values held by the register. + /// + /// When yanking to a register, all values are pushed into this `VecDeque`. The length + /// of those values is stored in `length`. So each yank is stored in this flat sequence, + /// but this `VecDeque` holds the sequence of all yanks. + /// + /// This `VecDeque` should only be empty when constructing `Register` via `Default`, which + /// we do in `Registers` for simplicity. (Note that it should be impossible to `write` + /// with an empty set of values.) + /// + /// Yanks are stored least to most recent. Within each yank, values are stored in order. + values: VecDeque<String>, + /// The length of each yank into the register. + lengths: VecDeque<NonZeroUsize>, +} + +impl Register { + fn latest_value(&self) -> Option<&String> { + self.values.back() + } + + fn values(&self) -> RegisterValues<'_> { + let length = self.lengths.back().map(|len| len.get()).unwrap_or_default(); + RegisterValues::new( + self.values + .iter() + .rev() + .take(length) + .rev() + .map(|s| Cow::Borrowed(s.as_str())), + ) + } + + /// An iterator over the history of the register yanks from newest to oldest. + fn history(&self) -> RegisterHistoryIter<'_> { + RegisterHistoryIter { + lengths: self.lengths.iter().enumerate().rev(), + values: &self.values, + cursor: self.values.len(), + } + } + + fn write<I: IntoIterator<Item = String>>(&mut self, values: I) { + // If the register is full, discard the oldest yank in history. + if self.lengths.len() > MAX_REGISTER_HISTORY_LEN { + // Greater than max length implies non-empty. + let oldest_len = self.lengths.pop_front().unwrap(); + self.values.drain(..oldest_len.get()); + } + + let pre_yank_len = self.values.len(); + self.values.extend(values.into_iter()); + let yank_len = NonZeroUsize::new(self.values.len() - pre_yank_len) + .expect("writes to registers must not be empty"); + self.lengths.push_back(yank_len); + } + + fn push(&mut self, value: String) { + self.values.push_back(value); + if let Some(last_length) = self.lengths.back_mut() { + *last_length = NonZeroUsize::new(last_length.get() + 1).unwrap(); + } else { + self.lengths.push_back(NonZeroUsize::new(1).unwrap()); + } + } + + fn select_history_entry(&mut self, index: usize) { + let Some(length) = self.lengths.remove(index) else { + return; + }; + self.lengths.push_back(length); + + let start: usize = self.lengths.range(..index).map(|len| len.get()).sum(); + // NOTE: due to borrow checker limitations we need to collect the drained values. + // Ideally this would be `self.values.extend(self.values.drain(...))`. + let mut entry: VecDeque<_> = self.values.drain(start..(start + length.get())).collect(); + self.values.append(&mut entry); + } +} + /// 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 @@ -21,25 +114,26 @@ use crate::{ /// * 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 DynAccess<ClipboardProvider>>, + /// This contains non-special registers plus '+' and '*'. + inner: HashMap<char, Register>, + clipboard_provider: Box<dyn ClipboardProvider>, pub last_search_register: char, } -impl Registers { - pub fn new(clipboard_provider: Box<dyn DynAccess<ClipboardProvider>>) -> Self { +impl Default for Registers { + fn default() -> Self { Self { inner: Default::default(), - clipboard_provider, + 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())), @@ -58,11 +152,18 @@ impl Registers { Some(RegisterValues::new(doc.selection(view.id).fragments(text))) } '%' => { - let path = doc!(editor).display_name(); + 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.load(), + self.clipboard_provider.as_ref(), self.inner.get(&name), match name { '+' => ClipboardType::Clipboard, @@ -70,34 +171,24 @@ impl Registers { _ => unreachable!(), }, )), - _ => self - .inner - .get(&name) - .map(|values| RegisterValues::new(values.iter().map(Cow::from).rev())), + _ => self.inner.get(&name).map(Register::values), } } - pub fn write(&mut self, name: char, mut values: Vec<String>) -> Result<()> { + pub fn history(&self, name: char) -> Option<RegisterHistoryIter<'_>> { + match name { + '_' | '#' | '.' | '%' => None, + _ => self.inner.get(&name).map(Register::history), + } + } + + pub fn write<I: IntoIterator<Item = String>>(&mut self, name: char, values: I) -> Result<()> { match name { '_' => Ok(()), '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")), - '*' | '+' => { - self.clipboard_provider.load().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(()) + self.inner.entry(name).or_default().write(values); + self.sync_clipboard_register(name) } } } @@ -112,24 +203,20 @@ impl Registers { '*' => ClipboardType::Selection, _ => unreachable!(), }; - let contents = self - .clipboard_provider - .load() - .get_contents(&clipboard_type)?; - let saved_values = self.inner.entry(name).or_default(); + let contents = self.clipboard_provider.get_contents(clipboard_type)?; + let register = self.inner.entry(name).or_default(); - if !contents_are_saved(saved_values, &contents) { + if !contents_are_saved(register.values(), &contents) { anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents"); } - saved_values.push(value.clone()); + register.push(value.clone()); if !contents.is_empty() { value.push_str(NATIVE_LINE_ENDING.as_str()); } value.push_str(&contents); self.clipboard_provider - .load() - .set_contents(&value, clipboard_type)?; + .set_contents(value, clipboard_type)?; Ok(()) } @@ -140,22 +227,66 @@ impl Registers { } } - 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()) + /// "Selects" the index at the given index for the given register. + /// + /// Selecting an item pulls it to the front of the register's history. + /// + /// If the register is a special register other than a clipboard register ('+' or '*') + /// or if the index is out of bounds for the given register, this command is a no-op. + pub fn select_history_entry(&mut self, name: char, index: usize) -> Result<()> { + match name { + '_' | '#' | '.' | '%' => { + Err(anyhow::anyhow!("Register {name} does not support writing")) + } + _ => { + let Some(register) = self.inner.get_mut(&name) else { + return Ok(()); + }; + register.select_history_entry(index); + self.sync_clipboard_register(name) + } + } } - pub fn last<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> { - self.read(name, editor) - .and_then(|mut values| values.next_back()) + fn sync_clipboard_register(&mut self, name: char) -> Result<()> { + let clipboard_type = match name { + '+' => ClipboardType::Clipboard, + '*' => ClipboardType::Selection, + _ => return Ok(()), + }; + + let mut contents = String::new(); + for val in self.inner[&name].values() { + if !contents.is_empty() { + contents.push_str(NATIVE_LINE_ENDING.as_str()); + } + contents.push_str(&val); + } + self.clipboard_provider + .set_contents(contents, clipboard_type) + } + + /// Returns the latest value in the given register. + /// + /// The latest value is the value most recently pushed to the register when + /// using `push`, or the last value returned by the iterator passed to [write]. + pub fn latest<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> { + self.read(name, editor).and_then(|values| values.last()) + } + + /// Returns the oldest value in the given register. + /// This is the opposite of `latest`. + pub fn oldest<'a>(&'a self, name: char, editor: &'a Editor) -> Option<Cow<'a, str>> { + self.read(name, editor).and_then(|mut values| values.next()) } pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> { self.inner .iter() .filter(|(name, _)| !matches!(name, '*' | '+')) - .map(|(name, values)| { - let preview = values - .last() + .map(|(name, register)| { + let preview = register + .latest_value() .and_then(|s| s.lines().next()) .unwrap_or("<empty>"); @@ -201,8 +332,7 @@ impl Registers { fn clear_clipboard(&mut self, clipboard_type: ClipboardType) { if let Err(err) = self .clipboard_provider - .load() - .set_contents("", clipboard_type) + .set_contents("".into(), clipboard_type) { log::error!( "Failed to clear {} clipboard: {err}", @@ -214,35 +344,31 @@ impl Registers { } } - pub fn clipboard_provider_name(&self) -> String { - self.clipboard_provider.load().name().into_owned() + pub fn clipboard_provider_name(&self) -> Cow<str> { + self.clipboard_provider.name() } } fn read_from_clipboard<'a>( - provider: &ClipboardProvider, - saved_values: Option<&'a Vec<String>>, + provider: &dyn ClipboardProvider, + register: Option<&'a Register>, clipboard_type: ClipboardType, ) -> RegisterValues<'a> { - match provider.get_contents(&clipboard_type) { + 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 { + let Some(register) = register else { return RegisterValues::new(iter::once(contents.into())); }; - if contents_are_saved(values, &contents) { - RegisterValues::new(values.iter().map(Cow::from).rev()) + if contents_are_saved(register.values(), &contents) { + register.values() } else { RegisterValues::new(iter::once(contents.into())) } } - Err(ClipboardError::ReadingNotSupported) => match saved_values { - Some(values) => RegisterValues::new(values.iter().map(Cow::from).rev()), - None => RegisterValues::new(iter::empty()), - }, Err(err) => { log::error!( "Failed to read {} clipboard: {err}", @@ -257,12 +383,11 @@ fn read_from_clipboard<'a>( } } -fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool { +fn contents_are_saved(mut values: RegisterValues<'_>, 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) => { + Some(first) if contents.starts_with(&*first) => { contents = &contents[first.len()..]; } None if contents.is_empty() => return true, @@ -270,7 +395,7 @@ fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool { } for value in values { - if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) { + if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(&*value) { contents = &contents[line_ending.len() + value.len()..]; } else { return false; @@ -312,18 +437,37 @@ impl<'a> Iterator for RegisterValues<'a> { } } -impl DoubleEndedIterator for RegisterValues<'_> { +impl<'a> DoubleEndedIterator for RegisterValues<'a> { fn next_back(&mut self) -> Option<Self::Item> { self.iter.next_back() } } -impl ExactSizeIterator for RegisterValues<'_> { +impl<'a> ExactSizeIterator for RegisterValues<'a> { fn len(&self) -> usize { self.iter.len() } } +pub struct RegisterHistoryIter<'a> { + lengths: iter::Rev<iter::Enumerate<vec_deque::Iter<'a, NonZeroUsize>>>, + values: &'a VecDeque<String>, + cursor: usize, +} + +impl<'a> Iterator for RegisterHistoryIter<'a> { + // A concretion of `impl DoubleEndedExactSizeIterator<Item = &String>`. + type Item = (usize, vec_deque::Iter<'a, String>); + + fn next(&mut self) -> Option<Self::Item> { + let (index, length) = self.lengths.next()?; + let length = length.get(); + let values = self.values.range((self.cursor - length)..self.cursor); + self.cursor -= length; + Some((index, values)) + } +} + // 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 @@ -333,3 +477,60 @@ impl ExactSizeIterator for RegisterValues<'_> { trait DoubleEndedExactSizeIterator: DoubleEndedIterator + ExactSizeIterator {} impl<I: DoubleEndedIterator + ExactSizeIterator> DoubleEndedExactSizeIterator for I {} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn select_from_register_history_test() { + let mut register = Register::default(); + // Three yanks with 'abc' as the oldest, 'xyz' as the newest. + register.write(["a".into()]); + register.write(["b".into(), "c".into()]); + register.write(["d".into(), "e".into(), "f".into()]); + let history: Vec<_> = register + .history() + .map(|(index, values)| (index, values.cloned().collect::<String>())) + .collect(); + assert_eq!( + history, + [(2usize, "def".into()), (1, "bc".into()), (0, "a".into())] + ); + + let mut reg = register.clone(); + reg.select_history_entry(0); + let history: Vec<_> = reg + .history() + .map(|(_index, values)| values.cloned().collect::<String>()) + .collect(); + assert_eq!(history, ["a", "def", "bc"]); + + let mut reg = register.clone(); + reg.select_history_entry(1); + let history: Vec<_> = reg + .history() + .map(|(_index, values)| values.cloned().collect::<String>()) + .collect(); + assert_eq!(history, ["bc", "def", "a"]); + + // Choosing the current value is a no-op for regular registers. It will write the + // value to the clipboard for clipboard registers though. + let mut reg = register.clone(); + reg.select_history_entry(2); + let history: Vec<_> = reg + .history() + .map(|(_index, values)| values.cloned().collect::<String>()) + .collect(); + assert_eq!(history, ["def", "bc", "a"]); + + // Providing an index outside of the bounds of the history is a no-op. + let mut reg = register.clone(); + reg.select_history_entry(3); + let history: Vec<_> = reg + .history() + .map(|(_index, values)| values.cloned().collect::<String>()) + .collect(); + assert_eq!(history, ["def", "bc", "a"]); + } +} |