use bevy::prelude::Resource;
use std::{
fmt::{Debug, Display, Formatter},
ops::Range,
str,
sync::atomic::{AtomicU64, Ordering},
};
static NEXT_STREAM_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TextRevision(u64);
impl TextRevision {
pub const ZERO: Self = Self(0);
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[must_use]
pub const fn next(self) -> Option<Self> {
match self.0.checked_add(1) {
Some(next) => Some(Self(next)),
None => None,
}
}
}
impl Display for TextRevision {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, formatter)
}
}
impl PartialEq<u64> for TextRevision {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
}
}
#[cfg(test)]
impl From<u64> for TextRevision {
fn from(revision: u64) -> Self {
Self(revision)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TextStreamId(u64);
impl TextStreamId {
fn fresh() -> Self {
Self(NEXT_STREAM_ID.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Eq, Resource)]
pub struct TextByteStream {
text: String,
revision: TextRevision,
id: TextStreamId,
}
impl Debug for TextByteStream {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TextByteStream")
.field("shape", &TextByteStreamShape::from(self))
.finish()
}
}
impl Clone for TextByteStream {
fn clone(&self) -> Self {
Self {
text: self.text.clone(),
revision: self.revision,
id: TextStreamId::fresh(),
}
}
}
impl Default for TextByteStream {
fn default() -> Self {
Self::new("")
}
}
impl PartialEq for TextByteStream {
fn eq(&self, other: &Self) -> bool {
self.text == other.text && self.revision == other.revision
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextByteStreamShape {
byte_len: usize,
revision: TextRevision,
id: TextStreamId,
}
impl TextByteStreamShape {
#[must_use]
pub const fn byte_len(self) -> usize {
self.byte_len
}
#[must_use]
pub const fn revision(self) -> TextRevision {
self.revision
}
#[must_use]
pub const fn id(self) -> TextStreamId {
self.id
}
}
impl From<&TextByteStream> for TextByteStreamShape {
fn from(stream: &TextByteStream) -> Self {
Self {
byte_len: stream.as_bytes().len(),
revision: stream.revision(),
id: stream.id(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextRange {
start: usize,
end: usize,
}
impl TextRange {
#[must_use]
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
#[must_use]
pub const fn start(self) -> usize {
self.start
}
#[must_use]
pub const fn end(self) -> usize {
self.end
}
#[must_use]
pub const fn as_range(self) -> Range<usize> {
self.start..self.end
}
}
impl From<Range<usize>> for TextRange {
fn from(range: Range<usize>) -> Self {
Self {
start: range.start,
end: range.end,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidatedTextRange {
range: TextRange,
stream_id: TextStreamId,
revision: TextRevision,
}
impl ValidatedTextRange {
const fn new(range: TextRange, stream_id: TextStreamId, revision: TextRevision) -> Self {
Self {
range,
stream_id,
revision,
}
}
#[must_use]
pub const fn start(self) -> usize {
self.range.start()
}
#[must_use]
pub const fn end(self) -> usize {
self.range.end()
}
#[must_use]
pub const fn revision(self) -> TextRevision {
self.revision
}
#[must_use]
pub const fn stream_id(self) -> TextStreamId {
self.stream_id
}
#[must_use]
pub const fn as_range(self) -> Range<usize> {
self.range.as_range()
}
}
impl TextByteStream {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
revision: TextRevision::ZERO,
id: TextStreamId::fresh(),
}
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, TextStreamError> {
String::from_utf8(bytes)
.map(Self::new)
.map_err(|error| TextStreamError::InvalidUtf8 {
valid_up_to: error.utf8_error().valid_up_to(),
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8] {
self.text.as_bytes()
}
#[must_use]
pub const fn revision(&self) -> TextRevision {
self.revision
}
#[must_use]
pub const fn id(&self) -> TextStreamId {
self.id
}
pub fn replace_all(&mut self, text: impl Into<String>) {
self.text = text.into();
self.bump_revision();
}
pub fn replace_all_bytes(&mut self, bytes: Vec<u8>) -> Result<(), TextStreamError> {
self.text = String::from_utf8(bytes).map_err(|error| TextStreamError::InvalidUtf8 {
valid_up_to: error.utf8_error().valid_up_to(),
})?;
self.bump_revision();
Ok(())
}
pub fn insert_str(&mut self, byte_index: usize, text: &str) -> Result<(), TextStreamError> {
self.validate_boundary(byte_index)?;
self.text.insert_str(byte_index, text);
self.bump_revision();
Ok(())
}
pub fn insert_bytes(&mut self, byte_index: usize, bytes: &[u8]) -> Result<(), TextStreamError> {
let text = str::from_utf8(bytes).map_err(|error| TextStreamError::InvalidUtf8 {
valid_up_to: error.valid_up_to(),
})?;
self.insert_str(byte_index, text)
}
pub fn replace_range(
&mut self,
range: Range<usize>,
text: &str,
) -> Result<(), TextStreamError> {
let range = self.validate_range(range)?;
self.replace_validated_range(range, text)
}
pub fn replace_validated_range(
&mut self,
range: ValidatedTextRange,
text: &str,
) -> Result<(), TextStreamError> {
self.validate_range_revision(range)?;
self.text.replace_range(range.as_range(), text);
self.bump_revision();
Ok(())
}
pub fn replace_range_bytes(
&mut self,
range: Range<usize>,
bytes: &[u8],
) -> Result<(), TextStreamError> {
let text = str::from_utf8(bytes).map_err(|error| TextStreamError::InvalidUtf8 {
valid_up_to: error.valid_up_to(),
})?;
self.replace_range(range, text)
}
pub fn delete_range(&mut self, range: Range<usize>) -> Result<(), TextStreamError> {
self.replace_range(range, "")
}
pub fn delete_validated_range(
&mut self,
range: ValidatedTextRange,
) -> Result<(), TextStreamError> {
self.replace_validated_range(range, "")
}
pub fn clear(&mut self) {
self.text.clear();
self.bump_revision();
}
const fn bump_revision(&mut self) {
self.revision = self
.revision
.next()
.expect("text revision exhausted; refusing to wrap monotonic revision");
}
fn validate_boundary(&self, index: usize) -> Result<(), TextStreamError> {
if self.text.len() < index {
return Err(TextStreamError::OutOfBounds {
index,
len: self.text.len(),
});
}
if !self.text.is_char_boundary(index) {
return Err(TextStreamError::NotCharBoundary { index });
}
Ok(())
}
pub fn validate_range(
&self,
range: impl Into<TextRange>,
) -> Result<ValidatedTextRange, TextStreamError> {
let range = range.into();
if range.end() < range.start() {
return Err(TextStreamError::InvalidRange {
start: range.start(),
end: range.end(),
});
}
self.validate_boundary(range.start())?;
self.validate_boundary(range.end())?;
Ok(ValidatedTextRange::new(range, self.id(), self.revision()))
}
pub fn validate_validated_range(
&self,
range: ValidatedTextRange,
) -> Result<ValidatedTextRange, TextStreamError> {
if range.stream_id() != self.id() {
return Err(TextStreamError::WrongTextStream {
validated_stream: range.stream_id(),
current_stream: self.id(),
});
}
if range.revision() != self.revision() {
return Err(TextStreamError::StaleValidatedRange {
validated_revision: range.revision(),
current_revision: self.revision(),
});
}
Ok(range)
}
fn validate_range_revision(&self, range: ValidatedTextRange) -> Result<(), TextStreamError> {
let _range = self.validate_validated_range(range)?;
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum TextStreamError {
InvalidUtf8 {
valid_up_to: usize,
},
OutOfBounds {
index: usize,
len: usize,
},
NotCharBoundary {
index: usize,
},
InvalidRange {
start: usize,
end: usize,
},
StaleValidatedRange {
validated_revision: TextRevision,
current_revision: TextRevision,
},
WrongTextStream {
validated_stream: TextStreamId,
current_stream: TextStreamId,
},
}
impl Display for TextStreamError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidUtf8 { valid_up_to } => {
write!(formatter, "invalid UTF-8 after byte {valid_up_to}")
}
Self::OutOfBounds { index, len } => {
write!(
formatter,
"byte index {index} is outside stream length {len}"
)
}
Self::NotCharBoundary { index } => {
write!(
formatter,
"byte index {index} is not a UTF-8 character boundary"
)
}
Self::InvalidRange { start, end } => {
write!(formatter, "invalid byte range {start}..{end}")
}
Self::StaleValidatedRange {
validated_revision,
current_revision,
} => write!(
formatter,
"validated range from revision {validated_revision} cannot be applied to revision {current_revision}"
),
Self::WrongTextStream {
validated_stream,
current_stream,
} => write!(
formatter,
"validated range from stream {validated_stream:?} cannot be applied to stream {current_stream:?}"
),
}
}
}
#[cfg(test)]
mod tests {
use super::{TextByteStream, TextRange, TextStreamError};
use proptest::{
prelude::{Strategy, any, proptest},
prop_assert_eq,
};
fn multibyte_char() -> impl Strategy<Value = char> {
any::<char>().prop_filter("character must use multiple UTF-8 bytes", |character| {
1 < character.len_utf8()
})
}
#[test]
fn validated_ranges_encode_stream_byte_invariants() {
let stream = TextByteStream::new("aλb");
let range = stream
.validate_range(TextRange::new(1, 3))
.expect("lambda range should be valid");
assert_eq!(range.start(), 1);
assert_eq!(range.end(), 3);
assert_eq!(range.stream_id(), stream.id());
assert_eq!(range.revision(), 0);
assert_eq!(range.as_range(), 1..3);
assert_eq!(
stream.validate_range(TextRange::new(2, 3)),
Err(TextStreamError::NotCharBoundary { index: 2 })
);
assert_eq!(
stream.validate_range(TextRange::new(4, 3)),
Err(TextStreamError::InvalidRange { start: 4, end: 3 })
);
}
#[test]
fn validated_ranges_are_revision_scoped() {
let mut stream = TextByteStream::new("abc");
let range = stream
.validate_range(TextRange::new(1, 2))
.expect("range should validate");
stream.replace_all("aλc");
assert_eq!(
stream.delete_validated_range(range),
Err(TextStreamError::StaleValidatedRange {
validated_revision: 0.into(),
current_revision: 1.into(),
})
);
assert_eq!(stream.as_str(), "aλc");
}
#[test]
fn validated_ranges_are_stream_scoped() {
let source = TextByteStream::new("abcdef");
let mut target = TextByteStream::new("λ");
let range = source
.validate_range(TextRange::new(1, 4))
.expect("source range should validate");
assert_eq!(
target.delete_validated_range(range),
Err(TextStreamError::WrongTextStream {
validated_stream: source.id(),
current_stream: target.id(),
})
);
assert_eq!(target.as_str(), "λ");
assert_eq!(target.revision(), 0);
}
proptest! {
#[test]
fn replace_all_round_trips_arbitrary_unicode(input in any::<String>()) {
let mut stream = TextByteStream::default();
stream.replace_all(input.clone());
prop_assert_eq!(stream.as_str(), input.as_str());
prop_assert_eq!(stream.as_bytes(), input.as_bytes());
prop_assert_eq!(stream.revision(), 1);
}
#[test]
fn from_bytes_accepts_valid_utf8(input in any::<String>()) {
let stream = TextByteStream::from_bytes(input.clone().into_bytes());
prop_assert_eq!(stream, Ok(TextByteStream::new(input)));
}
#[test]
fn insert_str_matches_string_model(
prefix in any::<String>(),
inserted in any::<String>(),
suffix in any::<String>(),
) {
let original = format!("{prefix}{suffix}");
let expected = format!("{prefix}{inserted}{suffix}");
let mut stream = TextByteStream::new(original);
prop_assert_eq!(stream.insert_str(prefix.len(), inserted.as_str()), Ok(()));
prop_assert_eq!(stream.as_str(), expected.as_str());
prop_assert_eq!(stream.revision(), 1);
}
#[test]
fn replace_range_matches_string_model(
prefix in any::<String>(),
removed in any::<String>(),
suffix in any::<String>(),
replacement in any::<String>(),
) {
let original = format!("{prefix}{removed}{suffix}");
let expected = format!("{prefix}{replacement}{suffix}");
let range = prefix.len()..prefix.len() + removed.len();
let mut stream = TextByteStream::new(original);
prop_assert_eq!(stream.replace_range(range, replacement.as_str()), Ok(()));
prop_assert_eq!(stream.as_str(), expected.as_str());
prop_assert_eq!(stream.revision(), 1);
}
#[test]
fn delete_range_matches_string_model(
prefix in any::<String>(),
removed in any::<String>(),
suffix in any::<String>(),
) {
let original = format!("{prefix}{removed}{suffix}");
let expected = format!("{prefix}{suffix}");
let range = prefix.len()..prefix.len() + removed.len();
let mut stream = TextByteStream::new(original);
prop_assert_eq!(stream.delete_range(range), Ok(()));
prop_assert_eq!(stream.as_str(), expected.as_str());
prop_assert_eq!(stream.revision(), 1);
}
#[test]
fn delete_range_rejects_split_scalar(
prefix in any::<String>(),
character in multibyte_char(),
) {
let mut stream = TextByteStream::new(format!("{prefix}{character}"));
let split_index = prefix.len() + 1;
prop_assert_eq!(
stream.delete_range(split_index..split_index),
Err(TextStreamError::NotCharBoundary { index: split_index }),
);
prop_assert_eq!(stream.revision(), 0);
}
#[test]
fn insert_bytes_matches_string_model(
prefix in any::<String>(),
inserted in any::<String>(),
suffix in any::<String>(),
) {
let original = format!("{prefix}{suffix}");
let expected = format!("{prefix}{inserted}{suffix}");
let mut stream = TextByteStream::new(original);
prop_assert_eq!(stream.insert_bytes(prefix.len(), inserted.as_bytes()), Ok(()));
prop_assert_eq!(stream.as_str(), expected.as_str());
}
}
#[test]
fn from_bytes_rejects_invalid_utf8() {
assert_eq!(
TextByteStream::from_bytes(vec![0x66, 0x80]),
Err(TextStreamError::InvalidUtf8 { valid_up_to: 1 })
);
}
#[test]
fn debug_output_redacts_stream_text() {
let stream = TextByteStream::new("secret buffer text");
let debug = format!("{stream:?}");
assert!(debug.contains("TextByteStream"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("secret"));
assert!(!debug.contains("buffer text"));
}
#[test]
fn newline_remains_layout_data() {
let mut stream = TextByteStream::new("ALMA");
assert_eq!(stream.insert_str(4, "\nΑλμα"), Ok(()));
assert_eq!(stream.as_str(), "ALMA\nΑλμα");
}
}