Merge branch 'main' into renovate/mini-css-extract-plugin-2.x-lockfile
All checks were successful
Check / build (pull_request) Successful in 6m54s
All checks were successful
Check / build (pull_request) Successful in 6m54s
This commit is contained in:
commit
ff2b955248
14 changed files with 222 additions and 100 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -428,7 +428,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reconcile-text"
|
name = "reconcile-text"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"diff-match-patch-rs",
|
"diff-match-patch-rs",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "reconcile-text"
|
name = "reconcile-text"
|
||||||
description = "Intelligent 3-way text merging with automated conflict resolution"
|
description = "Intelligent 3-way text merging with automated conflict resolution"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
authors = ["Andras Schmelczer <andras@schmelczer.dev>"]
|
authors = ["Andras Schmelczer <andras@schmelczer.dev>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
|
||||||
8
examples/website/package-lock.json
generated
8
examples/website/package-lock.json
generated
|
|
@ -28,7 +28,7 @@
|
||||||
},
|
},
|
||||||
"../../reconcile-js": {
|
"../../reconcile-js": {
|
||||||
"name": "reconcile-text",
|
"name": "reconcile-text",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -4501,9 +4501,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "16.0.7",
|
"version": "16.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz",
|
||||||
"integrity": "sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==",
|
"integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
14
reconcile-js/package-lock.json
generated
14
reconcile-js/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "reconcile-text",
|
"name": "reconcile-text",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "reconcile-text",
|
"name": "reconcile-text",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
},
|
},
|
||||||
"../pkg": {
|
"../pkg": {
|
||||||
"name": "reconcile-text",
|
"name": "reconcile-text",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +66,6 @@
|
||||||
"version": "7.28.0",
|
"version": "7.28.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
|
|
@ -1658,7 +1657,6 @@
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1685,7 +1683,6 @@
|
||||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
|
|
@ -1972,7 +1969,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -3076,7 +3072,6 @@
|
||||||
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
|
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.3.0",
|
"@jest/core": "30.3.0",
|
||||||
"@jest/types": "30.3.0",
|
"@jest/types": "30.3.0",
|
||||||
|
|
@ -4960,7 +4955,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -5097,7 +5091,6 @@
|
||||||
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
|
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
|
|
@ -5145,7 +5138,6 @@
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discoveryjs/json-ext": "^0.6.1",
|
"@discoveryjs/json-ext": "^0.6.1",
|
||||||
"@webpack-cli/configtest": "^3.0.1",
|
"@webpack-cli/configtest": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "reconcile-text",
|
"name": "reconcile-text",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"description": "Intelligent 3-way text merging with automated conflict resolution",
|
"description": "Intelligent 3-way text merging with automated conflict resolution",
|
||||||
"main": "dist/reconcile.node.js",
|
"main": "dist/reconcile.node.js",
|
||||||
"browser": "dist/reconcile.web.js",
|
"browser": "dist/reconcile.web.js",
|
||||||
|
|
|
||||||
4
reconcile-python/Cargo.lock
generated
4
reconcile-python/Cargo.lock
generated
|
|
@ -130,14 +130,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reconcile-text"
|
name = "reconcile-text"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reconcile-text-python"
|
name = "reconcile-text-python"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyo3",
|
"pyo3",
|
||||||
"reconcile-text",
|
"reconcile-text",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "reconcile-text-python"
|
name = "reconcile-text-python"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
authors = ["Andras Schmelczer <andras@schmelczer.dev>"]
|
authors = ["Andras Schmelczer <andras@schmelczer.dev>"]
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "reconcile-text"
|
name = "reconcile-text"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
description = "Intelligent 3-way text merging with automated conflict resolution"
|
description = "Intelligent 3-way text merging with automated conflict resolution"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
|
|
|
||||||
32
reconcile-python/uv.lock
generated
32
reconcile-python/uv.lock
generated
|
|
@ -20,7 +20,7 @@ name = "exceptiongroup"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
@ -132,13 +132,13 @@ resolution-markers = [
|
||||||
"python_full_version < '3.10'",
|
"python_full_version < '3.10'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.10'" },
|
{ name = "exceptiongroup" },
|
||||||
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "packaging", marker = "python_full_version < '3.10'" },
|
{ name = "packaging" },
|
||||||
{ name = "pluggy", marker = "python_full_version < '3.10'" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments", marker = "python_full_version < '3.10'" },
|
{ name = "pygments" },
|
||||||
{ name = "tomli", marker = "python_full_version < '3.10'" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
@ -153,13 +153,13 @@ resolution-markers = [
|
||||||
"python_full_version >= '3.10'",
|
"python_full_version >= '3.10'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
{ name = "packaging" },
|
||||||
{ name = "pluggy", marker = "python_full_version >= '3.10'" },
|
{ name = "pluggy" },
|
||||||
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
{ name = "pygments" },
|
||||||
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|
@ -168,7 +168,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reconcile-text"
|
name = "reconcile-text"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ else
|
||||||
echo "Your working directory is clean."
|
echo "Your working directory is clean."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# GNU sed takes the suffix as part of -i, BSD sed requires a separate argument
|
||||||
|
sed_inplace() {
|
||||||
|
if sed --version >/dev/null 2>&1; then
|
||||||
|
sed -i "$@"
|
||||||
|
else
|
||||||
|
sed -i '' "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
echo "Bumping versions"
|
echo "Bumping versions"
|
||||||
|
|
||||||
which cargo-set-version || cargo install cargo-edit
|
which cargo-set-version || cargo install cargo-edit
|
||||||
|
|
@ -39,8 +48,8 @@ npm install
|
||||||
|
|
||||||
NEWVER=$(grep '^version = ' ../Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
NEWVER=$(grep '^version = ' ../Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||||
cd ../reconcile-python
|
cd ../reconcile-python
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$NEWVER\"/" Cargo.toml
|
sed_inplace "s/^version = \".*\"/version = \"$NEWVER\"/" Cargo.toml
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$NEWVER\"/" pyproject.toml
|
sed_inplace "s/^version = \".*\"/version = \"$NEWVER\"/" pyproject.toml
|
||||||
cargo update --workspace
|
cargo update --workspace
|
||||||
uv lock
|
uv lock
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ use crate::{Tokenizer, types::text_with_cursors::TextWithCursors};
|
||||||
/// return a document containing all changes from both `left`
|
/// return a document containing all changes from both `left`
|
||||||
/// and `right`.
|
/// and `right`.
|
||||||
///
|
///
|
||||||
|
/// If both updated texts are identical, the resulting text is exactly that
|
||||||
|
/// text, regardless of the original document.
|
||||||
|
///
|
||||||
/// If a span has been inserted in either the `left` or `right`
|
/// If a span has been inserted in either the `left` or `right`
|
||||||
/// versions, it will be present in the return value. If both sides
|
/// versions, it will be present in the return value. If both sides
|
||||||
/// insert the same span with a common prefix, that prefix will only
|
/// insert the same span with a common prefix, that prefix will only
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use core::iter;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::raw_operation::RawOperation;
|
use crate::raw_operation::RawOperation;
|
||||||
|
|
@ -11,55 +10,40 @@ where
|
||||||
I: IntoIterator<Item = RawOperation<T>>,
|
I: IntoIterator<Item = RawOperation<T>>,
|
||||||
T: PartialEq + Clone + Debug,
|
T: PartialEq + Clone + Debug,
|
||||||
{
|
{
|
||||||
// This might look bad, but this makes sense. The inserts and deltes can be
|
let mut inserts: Vec<RawOperation<T>> = Vec::new();
|
||||||
// interleaved, such as: IDIDID and we need to turn this into IIIDDD.
|
let mut deletes: Vec<RawOperation<T>> = Vec::new();
|
||||||
// So we need to keep track of both the last insert and delete operations, not
|
let mut result = Vec::new();
|
||||||
// just the last one.
|
|
||||||
let mut maybe_previous_insert: Option<RawOperation<T>> = None;
|
|
||||||
let mut maybe_previous_delete: Option<RawOperation<T>> = None;
|
|
||||||
|
|
||||||
// We don't elongate `equals` as they're needed to maintain cursor positions
|
// Emit all deletions before insertions within each changed span, even
|
||||||
// when merging against deletes.
|
// when tokens cannot be joined. Otherwise an insertion followed by a
|
||||||
let mut result: Vec<RawOperation<T>> = raw_operations
|
// deletion at the same offset can separate identical insertions during
|
||||||
.into_iter()
|
// merging and prevent their deduplication.
|
||||||
.flat_map(|next| match next {
|
for next in raw_operations {
|
||||||
RawOperation::Insert(..) => match maybe_previous_insert.take() {
|
let pending = match next {
|
||||||
Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => {
|
RawOperation::Insert(..) => &mut inserts,
|
||||||
maybe_previous_insert = Some(prev.join(next));
|
RawOperation::Delete(..) => &mut deletes,
|
||||||
Box::new(iter::empty()) as Box<dyn Iterator<Item = RawOperation<T>>>
|
RawOperation::Equal(..) => {
|
||||||
}
|
result.append(&mut deletes);
|
||||||
prev => {
|
result.append(&mut inserts);
|
||||||
maybe_previous_insert = Some(next);
|
|
||||||
Box::new(prev.into_iter())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RawOperation::Delete(..) => match maybe_previous_delete.take() {
|
|
||||||
Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => {
|
|
||||||
maybe_previous_delete = Some(prev.join(next));
|
|
||||||
Box::new(iter::empty()) as Box<dyn Iterator<Item = RawOperation<T>>>
|
|
||||||
}
|
|
||||||
prev => {
|
|
||||||
maybe_previous_delete = Some(next);
|
|
||||||
Box::new(prev.into_iter())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RawOperation::Equal(..) => Box::new(
|
|
||||||
maybe_previous_delete
|
|
||||||
.take()
|
|
||||||
.into_iter()
|
|
||||||
.chain(maybe_previous_insert.take())
|
|
||||||
.chain(iter::once(next)),
|
|
||||||
) as Box<dyn Iterator<Item = RawOperation<T>>>,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(prev) = maybe_previous_delete {
|
// Keep retains separate for cursor positioning
|
||||||
result.push(prev);
|
result.push(next);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match pending.pop() {
|
||||||
|
Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => {
|
||||||
|
pending.push(prev.join(next));
|
||||||
|
}
|
||||||
|
Some(prev) => pending.extend([prev, next]),
|
||||||
|
None => pending.push(next),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prev) = maybe_previous_insert {
|
result.append(&mut deletes);
|
||||||
result.push(prev);
|
result.append(&mut inserts);
|
||||||
}
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +99,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merges_interleaved_insert_delete_sequences() {
|
fn merges_interleaved_insert_delete_sequences() {
|
||||||
// Pattern IDID -> II DD
|
// Pattern IDID -> DD II
|
||||||
let ops = vec![ins(&["i1"]), del(&["d1"]), ins(&["i2"]), del(&["d2"])];
|
let ops = vec![ins(&["i1"]), del(&["d1"]), ins(&["i2"]), del(&["d2"])];
|
||||||
let result = elongate_operations(ops);
|
let result = elongate_operations(ops);
|
||||||
|
|
||||||
|
|
@ -123,4 +107,40 @@ mod tests {
|
||||||
assert!(matches!(result[0], RawOperation::Delete(_)));
|
assert!(matches!(result[0], RawOperation::Delete(_)));
|
||||||
assert!(matches!(result[1], RawOperation::Insert(_)));
|
assert!(matches!(result[1], RawOperation::Insert(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orders_non_joinable_edits_before_each_retain() {
|
||||||
|
let insert1 = ins_custom("a", false, false);
|
||||||
|
let insert2 = ins_custom("\n", false, false);
|
||||||
|
let delete1 = RawOperation::Delete(vec![Token::new(
|
||||||
|
"b".to_owned(),
|
||||||
|
"b".to_owned(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)]);
|
||||||
|
let delete2 = del(&["c"]);
|
||||||
|
let retain = RawOperation::Equal(vec!["d".into()]);
|
||||||
|
let ops = vec![
|
||||||
|
insert1.clone(),
|
||||||
|
delete1.clone(),
|
||||||
|
insert2.clone(),
|
||||||
|
delete2.clone(),
|
||||||
|
retain.clone(),
|
||||||
|
insert1.clone(),
|
||||||
|
delete1.clone(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
elongate_operations(ops),
|
||||||
|
vec![
|
||||||
|
delete1.clone(),
|
||||||
|
delete2,
|
||||||
|
insert1.clone(),
|
||||||
|
insert2,
|
||||||
|
retain,
|
||||||
|
delete1,
|
||||||
|
insert1,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -306,10 +306,10 @@ fn conquer<T>(
|
||||||
// Check for common prefix
|
// Check for common prefix
|
||||||
let prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone());
|
let prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone());
|
||||||
if prefix_len > 0 {
|
if prefix_len > 0 {
|
||||||
result.extend(
|
emit_matches(
|
||||||
old[old_range.start..old_range.start + prefix_len]
|
&old[old_range.start..old_range.start + prefix_len],
|
||||||
.iter()
|
&new[new_range.start..new_range.start + prefix_len],
|
||||||
.map(|token| RawOperation::Equal(vec![token.clone()])),
|
result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
old_range.start += prefix_len;
|
old_range.start += prefix_len;
|
||||||
|
|
@ -317,9 +317,10 @@ fn conquer<T>(
|
||||||
|
|
||||||
// Check for common suffix
|
// Check for common suffix
|
||||||
let suffix_len = common_suffix_len(old, old_range.clone(), new, new_range.clone());
|
let suffix_len = common_suffix_len(old, old_range.clone(), new, new_range.clone());
|
||||||
let suffix_start = old_range.end - suffix_len;
|
let old_suffix_start = old_range.end - suffix_len;
|
||||||
old_range.end -= suffix_len;
|
let new_suffix_start = new_range.end - suffix_len;
|
||||||
new_range.end -= suffix_len;
|
old_range.end = old_suffix_start;
|
||||||
|
new_range.end = new_suffix_start;
|
||||||
|
|
||||||
if old_range.is_empty() && new_range.is_empty() {
|
if old_range.is_empty() && new_range.is_empty() {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
|
@ -377,14 +378,30 @@ fn conquer<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if suffix_len > 0 {
|
if suffix_len > 0 {
|
||||||
result.extend(
|
emit_matches(
|
||||||
old[suffix_start..suffix_start + suffix_len]
|
&old[old_suffix_start..old_suffix_start + suffix_len],
|
||||||
.iter()
|
&new[new_suffix_start..new_suffix_start + suffix_len],
|
||||||
.map(|token| RawOperation::Equal(vec![token.clone()])),
|
result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalized matches guide alignment, but only identical original text can
|
||||||
|
/// be retained. Emit replacements for matches with different spellings.
|
||||||
|
fn emit_matches<T>(old: &[Token<T>], new: &[Token<T>], result: &mut Vec<RawOperation<T>>)
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + Debug,
|
||||||
|
{
|
||||||
|
for (old_token, new_token) in old.iter().zip(new) {
|
||||||
|
if old_token.original() == new_token.original() {
|
||||||
|
result.push(RawOperation::Equal(vec![old_token.clone()]));
|
||||||
|
} else {
|
||||||
|
result.push(RawOperation::Delete(vec![old_token.clone()]));
|
||||||
|
result.push(RawOperation::Insert(vec![new_token.clone()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use insta::assert_debug_snapshot;
|
use insta::assert_debug_snapshot;
|
||||||
|
|
|
||||||
81
tests/idempotence.rs
Normal file
81
tests/idempotence.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
use reconcile_text::{BuiltinTokenizer, Token, reconcile};
|
||||||
|
use test_case::{test_case, test_matrix};
|
||||||
|
|
||||||
|
#[test_matrix([
|
||||||
|
BuiltinTokenizer::Character,
|
||||||
|
BuiltinTokenizer::Line,
|
||||||
|
BuiltinTokenizer::Markdown,
|
||||||
|
BuiltinTokenizer::Word
|
||||||
|
])]
|
||||||
|
fn identical_edits_preserve_text(tokenizer: BuiltinTokenizer) {
|
||||||
|
let texts = [
|
||||||
|
"",
|
||||||
|
"a",
|
||||||
|
"b\n",
|
||||||
|
"\r\n\r\n",
|
||||||
|
"# Héllo\n\n- 世界\n",
|
||||||
|
"a a a\nb b b\n",
|
||||||
|
];
|
||||||
|
for parent in texts {
|
||||||
|
for updated in texts {
|
||||||
|
let input = updated.into();
|
||||||
|
assert_eq!(
|
||||||
|
reconcile(parent, &input, &input, &*tokenizer)
|
||||||
|
.apply()
|
||||||
|
.text(),
|
||||||
|
updated,
|
||||||
|
"tokenizer={tokenizer:?}, parent={parent:?}, updated={updated:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_matrix([
|
||||||
|
BuiltinTokenizer::Character,
|
||||||
|
BuiltinTokenizer::Line,
|
||||||
|
BuiltinTokenizer::Markdown,
|
||||||
|
BuiltinTokenizer::Word
|
||||||
|
])]
|
||||||
|
fn identical_short_edits_preserve_text(tokenizer: BuiltinTokenizer) {
|
||||||
|
let mut texts = vec![String::new()];
|
||||||
|
let mut level = texts.clone();
|
||||||
|
for _ in 0..3 {
|
||||||
|
level = level
|
||||||
|
.iter()
|
||||||
|
.flat_map(|text| ['a', 'b', ' ', '\n'].map(|ch| format!("{text}{ch}")))
|
||||||
|
.collect();
|
||||||
|
texts.extend(level.iter().cloned());
|
||||||
|
}
|
||||||
|
|
||||||
|
for parent in &texts {
|
||||||
|
for updated in &texts {
|
||||||
|
let input = updated.into();
|
||||||
|
assert_eq!(
|
||||||
|
reconcile(parent, &input, &input, &*tokenizer)
|
||||||
|
.apply()
|
||||||
|
.text(),
|
||||||
|
*updated,
|
||||||
|
"tokenizer={tokenizer:?}, parent={parent:?}, updated={updated:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case("A", "a"; "lowercase")]
|
||||||
|
#[test_case("a", "A"; "uppercase")]
|
||||||
|
#[test_case("Hello WORLD", "HELLO world")]
|
||||||
|
#[test_case("X A", "x a\n")]
|
||||||
|
#[test_case("A", "new a"; "normalized_suffix")]
|
||||||
|
#[test_case("a old B", "A new b"; "normalized_prefix_and_suffix")]
|
||||||
|
fn identical_edits_preserve_original_spelling(parent: &str, updated: &str) {
|
||||||
|
let tokenizer = |text: &str| {
|
||||||
|
text.chars()
|
||||||
|
.map(|ch| Token::new(ch.to_ascii_lowercase(), ch.to_string(), false, false))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let input = updated.into();
|
||||||
|
assert_eq!(
|
||||||
|
reconcile(parent, &input, &input, &tokenizer).apply().text(),
|
||||||
|
updated,
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue