Improve example

This commit is contained in:
Andras Schmelczer 2025-06-22 21:57:45 +01:00
parent 779579d38f
commit a0cfef3238
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
5 changed files with 167 additions and 100 deletions

View file

@ -1,10 +1,7 @@
{ {
"jest.jestCommandLine": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest",
"jest.rootPath": "plugin",
"files.exclude": { "files.exclude": {
"**/dist": true, "**/dist": true,
"**/node_modules": true, "**/node_modules": true,
"**/.sqlx": true,
"**/snapshots": true, "**/snapshots": true,
} }
} }

View file

@ -23,39 +23,110 @@
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>
<header> <div class="page-wrapper">
<h1>3-Way Text Merge</h1> <header>
<p>Use this tool to merge three versions of a text.</p> <h1>3-Way Text Merge</h1>
</header> <p>
The
<a
href="https://github.com/schmelczer/reconcile"
target="_blank"
>reconcile</a
>
solves a fundamental challenge in collaborative editing:
what happens when multiple people edit the same text
simultaneously?
<code
>reconcile(parent: str, left: str, right: str) ->
str</code
>
takes conflicting concurrent edits and intelligently merges
them into a unified result. Beyond basic conflict
resolution, it offers sophisticated merging heuristics,
flexible tokenization options, and cursor position tracking.
</p>
<p>
The algorithm begins with your chosen tokenizer, then
applies Myers' diff algorithm to compare the original text
with both conflicting versions. These diffs undergo
transformation to preserve meaningful change sequences,
before a final merge strategy—inspired by Operational
Transformation (OT)—reconciles all conflicting modifications
without losing any edits.
</p>
<p>
For more details, see the
<a
href="https://github.com/schmelczer/reconcile"
target="_blank"
>README</a
>.
</p>
</header>
<main> <main>
<div class="text-area-card diamond-parent"> <div class="text-area-card diamond-parent">
<label for="original">Original</label> <label for="original">Original</label>
<textarea id="original" name="original"></textarea> <textarea id="original" name="original"></textarea>
</div> </div>
<div class="text-area-card diamond-left"> <div class="text-area-card diamond-left">
<label for="left"> <label for="left">
First concurrent edit First concurrent edit
<div class="box Left"></div> <div
</label> class="box Left"
<textarea id="left" name="left"></textarea> title="Colour-coded tokens mark the origin of each token, including ones that got deleted."
</div> ></div>
</label>
<textarea id="left" name="left"></textarea>
</div>
<div class="text-area-card diamond-right"> <div class="text-area-card diamond-right">
<label for="right" <label for="right"
>Second concurrent edit >Second concurrent edit
<div <div
class="box Right" class="box Right"
title="Indicates changes from the second concurrent edit" title="Colour-coded tokens mark the origin of each token, including ones that got deleted."
></div> ></div>
</label> </label>
<textarea id="right" name="right"></textarea> <textarea id="right" name="right"></textarea>
</div> </div>
<div class="text-area-card diamond-result"> <div class="text-area-card diamond-result">
<label <label>
><svg Deconflicted result
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
title="Read-only. Change the above text boxes to change the content of this box."
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10 10l-6 6v4h4l6 -6m1.99 -1.99l2.504 -2.504a2.828 2.828 0 1 0 -4 -4l-2.5 2.5"
/>
<path d="M13.5 6.5l4 4" />
<path d="M3 3l18 18" />
</svg>
</label>
<div id="merged"></div>
</div>
</main>
<footer>
<p>2025 Andras Schmelczer</p>
<a
href="https://github.com/schmelczer/reconcile"
class="github-link"
aria-label="GitHub repository"
>
<svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@ -68,42 +139,12 @@
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path <path
d="M10 10l-6 6v4h4l6 -6m1.99 -1.99l2.504 -2.504a2.828 2.828 0 1 0 -4 -4l-2.5 2.5" d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
/> />
<path d="M13.5 6.5l4 4" />
<path d="M3 3l18 18" />
</svg> </svg>
Deconflicted result (readonly)</label </a>
> </footer>
<div id="merged"></div> </div>
</div>
</main>
<footer>
<p>2025 Andras Schmelczer</p>
<a
href="https://github.com/schmelczer/reconcile"
class="github-link"
aria-label="GitHub repository"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
/>
</svg>
</a>
</footer>
<script type="module" src="script.js"></script> <script type="module" src="script.js"></script>
</body> </body>

View file

