use crate::{
reconcile::{NoKey, TextReconciler},
Hydrate, ReadDoc, Reconcile,
};
#[derive(Clone)]
pub struct Text(State);
impl std::default::Default for Text {
fn default() -> Self {
Text::with_value("")
}
}
impl std::fmt::Debug for Text {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Text")
.field("value", &self.as_str())
.finish()
}
}
impl Text {
pub fn with_value<S: AsRef<str>>(value: S) -> Text {
Self(State::Fresh(value.as_ref().to_string()))
}
pub fn splice<S: AsRef<str>>(&mut self, pos: usize, del: isize, insert: S) {
let start = if del < 0 {
pos.saturating_sub(del.unsigned_abs())
} else {
pos
};
match &mut self.0 {
State::Fresh(v) => {
v.replace_range(start..(start + del.unsigned_abs()), insert.as_ref())
}
State::Rehydrated { value, edits, .. } => {
value.replace_range(start..(start + del.unsigned_abs()), insert.as_ref());
edits.push(Splice {
pos,
delete: del,
insert: insert.as_ref().to_string(),
});
}
}
}
pub fn update<S: AsRef<str>>(&mut self, new_value: S) {
match &mut self.0 {
State::Fresh(v) => *v = new_value.as_ref().to_string(),
State::Rehydrated { value, .. } => {
let mut idx = 0;
let old = value.clone();
for change in similar::TextDiff::from_graphemes(old.as_str(), new_value.as_ref())
.iter_all_changes()
{
match change.tag() {
similar::ChangeTag::Delete => {
let len = change.value().len();
self.splice(idx, len as isize, "");
}
similar::ChangeTag::Insert => {
self.splice(idx, 0, change.value());
idx += change.value().len();
}
similar::ChangeTag::Equal => {
idx += change.value().len();
}
}
}
}
}
}
pub fn as_str(&self) -> &str {
match &self.0 {
State::Fresh(v) => v,
State::Rehydrated { value, .. } => value,
}
}
}
impl<S: AsRef<str>> From<S> for Text {
fn from(s: S) -> Self {
Text::with_value(s)
}
}
impl std::cmp::PartialEq for Text {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl std::cmp::Eq for Text {}
#[derive(Clone)]
enum State {
Fresh(String),
Rehydrated {
value: String,
edits: Vec<Splice>,
from_heads: Vec<automerge::ChangeHash>,
},
}
#[derive(Clone)]
struct Splice {
pos: usize,
delete: isize,
insert: String,
}
impl Reconcile for Text {
type Key<'a> = NoKey;
fn reconcile<R: crate::Reconciler>(&self, mut reconciler: R) -> Result<(), R::Error> {
let mut t = reconciler.text()?;
match &self.0 {
State::Fresh(v) => {
t.splice(0, 0, v)?;
}
State::Rehydrated {
edits, from_heads, ..
} => {
let to_heads = t.heads();
if to_heads != from_heads {
return Err(crate::reconcile::StaleHeads {
expected: from_heads.to_vec(),
found: to_heads.to_vec(),
}
.into());
} else {
for edit in edits {
t.splice(edit.pos, edit.delete, &edit.insert)?;
}
}
}
}
Ok(())
}
}
impl Hydrate for Text {
fn hydrate_text<D: ReadDoc>(
doc: &D,
obj: &automerge::ObjId,
) -> Result<Self, crate::HydrateError> {
let value = doc.text(obj)?;
Ok(Text(State::Rehydrated {
value,
edits: Vec::new(),
from_heads: doc.get_heads(),
}))
}
}
#[cfg(test)]
mod tests {
use automerge::ActorId;
use crate::{hydrate_prop, reconcile_prop};
use super::Text;
#[test]
fn merge_text() {
let mut doc1 = automerge::AutoCommit::new();
let text = Text::with_value("glitters");
reconcile_prop(&mut doc1, automerge::ROOT, "text", &text).unwrap();
let mut doc2 = doc1.fork().with_actor(ActorId::random());
let mut text1: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap();
let mut text2: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap();
text1.splice(0, 0, "all that ");
reconcile_prop(&mut doc1, automerge::ROOT, "text", &text1).unwrap();
let offset = text2.as_str().char_indices().last().unwrap().0;
text2.splice(offset + 1, 0, " is not gold");
reconcile_prop(&mut doc2, automerge::ROOT, "text", &text2).unwrap();
doc1.merge(&mut doc2).unwrap();
let result: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap();
assert_eq!(result.as_str(), "all that glitters is not gold");
}
#[test]
fn test_partial_eq() {
let text = Text::with_value("hello");
assert_eq!(text, Text::with_value("hello"));
assert_ne!(text, Text::with_value("world"));
}
#[test]
fn test_eq() {
let text: Text = Text::with_value("hello");
assert_eq!(text, text);
}
}