use super::offset_types::{DocByteOffset, DocCharOffset, RangeExt, RelCharOffset};
use super::operation_types::{InverseOperation, Operation, Replace};
use super::unicode_segs::UnicodeSegs;
use super::{diff, unicode_segs};
use std::ops::Index;
use unicode_segmentation::UnicodeSegmentation;
use web_time::{Duration, Instant};
#[derive(Default)]
pub struct Buffer {
pub current: Snapshot,
base: Snapshot,
ops: Ops,
external: External,
}
#[derive(Debug, Default)]
pub struct Snapshot {
pub text: String,
pub segs: UnicodeSegs,
pub selection: (DocCharOffset, DocCharOffset),
pub seq: usize,
}
impl Snapshot {
fn apply_select(&mut self, range: (DocCharOffset, DocCharOffset)) -> Response {
self.selection = range;
Response::default()
}
fn apply_replace(&mut self, replace: &Replace) -> Response {
let Replace { range, text } = replace;
let byte_range = self.segs.range_to_byte(*range);
self.text
.replace_range(byte_range.start().0..byte_range.end().0, text);
self.segs = unicode_segs::calc(&self.text);
adjust_subsequent_range(
*range,
text.graphemes(true).count().into(),
false,
&mut self.selection,
);
Response { text_updated: true, ..Default::default() }
}
fn invert(&self, op: &Operation) -> InverseOperation {
let mut inverse = InverseOperation { replace: None, select: self.selection };
if let Operation::Replace(replace) = op {
inverse.replace = Some(self.invert_replace(replace));
}
inverse
}
fn invert_replace(&self, replace: &Replace) -> Replace {
let Replace { range, text } = replace;
let byte_range = self.segs.range_to_byte(*range);
let replaced_text = self[byte_range].into();
let replacement_range = (range.start(), range.start() + text.graphemes(true).count());
Replace { range: replacement_range, text: replaced_text }
}
}
#[derive(Default)]
struct Ops {
all: Vec<Operation>,
meta: Vec<OpMeta>,
processed_seq: usize,
transformed: Vec<Operation>,
transformed_inverted: Vec<InverseOperation>,
}
impl Ops {
fn len(&self) -> usize {
self.all.len()
}
fn is_undo_checkpoint(&self, idx: usize) -> bool {
if idx == 0 {
return true;
}
if idx == self.len() {
return true;
}
let meta = &self.meta[idx];
let prev_meta = &self.meta[idx - 1];
if meta.timestamp - prev_meta.timestamp > Duration::from_millis(500) {
return true;
}
let mut prev_op_standalone = meta.base != prev_meta.base;
if idx > 1 {
let prev_prev_meta = &self.meta[idx - 2];
prev_op_standalone &= prev_meta.base != prev_prev_meta.base;
}
let prev_op_selection = matches!(&self.all[idx - 1], Operation::Select(..));
if prev_op_standalone && prev_op_selection {
return true;
}
false
}
}
#[derive(Default)]
struct External {
text: String,
seq: usize,
}
#[derive(Default)]
pub struct Response {
pub text_updated: bool,
pub open_camera: bool,
pub seq_before: usize,
pub seq_after: usize,
}
impl std::ops::BitOrAssign for Response {
fn bitor_assign(&mut self, other: Response) {
self.text_updated |= other.text_updated;
self.open_camera |= other.open_camera;
if self.seq_before == self.seq_after {
self.seq_before = other.seq_before;
}
self.seq_after = other.seq_after;
}
}
#[derive(Clone, Debug)]
struct OpMeta {
pub timestamp: Instant,
pub base: usize,
}
impl Buffer {
pub fn queue(&mut self, mut ops: Vec<Operation>) {
let timestamp = Instant::now();
let base = self.current.seq;
let mut combined_ops = Vec::new();
ops.sort_by_key(|op| match op {
Operation::Select(range) | Operation::Replace(Replace { range, .. }) => range.start(),
});
for op in ops.into_iter() {
match &op {
Operation::Replace(Replace { range: op_range, text: op_text }) => {
if let Some(Operation::Replace(Replace {
range: last_op_range,
text: last_op_text,
})) = combined_ops.last_mut()
{
if last_op_range.end() == op_range.start() {
last_op_range.1 = op_range.1;
last_op_text.push_str(op_text);
} else {
combined_ops.push(op);
}
} else {
combined_ops.push(op);
}
}
Operation::Select(_) => combined_ops.push(op),
}
}
self.ops
.meta
.extend(combined_ops.iter().map(|_| OpMeta { timestamp, base }));
self.ops.all.extend(combined_ops);
}
pub fn reload(&mut self, text: String) {
let timestamp = Instant::now();
let base = self.external.seq;
let ops = diff(&self.external.text, &text);
self.ops
.meta
.extend(ops.iter().map(|_| OpMeta { timestamp, base }));
self.ops.all.extend(ops.into_iter().map(Operation::Replace));
self.external.text = text;
self.external.seq = self.base.seq + self.ops.all.len();
}
pub fn saved(&mut self, external_seq: usize, external_text: String) {
self.external.text = external_text;
self.external.seq = external_seq;
}
pub fn merge(mut self, external_text_a: String, external_text_b: String) -> String {
let ops_a = diff(&self.external.text, &external_text_a);
let ops_b = diff(&self.external.text, &external_text_b);
let timestamp = Instant::now();
let base = self.external.seq;
self.ops
.meta
.extend(ops_a.iter().map(|_| OpMeta { timestamp, base }));
self.ops
.meta
.extend(ops_b.iter().map(|_| OpMeta { timestamp, base }));
self.ops
.all
.extend(ops_a.into_iter().map(Operation::Replace));
self.ops
.all
.extend(ops_b.into_iter().map(Operation::Replace));
self.update();
self.current.text
}
pub fn update(&mut self) -> Response {
let queue_len = self.base.seq + self.ops.len() - self.ops.processed_seq;
if queue_len > 0 {
let drain_range = self.current.seq..self.ops.processed_seq;
self.ops.all.drain(drain_range.clone());
self.ops.meta.drain(drain_range.clone());
self.ops.transformed.drain(drain_range.clone());
self.ops.transformed_inverted.drain(drain_range.clone());
self.ops.processed_seq = self.current.seq;
} else {
return Response::default();
}
let mut result = Response { seq_before: self.current.seq, ..Default::default() };
for idx in self.current_idx()..self.current_idx() + queue_len {
let mut op = self.ops.all[idx].clone();
let meta = &self.ops.meta[idx];
self.transform(&mut op, meta);
self.ops.transformed_inverted.push(self.current.invert(&op));
self.ops.transformed.push(op.clone());
self.ops.processed_seq += 1;
result |= self.redo();
}
result.seq_after = self.current.seq;
result
}
fn transform(&self, op: &mut Operation, meta: &OpMeta) {
let base_idx = meta.base - self.base.seq;
for transforming_idx in base_idx..self.ops.processed_seq {
let preceding_op = &self.ops.transformed[transforming_idx];
if let Operation::Replace(Replace {
range: preceding_replaced_range,
text: preceding_replacement_text,
}) = preceding_op
{
if let Operation::Replace(Replace { range: transformed_range, text }) = op {
if preceding_replaced_range.intersects(transformed_range, false)
&& !(preceding_replaced_range.is_empty() && transformed_range.is_empty())
{
*text = "".into();
transformed_range.1 = transformed_range.0;
}
}
match op {
Operation::Replace(Replace { range: transformed_range, .. })
| Operation::Select(transformed_range) => {
adjust_subsequent_range(
*preceding_replaced_range,
preceding_replacement_text.graphemes(true).count().into(),
true,
transformed_range,
);
}
}
}
}
}
pub fn can_redo(&self) -> bool {
self.current.seq < self.ops.processed_seq
}
pub fn can_undo(&self) -> bool {
self.current.seq > self.base.seq
}
pub fn redo(&mut self) -> Response {
let mut response = Response::default();
while self.can_redo() {
let op = &self.ops.transformed[self.current_idx()];
self.current.seq += 1;
response |= match op {
Operation::Replace(replace) => self.current.apply_replace(replace),
Operation::Select(range) => self.current.apply_select(*range),
};
if self.ops.is_undo_checkpoint(self.current_idx()) {
break;
}
}
response
}
pub fn undo(&mut self) -> Response {
let mut response = Response::default();
while self.can_undo() {
self.current.seq -= 1;
let op = &self.ops.transformed_inverted[self.current_idx()];
if let Some(replace) = &op.replace {
response |= self.current.apply_replace(replace);
}
response |= self.current.apply_select(op.select);
if self.ops.is_undo_checkpoint(self.current_idx()) {
break;
}
}
response
}
fn current_idx(&self) -> usize {
self.current.seq - self.base.seq
}
pub fn transform_range(
&self, since_seq: usize, range: &mut (DocCharOffset, DocCharOffset),
) -> bool {
let start = since_seq.saturating_sub(self.base.seq);
let end = self.current_idx();
for op in &self.ops.transformed[start..end] {
if let Operation::Replace(replace) = op {
if range.intersects(&replace.range, true)
&& !(range.is_empty() && replace.range.is_empty())
{
return false;
}
let replacement_len = replace.text.graphemes(true).count().into();
adjust_subsequent_range(replace.range, replacement_len, false, range);
}
}
true
}
pub fn is_empty(&self) -> bool {
self.current.text.is_empty()
}
pub fn selection_text(&self) -> String {
self[self.current.selection].to_string()
}
}
impl From<&str> for Buffer {
fn from(value: &str) -> Self {
let mut result = Self::default();
result.current.text = value.to_string();
result.current.segs = unicode_segs::calc(value);
result.external.text = value.to_string();
result
}
}
pub fn adjust_subsequent_range(
replaced_range: (DocCharOffset, DocCharOffset), replacement_len: RelCharOffset,
prefer_advance: bool, range: &mut (DocCharOffset, DocCharOffset),
) {
for position in [&mut range.0, &mut range.1] {
adjust_subsequent_position(replaced_range, replacement_len, prefer_advance, position);
}
}
fn adjust_subsequent_position(
replaced_range: (DocCharOffset, DocCharOffset), replacement_len: RelCharOffset,
prefer_advance: bool, position: &mut DocCharOffset,
) {
let replaced_len = replaced_range.len();
let replacement_start = replaced_range.start();
let replacement_end = replacement_start + replacement_len;
enum Mode {
Insert,
Replace,
}
let mode = if replaced_range.is_empty() { Mode::Insert } else { Mode::Replace };
let sorted_bounds = {
let mut bounds = vec![replaced_range.start(), replaced_range.end(), *position];
bounds.sort();
bounds
};
let bind = |start: &DocCharOffset, end: &DocCharOffset, pos: &DocCharOffset| {
start == &replaced_range.start() && end == &replaced_range.end() && pos == &*position
};
*position = match (mode, &sorted_bounds[..]) {
(Mode::Insert, [start, end, pos]) if bind(start, end, pos) && end == pos => {
if prefer_advance {
replacement_end
} else {
replacement_start
}
}
(Mode::Replace, [start, pos, end]) if bind(start, end, pos) && start == pos => {
if prefer_advance {
replacement_end
} else {
replacement_start
}
}
(Mode::Replace, [start, end, pos]) if bind(start, end, pos) && end == pos => {
if prefer_advance {
replacement_end
} else {
replacement_start
}
}
(_, [pos, start, end]) if bind(start, end, pos) => *position,
(Mode::Replace, [start, pos, end]) if bind(start, end, pos) => {
if prefer_advance {
replacement_end
} else {
replacement_start
}
}
(_, [start, end, pos]) if bind(start, end, pos) => {
*position + replacement_len - replaced_len
}
_ => unreachable!(),
}
}
impl Index<(DocByteOffset, DocByteOffset)> for Snapshot {
type Output = str;
fn index(&self, index: (DocByteOffset, DocByteOffset)) -> &Self::Output {
&self.text[index.start().0..index.end().0]
}
}
impl Index<(DocCharOffset, DocCharOffset)> for Snapshot {
type Output = str;
fn index(&self, index: (DocCharOffset, DocCharOffset)) -> &Self::Output {
let index = self.segs.range_to_byte(index);
&self.text[index.start().0..index.end().0]
}
}
impl Index<(DocByteOffset, DocByteOffset)> for Buffer {
type Output = str;
fn index(&self, index: (DocByteOffset, DocByteOffset)) -> &Self::Output {
&self.current[index]
}
}
impl Index<(DocCharOffset, DocCharOffset)> for Buffer {
type Output = str;
fn index(&self, index: (DocCharOffset, DocCharOffset)) -> &Self::Output {
&self.current[index]
}
}
#[cfg(test)]
mod test {
use super::Buffer;
use unicode_segmentation::UnicodeSegmentation;
#[test]
fn buffer_merge_nonintersecting_replace() {
let base_content = "base content base";
let local_content = "local content base";
let remote_content = "base content remote";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"local content remote"
);
assert_eq!(
Buffer::from(base_content).merge(remote_content.into(), local_content.into()),
"local content remote"
);
}
#[test]
fn buffer_merge_prefix_replace() {
let base_content = "base content";
let local_content = "local content";
let remote_content = "remote content";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"local content"
);
}
#[test]
fn buffer_merge_infix_replace() {
let base_content = "con base tent";
let local_content = "con local tent";
let remote_content = "con remote tent";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"con local tent"
);
assert_eq!(
Buffer::from(base_content).merge(remote_content.into(), local_content.into()),
"con remote tent"
);
}
#[test]
fn buffer_merge_postfix_replace() {
let base_content = "content base";
let local_content = "content local";
let remote_content = "content remote";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"content local"
);
assert_eq!(
Buffer::from(base_content).merge(remote_content.into(), local_content.into()),
"content remote"
);
}
#[test]
fn buffer_merge_insert() {
let base_content = "content";
let local_content = "content local";
let remote_content = "content remote";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"content local remote"
);
assert_eq!(
Buffer::from(base_content).merge(remote_content.into(), local_content.into()),
"content remote local"
);
}
#[test]
fn buffer_merge_insert_replace() {
let base_content = "content";
let local_content = "content local";
let remote_content = "remote";
assert_eq!(
Buffer::from(base_content).merge(local_content.into(), remote_content.into()),
"remote"
);
assert_eq!(
Buffer::from(base_content).merge(remote_content.into(), local_content.into()),
"remote local"
);
}
#[test]
fn buffer_merge_crash() {
let base_content = "con tent";
let local_content = "cont tent locallocal";
let remote_content = "cont remote tent";
let _ = Buffer::from(base_content).merge(local_content.into(), remote_content.into());
let _ = Buffer::from(base_content).merge(remote_content.into(), local_content.into());
}
use rand::prelude::*;
const POOL: &[&str] = &[
"a",
"b",
"z",
" ",
"\n",
"\t",
"é",
"ñ",
"ü",
"日",
"本",
"語",
"👋",
"🎉",
"🔥",
"❤️",
"👨👩👧👦",
"🏳️🌈",
"👍🏽",
"🇺🇸",
"🇯🇵",
"e\u{0301}",
"a\u{0308}", ];
fn random_edit(rng: &mut StdRng, doc: &str) -> String {
let graphemes: Vec<&str> = UnicodeSegmentation::graphemes(doc, true).collect();
let len = graphemes.len();
let mut g: Vec<String> = graphemes.iter().map(|s| s.to_string()).collect();
match rng.gen_range(0..4) {
0 if len > 0 => {
let pos = rng.gen_range(0..len);
let del = rng.gen_range(1..=(len - pos).min(5));
g.drain(pos..pos + del);
}
1 => {
let pos = rng.gen_range(0..=len);
let n = rng.gen_range(1..=5);
for j in 0..n {
g.insert(pos + j, POOL[rng.gen_range(0..POOL.len())].into());
}
}
2 if len > 0 => {
let pos = rng.gen_range(0..len);
let del = rng.gen_range(1..=(len - pos).min(5));
let ins: Vec<String> = (0..rng.gen_range(1..=3))
.map(|_| POOL[rng.gen_range(0..POOL.len())].into())
.collect();
g.splice(pos..pos + del, ins);
}
3 if len > 0 => {
g.clear();
}
_ => {
let n = rng.gen_range(1..=5);
for _ in 0..n {
g.push(POOL[rng.gen_range(0..POOL.len())].into());
}
}
}
g.concat()
}
#[test]
fn buffer_merge_fuzz() {
let mut rng = StdRng::seed_from_u64(42);
let bases = ["hello world", "👨👩👧👦🇺🇸🔥", "café ñoño 日本語", ""];
for _ in 0..10_000 {
let base = bases[rng.gen_range(0..bases.len())];
let a = random_edit(&mut rng, base);
let b = random_edit(&mut rng, base);
let _ = Buffer::from(base).merge(a.clone(), b.clone());
let _ = Buffer::from(base).merge(b, a);
}
}
struct SyncLink {
base: String,
}
impl SyncLink {
fn new(base: &str) -> Self {
Self { base: base.into() }
}
fn sync(&mut self, left: &mut String, right: &mut String) {
let merged = Buffer::from(self.base.as_str()).merge(left.clone(), right.clone());
*left = merged.clone();
*right = merged.clone();
self.base = merged;
}
}
fn full_sync(nodes: &mut [String], links: &mut [SyncLink]) {
for _ in 0..nodes.len() * 2 {
for i in 0..links.len() {
let (left, right) = nodes.split_at_mut(i + 1);
links[i].sync(&mut left[i], &mut right[0]);
}
for i in (0..links.len()).rev() {
let (left, right) = nodes.split_at_mut(i + 1);
links[i].sync(&mut left[i], &mut right[0]);
}
}
}
fn partial_sync(nodes: &mut [String], links: &mut [SyncLink], rng: &mut StdRng) {
for _ in 0..3 {
for i in 0..links.len() {
if rng.gen_bool(0.5) {
let (left, right) = nodes.split_at_mut(i + 1);
links[i].sync(&mut left[i], &mut right[0]);
}
}
}
}
fn assert_converged(nodes: &[String]) {
for (i, node) in nodes.iter().enumerate().skip(1) {
assert_eq!(
&nodes[0], node,
"node 0 and node {} diverged: {:?} vs {:?}",
i, nodes[0], node
);
}
}
#[test]
fn buffer_merge_fuzz_chain_2() {
let mut rng = StdRng::seed_from_u64(42);
for _ in 0..10_000 {
let init = if rng.gen_bool(0.5) { "hello 👋🏽" } else { "" };
let mut nodes: Vec<String> = (0..2).map(|_| init.into()).collect();
let mut links: Vec<SyncLink> = (0..1).map(|_| SyncLink::new(init)).collect();
for _ in 0..rng.gen_range(1..=4) {
for _ in 0..rng.gen_range(1..=3) {
let i = rng.gen_range(0..2);
nodes[i] = random_edit(&mut rng, &nodes[i]);
}
if rng.gen_bool(0.5) {
partial_sync(&mut nodes, &mut links, &mut rng);
}
}
full_sync(&mut nodes, &mut links);
assert_converged(&nodes);
}
}
#[test]
fn buffer_merge_fuzz_chain_5() {
let mut rng = StdRng::seed_from_u64(77);
for _ in 0..5_000 {
let init = if rng.gen_bool(0.5) { "café 日本語 🇯🇵" } else { "abc" };
let mut nodes: Vec<String> = (0..5).map(|_| init.into()).collect();
let mut links: Vec<SyncLink> = (0..4).map(|_| SyncLink::new(init)).collect();
for _ in 0..rng.gen_range(1..=3) {
for _ in 0..rng.gen_range(1..=5) {
let i = rng.gen_range(0..5);
nodes[i] = random_edit(&mut rng, &nodes[i]);
}
if rng.gen_bool(0.5) {
partial_sync(&mut nodes, &mut links, &mut rng);
}
}
full_sync(&mut nodes, &mut links);
assert_converged(&nodes);
}
}
}