@ -5,13 +5,7 @@ const leftTextArea = document.getElementById("left");
const rightTextArea = document.getElementById("right"); const rightTextArea = document.getElementById("right");
const mergedTextArea = document.getElementById("merged"); const mergedTextArea = document.getElementById("merged");
const sampleTexts = [ const sampleText = `The \`reconcile\` Rust library is embedded on this page a WASM module and it powers these text boxes. Experiment with the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to watch competing changes merge in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenization strategy, for example, deciding how casing or white-spacing is taken into account.`;
"The quick brown fox jumps over the lazy dog.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.",
"A journey of a thousand miles begins with a single step.",
"To be, or not to be, that is the question.",
];
async function run() { async function run() {
await init(); await init();
@ -19,6 +13,7 @@ async function run() {
originalTextArea.addEventListener("input", updateMergedText); originalTextArea.addEventListener("input", updateMergedText);
leftTextArea.addEventListener("input", updateMergedText); leftTextArea.addEventListener("input", updateMergedText);
rightTextArea.addEventListener("input", updateMergedText); rightTextArea.addEventListener("input", updateMergedText);
window.addEventListener("resize", resizeTextAreas);
loadSample(); loadSample();
updateMergedText(); updateMergedText();
@ -29,7 +24,15 @@ async function run() {
leftTextArea.selectionEnd = leftTextArea.value.length; leftTextArea.selectionEnd = leftTextArea.value.length;
} }
function resizeTextAreas() {
autoResize(originalTextArea);
autoResize(leftTextArea);
autoResize(rightTextArea);
}
function updateMergedText() { function updateMergedText() {
resizeTextAreas();
const original = originalTextArea.value; const original = originalTextArea.value;
const left = leftTextArea.value; const left = leftTextArea.value;
const right = rightTextArea.value; const right = rightTextArea.value;
@ -48,11 +51,18 @@ function updateMergedText() {
} }
function loadSample() { function loadSample() {
const randomIndex = Math.floor(Math.random() * sampleTexts.length); originalTextArea.value = sampleText;
const text = sampleTexts[randomIndex]; leftTextArea.value =
originalTextArea.value = text; sampleText.replace("color", "colour") +
leftTextArea.value = text; " Check out what's the most complex conflict you can come up with!";
rightTextArea.value = text; rightTextArea.value = sampleText
.replace(", for example,", " such as")
.replace("WASM", "WebAssembly");
}
function autoResize(textarea) {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
} }
run(); run();

View file

@ -11,24 +11,33 @@ body {
body { body {
font-family: "Segoe UI", Arial, sans-serif; font-family: "Segoe UI", Arial, sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ef 100%);
color: #23272f; color: #23272f;
overflow-y: auto;
}
.page-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
min-height: 100%;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ef 100%);
} }
header { header {
padding: 32px 20px 0 20px; padding: 32px 32px 0 32px;
text-align: center;
} }
header > h1 { header > h1 {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
color: #2451a6; color: #2451a6;
margin-bottom: 8px; margin-bottom: 24px;
text-align: center;
}
p,
p * {
user-select: text;
} }
header > p { header > p {
@ -37,16 +46,18 @@ header > p {
margin-bottom: 0; margin-bottom: 0;
} }
header > p:not(:first-of-type) {
margin-top: 16px;
}
main { main {
flex: 1;
display: grid; display: grid;
grid-template-rows: auto auto auto; grid-template-rows: min-content;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 20px; gap: 20px;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
padding: 32px 12vw 32px 12vw; padding: 32px 12vw 32px 12vw;
min-height: 540px;
} }
.diamond-parent { .diamond-parent {
@ -70,6 +81,15 @@ main {
align-items: center; align-items: center;
} }
.diamond-result label {
display: flex;
align-items: center;
}
.diamond-result svg {
margin-left: 6px;
}
.text-area-card { .text-area-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -108,12 +128,10 @@ textarea {
resize: none; resize: none;
outline: none; outline: none;
margin-bottom: 0; margin-bottom: 0;
height: 100%;
} }
#merged { #merged {
width: 100%; width: 100%;
text-align: left;
user-select: text; user-select: text;
} }
@ -143,39 +161,42 @@ textarea {
@media (max-width: 768px) { @media (max-width: 768px) {
main { main {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto; grid-template-rows: auto auto auto auto;
}
main > * {
grid-column: 1;
} }
.diamond-parent { .diamond-parent {
grid-column: 1;
grid-row: 1; grid-row: 1;
} }
.diamond-left { .diamond-left {
grid-column: 1;
grid-row: 2; grid-row: 2;
} }
.diamond-right { .diamond-right {
grid-column: 1;
grid-row: 3; grid-row: 3;
} }
.diamond-result { .diamond-result {
grid-row: 5; grid-column: 1;
grid-row: 4;
} }
} }
footer { footer {
padding: 16px;
width: 100%;
position: relative; position: relative;
margin-top: 32px; display: flex;
padding: 28px 0 18px 0; justify-content: center;
text-align: center; align-items: center;
color: #5a6272; color: #5a6272;
font-size: 1rem; font-size: 1rem;
box-shadow: 0 -4px 12px 0 rgba(28, 28, 87, 0.1), box-shadow: 0 -4px 12px 0 rgba(28, 28, 87, 0.1),
0 -1px 2px 0 rgba(1, 1, 3, 0.1); 0 -1px 2px 0 rgba(1, 1, 3, 0.1);
background-color: #fff;
} }
.github-link > svg { .github-link > svg {

View file

@ -28,8 +28,6 @@ cargo set-version --bump $1
wasm-pack build --target web --features wasm wasm-pack build --target web --features wasm
# Commit and tag # Commit and tag
git add . git add .
git commit -m "Bump versions to $TAG" git commit -m "Bump versions to $TAG"