A Rust and TypeScript library for merging conflicting text edits without manual intervention. http://schmelczer.dev/reconcile
  • Rust 71.1%
  • TypeScript 16.3%
  • JavaScript 5.9%
  • Python 5.1%
  • Shell 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andras Schmelczer 1435a2223e
Some checks failed
renovate/artifacts Artifact file update failure
Check and publish / build (pull_request) Failing after 1m33s
Check and publish / publish-crate (pull_request) Has been skipped
Check and publish / publish-pypi (pull_request) Has been skipped
Update all non-major dependencies
2026-11-03 14:47:39 +00:00
.forgejo/workflows Simplify build scripts 2026-10-29 08:18:48 +00:00
.vscode Fix intellisense 2026-03-12 20:25:32 +00:00
docs Fix docs mistake 2026-10-27 21:12:22 +00:00
examples Update all non-major dependencies 2026-11-03 14:47:39 +00:00
reconcile-js Update all non-major dependencies 2026-11-03 14:47:39 +00:00
reconcile-python Update all non-major dependencies 2026-11-03 14:47:39 +00:00
scripts Simplify build scripts 2026-10-29 08:18:48 +00:00
src Revert oversimplifications 2026-10-29 22:42:31 +00:00
tests Revert oversimplifications 2026-10-29 22:42:31 +00:00
.gitignore Add react-native support (#63) 2026-05-31 20:28:20 +01:00
.prettierrc Format files with prettier 2025-07-06 22:21:17 +01:00
Cargo.lock Bump versions to 0.14.1 2026-10-29 22:43:54 +00:00
Cargo.toml Bump versions to 0.14.1 2026-10-29 22:43:54 +00:00
LICENSE Update cargo toml metadata 2025-07-09 22:35:57 +01:00
README.md Simplify build scripts 2026-10-29 08:18:48 +00:00
renovate.json Add renovate.json 2026-09-19 10:41:27 +00:00
rust-toolchain.toml Update all non-major dependencies 2026-11-03 14:47:39 +00:00
rustfmt.toml Use stable rust 2026-03-11 21:07:23 +00:00

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:

  1. Splits the text into tokens, such as words, characters, or lines.
  2. Finds each side's changes using Myers' diff algorithm to compare the parent with left and with right.
  3. Cleans up the diffs by reordering and grouping operations to keep related changes together.
  4. 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

  1. Install nvm:
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
    
  2. Install and use Node 22:
    nvm install 22 && nvm use 22
    
  3. 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 (or scripts/build-js.sh for 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

License

MIT