Repository: spebern/operational-transform-rs
Branch: master
Commit: 9faa17f0a2b2
Files: 14
Total size: 40.8 KB
Directory structure:
gitextract_p95hh1cn/
├── .cargo/
│ └── config
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── operational-transform/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── benches/
│ │ └── benchmark.rs
│ └── src/
│ ├── lib.rs
│ ├── serde.rs
│ └── utilities.rs
└── xtask/
├── Cargo.toml
└── src/
└── main.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .cargo/config
================================================
[alias]
xtask = "run --package xtask --"
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
pull_request:
push:
branches:
- master
# you can enable a schedule to build
# schedule:
# - cron: '00 01 * * *'
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- uses: Swatinem/rust-cache@v1
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
coverage:
name: Coverage
strategy:
matrix:
os: [ubuntu-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v1
- name: Download grcov
run: |
mkdir -p "${HOME}/.local/bin"
curl -sL https://github.com/mozilla/grcov/releases/download/v0.8.10/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf - -C "${HOME}/.local/bin"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Run xtask coverage
uses: actions-rs/cargo@v1
with:
command: xtask
args: coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
with:
files: coverage/*.lcov
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v1
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
================================================
FILE: .gitignore
================================================
.DS_Store
.idea
*.log
tmp/
target/
coverage/
================================================
FILE: Cargo.toml
================================================
[workspace]
members = [
"operational-transform",
"xtask"
]
================================================
FILE: LICENSE
================================================
MIT License Copyright (c) 2020 bold
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# operational-transform
[![Crates.io][crates-badge]][crates-url]
[![docs.rs docs][docs-badge]][docs-url]
[![ci][ci-badge]][ci-url]
[](https://codecov.io/gh/spebern/operational-transform-rs)
[crates-badge]: https://img.shields.io/crates/v/operational-transform.svg
[crates-url]: https://crates.io/crates/operational-transform
[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg
[docs-url]: https://docs.rs/operational-transform
[ci-badge]: https://github.com/spebern/operational-transform-rs/workflows/Rust/badge.svg
[ci-url]: https://github.com/spebern/operational-transform-rs/actions
A library for Operational Transformation
Operational transformation (OT) is a technology for supporting a range of
collaboration functionalities in advanced collaborative software systems.
[[1]](https://en.wikipedia.org/wiki/Operational_transformation)
When working on the same document over the internet concurrent operations
from multiple users might be in conflict. **Operational Transform** can help
to resolve conflicts in such a way that documents stay in sync.
The basic operations that are supported are:
- Retain(n): Move the cursor `n` positions forward
- Delete(n): Delete `n` characters at the current position
- Insert(s): Insert the string `s` at the current position
This library can be used to...
... compose sequences of operations:
```rust
use operational_transform::OperationSeq;
let mut a = OperationSeq::default();
a.insert("abc");
let mut b = OperationSeq::default();
b.retain(3);
b.insert("def");
let after_a = a.apply("").unwrap();
let after_b = b.apply(&after_a).unwrap();
let c = a.compose(&b).unwrap();
let after_ab = a.compose(&b).unwrap().apply("").unwrap();
assert_eq!(after_ab, after_b);
```
... transform sequences of operations
```rust
use operational_transform::OperationSeq;
let s = "abc";
let mut a = OperationSeq::default();
a.retain(3);
a.insert("def");
let mut b = OperationSeq::default();
b.retain(3);
b.insert("ghi");
let (a_prime, b_prime) = a.transform(&b).unwrap();
let ab_prime = a.compose(&b_prime).unwrap();
let ba_prime = b.compose(&a_prime).unwrap();
let after_ab_prime = ab_prime.apply(s).unwrap();
let after_ba_prime = ba_prime.apply(s).unwrap();
assert_eq!(ab_prime, ba_prime);
assert_eq!(after_ab_prime, after_ba_prime);
```
... invert sequences of operations
```rust
use operational_transform::OperationSeq;
let s = "abc";
let mut o = OperationSeq::default();
o.retain(3);
o.insert("def");
let p = o.invert(s);
assert_eq!(p.apply(&o.apply(s).unwrap()).unwrap(), s);
```
### Features
Serialisation is supporeted by using the `serde` feature.
- Delete(n) will be serialized to -n
- Insert(s) will be serialized to "{s}"
- Retain(n) will be serailized to n
```rust
use operational_transform::OperationSeq;
use serde_json;
let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap();
let mut o_exp = OperationSeq::default();
o_exp.retain(1);
o_exp.delete(1);
o_exp.insert("abc");
assert_eq!(o, o_exp);
```
### Acknowledgement
In the current state the code is ported from
[here](https://github.com/Operational-Transformation/ot.js/). It might
change in the future as there is much room for optimisation and also
usability.
================================================
FILE: operational-transform/.gitignore
================================================
/target
Cargo.lock
================================================
FILE: operational-transform/Cargo.toml
================================================
[package]
name = "operational-transform"
version = "0.6.1"
authors = ["bold <bold@cryptoguru.com>"]
edition = "2018"
license = "MIT"
description = "A library for Operational Transformation"
readme = "README.md"
keywords = ["operational", "transform", "collaborative", "editing"]
repository = "https://github.com/spebern/operational-transform-rs"
[features]
runtime-dispatch-simd = ["bytecount/runtime-dispatch-simd"]
generic-simd = ["bytecount/generic-simd"]
[dependencies]
serde = { version = "1", default-features = false, optional = true }
bytecount = "0.6.0"
[dev-dependencies]
rand = "0.7.3"
serde_json = "1.0.50"
criterion = "0.3"
[[bench]]
name = "benchmark"
harness = false
================================================
FILE: operational-transform/benches/benchmark.rs
================================================
use operational_transform::OperationSeq;
#[path = "../src/utilities.rs"]
mod utilities;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use utilities::Rng;
pub fn compose(c: &mut Criterion) {
let mut rng = Rng::from_seed(Default::default());
let input = (0..1000)
.map(|_| {
let s = rng.gen_string(20);
let a = rng.gen_operation_seq(&s);
let after_a = a.apply(&s).unwrap();
let b = rng.gen_operation_seq(&after_a);
(a, b)
})
.collect::<Vec<_>>();
c.bench_function("compose", |b| {
b.iter(|| {
for (a, b) in input.iter() {
let _ = a.compose(black_box(b));
}
})
});
}
pub fn transform(c: &mut Criterion) {
let mut rng = Rng::from_seed(Default::default());
let input = (0..1000)
.map(|_| {
let s = rng.gen_string(20);
let a = rng.gen_operation_seq(&s);
let b = rng.gen_operation_seq(&s);
(a, b)
})
.collect::<Vec<_>>();
c.bench_function("transform", |b| {
b.iter(|| {
for (a, b) in input.iter() {
let _ = a.transform(black_box(b));
}
})
});
}
pub fn invert(c: &mut Criterion) {
let mut rng = Rng::from_seed(Default::default());
let input = (0..1000)
.map(|_| {
let s = rng.gen_string(50);
let o = rng.gen_operation_seq(&s);
(o, s)
})
.collect::<Vec<_>>();
c.bench_function("invert", |b| {
b.iter(|| {
for (o, s) in input.iter() {
let _ = o.invert(black_box(&s));
}
})
});
}
pub fn apply(c: &mut Criterion) {
let mut rng = Rng::from_seed(Default::default());
let input = (0..1000)
.map(|_| {
let s = rng.gen_string(50);
let o = rng.gen_operation_seq(&s);
(o, s)
})
.collect::<Vec<_>>();
c.bench_function("apply", |b| {
b.iter(|| {
for (o, s) in input.iter() {
let _ = o.apply(black_box(&s));
}
})
});
}
criterion_group!(benches, compose, transform, invert, apply);
criterion_main!(benches);
================================================
FILE: operational-transform/src/lib.rs
================================================
//! A library for Operational Transformation
//!
//! Operational transformation (OT) is a technology for supporting a range of
//! collaboration functionalities in advanced collaborative software systems.
//! [[1]](https://en.wikipedia.org/wiki/Operational_transformation)
//!
//! When working on the same document over the internet concurrent operations
//! from multiple users might be in conflict. **Operational Transform** can help
//! to resolve conflicts in such a way that documents stay in sync.
//!
//! The basic operations that are supported are:
//! - Retain(n): Move the cursor `n` positions forward
//! - Delete(n): Delete `n` characters at the current position
//! - Insert(s): Insert the string `s` at the current position
//!
//! This library can be used to...
//!
//! ... compose sequences of operations:
//! ```rust
//! use operational_transform::OperationSeq;
//!
//! let mut a = OperationSeq::default();
//! a.insert("abc");
//! let mut b = OperationSeq::default();
//! b.retain(3);
//! b.insert("def");
//! let after_a = a.apply("").unwrap();
//! let after_b = b.apply(&after_a).unwrap();
//! let c = a.compose(&b).unwrap();
//! let after_ab = a.compose(&b).unwrap().apply("").unwrap();
//! assert_eq!(after_ab, after_b);
//! ```
//!
//! ... transform sequences of operations
//! ```rust
//! use operational_transform::OperationSeq;
//!
//! let s = "abc";
//! let mut a = OperationSeq::default();
//! a.retain(3);
//! a.insert("def");
//! let mut b = OperationSeq::default();
//! b.retain(3);
//! b.insert("ghi");
//! let (a_prime, b_prime) = a.transform(&b).unwrap();
//! let ab_prime = a.compose(&b_prime).unwrap();
//! let ba_prime = b.compose(&a_prime).unwrap();
//! let after_ab_prime = ab_prime.apply(s).unwrap();
//! let after_ba_prime = ba_prime.apply(s).unwrap();
//! assert_eq!(ab_prime, ba_prime);
//! assert_eq!(after_ab_prime, after_ba_prime);
//! ```
//!
//! ... invert sequences of operations
//! ```rust
//! use operational_transform::OperationSeq;
//!
//! let s = "abc";
//! let mut o = OperationSeq::default();
//! o.retain(3);
//! o.insert("def");
//! let p = o.invert(s);
//! assert_eq!(p.apply(&o.apply(s).unwrap()).unwrap(), s);
//! ```
//!
//! ## Features
//!
//! Serialization is supported by using the `serde` feature.
//!
//! - Delete(n) will be serialized to -n
//! - Insert(s) will be serialized to "{s}"
//! - Retain(n) will be serialized to n
//!
//! ```rust,ignore
//! use operational_transform::OperationSeq;
//! use serde_json;
//!
//! let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap();
//! let mut o_exp = OperationSeq::default();
//! o_exp.retain(1);
//! o_exp.delete(1);
//! o_exp.insert("abc");
//! assert_eq!(o, o_exp);
//! ```
//!
//! ## Acknowledgment
//! In the current state the code is ported from
//! [here](https://github.com/Operational-Transformation/ot.js/). It might
//! change in the future as there is much room for optimization and also
//! usability.
#[cfg(feature = "serde")]
pub mod serde;
#[cfg(any(test, bench))]
pub mod utilities;
use bytecount::num_chars;
use std::{cmp::Ordering, error::Error, fmt, iter::FromIterator};
/// A single operation to be executed at the cursor's current position.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operation {
// Deletes n characters at the current cursor position.
Delete(u64),
// Moves the cursor n positions forward.
Retain(u64),
// Inserts string at the current cursor position.
Insert(String),
}
/// A sequence of `Operation`s on text.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct OperationSeq {
// The consecutive operations to be applied to the target.
ops: Vec<Operation>,
// The required length of a string these operations can be applied to.
base_len: usize,
// The length of the resulting string after the operations have been
// applied.
target_len: usize,
}
impl FromIterator<Operation> for OperationSeq {
fn from_iter<T: IntoIterator<Item = Operation>>(ops: T) -> Self {
let mut operations = OperationSeq::default();
for op in ops {
operations.add(op);
}
operations
}
}
/// Error for failed operational transform operations.
#[derive(Clone, Debug)]
pub struct OTError;
impl fmt::Display for OTError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "incompatible lengths")
}
}
impl Error for OTError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
impl OperationSeq {
/// Creates a store for operations which does not need to allocate until
/// `capacity` operations have been stored inside.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
ops: Vec::with_capacity(capacity),
base_len: 0,
target_len: 0,
}
}
/// Merges the operation with `other` into one operation while preserving
/// the changes of both. Or, in other words, for each input string S and a
/// pair of consecutive operations A and B.
/// `apply(apply(S, A), B) = apply(S, compose(A, B))`
/// must hold.
///
/// # Error
///
/// Returns an `OTError` if the operations are not composable due to length
/// conflicts.
pub fn compose(&self, other: &Self) -> Result<Self, OTError> {
if self.target_len != other.base_len {
return Err(OTError);
}
let mut new_op_seq = OperationSeq::default();
let mut ops1 = self.ops.iter().cloned();
let mut ops2 = other.ops.iter().cloned();
let mut maybe_op1 = ops1.next();
let mut maybe_op2 = ops2.next();
loop {
match (&maybe_op1, &maybe_op2) {
(None, None) => break,
(Some(Operation::Delete(i)), _) => {
new_op_seq.delete(*i);
maybe_op1 = ops1.next();
}
(_, Some(Operation::Insert(s))) => {
new_op_seq.insert(s);
maybe_op2 = ops2.next();
}
(None, _) | (_, None) => {
return Err(OTError);
}
(Some(Operation::Retain(i)), Some(Operation::Retain(j))) => match i.cmp(j) {
Ordering::Less => {
new_op_seq.retain(*i);
maybe_op2 = Some(Operation::Retain(*j - *i));
maybe_op1 = ops1.next();
}
std::cmp::Ordering::Equal => {
new_op_seq.retain(*i);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
std::cmp::Ordering::Greater => {
new_op_seq.retain(*j);
maybe_op1 = Some(Operation::Retain(*i - *j));
maybe_op2 = ops2.next();
}
},
(Some(Operation::Insert(s)), Some(Operation::Delete(j))) => {
match (num_chars(s.as_bytes()) as u64).cmp(j) {
Ordering::Less => {
maybe_op2 =
Some(Operation::Delete(*j - num_chars(s.as_bytes()) as u64));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
maybe_op1 =
Some(Operation::Insert(s.chars().skip(*j as usize).collect()));
maybe_op2 = ops2.next();
}
}
}
(Some(Operation::Insert(s)), Some(Operation::Retain(j))) => {
match (num_chars(s.as_bytes()) as u64).cmp(j) {
Ordering::Less => {
new_op_seq.insert(s);
maybe_op2 =
Some(Operation::Retain(*j - num_chars(s.as_bytes()) as u64));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
new_op_seq.insert(s);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
let chars = &mut s.chars();
new_op_seq.insert(&chars.take(*j as usize).collect::<String>());
maybe_op1 = Some(Operation::Insert(chars.collect()));
maybe_op2 = ops2.next();
}
}
}
(Some(Operation::Retain(i)), Some(Operation::Delete(j))) => match i.cmp(j) {
Ordering::Less => {
new_op_seq.delete(*i);
maybe_op2 = Some(Operation::Delete(*j - *i));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
new_op_seq.delete(*j);
maybe_op2 = ops2.next();
maybe_op1 = ops1.next();
}
Ordering::Greater => {
new_op_seq.delete(*j);
maybe_op1 = Some(Operation::Retain(*i - *j));
maybe_op2 = ops2.next();
}
},
};
}
Ok(new_op_seq)
}
fn add(&mut self, op: Operation) {
match op {
Operation::Delete(i) => self.delete(i),
Operation::Insert(s) => self.insert(&s),
Operation::Retain(i) => self.retain(i),
}
}
/// Deletes `n` characters at the current cursor position.
pub fn delete(&mut self, n: u64) {
if n == 0 {
return;
}
self.base_len += n as usize;
if let Some(Operation::Delete(n_last)) = self.ops.last_mut() {
*n_last += n;
} else {
self.ops.push(Operation::Delete(n));
}
}
/// Inserts a `s` at the current cursor position.
pub fn insert(&mut self, s: &str) {
if s.is_empty() {
return;
}
self.target_len += num_chars(s.as_bytes());
let new_last = match self.ops.as_mut_slice() {
[.., Operation::Insert(s_last)] => {
*s_last += s;
return;
}
[.., Operation::Insert(s_pre_last), Operation::Delete(_)] => {
*s_pre_last += s;
return;
}
[.., op_last @ Operation::Delete(_)] => {
let new_last = op_last.clone();
*op_last = Operation::Insert(s.to_owned());
new_last
}
_ => Operation::Insert(s.to_owned()),
};
self.ops.push(new_last);
}
/// Moves the cursor `n` characters forwards.
pub fn retain(&mut self, n: u64) {
if n == 0 {
return;
}
self.base_len += n as usize;
self.target_len += n as usize;
if let Some(Operation::Retain(i_last)) = self.ops.last_mut() {
*i_last += n;
} else {
self.ops.push(Operation::Retain(n));
}
}
/// Transforms two operations A and B that happened concurrently and produces
/// two operations A' and B' (in an array) such that
/// `apply(apply(S, A), B') = apply(apply(S, B), A')`.
/// This function is the heart of OT.
///
/// # Error
///
/// Returns an `OTError` if the operations cannot be transformed due to
/// length conflicts.
pub fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> {
if self.base_len != other.base_len {
return Err(OTError);
}
let mut a_prime = OperationSeq::default();
let mut b_prime = OperationSeq::default();
let mut ops1 = self.ops.iter().cloned();
let mut ops2 = other.ops.iter().cloned();
let mut maybe_op1 = ops1.next();
let mut maybe_op2 = ops2.next();
loop {
match (&maybe_op1, &maybe_op2) {
(None, None) => break,
(Some(Operation::Insert(s)), Some(Operation::Insert(t))) => match s.cmp(t) {
Ordering::Less => {
a_prime.insert(s);
b_prime.retain(num_chars(s.as_bytes()) as _);
maybe_op1 = ops1.next();
}
Ordering::Equal => {
a_prime.insert(s);
a_prime.retain(num_chars(s.as_bytes()) as _);
b_prime.insert(s);
b_prime.retain(num_chars(s.as_bytes()) as _);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
a_prime.retain(num_chars(t.as_bytes()) as _);
b_prime.insert(t);
maybe_op2 = ops2.next();
}
},
(Some(Operation::Insert(s)), _) => {
a_prime.insert(s);
b_prime.retain(num_chars(s.as_bytes()) as _);
maybe_op1 = ops1.next();
}
(_, Some(Operation::Insert(s))) => {
a_prime.retain(num_chars(s.as_bytes()) as _);
b_prime.insert(s);
maybe_op2 = ops2.next();
}
(None, _) | (_, None) => {
return Err(OTError);
}
(Some(Operation::Retain(i)), Some(Operation::Retain(j))) => {
match i.cmp(j) {
Ordering::Less => {
a_prime.retain(*i);
b_prime.retain(*i);
maybe_op2 = Some(Operation::Retain(*j - *i));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
a_prime.retain(*i);
b_prime.retain(*i);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
a_prime.retain(*j);
b_prime.retain(*j);
maybe_op1 = Some(Operation::Retain(*i - *j));
maybe_op2 = ops2.next();
}
};
}
(Some(Operation::Delete(i)), Some(Operation::Delete(j))) => match i.cmp(j) {
Ordering::Less => {
maybe_op2 = Some(Operation::Delete(*j - *i));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
maybe_op1 = Some(Operation::Delete(*i - *j));
maybe_op2 = ops2.next();
}
},
(Some(Operation::Delete(i)), Some(Operation::Retain(j))) => {
match i.cmp(j) {
Ordering::Less => {
a_prime.delete(*i);
maybe_op2 = Some(Operation::Retain(*j - *i));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
a_prime.delete(*i);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
a_prime.delete(*j);
maybe_op1 = Some(Operation::Delete(*i - *j));
maybe_op2 = ops2.next();
}
};
}
(Some(Operation::Retain(i)), Some(Operation::Delete(j))) => {
match i.cmp(j) {
Ordering::Less => {
b_prime.delete(*i);
maybe_op2 = Some(Operation::Delete(*j - *i));
maybe_op1 = ops1.next();
}
Ordering::Equal => {
b_prime.delete(*i);
maybe_op1 = ops1.next();
maybe_op2 = ops2.next();
}
Ordering::Greater => {
b_prime.delete(*j);
maybe_op1 = Some(Operation::Retain(*i - *j));
maybe_op2 = ops2.next();
}
};
}
}
}
Ok((a_prime, b_prime))
}
/// Applies an operation to a string, returning a new string.
///
/// # Error
///
/// Returns an error if the operation cannot be applied due to length
/// conflicts.
pub fn apply(&self, s: &str) -> Result<String, OTError> {
if num_chars(s.as_bytes()) != self.base_len {
return Err(OTError);
}
let mut new_s = String::new();
let chars = &mut s.chars();
for op in &self.ops {
match op {
Operation::Retain(retain) => {
for c in chars.take(*retain as usize) {
new_s.push(c);
}
}
Operation::Delete(delete) => {
for _ in 0..*delete {
chars.next();
}
}
Operation::Insert(insert) => {
new_s += insert;
}
}
}
Ok(new_s)
}
/// Computes the inverse of an operation. The inverse of an operation is the
/// operation that reverts the effects of the operation, e.g. when you have
/// an operation 'insert("hello "); skip(6);' then the inverse is
/// 'delete("hello "); skip(6);'. The inverse should be used for
/// implementing undo.
pub fn invert(&self, s: &str) -> Self {
let mut inverse = OperationSeq::default();
let chars = &mut s.chars();
for op in &self.ops {
match op {
Operation::Retain(retain) => {
inverse.retain(*retain);
for _ in 0..*retain {
chars.next();
}
}
Operation::Insert(insert) => {
inverse.delete(num_chars(insert.as_bytes()) as u64);
}
Operation::Delete(delete) => {
inverse.insert(&chars.take(*delete as usize).collect::<String>());
}
}
}
inverse
}
/// Checks if this operation has no effect.
#[inline]
pub fn is_noop(&self) -> bool {
matches!(self.ops.as_slice(), [] | [Operation::Retain(_)])
}
/// Returns the length of a string these operations can be applied to
#[inline]
pub fn base_len(&self) -> usize {
self.base_len
}
/// Returns the length of the resulting string after the operations have
/// been applied.
#[inline]
pub fn target_len(&self) -> usize {
self.target_len
}
/// Returns the wrapped sequence of operations.
#[inline]
pub fn ops(&self) -> &[Operation] {
&self.ops
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utilities::Rng;
#[test]
fn lengths() {
let mut o = OperationSeq::default();
assert_eq!(o.base_len, 0);
assert_eq!(o.target_len, 0);
o.retain(5);
assert_eq!(o.base_len, 5);
assert_eq!(o.target_len, 5);
o.insert("abc");
assert_eq!(o.base_len, 5);
assert_eq!(o.target_len, 8);
o.retain(2);
assert_eq!(o.base_len, 7);
assert_eq!(o.target_len, 10);
o.delete(2);
assert_eq!(o.base_len, 9);
assert_eq!(o.target_len, 10);
}
#[test]
fn sequence() {
let mut o = OperationSeq::default();
o.retain(5);
o.retain(0);
o.insert("lorem");
o.insert("");
o.delete(3);
o.delete(0);
assert_eq!(o.ops.len(), 3);
}
#[test]
fn apply() {
for _ in 0..1000 {
let mut rng = Rng::default();
let s = rng.gen_string(50);
let o = rng.gen_operation_seq(&s);
assert_eq!(num_chars(s.as_bytes()), o.base_len);
assert_eq!(o.apply(&s).unwrap().chars().count(), o.target_len);
}
}
#[test]
fn invert() {
for _ in 0..1000 {
let mut rng = Rng::default();
let s = rng.gen_string(50);
let o = rng.gen_operation_seq(&s);
let p = o.invert(&s);
assert_eq!(o.base_len, p.target_len);
assert_eq!(o.target_len, p.base_len);
assert_eq!(p.apply(&o.apply(&s).unwrap()).unwrap(), s);
}
}
#[test]
fn empty_ops() {
let mut o = OperationSeq::default();
o.retain(0);
o.insert("");
o.delete(0);
assert_eq!(o.ops.len(), 0);
}
#[test]
fn eq() {
let mut o1 = OperationSeq::default();
o1.delete(1);
o1.insert("lo");
o1.retain(2);
o1.retain(3);
let mut o2 = OperationSeq::default();
o2.delete(1);
o2.insert("l");
o2.insert("o");
o2.retain(5);
assert_eq!(o1, o2);
o1.delete(1);
o2.retain(1);
assert_ne!(o1, o2);
}
#[test]
fn ops_merging() {
let mut o = OperationSeq::default();
assert_eq!(o.ops.len(), 0);
o.retain(2);
assert_eq!(o.ops.len(), 1);
assert_eq!(o.ops.last(), Some(&Operation::Retain(2)));
o.retain(3);
assert_eq!(o.ops.len(), 1);
assert_eq!(o.ops.last(), Some(&Operation::Retain(5)));
o.insert("abc");
assert_eq!(o.ops.len(), 2);
assert_eq!(o.ops.last(), Some(&Operation::Insert("abc".to_owned())));
o.insert("xyz");
assert_eq!(o.ops.len(), 2);
assert_eq!(o.ops.last(), Some(&Operation::Insert("abcxyz".to_owned())));
o.delete(1);
assert_eq!(o.ops.len(), 3);
assert_eq!(o.ops.last(), Some(&Operation::Delete(1)));
o.delete(1);
assert_eq!(o.ops.len(), 3);
assert_eq!(o.ops.last(), Some(&Operation::Delete(2)));
}
#[test]
fn is_noop() {
let mut o = OperationSeq::default();
assert!(o.is_noop());
o.retain(5);
assert!(o.is_noop());
o.retain(3);
assert!(o.is_noop());
o.insert("lorem");
assert!(!o.is_noop());
}
#[test]
fn compose() {
for _ in 0..1000 {
let mut rng = Rng::default();
let s = rng.gen_string(20);
let a = rng.gen_operation_seq(&s);
let after_a = a.apply(&s).unwrap();
assert_eq!(a.target_len, num_chars(after_a.as_bytes()));
let b = rng.gen_operation_seq(&after_a);
let after_b = b.apply(&after_a).unwrap();
assert_eq!(b.target_len, num_chars(after_b.as_bytes()));
let ab = a.compose(&b).unwrap();
assert_eq!(ab.target_len, b.target_len);
let after_ab = ab.apply(&s).unwrap();
assert_eq!(after_b, after_ab);
}
}
#[test]
fn transform() {
for _ in 0..1000 {
let mut rng = Rng::default();
let s = rng.gen_string(20);
let a = rng.gen_operation_seq(&s);
let b = rng.gen_operation_seq(&s);
let (a_prime, b_prime) = a.transform(&b).unwrap();
let (b_prime_2, a_prime_2) = b.transform(&a).unwrap();
assert_eq!(a_prime, a_prime_2);
assert_eq!(b_prime, b_prime_2);
let ab_prime = a.compose(&b_prime).unwrap();
let ba_prime = b.compose(&a_prime).unwrap();
let after_ab_prime = ab_prime.apply(&s).unwrap();
let after_ba_prime = ba_prime.apply(&s).unwrap();
assert_eq!(ab_prime, ba_prime);
assert_eq!(after_ab_prime, after_ba_prime);
}
}
#[test]
#[cfg(feature = "serde")]
fn serde() {
use serde_json;
let mut rng = Rng::default();
let o: OperationSeq = serde_json::from_str("[1,-1,\"abc\"]").unwrap();
let mut o_exp = OperationSeq::default();
o_exp.retain(1);
o_exp.delete(1);
o_exp.insert("abc");
assert_eq!(o, o_exp);
for _ in 0..1000 {
let s = rng.gen_string(20);
let o = rng.gen_operation_seq(&s);
assert_eq!(
o,
serde_json::from_str(&serde_json::to_string(&o).unwrap()).unwrap()
);
}
}
}
================================================
FILE: operational-transform/src/serde.rs
================================================
use crate::{Operation, OperationSeq};
use serde::{
de::{self, Deserializer, SeqAccess, Visitor},
ser::{SerializeSeq, Serializer},
Deserialize, Serialize,
};
use std::fmt;
impl Serialize for Operation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Operation::Retain(i) => serializer.serialize_u64(*i),
Operation::Delete(i) => serializer.serialize_i64(-(*i as i64)),
Operation::Insert(s) => serializer.serialize_str(s),
}
}
}
impl<'de> Deserialize<'de> for Operation {
fn deserialize<D>(deserializer: D) -> Result<Operation, D::Error>
where
D: Deserializer<'de>,
{
struct OperationVisitor;
impl<'de> Visitor<'de> for OperationVisitor {
type Value = Operation;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between -2^64 and 2^63 or a string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Operation::Retain(value as u64))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Operation::Delete((-value) as u64))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Operation::Insert(value.to_owned()))
}
}
deserializer.deserialize_any(OperationVisitor)
}
}
impl Serialize for OperationSeq {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.ops.len()))?;
for op in self.ops.iter() {
seq.serialize_element(op)?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for OperationSeq {
fn deserialize<D>(deserializer: D) -> Result<OperationSeq, D::Error>
where
D: Deserializer<'de>,
{
struct OperationSeqVisitor;
impl<'de> Visitor<'de> for OperationSeqVisitor {
type Value = OperationSeq;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut o = OperationSeq::default();
while let Some(op) = seq.next_element()? {
o.add(op);
}
Ok(o)
}
}
deserializer.deserialize_seq(OperationSeqVisitor)
}
}
================================================
FILE: operational-transform/src/utilities.rs
================================================
use crate::OperationSeq;
use rand::prelude::*;
use rand::Rng as WrappedRng;
pub struct Rng(StdRng);
impl Default for Rng {
fn default() -> Self {
Rng(StdRng::from_rng(thread_rng()).unwrap())
}
}
impl Rng {
pub fn from_seed(seed: [u8; 32]) -> Self {
Rng(StdRng::from_seed(seed))
}
pub fn gen_string(&mut self, len: usize) -> String {
(0..len).map(|_| self.0.gen::<char>()).collect()
}
pub fn gen_operation_seq(&mut self, s: &str) -> OperationSeq {
let mut op = OperationSeq::default();
loop {
let left = s.chars().count() - op.base_len();
if left == 0 {
break;
}
let i = if left == 1 {
1
} else {
1 + self.0.gen_range(0, std::cmp::min(left - 1, 20))
};
match self.0.gen_range(0.0, 1.0) {
f if f < 0.2 => {
op.insert(&self.gen_string(i));
}
f if f < 0.4 => {
op.delete(i as u64);
}
_ => {
op.retain(i as u64);
}
}
}
if self.0.gen_range(0.0, 1.0) < 0.3 {
op.insert(&("1".to_owned() + &self.gen_string(10)));
}
op
}
}
================================================
FILE: xtask/Cargo.toml
================================================
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
[dependencies]
xtaskops = "^0.2.0"
anyhow = "1"
clap = "3"
================================================
FILE: xtask/src/main.rs
================================================
#![allow(dead_code)]
use clap::{AppSettings, Arg, Command};
use xtaskops::ops;
use xtaskops::tasks;
fn main() -> Result<(), anyhow::Error> {
let cli = Command::new("xtask")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
Command::new("coverage").arg(
Arg::new("dev")
.short('d')
.long("dev")
.help("generate an html report")
.takes_value(false),
),
)
.subcommand(Command::new("vars"))
.subcommand(Command::new("ci"))
.subcommand(Command::new("powerset"))
.subcommand(Command::new("bloat-deps"))
.subcommand(Command::new("bloat-time"))
.subcommand(Command::new("docs"));
let matches = cli.get_matches();
let root = ops::root_dir();
let res = match matches.subcommand() {
Some(("coverage", sm)) => tasks::coverage(sm.is_present("dev")),
Some(("vars", _)) => {
println!("root: {:?}", root);
Ok(())
}
Some(("ci", _)) => tasks::ci(),
Some(("docs", _)) => tasks::docs(),
Some(("powerset", _)) => tasks::powerset(),
Some(("bloat-deps", _)) => tasks::bloat_deps(),
Some(("bloat-time", _)) => tasks::bloat_time(),
_ => unreachable!("unreachable branch"),
};
res
}
gitextract_p95hh1cn/
├── .cargo/
│ └── config
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── operational-transform/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── benches/
│ │ └── benchmark.rs
│ └── src/
│ ├── lib.rs
│ ├── serde.rs
│ └── utilities.rs
└── xtask/
├── Cargo.toml
└── src/
└── main.rs
SYMBOL INDEX (44 symbols across 5 files)
FILE: operational-transform/benches/benchmark.rs
function compose (line 9) | pub fn compose(c: &mut Criterion) {
function transform (line 29) | pub fn transform(c: &mut Criterion) {
function invert (line 48) | pub fn invert(c: &mut Criterion) {
function apply (line 66) | pub fn apply(c: &mut Criterion) {
FILE: operational-transform/src/lib.rs
type Operation (line 103) | pub enum Operation {
type OperationSeq (line 114) | pub struct OperationSeq {
method from_iter (line 125) | fn from_iter<T: IntoIterator<Item = Operation>>(ops: T) -> Self {
method with_capacity (line 154) | pub fn with_capacity(capacity: usize) -> Self {
method compose (line 172) | pub fn compose(&self, other: &Self) -> Result<Self, OTError> {
method add (line 275) | fn add(&mut self, op: Operation) {
method delete (line 284) | pub fn delete(&mut self, n: u64) {
method insert (line 297) | pub fn insert(&mut self, s: &str) {
method retain (line 322) | pub fn retain(&mut self, n: u64) {
method transform (line 344) | pub fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> {
method apply (line 479) | pub fn apply(&self, s: &str) -> Result<String, OTError> {
method invert (line 510) | pub fn invert(&self, s: &str) -> Self {
method is_noop (line 534) | pub fn is_noop(&self) -> bool {
method base_len (line 540) | pub fn base_len(&self) -> usize {
method target_len (line 547) | pub fn target_len(&self) -> usize {
method ops (line 553) | pub fn ops(&self) -> &[Operation] {
type OTError (line 136) | pub struct OTError;
method fmt (line 139) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method source (line 145) | fn source(&self) -> Option<&(dyn Error + 'static)> {
function lengths (line 564) | fn lengths() {
function sequence (line 583) | fn sequence() {
function apply (line 595) | fn apply() {
function invert (line 606) | fn invert() {
function empty_ops (line 619) | fn empty_ops() {
function eq (line 628) | fn eq() {
function ops_merging (line 646) | fn ops_merging() {
function is_noop (line 670) | fn is_noop() {
function compose (line 682) | fn compose() {
function transform (line 700) | fn transform() {
function serde (line 721) | fn serde() {
FILE: operational-transform/src/serde.rs
method serialize (line 10) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
method deserialize (line 23) | fn deserialize<D>(deserializer: D) -> Result<Operation, D::Error>
method serialize (line 63) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
method deserialize (line 76) | fn deserialize<D>(deserializer: D) -> Result<OperationSeq, D::Error>
FILE: operational-transform/src/utilities.rs
type Rng (line 5) | pub struct Rng(StdRng);
method from_seed (line 14) | pub fn from_seed(seed: [u8; 32]) -> Self {
method gen_string (line 18) | pub fn gen_string(&mut self, len: usize) -> String {
method gen_operation_seq (line 22) | pub fn gen_operation_seq(&mut self, s: &str) -> OperationSeq {
method default (line 8) | fn default() -> Self {
FILE: xtask/src/main.rs
function main (line 7) | fn main() -> Result<(), anyhow::Error> {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (44K chars).
[
{
"path": ".cargo/config",
"chars": 41,
"preview": "[alias]\nxtask = \"run --package xtask --\"\n"
},
{
"path": ".github/workflows/build.yml",
"chars": 2850,
"preview": "name: Build\non:\n pull_request:\n push:\n branches:\n - master\n # you can enable a schedule to build\n # schedule"
},
{
"path": ".gitignore",
"chars": 46,
"preview": ".DS_Store\n.idea\n*.log\ntmp/\n\ntarget/\ncoverage/\n"
},
{
"path": "Cargo.toml",
"chars": 67,
"preview": "[workspace]\nmembers = [\n \"operational-transform\",\n \"xtask\"\n]\n"
},
{
"path": "LICENSE",
"chars": 1091,
"preview": "MIT License Copyright (c) 2020 bold\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi"
},
{
"path": "README.md",
"chars": 3317,
"preview": "# operational-transform\n\n[![Crates.io][crates-badge]][crates-url]\n[![docs.rs docs][docs-badge]][docs-url]\n[![ci][ci-badg"
},
{
"path": "operational-transform/.gitignore",
"chars": 19,
"preview": "/target\nCargo.lock\n"
},
{
"path": "operational-transform/Cargo.toml",
"chars": 686,
"preview": "[package]\nname = \"operational-transform\"\nversion = \"0.6.1\"\nauthors = [\"bold <bold@cryptoguru.com>\"]\nedition = \"2018\"\nlic"
},
{
"path": "operational-transform/benches/benchmark.rs",
"chars": 2282,
"preview": "use operational_transform::OperationSeq;\n\n#[path = \"../src/utilities.rs\"]\nmod utilities;\n\nuse criterion::{black_box, cri"
},
{
"path": "operational-transform/src/lib.rs",
"chars": 25653,
"preview": "//! A library for Operational Transformation\n//!\n//! Operational transformation (OT) is a technology for supporting a ra"
},
{
"path": "operational-transform/src/serde.rs",
"chars": 2911,
"preview": "use crate::{Operation, OperationSeq};\nuse serde::{\n de::{self, Deserializer, SeqAccess, Visitor},\n ser::{Serialize"
},
{
"path": "operational-transform/src/utilities.rs",
"chars": 1335,
"preview": "use crate::OperationSeq;\nuse rand::prelude::*;\nuse rand::Rng as WrappedRng;\n\npub struct Rng(StdRng);\n\nimpl Default for R"
},
{
"path": "xtask/Cargo.toml",
"chars": 120,
"preview": "[package]\nname = \"xtask\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nxtaskops = \"^0.2.0\"\nanyhow = \"1\"\nclap = \"3\"\n"
},
{
"path": "xtask/src/main.rs",
"chars": 1385,
"preview": "#![allow(dead_code)]\n\nuse clap::{AppSettings, Arg, Command};\nuse xtaskops::ops;\nuse xtaskops::tasks;\n\nfn main() -> Resul"
}
]
About this extraction
This page contains the full source code of the spebern/operational-transform-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (40.8 KB), approximately 10.1k tokens, and a symbol index with 44 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.