Improve example
This commit is contained in:
parent
779579d38f
commit
a0cfef3238
5 changed files with 167 additions and 100 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue