use std::fmt;
use std::ops::Deref;
use automerge::{ChangeHash, ObjId, ObjType, Prop, ReadDoc, Value, transaction::Transactable};
use crate::{Automorph, Error, PrimitiveChanged, Result, ScalarCursor};
#[derive(Clone, PartialEq, Eq, Hash, Default)]
pub struct Text {
content: String,
pending_ops: Vec<TextOp>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum TextOp {
Insert { pos: usize, text: String },
Delete { pos: usize, count: usize },
Replace { text: String },
}
impl Text {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
pending_ops: Vec::new(),
}
}
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.content
}
#[must_use]
pub fn len(&self) -> usize {
self.content.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn insert(&mut self, pos: usize, text: impl Into<String>) {
let text = text.into();
if !text.is_empty() {
self.content.insert_str(pos, &text);
self.pending_ops.push(TextOp::Insert { pos, text });
}
}
pub fn push_str(&mut self, text: impl Into<String>) {
let pos = self.content.len();
self.insert(pos, text);
}
pub fn delete(&mut self, pos: usize, count: usize) {
if count > 0 && pos < self.content.len() {
let actual_count = count.min(self.content.len() - pos);
self.content.drain(pos..pos + actual_count);
self.pending_ops.push(TextOp::Delete {
pos,
count: actual_count,
});
}
}
pub fn splice(&mut self, pos: usize, delete_count: usize, text: impl Into<String>) {
self.delete(pos, delete_count);
self.insert(pos, text);
}
pub fn set(&mut self, content: impl Into<String>) {
let text = content.into();
self.content = text.clone();
self.pending_ops.clear();
self.pending_ops.push(TextOp::Replace { text });
}
#[must_use]
pub fn has_pending_changes(&self) -> bool {
!self.pending_ops.is_empty()
}
pub fn clear_pending(&mut self) {
self.pending_ops.clear();
}
}
impl fmt::Debug for Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Text")
.field("content", &self.content)
.field("pending_ops", &self.pending_ops.len())
.finish()
}
}
impl fmt::Display for Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.content)
}
}
impl Deref for Text {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.content
}
}
impl From<String> for Text {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for Text {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<Text> for String {
fn from(text: Text) -> Self {
text.content
}
}
impl AsRef<str> for Text {
fn as_ref(&self) -> &str {
&self.content
}
}
impl Automorph for Text {
type Changes = PrimitiveChanged;
type Cursor = ScalarCursor;
fn save<D: Transactable + ReadDoc>(
&self,
doc: &mut D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<()> {
let prop: Prop = prop.into();
let obj = obj.as_ref();
let text_id = match doc.get(obj, prop.clone())? {
Some((Value::Object(ObjType::Text), id)) => {
id
}
_ => {
doc.put_object(obj, prop.clone(), ObjType::Text)?
}
};
if !self.pending_ops.is_empty() {
for op in &self.pending_ops {
match op {
TextOp::Insert { pos, text } => {
doc.splice_text(&text_id, *pos, 0, text)?;
}
TextOp::Delete { pos, count } => {
doc.splice_text(&text_id, *pos, *count as isize, "")?;
}
TextOp::Replace { text } => {
let current_len = doc.text(&text_id)?.len();
if current_len > 0 {
doc.splice_text(&text_id, 0, current_len as isize, "")?;
}
if !text.is_empty() {
doc.splice_text(&text_id, 0, 0, text)?;
}
}
}
}
} else {
let current = doc.text(&text_id)?;
if current != self.content {
let current_len = current.len();
if current_len > 0 {
doc.splice_text(&text_id, 0, current_len as isize, "")?;
}
if !self.content.is_empty() {
doc.splice_text(&text_id, 0, 0, &self.content)?;
}
}
}
Ok(())
}
fn load<D: ReadDoc>(doc: &D, obj: impl AsRef<ObjId>, prop: impl Into<Prop>) -> Result<Self> {
let prop: Prop = prop.into();
let obj = obj.as_ref();
match doc.get(obj, prop)? {
Some((Value::Object(ObjType::Text), text_id)) => {
let content = doc.text(&text_id)?;
Ok(Self {
content,
pending_ops: Vec::new(),
})
}
Some((Value::Scalar(s), _)) => {
if let Some(str_val) = s.to_str() {
Ok(Self::new(str_val))
} else {
Err(Error::type_mismatch("Text", Some(format!("{:?}", s))))
}
}
Some((v, _)) => Err(Error::type_mismatch("Text", Some(format!("{:?}", v)))),
None => Err(Error::missing_value()),
}
}
fn load_at<D: ReadDoc>(
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
heads: &[ChangeHash],
) -> Result<Self> {
let prop: Prop = prop.into();
let obj = obj.as_ref();
match doc.get_at(obj, prop, heads)? {
Some((Value::Object(ObjType::Text), text_id)) => {
let content = doc.text_at(&text_id, heads)?;
Ok(Self {
content,
pending_ops: Vec::new(),
})
}
Some((Value::Scalar(s), _)) => {
if let Some(str_val) = s.to_str() {
Ok(Self::new(str_val))
} else {
Err(Error::type_mismatch("Text", Some(format!("{:?}", s))))
}
}
Some((v, _)) => Err(Error::type_mismatch("Text", Some(format!("{:?}", v)))),
None => Err(Error::missing_value()),
}
}
fn diff<D: ReadDoc>(
&self,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<Self::Changes> {
let loaded = Self::load(doc, obj, prop)?;
Ok(PrimitiveChanged::new(self.content != loaded.content))
}
fn diff_at<D: ReadDoc>(
&self,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
heads: &[ChangeHash],
) -> Result<Self::Changes> {
let loaded = Self::load_at(doc, obj, prop, heads)?;
Ok(PrimitiveChanged::new(self.content != loaded.content))
}
fn update<D: ReadDoc>(
&mut self,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
) -> Result<Self::Changes> {
let loaded = Self::load(doc, obj, prop)?;
let changed = self.content != loaded.content;
self.content = loaded.content;
self.pending_ops.clear();
Ok(PrimitiveChanged::new(changed))
}
fn update_at<D: ReadDoc>(
&mut self,
doc: &D,
obj: impl AsRef<ObjId>,
prop: impl Into<Prop>,
heads: &[ChangeHash],
) -> Result<Self::Changes> {
let loaded = Self::load_at(doc, obj, prop, heads)?;
let changed = self.content != loaded.content;
self.content = loaded.content;
self.pending_ops.clear();
Ok(PrimitiveChanged::new(changed))
}
}
#[cfg(test)]
mod tests {
use super::*;
use automerge::{ActorId, AutoCommit, ROOT};
#[test]
fn test_text_basic_operations() {
let mut text = Text::new("hello");
assert_eq!(text.as_str(), "hello");
text.insert(5, " world");
assert_eq!(text.as_str(), "hello world");
text.delete(5, 6);
assert_eq!(text.as_str(), "hello");
}
#[test]
fn test_text_roundtrip() {
let mut doc = AutoCommit::new();
let text = Text::new("Hello, World!");
text.save(&mut doc, &ROOT, "greeting").unwrap();
let loaded = Text::load(&doc, &ROOT, "greeting").unwrap();
assert_eq!(loaded.as_str(), "Hello, World!");
}
#[test]
fn test_text_incremental_edits() {
let mut doc = AutoCommit::new();
let text = Text::new("hello");
text.save(&mut doc, &ROOT, "text").unwrap();
let mut text = Text::load(&doc, &ROOT, "text").unwrap();
text.insert(5, " world");
text.save(&mut doc, &ROOT, "text").unwrap();
let loaded = Text::load(&doc, &ROOT, "text").unwrap();
assert_eq!(loaded.as_str(), "hello world");
}
#[test]
fn test_text_concurrent_inserts() {
let mut doc1 = AutoCommit::new();
let text = Text::new("hello");
text.save(&mut doc1, &ROOT, "text").unwrap();
let mut doc2 = doc1.fork().with_actor(ActorId::random());
let mut text1 = Text::load(&doc1, &ROOT, "text").unwrap();
text1.insert(5, " world");
text1.save(&mut doc1, &ROOT, "text").unwrap();
let mut text2 = Text::load(&doc2, &ROOT, "text").unwrap();
text2.insert(5, " there");
text2.save(&mut doc2, &ROOT, "text").unwrap();
doc1.merge(&mut doc2).unwrap();
let merged = Text::load(&doc1, &ROOT, "text").unwrap();
let merged_str = merged.as_str();
assert!(
merged_str.contains("world"),
"Should contain 'world': {}",
merged_str
);
assert!(
merged_str.contains("there"),
"Should contain 'there': {}",
merged_str
);
}
#[test]
fn test_text_splice() {
let mut text = Text::new("hello world");
text.splice(6, 5, "there");
assert_eq!(text.as_str(), "hello there");
}
#[test]
fn test_text_push_str() {
let mut text = Text::new("hello");
text.push_str(" world");
assert_eq!(text.as_str(), "hello world");
}
#[test]
fn test_text_set_replaces_all() {
let mut doc = AutoCommit::new();
let text = Text::new("original content");
text.save(&mut doc, &ROOT, "text").unwrap();
let mut text = Text::load(&doc, &ROOT, "text").unwrap();
text.set("completely new content");
text.save(&mut doc, &ROOT, "text").unwrap();
let loaded = Text::load(&doc, &ROOT, "text").unwrap();
assert_eq!(loaded.as_str(), "completely new content");
}
}