@ -1,8 +1,13 @@
use std ::{ borrow ::Cow , collections ::HashMap , iter } ;
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.
///
@ -14,9 +19,21 @@ use crate::{document::SCRATCH_BUFFER_NAME, Editor};
/// * Selection indices (`#`): index number of each selection starting at 1
/// * Selection contents (`.`)
/// * Document path (`%`): filename of the current buffer
#[ derive(Debug, Default) ]
/// * System clipboard (`*`)
/// * Primary clipboard (`+`)
#[ derive(Debug) ]
pub struct Registers {
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 {
@ -48,6 +65,15 @@ impl Registers {
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 )
@ -59,6 +85,18 @@ impl Registers {
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! ( ) ,
} ,
) ? ;
self . inner . insert ( name , values ) ;
Ok ( ( ) )
}
_ = > {
self . inner . insert ( name , values ) ;
Ok ( ( ) )
@ -70,6 +108,27 @@ impl Registers {
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 ) ;
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 ) ;
Ok ( ( ) )
@ -88,6 +147,7 @@ impl Registers {
pub fn iter_preview ( & self ) -> impl Iterator < Item = ( char , & str ) > {
self . inner
. iter ( )
. filter ( | ( name , _ ) | ! matches! ( name , '*' | '+' ) )
. map ( | ( name , values ) | {
let preview = values
. first ( )
@ -102,6 +162,8 @@ impl Registers {
( '#' , "<selection indices>" ) ,
( '.' , "<selection contents>" ) ,
( '%' , "<document path>" ) ,
( '*' , "<system clipboard>" ) ,
( '+' , "<primary clipboard>" ) ,
]
. iter ( )
. copied ( ) ,
@ -109,15 +171,97 @@ impl Registers {
}
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 < 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,