Fix syncing when network latency is present (#4)
* WIP * Add debug * Dedupe inserts * Add deterministic ordering * Fix whitespaces * Update insta * Add integration test script * Rename * Add test * Working for non-deletes * omg it mostly works for deletes * Isdeleted fix * remove created dates * update api * Take document id * No max attempt * works * Use string uuids * . * working!!!! (hopefully) * Improve bundling * Add module * lint * . * lint * Fix CI * use toolchain * clean up * Add useSlowFileEvents * Delete fuzz * Fix CI * use docker * fix script * clean up * Clean up * change node version * Build docker image on every commit * fix ci * 1 db per vault * Add scritps folder * Bump versions * Lint * . * Fix tests for real * Style * . * try * Consistent ordering * Fix tests * hmm * . * Clean up diff * Fixes * . * Fix version bump * . * . * .
This commit is contained in:
parent
bcf48c428d
commit
8b8f1d91d9
91 changed files with 2216 additions and 1550 deletions
13
.github/workflows/check.yml
vendored
13
.github/workflows/check.yml
vendored
|
|
@ -17,11 +17,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v4.2.0
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Setup rust
|
||||||
run: |
|
run: |
|
||||||
rustup install nightly
|
|
||||||
rustup default nightly
|
|
||||||
rustup component add clippy rustfmt
|
|
||||||
cargo install sqlx-cli
|
cargo install sqlx-cli
|
||||||
cd backend
|
cd backend
|
||||||
sqlx database create --database-url sqlite://db.sqlite3
|
sqlx database create --database-url sqlite://db.sqlite3
|
||||||
|
|
@ -44,7 +47,7 @@ jobs:
|
||||||
cd backend
|
cd backend
|
||||||
cargo test --verbose
|
cargo test --verbose
|
||||||
cd sync_lib
|
cd sync_lib
|
||||||
# wasm-pack test --node # todo: fix this in CI
|
wasm-pack test --node
|
||||||
|
|
||||||
- name: Lint frontend
|
- name: Lint frontend
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
45
.github/workflows/e2e.yml
vendored
Normal file
45
.github/workflows/e2e.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: E2E tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["master"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: "-Dwarnings"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v4.2.0
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Setup rust
|
||||||
|
run: |
|
||||||
|
cargo install sqlx-cli wasm-pack
|
||||||
|
cd backend
|
||||||
|
sqlx database create --database-url sqlite://db.sqlite3
|
||||||
|
sqlx migrate run --source sync_server/src/database/migrations --database-url sqlite://db.sqlite3
|
||||||
|
|
||||||
|
- name: Build wasm
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
wasm-pack build --target web sync_lib
|
||||||
|
|
||||||
|
- name: E2E tests
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
RUST_BACKTRACE=1 cargo run -p sync_server &
|
||||||
|
cd ../frontend
|
||||||
|
npm ci
|
||||||
|
cd ..
|
||||||
|
scripts/e2e.sh 32
|
||||||
18
.github/workflows/publish-docker.yml
vendored
18
.github/workflows/publish-docker.yml
vendored
|
|
@ -7,8 +7,9 @@ name: Publish server Docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches: ["master"]
|
||||||
- "*"
|
pull_request:
|
||||||
|
branches: ["master"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Use docker.io for Docker Hub if empty
|
# Use docker.io for Docker Hub if empty
|
||||||
|
|
@ -17,8 +18,9 @@ env:
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
publish-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: self-hosted
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
@ -33,7 +35,7 @@ jobs:
|
||||||
# Install the cosign tool except on PR
|
# Install the cosign tool except on PR
|
||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
|
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.4"
|
cosign-release: "v2.2.4"
|
||||||
|
|
@ -47,7 +49,7 @@ jobs:
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
|
|
@ -69,7 +71,7 @@ jobs:
|
||||||
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
||||||
with:
|
with:
|
||||||
context: backend
|
context: backend
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.ref_type == 'tag' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|
@ -81,7 +83,7 @@ jobs:
|
||||||
# transparency data even for private images, pass --force to cosign below.
|
# transparency data even for private images, pass --force to cosign below.
|
||||||
# https://github.com/sigstore/cosign
|
# https://github.com/sigstore/cosign
|
||||||
- name: Sign the published Docker image
|
- name: Sign the published Docker image
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.ref_type == 'tag' }}
|
||||||
env:
|
env:
|
||||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||||
TAGS: ${{ steps.meta.outputs.tags }}
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
|
|
|
||||||
16
.github/workflows/publish-plugin.yml
vendored
16
.github/workflows/publish-plugin.yml
vendored
|
|
@ -2,29 +2,27 @@ name: Publish Obsidian plugin
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags: ["*"]
|
||||||
- "*"
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-plugin:
|
publish-plugin:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4.2.0
|
||||||
with:
|
with:
|
||||||
node-version: "18.x"
|
node-version: "22.x"
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Build wasm
|
- name: Build wasm
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
rustup install nightly
|
|
||||||
rustup default nightly
|
|
||||||
cargo install wasm-pack
|
cargo install wasm-pack
|
||||||
wasm-pack build --target web sync_lib
|
wasm-pack build --target web sync_lib
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ backend/target
|
||||||
frontend/*/dist
|
frontend/*/dist
|
||||||
|
|
||||||
backend/db.sqlite3*
|
backend/db.sqlite3*
|
||||||
|
backend/databases
|
||||||
backend/config.yml
|
backend/config.yml
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
|
|
|
||||||
79
README.md
79
README.md
|
|
@ -1,65 +1,56 @@
|
||||||
## VaultLink self-hosted Obsidian sync plugin
|
# VaultLink self-hosted Obsidian plugin for file syncing
|
||||||
|
|
||||||
[](https://github.com/schmelczer/vault-link/actions/workflows/check.yml)
|
[](https://github.com/schmelczer/vault-link/actions/workflows/check.yml)
|
||||||
|
[](https://github.com/schmelczer/vault-link/actions/workflows/e2e.yml)
|
||||||
[](https://github.com/schmelczer/vault-link/actions/workflows/publish-docker.yml)
|
[](https://github.com/schmelczer/vault-link/actions/workflows/publish-docker.yml)
|
||||||
[](https://github.com/schmelczer/vault-link/actions/workflows/publish-plugin.yml)
|
[](https://github.com/schmelczer/vault-link/actions/workflows/publish-plugin.yml)
|
||||||
|
|
||||||
|
|
||||||
## Install [nvm](https://github.com/nvm-sh/nvm)
|
## Develop
|
||||||
|
|
||||||
|
### Install [nvm](https://github.com/nvm-sh/nvm)
|
||||||
|
|
||||||
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash`
|
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash`
|
||||||
- `nvm install 20`
|
- `nvm install 22`
|
||||||
- `nvm use 20`
|
- `nvm use 22`
|
||||||
- Optionally set the system-wide default: `nvm alias default 20`
|
- Optionally set the system-wide default: `nvm alias default 22`
|
||||||
|
|
||||||
|
|
||||||
## Set up Rust
|
### Set up Rust
|
||||||
|
|
||||||
- Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
- Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
||||||
- `sudo apt install llvm -y`
|
|
||||||
- `rustup self update`
|
|
||||||
- `rustup update`
|
|
||||||
- `rustup install nightly`
|
|
||||||
- `rustup default nightly`
|
|
||||||
- `rustup component add llvm-tools-preview`
|
|
||||||
- `cargo install cargo-generate cargo-fuzz cargo-insta rustfilt cargo-binutils`
|
|
||||||
- Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer): `curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh`
|
- Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer): `curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh`
|
||||||
- `cargo install cargo-insta sqlx-cli cargo-edit`
|
- `cargo install cargo-insta sqlx-cli cargo-edit`
|
||||||
|
|
||||||
|
### Install Obsidian on Linux
|
||||||
## Publish new version
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./bump-version.sh patch
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Update HTTP API TS bindings
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install -g openapi-typescript
|
|
||||||
openapi-typescript http://localhost:3030/api.json --output frontend/sync-client/src/services/types.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
todo: enable
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
single_call_fn = { level = "allow", priority = 1 }
|
|
||||||
absolute_paths = { level = "allow", priority = 1 }
|
|
||||||
arithmetic_side_effects = { level = "allow", priority = 1 }
|
|
||||||
similar_names = { level = "allow", priority = 1 }
|
|
||||||
self_named_module_files = { level = "allow", priority = 1 }
|
|
||||||
single_char_lifetime_names = { level = "allow", priority = 1 }
|
|
||||||
missing_docs_in_private_items = { level = "allow", priority = 1 }
|
|
||||||
question_mark_used = { level = "allow", priority = 1 }
|
|
||||||
implicit_return = { level = "allow", priority = 1 }
|
|
||||||
pedantic = { level = "warn", priority = 0 }
|
|
||||||
cargo = { level = "warn", priority = 0 }
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
apt install flatpak
|
apt install flatpak
|
||||||
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
flatpak install flathub md.obsidian.Obsidian
|
flatpak install flathub md.obsidian.Obsidian
|
||||||
flatpak run md.obsidian.Obsidian
|
flatpak run md.obsidian.Obsidian
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
#### Update HTTP API TS bindings
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/update-api-types.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Publish new version
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/bump-version.sh patch
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Run E2E tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/e2e.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
And to clean up the logs & database files, run `scripts/clean-up.sh`
|
||||||
|
```
|
||||||
|
|
|
||||||
35
backend/Cargo.lock
generated
35
backend/Cargo.lock
generated
|
|
@ -105,12 +105,6 @@ dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbitrary"
|
|
||||||
version = "1.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.85"
|
version = "0.1.85"
|
||||||
|
|
@ -381,8 +375,6 @@ version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
|
||||||
"libc",
|
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1228,15 +1220,6 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.76"
|
||||||
|
|
@ -1290,16 +1273,6 @@ version = "0.2.167"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libfuzzer-sys"
|
|
||||||
version = "0.4.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
|
|
||||||
dependencies = [
|
|
||||||
"arbitrary",
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|
@ -1781,14 +1754,6 @@ dependencies = [
|
||||||
"test-case",
|
"test-case",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "reconcile-fuzz"
|
|
||||||
version = "0.0.30"
|
|
||||||
dependencies = [
|
|
||||||
"libfuzzer-sys",
|
|
||||||
"reconcile",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"reconcile",
|
"reconcile",
|
||||||
"fuzz",
|
|
||||||
"sync_server",
|
"sync_server",
|
||||||
"sync_lib"
|
"sync_lib"
|
||||||
]
|
]
|
||||||
|
|
@ -57,3 +56,19 @@ uninlined_format_args = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
unused_self = "warn"
|
unused_self = "warn"
|
||||||
verbose_file_reads = "warn"
|
verbose_file_reads = "warn"
|
||||||
|
|
||||||
|
cast_possible_truncation = { level = "allow", priority = 1 }
|
||||||
|
doc_link_with_quotes = { level = "allow", priority = 1 }
|
||||||
|
cast_sign_loss = { level = "allow", priority = 1 }
|
||||||
|
cast_possible_wrap = { level = "allow", priority = 1 }
|
||||||
|
struct_field_names = { level = "allow", priority = 1 }
|
||||||
|
single_call_fn = { level = "allow", priority = 1 }
|
||||||
|
absolute_paths = { level = "allow", priority = 1 }
|
||||||
|
arithmetic_side_effects = { level = "allow", priority = 1 }
|
||||||
|
similar_names = { level = "allow", priority = 1 }
|
||||||
|
self_named_module_files = { level = "allow", priority = 1 }
|
||||||
|
single_char_lifetime_names = { level = "allow", priority = 1 }
|
||||||
|
missing_docs_in_private_items = { level = "allow", priority = 1 }
|
||||||
|
question_mark_used = { level = "allow", priority = 1 }
|
||||||
|
implicit_return = { level = "allow", priority = 1 }
|
||||||
|
pedantic = { level = "warn", priority = 0 }
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ FROM rust:1.83 AS builder
|
||||||
WORKDIR /usr/src/backend
|
WORKDIR /usr/src/backend
|
||||||
|
|
||||||
RUN apt update && apt install -y musl-tools
|
RUN apt update && apt install -y musl-tools
|
||||||
RUN rustup install nightly && rustup default nightly
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
RUN cargo install sqlx-cli
|
RUN cargo install sqlx-cli
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
@ -23,7 +21,7 @@ RUN apk add --no-cache curl
|
||||||
|
|
||||||
COPY --from=builder /usr/src/backend/target/x86_64-unknown-linux-musl/release/sync_server /app/sync_server
|
COPY --from=builder /usr/src/backend/target/x86_64-unknown-linux-musl/release/sync_server /app/sync_server
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data/databases
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
|
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|
|
||||||
4
backend/fuzz/.gitignore
vendored
4
backend/fuzz/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
target
|
|
||||||
corpus
|
|
||||||
artifacts
|
|
||||||
coverage
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "reconcile-fuzz"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
cargo-fuzz = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
libfuzzer-sys = "0.4"
|
|
||||||
reconcile = { path = "../reconcile" }
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "reconcile"
|
|
||||||
path = "fuzz_targets/reconcile.rs"
|
|
||||||
test = false
|
|
||||||
doc = false
|
|
||||||
bench = false
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#![no_main]
|
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
|
||||||
|
|
||||||
fuzz_target!(|texts: (String, String, String)| {
|
|
||||||
let (original, left, right) = texts;
|
|
||||||
let _ = reconcile::reconcile(&original, &left, &right);
|
|
||||||
});
|
|
||||||
|
|
@ -38,7 +38,7 @@ use crate::{
|
||||||
/// execution time permitted before it bails and falls back to an approximation.
|
/// execution time permitted before it bails and falls back to an approximation.
|
||||||
pub fn diff<T>(old: &[Token<T>], new: &[Token<T>]) -> Vec<RawOperation<T>>
|
pub fn diff<T>(old: &[Token<T>], new: &[Token<T>]) -> Vec<RawOperation<T>>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let max_d = (old.len() + new.len()).div_ceil(2) + 1;
|
let max_d = (old.len() + new.len()).div_ceil(2) + 1;
|
||||||
let mut vb = V::new(max_d);
|
let mut vb = V::new(max_d);
|
||||||
|
|
@ -99,7 +99,6 @@ impl IndexMut<isize> for V {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn split_at(range: Range<usize>, at: usize) -> (Range<usize>, Range<usize>) {
|
fn split_at(range: Range<usize>, at: usize) -> (Range<usize>, Range<usize>) {
|
||||||
(range.start..at, at..range.end)
|
(range.start..at, at..range.end)
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +123,7 @@ fn find_middle_snake<T>(
|
||||||
vb: &mut V,
|
vb: &mut V,
|
||||||
) -> Option<(usize, usize)>
|
) -> Option<(usize, usize)>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let n = old_range.len();
|
let n = old_range.len();
|
||||||
let m = new_range.len();
|
let m = new_range.len();
|
||||||
|
|
@ -230,7 +229,7 @@ fn conquer<T>(
|
||||||
vb: &mut V,
|
vb: &mut V,
|
||||||
result: &mut Vec<RawOperation<T>>,
|
result: &mut Vec<RawOperation<T>>,
|
||||||
) where
|
) where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
// Check for common prefix
|
// Check for common prefix
|
||||||
let common_prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone());
|
let common_prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone());
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::tokenizer::token::Token;
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum RawOperation<T>
|
pub enum RawOperation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
Insert(Vec<Token<T>>),
|
Insert(Vec<Token<T>>),
|
||||||
Delete(Vec<Token<T>>),
|
Delete(Vec<Token<T>>),
|
||||||
|
|
@ -12,13 +12,13 @@ where
|
||||||
|
|
||||||
impl<T> RawOperation<T>
|
impl<T> RawOperation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
pub fn tokens(&self) -> &Vec<Token<T>> {
|
pub fn tokens(&self) -> &Vec<Token<T>> {
|
||||||
match self {
|
match self {
|
||||||
RawOperation::Insert(tokens) => tokens,
|
RawOperation::Insert(tokens)
|
||||||
RawOperation::Delete(tokens) => tokens,
|
| RawOperation::Delete(tokens)
|
||||||
RawOperation::Equal(tokens) => tokens,
|
| RawOperation::Equal(tokens) => tokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ pub fn reconcile_with_tokenizer<F, T>(
|
||||||
tokenizer: &Tokenizer<T>,
|
tokenizer: &Tokenizer<T>,
|
||||||
) -> String
|
) -> String
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer);
|
let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer);
|
||||||
let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer);
|
let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer);
|
||||||
|
|
@ -73,7 +73,8 @@ mod test {
|
||||||
"original_1 edit_1 original_3",
|
"original_1 edit_1 original_3",
|
||||||
);
|
);
|
||||||
|
|
||||||
// One deleted a large range, the other deleted subranges and inserted as well
|
// One deleted a large range, the other deleted subranges and inserted as
|
||||||
|
// well
|
||||||
test_merge_both_ways(
|
test_merge_both_ways(
|
||||||
"original_1 original_2 original_3 original_4 original_5",
|
"original_1 original_2 original_3 original_4 original_5",
|
||||||
"original_1 original_5",
|
"original_1 original_5",
|
||||||
|
|
@ -120,9 +121,6 @@ mod test {
|
||||||
"hi, my friend!",
|
"hi, my friend!",
|
||||||
);
|
);
|
||||||
|
|
||||||
// test_merge_both_ways("hello world", "world !", "hi hello world", "hi world
|
|
||||||
// !");
|
|
||||||
|
|
||||||
test_merge_both_ways(
|
test_merge_both_ways(
|
||||||
"both delete the same word",
|
"both delete the same word",
|
||||||
"both the same word",
|
"both the same word",
|
||||||
|
|
@ -147,7 +145,33 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore = "it's too slow"]
|
#[test]
|
||||||
|
fn test_reconcile_idempotent_inserts() {
|
||||||
|
// Both inserted the same prefix; this should get deduped
|
||||||
|
test_merge_both_ways(
|
||||||
|
"hi ",
|
||||||
|
"hi there ",
|
||||||
|
"hi there my friend ",
|
||||||
|
"hi there my friend ",
|
||||||
|
);
|
||||||
|
|
||||||
|
// The prefix of the 2nd appears on the 1st so it shouldn't get duplicated
|
||||||
|
test_merge_both_ways(
|
||||||
|
"hi ",
|
||||||
|
"hi there you ",
|
||||||
|
"hi there my friend ",
|
||||||
|
"hi there my friend you ",
|
||||||
|
);
|
||||||
|
|
||||||
|
test_merge_both_ways("a", "a b c", "a b c d", "a b c d");
|
||||||
|
|
||||||
|
test_merge_both_ways(
|
||||||
|
" |7ca2b36d-6ee7-49eb-8eb1-d77e4cc1a001| ",
|
||||||
|
" |7ca2b36d-6ee7-49eb-8eb1-d77e4cc1a001| |cd9195cc-103a-4f13-90c8-4fba0ba421ee| |d39156cc-cfd6-42a8-b70a-75020896069d| |fbad794c-9c47-41f2-a343-490284ecb5a0| |dup| ",
|
||||||
|
" |7ca2b36d-6ee7-49eb-8eb1-d77e4cc1a001| |cd9195cc-103a-4f13-90c8-4fba0ba421ee| |dup| ",
|
||||||
|
" |7ca2b36d-6ee7-49eb-8eb1-d77e4cc1a001| |cd9195cc-103a-4f13-90c8-4fba0ba421ee| |d39156cc-cfd6-42a8-b70a-75020896069d| |fbad794c-9c47-41f2-a343-490284ecb5a0| |dup| |dup| ");
|
||||||
|
}
|
||||||
|
|
||||||
#[test_matrix( [
|
#[test_matrix( [
|
||||||
"pride_and_prejudice.txt",
|
"pride_and_prejudice.txt",
|
||||||
"romeo_and_juliet.txt",
|
"romeo_and_juliet.txt",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use crate::{
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct EditedText<'a, T>
|
pub struct EditedText<'a, T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
operations: Vec<OrderedOperation<T>>,
|
operations: Vec<OrderedOperation<T>>,
|
||||||
|
|
@ -46,7 +46,7 @@ impl<'a> EditedText<'a, String> {
|
||||||
|
|
||||||
impl<'a, T> EditedText<'a, T>
|
impl<'a, T> EditedText<'a, T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create an `EditedText` from the given original (old) and updated (new)
|
/// Create an `EditedText` from the given original (old) and updated (new)
|
||||||
/// strings. The returned `EditedText` represents the changes from the
|
/// strings. The returned `EditedText` represents the changes from the
|
||||||
|
|
@ -65,7 +65,6 @@ where
|
||||||
|
|
||||||
Self::new(
|
Self::new(
|
||||||
original,
|
original,
|
||||||
// Self::cook_operations(diff),
|
|
||||||
Self::cook_operations(Self::elongate_operations(diff)).collect(),
|
Self::cook_operations(Self::elongate_operations(diff)).collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +190,7 @@ where
|
||||||
pub fn merge(self, other: Self) -> Self {
|
pub fn merge(self, other: Self) -> Self {
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
self.text, other.text,
|
self.text, other.text,
|
||||||
"EditedText-s must be derived from the same text to be mergable"
|
"`EditedText`-s must be derived from the same text to be mergable"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut left_merge_context = MergeContext::default();
|
let mut left_merge_context = MergeContext::default();
|
||||||
|
|
@ -207,9 +206,21 @@ where
|
||||||
|(operation, _)| {
|
|(operation, _)| {
|
||||||
(
|
(
|
||||||
operation.order,
|
operation.order,
|
||||||
// Operations on left and right must come in the same order so that
|
// Operations on the left and right must come in the same order so that
|
||||||
// inserts can be merged with other inserts and deletes with deletes.
|
// inserts can be merged with other inserts and deletes with deletes.
|
||||||
usize::from(matches!(operation.operation, Operation::Delete { .. })),
|
usize::from(matches!(operation.operation, Operation::Delete { .. })),
|
||||||
|
// Make sure that the ordering is deterministic regardless which text
|
||||||
|
// is left or right.
|
||||||
|
match &operation.operation {
|
||||||
|
Operation::Insert { text, .. } => text
|
||||||
|
.iter()
|
||||||
|
.map(super::super::tokenizer::token::Token::original)
|
||||||
|
.collect::<String>(),
|
||||||
|
Operation::Delete {
|
||||||
|
deleted_character_count,
|
||||||
|
..
|
||||||
|
} => deleted_character_count.to_string(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -232,6 +243,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the operations to the text and return the resulting text.
|
/// Apply the operations to the text and return the resulting text.
|
||||||
|
#[must_use]
|
||||||
pub fn apply(&self) -> String {
|
pub fn apply(&self) -> String {
|
||||||
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
|
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
|
||||||
|
|
||||||
|
|
@ -282,7 +294,7 @@ mod tests {
|
||||||
let original = "hello world! ...";
|
let original = "hello world! ...";
|
||||||
let left = "Hello world! I'm Andras.";
|
let left = "Hello world! I'm Andras.";
|
||||||
let right = "Hello world! How are you?";
|
let right = "Hello world! How are you?";
|
||||||
let expected = "Hello world! I'm Andras.How are you?";
|
let expected = "Hello world! How are you? I'm Andras.";
|
||||||
|
|
||||||
let operations_1 = EditedText::from_strings(original, left);
|
let operations_1 = EditedText::from_strings(original, left);
|
||||||
let operations_2 = EditedText::from_strings(original, right);
|
let operations_2 = EditedText::from_strings(original, right);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::operation_transformation::Operation;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MergeContext<T>
|
pub struct MergeContext<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
last_operation: Option<Operation<T>>,
|
last_operation: Option<Operation<T>>,
|
||||||
pub shift: i64,
|
pub shift: i64,
|
||||||
|
|
@ -13,7 +13,7 @@ where
|
||||||
|
|
||||||
impl<T> Default for MergeContext<T>
|
impl<T> Default for MergeContext<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
MergeContext {
|
MergeContext {
|
||||||
|
|
@ -25,7 +25,7 @@ where
|
||||||
|
|
||||||
impl<T> Debug for MergeContext<T>
|
impl<T> Debug for MergeContext<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.debug_struct("MergeContext")
|
f.debug_struct("MergeContext")
|
||||||
|
|
@ -37,7 +37,7 @@ where
|
||||||
|
|
||||||
impl<T> MergeContext<T>
|
impl<T> MergeContext<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
pub fn last_operation(&self) -> Option<&Operation<T>> { self.last_operation.as_ref() }
|
pub fn last_operation(&self) -> Option<&Operation<T>> { self.last_operation.as_ref() }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use core::{
|
use core::fmt::{Debug, Display};
|
||||||
fmt::{Debug, Display},
|
use std::ops::Range;
|
||||||
ops::Range,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -9,7 +7,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use super::merge_context::MergeContext;
|
use super::merge_context::MergeContext;
|
||||||
use crate::{
|
use crate::{
|
||||||
Token,
|
Token,
|
||||||
utils::{find_common_overlap::find_common_overlap, string_builder::StringBuilder},
|
utils::{
|
||||||
|
find_longest_prefix_contained_within::find_longest_prefix_contained_within,
|
||||||
|
string_builder::StringBuilder,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Represents a change that can be applied to a text document.
|
/// Represents a change that can be applied to a text document.
|
||||||
|
|
@ -19,7 +20,7 @@ use crate::{
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum Operation<T>
|
pub enum Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
Insert {
|
Insert {
|
||||||
index: usize,
|
index: usize,
|
||||||
|
|
@ -37,7 +38,7 @@ where
|
||||||
|
|
||||||
impl<T> Operation<T>
|
impl<T> Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Creates an insert operation with the given index and text.
|
/// Creates an insert operation with the given index and text.
|
||||||
/// If the text is empty (meaning that the operation would be a no-op),
|
/// If the text is empty (meaning that the operation would be a no-op),
|
||||||
|
|
@ -81,15 +82,8 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to apply the operation to the given `ropey::Rope` text, returning
|
/// Applies the operation to the given `StringBuilder`, returning the
|
||||||
/// the modified text.
|
/// modified `StringBuilder`.
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns a `SyncLibError::OperationApplicationError` if the operation
|
|
||||||
/// cannot be applied.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
///
|
||||||
/// When compiled in debug mode, panics if a delete operation is attempted
|
/// When compiled in debug mode, panics if a delete operation is attempted
|
||||||
/// on a range of text that does not match the text to be deleted.
|
/// on a range of text that does not match the text to be deleted.
|
||||||
|
|
@ -114,7 +108,7 @@ where
|
||||||
|
|
||||||
builder.delete(self.range());
|
builder.delete(self.range());
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +116,7 @@ where
|
||||||
/// Returns the index of the first character that the operation affects.
|
/// Returns the index of the first character that the operation affects.
|
||||||
pub fn start_index(&self) -> usize {
|
pub fn start_index(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Operation::Insert { index, .. } => *index,
|
Operation::Insert { index, .. } | Operation::Delete { index, .. } => *index,
|
||||||
Operation::Delete { index, .. } => *index,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +130,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the range of indices of characters that the operation affects.
|
/// Returns the range of indices of characters that the operation affects.
|
||||||
|
#[allow(clippy::range_plus_one)]
|
||||||
pub fn range(&self) -> Range<usize> { self.start_index()..self.end_index() + 1 }
|
pub fn range(&self) -> Range<usize> { self.start_index()..self.end_index() + 1 }
|
||||||
|
|
||||||
/// Returns the number of affected characters. It is always greater than 0
|
/// Returns the number of affected characters. It is always greater than 0
|
||||||
|
|
@ -212,17 +206,20 @@ where
|
||||||
..
|
..
|
||||||
}),
|
}),
|
||||||
) => {
|
) => {
|
||||||
let offset_in_tokens = find_common_overlap(previous_inserted_text, &text);
|
// In case the current insert's prefix appears in the previously inserted text,
|
||||||
let trimmed_length_in_tokens = previous_inserted_text.len() - offset_in_tokens;
|
// we can trim the current insert to only include the non-overlapping part.
|
||||||
let trimmed_length = previous_inserted_text
|
// This way, we don't end up duplicating text.
|
||||||
|
let offset_in_tokens =
|
||||||
|
find_longest_prefix_contained_within(previous_inserted_text, &text);
|
||||||
|
let offset_in_length = text
|
||||||
.iter()
|
.iter()
|
||||||
.skip(offset_in_tokens)
|
.take(offset_in_tokens)
|
||||||
.map(Token::get_original_length)
|
.map(Token::get_original_length)
|
||||||
.sum::<usize>();
|
.sum::<usize>();
|
||||||
let trimmed_operation =
|
let trimmed_operation =
|
||||||
Operation::create_insert(index, text[trimmed_length_in_tokens..].to_vec());
|
Operation::create_insert(index, text[offset_in_tokens..].to_vec());
|
||||||
|
|
||||||
affecting_context.shift -= trimmed_length as i64;
|
affecting_context.shift -= offset_in_length as i64;
|
||||||
produced_context.shift += trimmed_operation
|
produced_context.shift += trimmed_operation
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(Operation::len)
|
.map(Operation::len)
|
||||||
|
|
@ -297,7 +294,7 @@ where
|
||||||
|
|
||||||
impl<T> Display for Operation<T>
|
impl<T> Display for Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -341,7 +338,7 @@ where
|
||||||
|
|
||||||
impl<T> Debug for Operation<T>
|
impl<T> Debug for Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{self}") }
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{self}") }
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +350,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic(expected = "Shifted index must be non-negative")]
|
||||||
fn test_shifting_error() {
|
fn test_shifting_error() {
|
||||||
insta::assert_debug_snapshot!(
|
insta::assert_debug_snapshot!(
|
||||||
Operation::create_insert(1, vec!["hi".into()])
|
Operation::create_insert(1, vec!["hi".into()])
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,19 @@ EditedText {
|
||||||
operations: [
|
operations: [
|
||||||
OrderedOperation {
|
OrderedOperation {
|
||||||
order: 0,
|
order: 0,
|
||||||
operation: <insert 'Hello, my friend! ' from index 0>,
|
operation: <insert 'Hello, my friend!' from index 0>,
|
||||||
},
|
},
|
||||||
OrderedOperation {
|
OrderedOperation {
|
||||||
order: 0,
|
order: 0,
|
||||||
operation: <delete 'hello world! ' from index 18>,
|
operation: <delete 'hello world!' from index 17>,
|
||||||
},
|
},
|
||||||
OrderedOperation {
|
OrderedOperation {
|
||||||
order: 21,
|
order: 20,
|
||||||
operation: <insert 'you doing? Albert' from index 26>,
|
operation: <insert ' you doing? Albert' from index 25>,
|
||||||
},
|
},
|
||||||
OrderedOperation {
|
OrderedOperation {
|
||||||
order: 21,
|
order: 20,
|
||||||
operation: <delete 'you? Adam' from index 43>,
|
operation: <delete ' you? Adam' from index 43>,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: reconcile/src/tokenizer/word_tokenizer.rs
|
||||||
|
expression: "word_tokenizer(\"\")"
|
||||||
|
snapshot_kind: text
|
||||||
|
---
|
||||||
|
[]
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue