- Rust 71.1%
- TypeScript 16.3%
- JavaScript 5.9%
- Python 5.1%
- Shell 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| docs | ||
| examples | ||
| reconcile-js | ||
| reconcile-python | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| .prettierrc | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
| renovate.json | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
reconcile-text: conflict-free 3-way text merging
Merge a common parent and two edited versions into one text, without conflict markers. reconcile-text combines changes from both sides and updates any cursor positions you pass in. It is available as a library for Rust, JavaScript/TypeScript, and Python.
It is built for syncing text files when all you have are snapshots: the document before editing and the two versions it became. The merge needs no saved operation history or persistent document metadata beyond that parent text.
Try it
✨ Try the interactive demo to see it in action!
For the motivation and algorithm behind the library, read Conflict-Free Three-Way Text Merging.
Install it in your project
cargo add reconcile-text(reconcile-text on crates.io)npm install reconcile-text(reconcile-text on NPM)uv add reconcile-textorpip install reconcile-text(reconcile-text on PyPI)
Quick start
Each example merges two changes to the same greeting: one side adds a word, while the other changes the greeting itself.
Rust
Install from crates.io:
cargo add reconcile-text
use reconcile_text::{reconcile, BuiltinTokenizer};
// Start with the original text
let parent = "Hello world";
// Two users edit simultaneously
let left = "Hello beautiful world"; // Added "beautiful"
let right = "Hi world"; // Changed "Hello" to "Hi"
// Reconcile combines both changes
let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
assert_eq!(result.apply().text(), "Hi beautiful world");
See the merge-file example for a file-merging CLI, or the Rust API documentation.
JavaScript/TypeScript
Install from npm:
npm install reconcile-text
Then use it in your application:
import { reconcile } from 'reconcile-text';
// Start with the original text
const parent = 'Hello world';
// Two users edit simultaneously
const left = 'Hello beautiful world';
const right = 'Hi world';
const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
See the TypeScript guide for cursors, change provenance, and compact diffs, or the demo website source for an editor integration.
The Node.js and browser builds use the Rust library compiled to WebAssembly. For React Native, the package ships a pure-JavaScript build produced by Binaryen's wasm2js via its react-native entry point, so it can run in Hermes without a WebAssembly global.
Python
Install from PyPI with uv or pip:
uv add reconcile-text
# or: pip install reconcile-text
Then use it in your application:
from reconcile_text import reconcile
parent = "Hello world"
left = "Hello beautiful world"
right = "Hi world"
result = reconcile(parent, left, right)
print(result["text"]) # "Hi beautiful world"
The Python package provides native bindings to the same Rust implementation. See the merge-file example for a file-merging CLI, or the Python guide for cursors, change provenance, and compact diffs.
Why merge snapshots?
Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously or asynchronously whilst offline. A note might be edited in Obsidian on one device and Vim on another, with a file sync service carrying changes between them. By the time those changes meet, the syncer sees two files. It cannot ask either editor for the operations that produced them.
A common ancestor gives it enough context to compare both sets of edits. Git works with the same inputs, but usually leaves ambiguous conflicts for a person to resolve. That is a useful default for source code. For notes and prose, an automatic merge can be more useful, even if the wording occasionally needs tidying up.
How it works
reconcile-text starts off similarly to diff3 but adds automated conflict resolution. Given a parent document and two modified versions (left and right), the following happens:
- Tokenisation - Input texts are split into meaningful units (words, characters, etc.) for granular merging
- Diff computation - Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
- Diff optimisation - Operations are reordered and consolidated to maximise chained changes
- Operational Transformation - Edits are woven together using OT principles, preserving all modifications and updating cursors
Whilst the primary goal of reconcile-text isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see operational-transform-rs.) The same could be achieved with CRDTs, which many libraries implement well for text (see Loro, cola, and automerge).
However, when only the end result of concurrent changes is observable, merge quality depends entirely on the quality of the underlying 2-way diffs. For instance, move operations cannot be supported because Myers' algorithm decomposes them into separate insert and delete operations, regardless of the merging algorithm used.
Choosing a tokeniser
Tokenisation affects the result, especially when edits overlap. The built-in options are:
- Word: keeps words together; the default in JavaScript/TypeScript and Python.
- Character: merges individual Unicode code points for finer-grained changes.
- Line: treats each line as a unit.
- Markdown: builds on word tokenisation, preserving newlines and attaching block prefixes such as list markers to their first word.
Rust callers choose a tokeniser explicitly and can also supply a custom function. The merge keeps tokens intact, but the Markdown tokeniser does not validate the resulting document's syntax. Try different tokenisers in the demo to see how they handle your text.
Comparison with other approaches
diff3 and Git
diff3 and Git's default text merge leave conflict markers where changes cannot be combined automatically. Git also has a union merge driver that retains lines from both sides of a conflict. Because it works with whole lines, two edits to different words in a sentence can leave two versions of the sentence. Word tokenisation lets reconcile-text combine those edits within the line.
diff-match-patch
diff-match-patch computes two-way diffs and applies patches using fuzzy matching. It is useful when you need to apply changes to text whose context may have shifted. A patch can fail, so callers need to inspect its success flags.
reconcile-text uses the common parent to derive both sides' changes before combining them. The comparison example shows how the two approaches handle overlapping replacements and a sentence inserted into a rewritten paragraph.
CRDTs and OT
Libraries based on conflict-free replicated data types (CRDTs), such as Yjs, Automerge, and Loro, maintain document structures and metadata that let replicas merge updates. They fit applications that can persist that state and integrate edits into it. Snapshot changes can also be translated into CRDT updates; using a CRDT does not require recording every keystroke.
OT libraries such as ot.js work with operations and a protocol for coordinating them. reconcile-text uses OT ideas internally, deriving the operations from each pair of snapshots at merge time.
The practical distinction is what your application keeps between merges. reconcile-text accepts three texts and merges two forks at a time. A collaborative editing system built around a CRDT or OT also manages the state and protocol needed to coordinate replicas over time.
Development
Contributions are welcome!
Environment
Python setup
Install uv and build the extension for development:
cd reconcile-python
uv run maturin develop
Node.js setup
- Install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash - Install and use Node 22:
nvm install 22 && nvm use 22 - Optionally, set as default:
nvm alias default 22
Rust toolchain
Install rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Scripts
- Run tests:
scripts/test.sh - Check lint and formatting:
scripts/lint.sh - Develop demo website:
scripts/dev-website.sh - Build demo website:
scripts/build-website.sh - Publish new version:
scripts/bump-version.sh patch