use crate::{
error::Error,
id::{OpId, ReplicaId},
list::{List, ListOp},
version::VersionVector,
};
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum AnchorSide {
Before,
After,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Anchor {
Start,
End,
Char(OpId, AnchorSide),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum SpanValue {
On,
Off,
Set(String),
Unset,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Span {
pub id: OpId,
pub start: Anchor,
pub end: Anchor,
pub name: String,
pub value: SpanValue,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TextOp {
Char(ListOp<char>),
Mark(Span),
}
impl TextOp {
#[must_use]
pub fn id(&self) -> OpId {
match self {
TextOp::Char(op) => op.id(),
TextOp::Mark(s) => s.id,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ExpandRule {
#[default]
None,
Right,
Left,
Both,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MarkValue {
Boolean,
Value(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MarkSet {
inner: std::collections::BTreeMap<String, MarkValue>,
}
impl MarkSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn contains(&self, name: &str) -> bool {
self.inner.contains_key(name)
}
pub fn value_of(&self, name: &str) -> Option<&str> {
match self.inner.get(name) {
Some(MarkValue::Value(s)) => Some(s.as_str()),
_ => None,
}
}
pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
self.inner.keys().map(String::as_str)
}
pub fn iter_with_values(&self) -> impl Iterator<Item = (&str, &MarkValue)> + '_ {
self.inner.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn iter_booleans(&self) -> impl Iterator<Item = &str> + '_ {
self.inner.iter().filter_map(|(k, v)| match v {
MarkValue::Boolean => Some(k.as_str()),
MarkValue::Value(_) => None,
})
}
pub fn iter_values(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
self.inner.iter().filter_map(|(k, v)| match v {
MarkValue::Boolean => None,
MarkValue::Value(s) => Some((k.as_str(), s.as_str())),
})
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Text {
chars: List<char>,
spans: Vec<Span>,
replica: ReplicaId,
log: Vec<TextOp>,
version: VersionVector,
}
impl Text {
#[must_use]
pub fn new(replica: ReplicaId) -> Self {
Self {
chars: List::<char>::new(replica),
spans: Vec::new(),
replica,
log: Vec::new(),
version: VersionVector::new(),
}
}
#[must_use]
pub fn new_random() -> Self {
Self::new(crate::id::new_replica_id())
}
#[must_use]
pub fn replica_id(&self) -> ReplicaId {
self.replica
}
#[must_use]
pub fn len(&self) -> usize {
self.chars.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.chars.is_empty()
}
#[must_use]
pub fn grapheme_count(&self) -> usize {
use unicode_segmentation::UnicodeSegmentation;
let s: String = self.chars.iter().collect();
s.graphemes(true).count()
}
#[must_use]
pub fn grapheme_to_char_pos(&self, grapheme_pos: usize) -> usize {
use unicode_segmentation::UnicodeSegmentation;
let s: String = self.chars.iter().collect();
let mut char_acc = 0usize;
for (gi, g) in s.graphemes(true).enumerate() {
if gi == grapheme_pos {
return char_acc;
}
char_acc += g.chars().count();
}
self.len()
}
#[must_use]
pub fn char_to_grapheme_pos(&self, char_pos: usize) -> usize {
use unicode_segmentation::UnicodeSegmentation;
let s: String = self.chars.iter().collect();
let mut char_acc = 0usize;
for (gi, g) in s.graphemes(true).enumerate() {
if char_acc >= char_pos {
return gi;
}
char_acc += g.chars().count();
}
s.graphemes(true).count()
}
pub fn insert_grapheme_str(&mut self, g_pos: usize, s: &str) -> Vec<TextOp> {
let char_pos = self.grapheme_to_char_pos(g_pos);
self.insert_str(char_pos, s)
}
pub fn delete_grapheme(&mut self, g_pos: usize) -> Vec<TextOp> {
use unicode_segmentation::UnicodeSegmentation;
let s: String = self.chars.iter().collect();
let mut char_pos = 0usize;
let mut g_chars = 0usize;
for (gi, g) in s.graphemes(true).enumerate() {
if gi == g_pos {
g_chars = g.chars().count();
break;
}
char_pos += g.chars().count();
if gi == g_pos {
g_chars = g.chars().count();
break;
}
}
if g_chars == 0 {
return Vec::new();
}
let mut ops = Vec::with_capacity(g_chars);
for _ in 0..g_chars {
ops.push(self.delete(char_pos));
}
ops
}
#[must_use]
pub fn as_string(&self) -> String {
self.chars.iter().collect()
}
pub fn insert(&mut self, pos: usize, ch: char) -> TextOp {
let op = self.chars.insert(pos, ch);
let text_op = TextOp::Char(op);
self.version.observe(text_op.id());
self.log.push(text_op.clone());
text_op
}
pub fn insert_str(&mut self, pos: usize, s: &str) -> Vec<TextOp> {
let mut ops = Vec::new();
for (i, ch) in s.chars().enumerate() {
ops.push(self.insert(pos + i, ch));
}
ops
}
pub fn delete(&mut self, pos: usize) -> TextOp {
let op = self.chars.delete(pos);
let text_op = TextOp::Char(op);
self.version.observe(text_op.id());
self.log.push(text_op.clone());
text_op
}
pub fn delete_range(&mut self, range: std::ops::Range<usize>) -> Vec<TextOp> {
let mut ops = Vec::new();
for _ in range.clone() {
ops.push(self.delete(range.start));
}
ops
}
pub fn set_mark(&mut self, range: std::ops::Range<usize>, name: &str, on: bool) -> TextOp {
let value = if on { SpanValue::On } else { SpanValue::Off };
self.set_mark_with_rule(range, name, value, ExpandRule::None)
}
pub fn set_value_mark(
&mut self,
range: std::ops::Range<usize>,
name: &str,
value: Option<&str>,
) -> TextOp {
let v = match value {
Some(s) => SpanValue::Set(s.to_string()),
None => SpanValue::Unset,
};
self.set_mark_with_rule(range, name, v, ExpandRule::None)
}
pub fn set_mark_with_rule(
&mut self,
range: std::ops::Range<usize>,
name: &str,
value: SpanValue,
rule: ExpandRule,
) -> TextOp {
assert!(range.start <= range.end, "set_mark: empty/inverted range");
assert!(range.end <= self.len(), "set_mark: range past end of text");
let (start, end) = self.anchors_for_range(range, rule);
self.set_mark_with_anchors(start, end, name, value)
}
pub fn set_mark_with_anchors(
&mut self,
start: Anchor,
end: Anchor,
name: &str,
value: SpanValue,
) -> TextOp {
let id = self.chars.next_op_id();
let span = Span {
id,
start,
end,
name: name.to_string(),
value,
};
self.insert_span_sorted(span.clone());
let text_op = TextOp::Mark(span);
self.version.observe(id);
self.log.push(text_op.clone());
text_op
}
fn anchors_for_range(
&self,
range: std::ops::Range<usize>,
rule: ExpandRule,
) -> (Anchor, Anchor) {
let len = self.len();
let expand_left = matches!(rule, ExpandRule::Left | ExpandRule::Both);
let expand_right = matches!(rule, ExpandRule::Right | ExpandRule::Both);
let start = if range.start == 0 {
if expand_left {
Anchor::Start
} else if len == 0 {
Anchor::Start
} else {
Anchor::Char(self.chars.id_at(0).unwrap(), AnchorSide::Before)
}
} else if expand_left {
Anchor::Char(
self.chars.id_at(range.start - 1).unwrap(),
AnchorSide::After,
)
} else {
Anchor::Char(self.chars.id_at(range.start).unwrap(), AnchorSide::Before)
};
let end = if range.end == 0 {
if expand_left {
Anchor::Start
} else {
start
}
} else if range.end == len {
if expand_right {
Anchor::End
} else if len == 0 {
Anchor::Start
} else {
Anchor::Char(self.chars.id_at(range.end - 1).unwrap(), AnchorSide::After)
}
} else if expand_right {
Anchor::Char(self.chars.id_at(range.end).unwrap(), AnchorSide::Before)
} else {
Anchor::Char(self.chars.id_at(range.end - 1).unwrap(), AnchorSide::After)
};
(start, end)
}
pub fn apply_inverse(&mut self, op: &TextOp) -> Option<TextOp> {
match op {
TextOp::Char(char_op) => {
let inv = self.chars.apply_inverse(char_op)?;
let text_op = TextOp::Char(inv);
self.version.observe(text_op.id());
self.log.push(text_op.clone());
Some(text_op)
}
TextOp::Mark(span) => {
let inverse_value = match &span.value {
SpanValue::On => SpanValue::Off,
SpanValue::Set(_) => SpanValue::Unset,
SpanValue::Off | SpanValue::Unset => return None,
};
let id = self.chars.next_op_id();
let inv_span = Span {
id,
start: span.start,
end: span.end,
name: span.name.clone(),
value: inverse_value,
};
self.insert_span_sorted(inv_span.clone());
let text_op = TextOp::Mark(inv_span);
self.version.observe(id);
self.log.push(text_op.clone());
Some(text_op)
}
}
}
pub fn apply(&mut self, op: TextOp) -> Result<(), Error> {
let op_id = op.id();
if self.version.contains(op_id) {
return Ok(());
}
match &op {
TextOp::Char(char_op) => {
self.chars.apply(char_op.clone())?;
}
TextOp::Mark(span) => {
self.chars.observe_external(span.id);
self.insert_span_sorted(span.clone());
}
}
self.version.observe(op_id);
self.log.push(op);
Ok(())
}
pub fn merge(&mut self, other: &Self) {
let mut to_apply: Vec<&TextOp> = other
.log
.iter()
.filter(|op| !self.version.contains(op.id()))
.collect();
to_apply.sort_by_key(|op| op.id());
for op in to_apply {
self.apply(op.clone())
.expect("text apply cannot fail in merge");
}
}
#[must_use]
pub fn ops(&self) -> &[TextOp] {
&self.log
}
pub fn ops_since<'a>(
&'a self,
since: &'a VersionVector,
) -> impl Iterator<Item = &'a TextOp> + 'a {
self.log.iter().filter(move |op| !since.contains(op.id()))
}
#[must_use]
pub fn spans(&self) -> &[Span] {
&self.spans
}
#[must_use]
pub fn version(&self) -> &VersionVector {
&self.version
}
pub fn iter_with_marks(&self) -> Box<dyn Iterator<Item = (char, MarkSet)> + '_> {
let positions = self.chars.phantom_positions();
let visible_ids = self.chars.op_ids();
let len = visible_ids.len();
let resolved: Vec<(usize, usize, &Span)> = self
.spans
.iter()
.filter_map(|s| {
let sp = self.resolve_anchor(&s.start, &positions, len, false)?;
let ep = self.resolve_anchor(&s.end, &positions, len, true)?;
if sp >= ep {
None
} else {
Some((sp, ep, s))
}
})
.collect();
Box::new((0..len).map(move |idx| {
let _ = visible_ids[idx]; let ch = self.chars.get(idx).copied().unwrap_or('\0');
let mut state: HashMap<&str, &SpanValue> = HashMap::new();
for &(sp, ep, span) in &resolved {
if sp <= idx && idx < ep {
state.insert(span.name.as_str(), &span.value);
}
}
let mut marks = MarkSet::new();
for (name, value) in state {
match value {
SpanValue::On => {
marks.inner.insert(name.to_string(), MarkValue::Boolean);
}
SpanValue::Set(s) => {
marks
.inner
.insert(name.to_string(), MarkValue::Value(s.clone()));
}
SpanValue::Off | SpanValue::Unset => {}
}
}
(ch, marks)
}))
}
#[must_use]
pub fn render(&self) -> Vec<(char, MarkSet)> {
self.iter_with_marks().collect()
}
fn insert_span_sorted(&mut self, span: Span) {
let pos = self
.spans
.binary_search_by_key(&span.id, |s| s.id)
.unwrap_or_else(|e| e);
self.spans.insert(pos, span);
}
fn resolve_anchor(
&self,
anchor: &Anchor,
positions: &HashMap<OpId, usize>,
len: usize,
_as_end: bool,
) -> Option<usize> {
match anchor {
Anchor::Start => Some(0),
Anchor::End => Some(len),
Anchor::Char(id, side) => {
let &pos = positions.get(id)?;
let visible = self.chars.is_visible(*id).unwrap_or(false);
Some(match (side, visible) {
(AnchorSide::After, true) => pos + 1,
_ => pos,
})
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum AttrValue {
Bool(bool),
String(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DeltaOp {
pub insert: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")
)]
pub attributes: std::collections::BTreeMap<String, AttrValue>,
}
impl Text {
#[must_use]
pub fn to_delta(&self) -> Vec<DeltaOp> {
let mut deltas: Vec<DeltaOp> = Vec::new();
let mut current_text = String::new();
let mut current_attrs: std::collections::BTreeMap<String, AttrValue> =
std::collections::BTreeMap::new();
for (ch, marks) in self.iter_with_marks() {
let mut new_attrs: std::collections::BTreeMap<String, AttrValue> =
std::collections::BTreeMap::new();
for (name, val) in marks.iter_with_values() {
let attr = match val {
MarkValue::Boolean => AttrValue::Bool(true),
MarkValue::Value(s) => AttrValue::String(s.clone()),
};
new_attrs.insert(name.to_string(), attr);
}
if new_attrs != current_attrs && !current_text.is_empty() {
deltas.push(DeltaOp {
insert: std::mem::take(&mut current_text),
attributes: std::mem::take(&mut current_attrs),
});
}
current_text.push(ch);
current_attrs = new_attrs;
}
if !current_text.is_empty() {
deltas.push(DeltaOp {
insert: current_text,
attributes: current_attrs,
});
}
deltas
}
pub fn from_delta(replica: ReplicaId, deltas: &[DeltaOp]) -> Self {
let mut text = Self::new(replica);
for op in deltas {
let start = text.len();
for ch in op.insert.chars() {
text.insert(text.len(), ch);
}
let end = text.len();
if end == start {
continue;
}
for (name, attr) in &op.attributes {
let value = match attr {
AttrValue::Bool(true) => SpanValue::On,
AttrValue::Bool(false) => SpanValue::Off,
AttrValue::String(s) => SpanValue::Set(s.clone()),
};
text.set_mark_with_rule(start..end, name, value, ExpandRule::None);
}
}
text
}
}
impl Default for Text {
fn default() -> Self {
Self::new(0)
}
}
impl std::fmt::Display for Text {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for c in self.chars.iter() {
f.write_str(c.encode_utf8(&mut [0u8; 4]))?;
}
Ok(())
}
}
#[cfg(test)]
impl Text {
fn chars_for_test(&self) -> &List<char> {
&self.chars
}
}
#[cfg(test)]
mod tests {
use super::*;
fn marks_at(text: &Text, pos: usize) -> Vec<String> {
text.iter_with_marks()
.nth(pos)
.map(|(_, m)| m.iter().map(String::from).collect())
.unwrap_or_default()
}
#[test]
fn empty_doc() {
let t = Text::new(1);
assert!(t.is_empty());
assert_eq!(t.to_string(), "");
assert_eq!(t.iter_with_marks().count(), 0);
}
#[test]
fn insert_and_render() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
assert_eq!(t.len(), 5);
assert_eq!(t.to_string(), "Hello");
for (_, marks) in t.iter_with_marks() {
assert!(marks.is_empty());
}
}
#[test]
fn bold_a_range() {
let mut t = Text::new(1);
t.insert_str(0, "Hello, world!");
t.set_mark(0..5, "bold", true);
assert_eq!(marks_at(&t, 0), vec!["bold"]);
assert_eq!(marks_at(&t, 4), vec!["bold"]);
assert!(marks_at(&t, 5).is_empty());
}
#[test]
fn unbold_a_range_via_off_op() {
let mut t = Text::new(1);
t.insert_str(0, "Hello, world!");
t.set_mark(0..5, "bold", true);
t.set_mark(0..5, "bold", false);
assert!(marks_at(&t, 0).is_empty());
assert!(marks_at(&t, 4).is_empty());
}
#[test]
fn typing_in_middle_of_bold_range_inherits() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..5, "bold", true);
t.insert(2, 'X');
assert_eq!(t.to_string(), "HeXllo");
assert_eq!(marks_at(&t, 2), vec!["bold"], "X should inherit bold");
assert_eq!(marks_at(&t, 5), vec!["bold"]);
}
#[test]
fn typing_after_span_does_not_inherit() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..5, "bold", true);
t.insert(5, '!');
assert_eq!(t.to_string(), "Hello!");
assert!(marks_at(&t, 5).is_empty());
}
#[test]
fn concurrent_mark_higher_op_wins() {
let mut alice = Text::new(1);
let mut bob = Text::new(2);
alice.insert_str(0, "Hello");
bob.merge(&alice);
alice.set_mark(0..5, "bold", true);
bob.set_mark(0..5, "bold", false);
let mut a2 = alice.clone();
a2.merge(&bob);
let mut b2 = bob.clone();
b2.merge(&alice);
let render_a: Vec<bool> = a2
.iter_with_marks()
.map(|(_, m)| m.contains("bold"))
.collect();
let render_b: Vec<bool> = b2
.iter_with_marks()
.map(|(_, m)| m.contains("bold"))
.collect();
assert_eq!(render_a, render_b);
assert_eq!(render_a, vec![false; 5]);
}
#[test]
fn idempotent_apply() {
let mut a = Text::new(1);
a.insert_str(0, "Hi");
a.set_mark(0..2, "italic", true);
let mut b = Text::new(2);
for op in a.ops().to_vec() {
b.apply(op.clone()).unwrap();
b.apply(op).unwrap(); }
assert_eq!(b.to_string(), "Hi");
assert_eq!(marks_at(&b, 0), vec!["italic"]);
assert_eq!(marks_at(&b, 1), vec!["italic"]);
}
#[test]
fn deleting_marked_text() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..5, "bold", true);
t.delete(0); assert_eq!(t.to_string(), "ello");
for i in 0..t.len() {
assert_eq!(marks_at(&t, i), vec!["bold"]);
}
}
#[test]
fn multiple_marks_layer() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..3, "bold", true);
t.set_mark(2..5, "italic", true);
assert_eq!(marks_at(&t, 0), vec!["bold"]);
let m2 = marks_at(&t, 2);
assert!(m2.contains(&"bold".to_string()));
assert!(m2.contains(&"italic".to_string()));
assert_eq!(marks_at(&t, 4), vec!["italic"]);
}
#[test]
fn valued_mark_set_and_read() {
let mut t = Text::new(1);
t.insert_str(0, "Click here for the link");
t.set_value_mark(6..10, "href", Some("https://example.com"));
let row = t.iter_with_marks().nth(6).unwrap();
assert_eq!(row.0, 'h');
assert_eq!(row.1.value_of("href"), Some("https://example.com"));
let outside = t.iter_with_marks().next().unwrap();
assert_eq!(outside.1.value_of("href"), None);
}
#[test]
fn valued_mark_unset() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_value_mark(0..5, "color", Some("red"));
t.set_value_mark(0..5, "color", None); for (_, m) in t.iter_with_marks() {
assert_eq!(m.value_of("color"), None);
assert!(!m.contains("color"));
}
}
#[test]
fn valued_mark_concurrent_resolution() {
let mut a = Text::new(1);
let mut b = Text::new(2);
a.insert_str(0, "Link");
b.merge(&a);
a.set_value_mark(0..4, "href", Some("https://alice.example/"));
b.set_value_mark(0..4, "href", Some("https://bob.example/"));
let mut a2 = a.clone();
a2.merge(&b);
let mut b2 = b.clone();
b2.merge(&a);
let render_a: Vec<Option<String>> = a2
.iter_with_marks()
.map(|(_, m)| m.value_of("href").map(String::from))
.collect();
let render_b: Vec<Option<String>> = b2
.iter_with_marks()
.map(|(_, m)| m.value_of("href").map(String::from))
.collect();
assert_eq!(render_a, render_b);
assert_eq!(render_a[0].as_deref(), Some("https://bob.example/"));
}
#[test]
fn boolean_and_valued_marks_coexist() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..5, "bold", true);
t.set_value_mark(0..5, "color", Some("blue"));
let row = t.iter_with_marks().next().unwrap();
assert!(row.1.contains("bold"));
assert!(row.1.contains("color"));
assert_eq!(row.1.value_of("color"), Some("blue"));
assert_eq!(row.1.value_of("bold"), None);
let booleans: Vec<&str> = row.1.iter_booleans().collect();
let values: Vec<(&str, &str)> = row.1.iter_values().collect();
assert_eq!(booleans, vec!["bold"]);
assert_eq!(values, vec![("color", "blue")]);
}
fn marks_at_with_set(text: &Text, pos: usize) -> Vec<String> {
text.iter_with_marks()
.nth(pos)
.map(|(_, m)| m.iter().map(String::from).collect())
.unwrap_or_default()
}
#[test]
fn expand_right_extends_to_new_typing() {
let mut t = Text::new(1);
t.insert_str(0, "Hi");
t.set_mark_with_rule(0..2, "bold", SpanValue::On, ExpandRule::Right);
t.insert(2, '!');
t.insert(3, '?');
assert_eq!(t.as_string(), "Hi!?");
for i in 0..t.len() {
assert_eq!(marks_at_with_set(&t, i), vec!["bold"], "pos {i}");
}
}
#[test]
fn expand_right_does_not_extend_left() {
let mut t = Text::new(1);
t.insert_str(0, "Hi");
t.set_mark_with_rule(0..2, "bold", SpanValue::On, ExpandRule::Right);
t.insert(0, 'X');
assert_eq!(t.as_string(), "XHi");
assert!(marks_at_with_set(&t, 0).is_empty());
assert_eq!(marks_at_with_set(&t, 1), vec!["bold"]);
}
#[test]
fn expand_left_extends_to_new_typing_at_front() {
let mut t = Text::new(1);
t.insert_str(0, "Hi");
t.set_mark_with_rule(0..2, "bold", SpanValue::On, ExpandRule::Left);
t.insert(0, 'X');
assert_eq!(t.as_string(), "XHi");
assert_eq!(marks_at_with_set(&t, 0), vec!["bold"]);
}
#[test]
fn expand_both_extends_both_ways() {
let mut t = Text::new(1);
t.insert_str(0, "ab");
t.set_mark_with_rule(0..2, "bold", SpanValue::On, ExpandRule::Both);
t.insert(0, 'X'); t.insert(t.len(), 'Y'); assert_eq!(t.as_string(), "XabY");
for i in 0..t.len() {
assert_eq!(marks_at_with_set(&t, i), vec!["bold"], "pos {i}");
}
}
#[test]
fn anchor_expand_right() {
let mut t = Text::new(1);
t.insert_str(0, "Hi");
let start_id = t.chars_for_test().id_at(0).unwrap();
let end_id = t.chars_for_test().id_at(1).unwrap();
let _ = (start_id, end_id);
t.set_mark(0..2, "bold", true);
t.insert(2, '!');
assert!(marks_at(&t, 2).is_empty());
}
#[test]
fn grapheme_count_for_emoji_family() {
let mut t = Text::new(1);
t.insert_str(0, "Hi 👨👩👧!");
assert_eq!(t.len(), 9); assert_eq!(t.grapheme_count(), 5); }
#[test]
fn grapheme_pos_conversion_round_trip() {
let mut t = Text::new(1);
t.insert_str(0, "a👨👩👧b");
assert_eq!(t.grapheme_count(), 3);
assert_eq!(t.grapheme_to_char_pos(0), 0);
assert_eq!(t.grapheme_to_char_pos(1), 1);
assert_eq!(t.grapheme_to_char_pos(2), 6);
assert_eq!(t.char_to_grapheme_pos(0), 0);
assert_eq!(t.char_to_grapheme_pos(1), 1);
assert_eq!(t.char_to_grapheme_pos(6), 2);
}
#[test]
fn insert_grapheme_str_at_grapheme_position() {
let mut t = Text::new(1);
t.insert_str(0, "a👨👩👧b");
t.insert_grapheme_str(2, "Z");
assert_eq!(t.as_string(), "a👨👩👧Zb");
assert_eq!(t.grapheme_count(), 4);
}
#[test]
fn delete_grapheme_removes_whole_emoji() {
let mut t = Text::new(1);
t.insert_str(0, "a👨👩👧b");
t.delete_grapheme(1);
assert_eq!(t.as_string(), "ab");
assert_eq!(t.grapheme_count(), 2);
}
#[test]
fn delta_round_trip_plain_text() {
let mut t = Text::new(1);
t.insert_str(0, "Hello, world!");
let delta = t.to_delta();
assert_eq!(delta.len(), 1);
assert_eq!(delta[0].insert, "Hello, world!");
assert!(delta[0].attributes.is_empty());
let restored = Text::from_delta(2, &delta);
assert_eq!(restored.as_string(), "Hello, world!");
}
#[test]
fn delta_round_trip_with_marks() {
let mut t = Text::new(1);
t.insert_str(0, "Hello world");
t.set_mark(0..5, "bold", true);
t.set_mark(6..11, "italic", true);
let delta = t.to_delta();
assert_eq!(delta.len(), 3);
assert_eq!(delta[0].insert, "Hello");
assert_eq!(delta[0].attributes.len(), 1);
assert!(matches!(
delta[0].attributes.get("bold"),
Some(AttrValue::Bool(true))
));
assert_eq!(delta[1].insert, " ");
assert!(delta[1].attributes.is_empty());
assert_eq!(delta[2].insert, "world");
assert!(matches!(
delta[2].attributes.get("italic"),
Some(AttrValue::Bool(true))
));
let restored = Text::from_delta(2, &delta);
let delta2 = restored.to_delta();
assert_eq!(delta, delta2);
assert_eq!(restored.as_string(), "Hello world");
}
#[test]
fn delta_with_valued_marks() {
let mut t = Text::new(1);
t.insert_str(0, "Click here");
t.set_value_mark(6..10, "href", Some("https://example.com"));
let delta = t.to_delta();
let last = delta.last().unwrap();
assert_eq!(last.insert, "here");
assert_eq!(
last.attributes.get("href"),
Some(&AttrValue::String("https://example.com".to_string()))
);
let restored = Text::from_delta(2, &delta);
let restored_delta = restored.to_delta();
assert_eq!(delta, restored_delta);
}
#[test]
fn delta_overlapping_marks() {
let mut t = Text::new(1);
t.insert_str(0, "Hello");
t.set_mark(0..5, "bold", true);
t.set_mark(2..5, "italic", true);
let delta = t.to_delta();
assert_eq!(delta.len(), 2);
assert_eq!(delta[0].insert, "He");
assert_eq!(delta[0].attributes.len(), 1);
assert_eq!(delta[1].insert, "llo");
assert_eq!(delta[1].attributes.len(), 2);
}
}