- 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
reconcile-text merges two edited versions of a document using their common parent, without leaving conflict markers. It combines changes from both versions and adjusts any cursor positions you pass in. You can use it from Rust, JavaScript/TypeScript, or Python.
It's designed for syncing text files when all you have are snapshots: the original document and two edited copies. You don't need to record editing operations or keep extra document metadata between merges.
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.
Quick start
In each example, one edit adds "beautiful" and the other changes "Hello" to "Hi". The merge keeps both changes.
Rust
Install from crates.io:
cargo add reconcile-text
use reconcile_text::{reconcile, BuiltinTokenizer};
let parent = "Hello world";
let left = "Hello beautiful world"; // Added "beautiful"
let right = "Hi world"; // Changed "Hello" to "Hi"
let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
assert_eq!(result.apply().text(), "Hi beautiful world");
The merge-file example shows how to merge files from the command line. See the Rust API documentation for the full API.
JavaScript/TypeScript
Install from npm:
npm install reconcile-text
import { reconcile } from 'reconcile-text';
const parent = 'Hello world';
const left = 'Hello beautiful world';
const right = 'Hi world';
const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
The TypeScript guide shows how to track cursors, see where changes came from, and work with compact diffs. For an example of using the library in an editor, see the demo website source.
The Node.js and browser builds use the Rust library compiled to WebAssembly. React Native uses a JavaScript build generated by Binaryen's wasm2js. It's available through the package's react-native entry point and runs in Hermes without a WebAssembly global.
Python
Install from PyPI with uv or pip:
uv add reconcile-text
# or: pip install reconcile-text
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 uses native bindings to the same Rust library. For merging files from the command line, see the merge-file example. The Python guide shows how to track cursors, see where changes came from, and work with compact diffs.
Why merge snapshots?
Suppose you edit a note in Obsidian on one device and Vim on another while they're offline. When the devices reconnect, your file sync service has two edited copies to combine. It can read the files, but it can't ask either editor which operations produced them.
Keeping the original version gives it a common parent to compare both copies against. Git works with the same inputs and leaves conflicts it can't resolve for a person to handle. That's 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
Like diff3, reconcile-text starts with a parent document and two edited versions, left and right. It then:
- Splits the text into tokens, such as words, characters, or lines.
- Finds each side's changes using Myers' diff algorithm to compare the parent with
leftand withright. - Cleans up the diffs by reordering and grouping operations to keep related changes together.
- Combines the edits using operational transformation (OT) principles to account for changes from both sides and update cursor positions.
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).
Choosing a tokeniser
The tokeniser decides which pieces of text stay together during a merge:
- Word keeps words together. This is the default in JavaScript/TypeScript and Python.
- Character works with individual Unicode code points, so edits can be combined within a word.
- Line treats each line as a unit.
- Markdown builds on word tokenisation. It preserves newlines and keeps block prefixes, such as list markers, attached to the first word.
In Rust, you choose a tokeniser explicitly and can supply your own function. The merge keeps tokens intact, though the Markdown tokeniser doesn't check whether the merged document is valid Markdown. Try the options 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 they can't combine changes automatically. Git also has a union merge driver that keeps lines from both sides of a conflict. Because it works with whole lines, edits to different words in the same sentence can leave you with two versions of that sentence. With word tokenisation, reconcile-text can combine those edits within the line.
diff-match-patch
diff-match-patch compares two texts and applies patches using fuzzy matching. This is useful when the text around a change may have shifted. Patches can fail, so you need to check the success flags returned when applying them.
reconcile-text compares each edited version with the common parent before combining the changes. The comparison example shows how both approaches handle overlapping replacements and a sentence inserted into a rewritten paragraph.
CRDTs and OT
Libraries such as Yjs, Automerge, Loro, and cola use conflict-free replicated data types (CRDTs) to merge updates across replicas. They keep document structures and metadata between edits, so your application needs to store and work with that state. You can also turn differences between snapshots into CRDT updates; using a CRDT doesn't require recording every keystroke.
With OT, applications exchange editing operations and use a protocol to coordinate them. Libraries such as ot.js and Rust's operational-transform-rs provide tools for transforming those operations. reconcile-text uses OT ideas internally, deriving its operations from the snapshots when you call it.
The main difference is what your application keeps between merges. reconcile-text takes a parent and two edited copies, and merges those two copies in one call. A collaborative editing system built around CRDTs or OT also manages the state and coordination needed to keep replicas in sync over time.
Development
Contributions are welcome!
Python setup
Install uv, then build the extension for Python-only development:
cp README.md LICENSE reconcile-python/
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, make Node 22 your default:
nvm alias default 22
Rust toolchain
Install rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Scripts
After installing Node 22, rustup, and uv, run scripts/check.sh for the full
install, build, lint, test, and demo build sequence.
For individual steps, run scripts/setup.sh once to install dependencies and
scripts/build.sh to build the JS and Python bindings. Run the build again after
changing Rust or binding code. The lint, test, and website scripts use those
existing dependencies and build outputs.
- Install dependencies:
scripts/setup.sh - Build bindings:
scripts/build.sh(orscripts/build-js.shfor JS only) - Run tests:
scripts/test.sh - Check lint and formatting:
scripts/lint.sh - Run the demo locally:
scripts/dev-website.sh - Build the demo:
scripts/build-website.sh - Publish a new version:
scripts/bump-version.sh patch