use std::fmt::{Debug, Display, Formatter};
use crate::text_stream::TextByteStream;
use super::{OperatorTarget, TargetKind};
pub const DEFAULT_REGISTER_TEXT_LIMIT: usize = 1024 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RegisterName {
Unnamed,
Yank0,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RegisterKind {
Characterwise,
Linewise,
}
impl TryFrom<TargetKind> for RegisterKind {
type Error = RegisterWriteError;
fn try_from(kind: TargetKind) -> Result<Self, Self::Error> {
match kind {
TargetKind::Characterwise => Ok(Self::Characterwise),
TargetKind::Linewise => Ok(Self::Linewise),
TargetKind::Blockwise => Err(RegisterWriteError::UnsupportedTargetKind(kind)),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct RegisterText {
text: String,
kind: RegisterKind,
}
impl RegisterText {
#[must_use]
pub fn new(text: impl Into<String>, kind: RegisterKind) -> Self {
Self {
text: text.into(),
kind,
}
}
pub fn from_target(
stream: &TextByteStream,
target: OperatorTarget,
) -> Result<Self, RegisterWriteError> {
let kind = RegisterKind::try_from(target.kind())?;
let range = stream
.validate_validated_range(target.range().validated_text_range())
.map_err(|_error| RegisterWriteError::StaleTarget)?
.as_range();
Ok(Self::new(&stream.as_str()[range], kind))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn into_string(self) -> String {
self.text
}
#[must_use]
pub const fn kind(&self) -> RegisterKind {
self.kind
}
#[must_use]
pub const fn byte_len(&self) -> usize {
self.text.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
impl Debug for RegisterText {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RegisterText")
.field("shape", &RegisterTextShape::from(self))
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RegisterTextShape {
kind: RegisterKind,
byte_len: usize,
}
impl RegisterTextShape {
#[must_use]
pub fn from_register_text(text: &RegisterText) -> Self {
Self::from(text)
}
#[must_use]
pub const fn kind(self) -> RegisterKind {
self.kind
}
#[must_use]
pub const fn byte_len(self) -> usize {
self.byte_len
}
}
impl From<&RegisterText> for RegisterTextShape {
fn from(text: &RegisterText) -> Self {
Self {
kind: text.kind(),
byte_len: text.byte_len(),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct RegisterBank {
unnamed: Option<RegisterText>,
yank0: Option<RegisterText>,
max_register_text_bytes: usize,
}
impl Default for RegisterBank {
fn default() -> Self {
Self::bounded(DEFAULT_REGISTER_TEXT_LIMIT)
}
}
impl RegisterBank {
#[must_use]
pub const fn bounded(max_register_text_bytes: usize) -> Self {
Self {
unnamed: None,
yank0: None,
max_register_text_bytes,
}
}
#[must_use]
pub const fn get(&self, name: RegisterName) -> Option<&RegisterText> {
match name {
RegisterName::Unnamed => self.unnamed(),
RegisterName::Yank0 => self.yank0(),
}
}
#[must_use]
pub const fn unnamed(&self) -> Option<&RegisterText> {
self.unnamed.as_ref()
}
#[must_use]
pub const fn yank0(&self) -> Option<&RegisterText> {
self.yank0.as_ref()
}
pub fn write_yank(
&mut self,
stream: &TextByteStream,
target: OperatorTarget,
) -> Result<(), RegisterWriteError> {
let register_text = RegisterText::from_target(stream, target)?;
self.validate_text_limit(®ister_text)?;
self.yank0 = Some(register_text.clone());
self.unnamed = Some(register_text);
Ok(())
}
pub fn write_delete(
&mut self,
stream: &TextByteStream,
target: OperatorTarget,
) -> Result<(), RegisterWriteError> {
let register_text = RegisterText::from_target(stream, target)?;
self.validate_text_limit(®ister_text)?;
self.unnamed = Some(register_text);
Ok(())
}
const fn validate_text_limit(&self, text: &RegisterText) -> Result<(), RegisterWriteError> {
if text.byte_len() > self.max_register_text_bytes {
return Err(RegisterWriteError::TextTooLarge {
byte_len: text.byte_len(),
max_byte_len: self.max_register_text_bytes,
});
}
Ok(())
}
}
impl Debug for RegisterBank {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RegisterBank")
.field("shape", &RegisterBankShape::from(self))
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RegisterBankShape {
unnamed: Option<RegisterTextShape>,
yank0: Option<RegisterTextShape>,
}
impl RegisterBankShape {
#[must_use]
pub fn from_register_bank(bank: &RegisterBank) -> Self {
Self::from(bank)
}
#[must_use]
pub const fn unnamed(self) -> Option<RegisterTextShape> {
self.unnamed
}
#[must_use]
pub const fn yank0(self) -> Option<RegisterTextShape> {
self.yank0
}
}
impl From<&RegisterBank> for RegisterBankShape {
fn from(bank: &RegisterBank) -> Self {
Self {
unnamed: bank.unnamed().map(RegisterTextShape::from),
yank0: bank.yank0().map(RegisterTextShape::from),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum RegisterWriteError {
UnsupportedTargetKind(TargetKind),
StaleTarget,
TextTooLarge {
byte_len: usize,
max_byte_len: usize,
},
}
impl Display for RegisterWriteError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedTargetKind(_) => {
formatter.write_str("register target kind is unsupported")
}
Self::StaleTarget => formatter.write_str("register target is stale"),
Self::TextTooLarge { .. } => {
formatter.write_str("register text exceeds retention limit")
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
RegisterBank, RegisterBankShape, RegisterKind, RegisterName, RegisterText,
RegisterTextShape, RegisterWriteError,
};
use crate::text_stream::TextByteStream;
use crate::vim::{OperatorTarget, TargetKind};
use proptest::prelude::*;
use std::ops::Range;
#[test]
fn register_text_debug_redacts_contents() {
let text = RegisterText::new("secret-buffer-text", RegisterKind::Characterwise);
let debug = format!("{text:?}");
assert!(debug.contains("RegisterText"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("secret-buffer-text"));
}
#[test]
fn yank_writes_unnamed_and_yank0() {
let text = "alpha\nbeta\n";
let stream = TextByteStream::new(text);
let target = OperatorTarget::characterwise(&stream, 0.."alpha".len()).unwrap();
let mut bank = RegisterBank::default();
bank.write_yank(&stream, target).unwrap();
assert_eq!(
bank.get(RegisterName::Unnamed).map(RegisterText::as_str),
Some("alpha")
);
assert_eq!(
bank.get(RegisterName::Yank0).map(RegisterText::as_str),
Some("alpha")
);
assert_eq!(
RegisterBankShape::from_register_bank(&bank).unnamed(),
Some(RegisterTextShape {
kind: RegisterKind::Characterwise,
byte_len: "alpha".len(),
})
);
}
#[test]
fn delete_writes_unnamed_without_replacing_yank0() {
let text = "alpha\nbeta\n";
let stream = TextByteStream::new(text);
let yank = OperatorTarget::characterwise(&stream, 0.."alpha".len()).unwrap();
let delete =
OperatorTarget::linewise(&stream, "alpha\n".len().."alpha\nbeta\n".len()).unwrap();
let mut bank = RegisterBank::default();
bank.write_yank(&stream, yank).unwrap();
bank.write_delete(&stream, delete).unwrap();
assert_eq!(
bank.get(RegisterName::Unnamed).map(RegisterText::as_str),
Some("beta\n")
);
assert_eq!(
bank.get(RegisterName::Unnamed).map(RegisterText::kind),
Some(RegisterKind::Linewise)
);
assert_eq!(
bank.get(RegisterName::Yank0).map(RegisterText::as_str),
Some("alpha")
);
}
#[test]
fn stale_target_is_rejected_without_copying_text() {
let original = "alpha";
let changed = "λ";
let original_stream = TextByteStream::new(original);
let changed_stream = TextByteStream::new(changed);
let target = OperatorTarget::characterwise(&original_stream, 0..original.len()).unwrap();
assert_eq!(
RegisterText::from_target(&changed_stream, target),
Err(RegisterWriteError::StaleTarget)
);
}
#[test]
fn blockwise_targets_are_reserved() {
assert_eq!(
RegisterKind::try_from(TargetKind::Blockwise),
Err(RegisterWriteError::UnsupportedTargetKind(
TargetKind::Blockwise
))
);
}
#[test]
fn register_bank_rejects_entries_over_limit_without_replacing_existing_text() {
let stream = TextByteStream::new("alpha beta");
let small = OperatorTarget::characterwise(&stream, 0.."alpha".len()).unwrap();
let large = OperatorTarget::characterwise(&stream, 0..stream.as_str().len()).unwrap();
let mut bank = RegisterBank::bounded("alpha".len());
bank.write_yank(&stream, small).unwrap();
assert_eq!(
bank.write_delete(&stream, large),
Err(RegisterWriteError::TextTooLarge {
byte_len: stream.as_str().len(),
max_byte_len: "alpha".len(),
})
);
assert_eq!(
bank.get(RegisterName::Unnamed).map(RegisterText::as_str),
Some("alpha")
);
}
proptest! {
#[test]
fn target_copy_preserves_any_valid_characterwise_slice(
(text, left, right) in text_with_boundary_offsets()
) {
let stream = TextByteStream::new(text.clone());
let range = boundary_range(&text, left, right);
let target = OperatorTarget::characterwise(&stream, range.clone()).unwrap();
let register_text = RegisterText::from_target(&stream, target).unwrap();
prop_assert_eq!(register_text.as_str(), &text[range.clone()]);
prop_assert_eq!(register_text.kind(), RegisterKind::Characterwise);
prop_assert_eq!(register_text.byte_len(), range.end - range.start);
}
#[test]
fn yank_registers_preserve_any_valid_characterwise_slice_and_shape(
(text, left, right) in text_with_boundary_offsets()
) {
let stream = TextByteStream::new(text.clone());
let range = boundary_range(&text, left, right);
let target = OperatorTarget::characterwise(&stream, range.clone()).unwrap();
let mut bank = RegisterBank::default();
bank.write_yank(&stream, target).unwrap();
let expected = &text[range.clone()];
prop_assert_eq!(
bank.get(RegisterName::Unnamed).map(RegisterText::as_str),
Some(expected)
);
prop_assert_eq!(
bank.get(RegisterName::Yank0).map(RegisterText::as_str),
Some(expected)
);
prop_assert_eq!(
RegisterBankShape::from_register_bank(&bank).unnamed(),
Some(RegisterTextShape {
kind: RegisterKind::Characterwise,
byte_len: range.end - range.start,
})
);
}
}
fn text_with_boundary_offsets() -> impl Strategy<Value = (String, usize, usize)> {
prop::collection::vec(any::<char>(), 0..64).prop_flat_map(|characters| {
let text = characters.into_iter().collect::<String>();
let boundary_count = text.chars().count() + 1;
(Just(text), 0..boundary_count, 0..boundary_count)
})
}
fn boundary_range(text: &str, left: usize, right: usize) -> Range<usize> {
let boundaries = text
.char_indices()
.map(|(index, _character)| index)
.chain(std::iter::once(text.len()))
.collect::<Vec<_>>();
let start = left.min(right);
let end = left.max(right);
boundaries[start]..boundaries[end]
}
}