mirror of
https://github.com/loro-dev/loro.git
synced 2024-11-24 12:20:06 +00:00
feat: add FFI for Loro (#420)
* chore: init ffi * feat: impl doc and LoroList * feat: impl containers * feat: unknown container * feat: event ffi * chore: clean * feat: ffi undo manager * chore: cargo fix * chore: cargo fix * fix: ffi value or container * fix: ffi arc * fix: is attached for movable list * bk * feat: all LoroDoc func * feat: refine vv * feat: ffi frontiers * feat: ffi awareness * fix: merge
This commit is contained in:
parent
5e3f269c8c
commit
4414053a82
50 changed files with 3234 additions and 698 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -572,9 +572,9 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.9.0"
|
version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embedded-io"
|
name = "embedded-io"
|
||||||
|
@ -694,7 +694,7 @@ dependencies = [
|
||||||
"ctor 0.2.6",
|
"ctor 0.2.6",
|
||||||
"dev-utils",
|
"dev-utils",
|
||||||
"ensure-cov",
|
"ensure-cov",
|
||||||
"enum-as-inner 0.5.1",
|
"enum-as-inner 0.6.0",
|
||||||
"enum_dispatch",
|
"enum_dispatch",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
|
@ -1052,7 +1052,6 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ctor 0.2.6",
|
"ctor 0.2.6",
|
||||||
"dev-utils",
|
"dev-utils",
|
||||||
"either",
|
|
||||||
"enum-as-inner 0.6.0",
|
"enum-as-inner 0.6.0",
|
||||||
"generic-btree",
|
"generic-btree",
|
||||||
"loro-common 0.16.12",
|
"loro-common 0.16.12",
|
||||||
|
@ -1156,6 +1155,14 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loro-ffi"
|
||||||
|
version = "0.16.2"
|
||||||
|
dependencies = [
|
||||||
|
"loro 0.16.12",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "loro-internal"
|
name = "loro-internal"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
|
@ -1243,7 +1250,7 @@ dependencies = [
|
||||||
"dhat",
|
"dhat",
|
||||||
"either",
|
"either",
|
||||||
"ensure-cov",
|
"ensure-cov",
|
||||||
"enum-as-inner 0.5.1",
|
"enum-as-inner 0.6.0",
|
||||||
"enum_dispatch",
|
"enum_dispatch",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"generic-btree",
|
"generic-btree",
|
||||||
|
|
|
@ -12,12 +12,13 @@ members = [
|
||||||
"crates/dev-utils",
|
"crates/dev-utils",
|
||||||
"crates/delta",
|
"crates/delta",
|
||||||
"crates/kv-store",
|
"crates/kv-store",
|
||||||
|
"crates/loro-ffi",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
enum_dispatch = "0.3.11"
|
enum_dispatch = "0.3.11"
|
||||||
enum-as-inner = "0.5.1"
|
enum-as-inner = "0.6.0"
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
tracing = { version = "0.1" }
|
tracing = { version = "0.1" }
|
||||||
serde_columnar = { version = "0.3.10" }
|
serde_columnar = { version = "0.3.10" }
|
||||||
|
@ -30,3 +31,4 @@ bytes = "1"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
xxhash-rust = { version = "0.8.12", features = ["xxh32"] }
|
xxhash-rust = { version = "0.8.12", features = ["xxh32"] }
|
||||||
ensure-cov = "0.1.0"
|
ensure-cov = "0.1.0"
|
||||||
|
either = "1.13.0"
|
||||||
|
|
|
@ -93,9 +93,27 @@ impl ActorTrait for DrawActor {
|
||||||
};
|
};
|
||||||
|
|
||||||
let map = self.doc.get_map(id);
|
let map = self.doc.get_map(id);
|
||||||
let pos_map = map.get("pos").unwrap().unwrap_right().into_map().unwrap();
|
let pos_map = map
|
||||||
let x = pos_map.get("x").unwrap().unwrap_left().into_i64().unwrap();
|
.get("pos")
|
||||||
let y = pos_map.get("y").unwrap().unwrap_left().into_i64().unwrap();
|
.unwrap()
|
||||||
|
.into_container()
|
||||||
|
.unwrap()
|
||||||
|
.into_map()
|
||||||
|
.unwrap();
|
||||||
|
let x = pos_map
|
||||||
|
.get("x")
|
||||||
|
.unwrap()
|
||||||
|
.into_value()
|
||||||
|
.unwrap()
|
||||||
|
.into_i64()
|
||||||
|
.unwrap();
|
||||||
|
let y = pos_map
|
||||||
|
.get("y")
|
||||||
|
.unwrap()
|
||||||
|
.into_value()
|
||||||
|
.unwrap()
|
||||||
|
.into_i64()
|
||||||
|
.unwrap();
|
||||||
pos_map
|
pos_map
|
||||||
.insert("x", x.overflowing_add(relative_to.x as i64).0)
|
.insert("x", x.overflowing_add(relative_to.x as i64).0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -62,7 +62,17 @@ impl ActorTrait for CounterActor {
|
||||||
let counter = loro.get_counter("counter");
|
let counter = loro.get_counter("counter");
|
||||||
let result = counter.get_value();
|
let result = counter.get_value();
|
||||||
let tracker = self.tracker.try_lock().unwrap().to_value();
|
let tracker = self.tracker.try_lock().unwrap().to_value();
|
||||||
assert_eq!(&result, tracker.into_map().unwrap().get("counter").unwrap());
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
tracker
|
||||||
|
.into_map()
|
||||||
|
.unwrap()
|
||||||
|
.get("counter")
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_double()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
use loro_without_counter::LoroDoc as LoroDocWithoutCounter;
|
use loro_without_counter::LoroDoc as LoroDocWithoutCounter;
|
||||||
// snapshot to snapshot
|
// snapshot to snapshot
|
||||||
|
|
|
@ -155,7 +155,7 @@ impl Actionable for TreeAction {
|
||||||
let nodes = tree
|
let nodes = tree
|
||||||
.nodes()
|
.nodes()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|x| !tree.is_node_deleted(*x).unwrap())
|
.filter(|x| !tree.is_node_deleted(x).unwrap())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let node_num = nodes.len();
|
let node_num = nodes.len();
|
||||||
let TreeAction { target, action } = self;
|
let TreeAction { target, action } = self;
|
||||||
|
|
|
@ -164,7 +164,7 @@ impl OneDocFuzzer {
|
||||||
let nodes = tree
|
let nodes = tree
|
||||||
.nodes()
|
.nodes()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|x| !tree.is_node_deleted(*x).unwrap())
|
.filter(|x| !tree.is_node_deleted(x).unwrap())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let node_num = nodes.len();
|
let node_num = nodes.len();
|
||||||
let crate::container::TreeAction { target, action } = tree_action;
|
let crate::container::TreeAction { target, action } = tree_action;
|
||||||
|
|
|
@ -129,7 +129,7 @@ fn snapshot_from_016_can_be_imported_in_cur_version() {
|
||||||
.get_map("map")
|
.get_map("map")
|
||||||
.get("new_key")
|
.get("new_key")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.left()
|
.into_value()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
loro::LoroValue::String(Arc::new("new_value".into()))
|
loro::LoroValue::String(Arc::new("new_value".into()))
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,20 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "loro-ffi"
|
name = "loro-ffi"
|
||||||
version = "0.1.0"
|
version = "0.16.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
loro-internal = { path = "../loro-internal" }
|
loro = { path = "../loro", features = ["counter","jsonpath"] }
|
||||||
|
serde_json = {workspace = true}
|
||||||
[lib]
|
|
||||||
crate-type = ["staticlib", "cdylib"]
|
|
||||||
name = "loro"
|
|
||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
cbindgen = "0.24.3"
|
|
|
@ -1,47 +0,0 @@
|
||||||
# loro-ffi
|
|
||||||
|
|
||||||
- `cargo build --release`
|
|
||||||
- move `libloro.a` and `loro_ffi.h` to directory `examples/lib`
|
|
||||||
- run
|
|
||||||
|
|
||||||
## C++
|
|
||||||
|
|
||||||
Read more: [cbindgen](https://github.com/eqrion/cbindgen)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
g++ loro.cpp -Bstatic -framework Security -L. -lloro -o loro
|
|
||||||
```
|
|
||||||
|
|
||||||
## Go
|
|
||||||
|
|
||||||
Read more: [cgo](https://pkg.go.dev/cmd/cgo)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go run main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
## [Python](../loro-python/)
|
|
||||||
|
|
||||||
## Java
|
|
||||||
|
|
||||||
Candidates:
|
|
||||||
|
|
||||||
- [JNR](https://github.com/jnr/jnr-ffi)
|
|
||||||
- [Panama](https://jdk.java.net/panama/) [blog](https://jornvernee.github.io/java/panama/rust/panama-ffi/2021/09/03/rust-panama-helloworld.html)
|
|
||||||
- [JNI](https://github.com/jni-rs/jni-rs)
|
|
||||||
|
|
||||||
### Panama
|
|
||||||
|
|
||||||
install panama-jdk and jextract
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jextract -I /Library/Developer/CommandLineTools/usr/include/c++/v1 -I /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -d loro_java -t org.loro -l loro -- lib/loro_ffi.h
|
|
||||||
```
|
|
||||||
|
|
||||||
### JNR
|
|
||||||
|
|
||||||
move `libloro.dylib` into `jnr/app`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gradle run
|
|
||||||
```
|
|
|
@ -1,8 +0,0 @@
|
||||||
fn main() {
|
|
||||||
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
||||||
let config = cbindgen::Config::from_file("cbindgen.toml")
|
|
||||||
.expect("Unable to find cbindgen.toml configuration file");
|
|
||||||
cbindgen::generate_with_config(crate_dir, config)
|
|
||||||
.unwrap()
|
|
||||||
.write_to_file("target/loro_ffi.h");
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
header = """
|
|
||||||
typedef struct LoroCore {} LoroCore;
|
|
||||||
|
|
||||||
typedef struct Text {} Text;
|
|
||||||
"""
|
|
||||||
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
|
|
||||||
language = "C"
|
|
9
crates/loro-ffi/examples/jnr/.gitattributes
vendored
9
crates/loro-ffi/examples/jnr/.gitattributes
vendored
|
@ -1,9 +0,0 @@
|
||||||
#
|
|
||||||
# https://help.github.com/articles/dealing-with-line-endings/
|
|
||||||
#
|
|
||||||
# Linux start script should use lf
|
|
||||||
/gradlew text eol=lf
|
|
||||||
|
|
||||||
# These are Windows script files and should use crlf
|
|
||||||
*.bat text eol=crlf
|
|
||||||
|
|
5
crates/loro-ffi/examples/jnr/.gitignore
vendored
5
crates/loro-ffi/examples/jnr/.gitignore
vendored
|
@ -1,5 +0,0 @@
|
||||||
# Ignore Gradle project-specific cache directory
|
|
||||||
.gradle
|
|
||||||
|
|
||||||
# Ignore Gradle build output directory
|
|
||||||
build
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* This file was generated by the Gradle 'init' task.
|
|
||||||
*
|
|
||||||
* This generated file contains a sample Java application project to get you started.
|
|
||||||
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
|
|
||||||
* User Manual available at https://docs.gradle.org/7.6/userguide/building_java_projects.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
// Apply the application plugin to add support for building a CLI application in Java.
|
|
||||||
application
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
// Use Maven Central for resolving dependencies.
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Use JUnit Jupiter for testing.
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
|
|
||||||
|
|
||||||
// This dependency is used by the application.
|
|
||||||
implementation("com.google.guava:guava:31.1-jre")
|
|
||||||
|
|
||||||
implementation("com.github.jnr:jnr-ffi:2.2.13")
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
// Define the main class for the application.
|
|
||||||
mainClass.set("jnr.App")
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* This Java source file was generated by the Gradle 'init' task.
|
|
||||||
*/
|
|
||||||
package jnr;
|
|
||||||
import jnr.ffi.Pointer;
|
|
||||||
import jnr.ffi.types.u_int32_t;
|
|
||||||
import jnr.ffi.LibraryLoader;
|
|
||||||
import jnr.ffi.Runtime;
|
|
||||||
|
|
||||||
|
|
||||||
public class App {
|
|
||||||
public interface LibLoro{
|
|
||||||
Pointer loro_new();
|
|
||||||
void loro_free(Pointer loro);
|
|
||||||
Pointer loro_get_text(Pointer loro, String id);
|
|
||||||
void text_free(Pointer text);
|
|
||||||
void text_insert(Pointer text, Pointer ctx, @u_int32_t long pos, String value);
|
|
||||||
String text_value(Pointer text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
LibLoro libLoro = LibraryLoader.create(LibLoro.class).load("loro");
|
|
||||||
var loro = libLoro.loro_new();
|
|
||||||
var text = libLoro.loro_get_text(loro, "text");
|
|
||||||
// var pos = Pointer.wrap(u_int32_t.class, 0);
|
|
||||||
libLoro.text_insert(text, loro, 0, "abc");
|
|
||||||
var value = libLoro.text_value(text);
|
|
||||||
System.out.println(value);
|
|
||||||
libLoro.text_free(text);
|
|
||||||
libLoro.loro_free(loro);
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
|
@ -1,6 +0,0 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
244
crates/loro-ffi/examples/jnr/gradlew
vendored
244
crates/loro-ffi/examples/jnr/gradlew
vendored
|
@ -1,244 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
92
crates/loro-ffi/examples/jnr/gradlew.bat
vendored
92
crates/loro-ffi/examples/jnr/gradlew.bat
vendored
|
@ -1,92 +0,0 @@
|
||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
* This file was generated by the Gradle 'init' task.
|
|
||||||
*
|
|
||||||
* The settings file is used to specify which projects to include in your build.
|
|
||||||
*
|
|
||||||
* Detailed information about configuring a multi-project build in Gradle can be found
|
|
||||||
* in the user manual at https://docs.gradle.org/7.6/userguide/multi_project_builds.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
rootProject.name = "jnr"
|
|
||||||
include("app")
|
|
|
@ -1,14 +0,0 @@
|
||||||
#include <stdio.h>
|
|
||||||
extern "C" {
|
|
||||||
#include "../target/loro_ffi.h"
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
LoroCore* loro = loro_new();
|
|
||||||
Text* text = loro_get_text(loro, "text");
|
|
||||||
text_insert(text, loro, 0, "abc");
|
|
||||||
char* str = text_value(text);
|
|
||||||
printf("%s", str);
|
|
||||||
text_free(text);
|
|
||||||
loro_free(loro);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package main;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo LDFLAGS: -L./lib -framework Security -lloro
|
|
||||||
#include "./lib/loro_ffi.h"
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
loro := C.loro_new();
|
|
||||||
text := C.loro_get_text(loro, C.CString("text"));
|
|
||||||
// pos := C.uint(0);
|
|
||||||
C.text_insert(text, loro, 0, C.CString("abc"));
|
|
||||||
value := C.text_value(text);
|
|
||||||
fmt.Println(C.GoString(value));
|
|
||||||
C.text_free(text);
|
|
||||||
C.loro_free(loro);
|
|
||||||
}
|
|
83
crates/loro-ffi/src/awareness.rs
Normal file
83
crates/loro-ffi/src/awareness.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use loro::PeerID;
|
||||||
|
|
||||||
|
use crate::{LoroValue, LoroValueLike};
|
||||||
|
pub struct Awareness(Mutex<loro::awareness::Awareness>);
|
||||||
|
|
||||||
|
impl Awareness {
|
||||||
|
pub fn new(peer: PeerID, timeout: i64) -> Self {
|
||||||
|
Self(Mutex::new(loro::awareness::Awareness::new(peer, timeout)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self, peers: &[PeerID]) -> Vec<u8> {
|
||||||
|
self.0.try_lock().unwrap().encode(peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_all(&self) -> Vec<u8> {
|
||||||
|
self.0.try_lock().unwrap().encode_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, encoded_peers_info: &[u8]) -> AwarenessPeerUpdate {
|
||||||
|
let (updated, added) = self.0.try_lock().unwrap().apply(encoded_peers_info);
|
||||||
|
AwarenessPeerUpdate { updated, added }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_local_state(&self, value: Arc<dyn LoroValueLike>) {
|
||||||
|
self.0
|
||||||
|
.try_lock()
|
||||||
|
.unwrap()
|
||||||
|
.set_local_state(value.as_loro_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_local_state(&self) -> Option<LoroValue> {
|
||||||
|
self.0
|
||||||
|
.try_lock()
|
||||||
|
.unwrap()
|
||||||
|
.get_local_state()
|
||||||
|
.map(|x| x.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_outdated(&self) -> Vec<PeerID> {
|
||||||
|
self.0.try_lock().unwrap().remove_outdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_states(&self) -> HashMap<PeerID, PeerInfo> {
|
||||||
|
self.0
|
||||||
|
.try_lock()
|
||||||
|
.unwrap()
|
||||||
|
.get_all_states()
|
||||||
|
.iter()
|
||||||
|
.map(|(p, i)| (*p, i.into()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peer(&self) -> PeerID {
|
||||||
|
self.0.try_lock().unwrap().peer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AwarenessPeerUpdate {
|
||||||
|
pub updated: Vec<PeerID>,
|
||||||
|
pub added: Vec<PeerID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PeerInfo {
|
||||||
|
pub state: LoroValue,
|
||||||
|
pub counter: i32,
|
||||||
|
// This field is generated locally
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&loro::awareness::PeerInfo> for PeerInfo {
|
||||||
|
fn from(value: &loro::awareness::PeerInfo) -> Self {
|
||||||
|
PeerInfo {
|
||||||
|
state: value.state.clone().into(),
|
||||||
|
counter: value.counter,
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
crates/loro-ffi/src/config.rs
Normal file
66
crates/loro-ffi/src/config.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use loro::StyleConfig;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Configure(loro::Configure);
|
||||||
|
|
||||||
|
impl Configure {
|
||||||
|
pub fn fork(&self) -> Arc<Self> {
|
||||||
|
Arc::new(Self(self.0.fork()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_timestamp(&self) -> bool {
|
||||||
|
self.0.record_timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_record_timestamp(&self, record: bool) {
|
||||||
|
self.0.set_record_timestamp(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge_interval(&self) -> i64 {
|
||||||
|
self.0.merge_interval()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_merge_interval(&self, interval: i64) {
|
||||||
|
self.0.set_merge_interval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_style_config(&self) -> Arc<StyleConfigMap> {
|
||||||
|
Arc::new(StyleConfigMap(self.0.text_style_config().clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct StyleConfigMap(Arc<RwLock<loro::StyleConfigMap>>);
|
||||||
|
|
||||||
|
impl StyleConfigMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&self, key: &str, value: StyleConfig) {
|
||||||
|
self.0.write().unwrap().insert(key.into(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &str) -> Option<StyleConfig> {
|
||||||
|
let m = self.0.read().unwrap();
|
||||||
|
m.get(&(key.into())).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_rich_text_config() -> Self {
|
||||||
|
Self(Arc::new(RwLock::new(
|
||||||
|
loro::StyleConfigMap::default_rich_text_config(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_loro(self) -> loro::StyleConfigMap {
|
||||||
|
self.0.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::Configure> for Configure {
|
||||||
|
fn from(value: loro::Configure) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
36
crates/loro-ffi/src/container.rs
Normal file
36
crates/loro-ffi/src/container.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
mod counter;
|
||||||
|
mod list;
|
||||||
|
mod map;
|
||||||
|
mod movable_list;
|
||||||
|
mod text;
|
||||||
|
mod tree;
|
||||||
|
mod unknown;
|
||||||
|
|
||||||
|
pub use counter::LoroCounter;
|
||||||
|
pub use list::{Cursor, LoroList};
|
||||||
|
pub use map::LoroMap;
|
||||||
|
pub use movable_list::LoroMovableList;
|
||||||
|
pub use text::LoroText;
|
||||||
|
pub use tree::{LoroTree, TreeParentId};
|
||||||
|
pub use unknown::LoroUnknown;
|
||||||
|
|
||||||
|
use crate::{ContainerID, ContainerType};
|
||||||
|
|
||||||
|
pub trait ContainerIdLike: Send + Sync {
|
||||||
|
fn as_container_id(&self, ty: ContainerType) -> ContainerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerIdLike for ContainerID {
|
||||||
|
fn as_container_id(&self, _ty: ContainerType) -> ContainerID {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerIdLike for String {
|
||||||
|
fn as_container_id(&self, ty: ContainerType) -> ContainerID {
|
||||||
|
ContainerID::Root {
|
||||||
|
name: String::from(self),
|
||||||
|
container_type: ty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
crates/loro-ffi/src/container/counter.rs
Normal file
42
crates/loro-ffi/src/container/counter.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use loro::LoroResult;
|
||||||
|
|
||||||
|
use crate::ContainerID;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroCounter {
|
||||||
|
pub(crate) counter: loro::LoroCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroCounter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
counter: loro::LoroCounter::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return container id of the Counter.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.counter.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the counter by the given value.
|
||||||
|
pub fn increment(&self, value: f64) -> LoroResult<()> {
|
||||||
|
self.counter.increment(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrement the counter by the given value.
|
||||||
|
pub fn decrement(&self, value: f64) -> LoroResult<()> {
|
||||||
|
self.counter.decrement(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current value of the counter.
|
||||||
|
pub fn get_value(&self) -> f64 {
|
||||||
|
self.counter.get_value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroCounter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
219
crates/loro-ffi/src/container/list.rs
Normal file
219
crates/loro-ffi/src/container/list.rs
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
use loro::{cursor::Side, LoroList as InnerLoroList, LoroResult, ID};
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue, LoroValueLike, ValueOrContainer};
|
||||||
|
|
||||||
|
use super::{LoroCounter, LoroMap, LoroMovableList, LoroText, LoroTree};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroList {
|
||||||
|
pub(crate) list: InnerLoroList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroList {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
list: InnerLoroList::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the container is attached to a document
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.list.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a value at the given position.
|
||||||
|
pub fn insert(&self, pos: u32, v: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.list.insert(pos as usize, v.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete values at the given position.
|
||||||
|
#[inline]
|
||||||
|
pub fn delete(&self, pos: u32, len: u32) -> LoroResult<()> {
|
||||||
|
self.list.delete(pos as usize, len as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value at the given position.
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, index: u32) -> Option<Arc<dyn ValueOrContainer>> {
|
||||||
|
self.list
|
||||||
|
.get(index as usize)
|
||||||
|
.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the deep value of the container.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_deep_value(&self) -> LoroValue {
|
||||||
|
self.list.get_deep_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shallow value of the container.
|
||||||
|
///
|
||||||
|
/// This does not convert the state of sub-containers; instead, it represents them as [LoroValue::Container].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_value(&self) -> LoroValue {
|
||||||
|
self.list.get_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ID of the container.
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.list.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> u32 {
|
||||||
|
self.list.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.list.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the last element of the list.
|
||||||
|
#[inline]
|
||||||
|
pub fn pop(&self) -> LoroResult<Option<LoroValue>> {
|
||||||
|
self.list.pop().map(|v| v.map(|v| v.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn push(&self, v: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.list.push(v.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the elements of the list.
|
||||||
|
// TODO: wrap it in ffi side
|
||||||
|
pub fn for_each<I>(&self, f: I)
|
||||||
|
where
|
||||||
|
I: FnMut((usize, loro::ValueOrContainer)),
|
||||||
|
{
|
||||||
|
self.list.for_each(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a container to the list.
|
||||||
|
// #[inline]
|
||||||
|
// pub fn push_container(&self, child: Arc<dyn ContainerLike>) -> LoroResult<()> {
|
||||||
|
// let c = child.to_container();
|
||||||
|
// self.list.push_container(c)?;
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_list_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroList>,
|
||||||
|
) -> LoroResult<Arc<LoroList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_map_container(&self, pos: u32, child: Arc<LoroMap>) -> LoroResult<Arc<LoroMap>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().map)?;
|
||||||
|
Ok(Arc::new(LoroMap { map: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_text_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroText>,
|
||||||
|
) -> LoroResult<Arc<LoroText>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().text)?;
|
||||||
|
Ok(Arc::new(LoroText { text: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_tree_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroTree>,
|
||||||
|
) -> LoroResult<Arc<LoroTree>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().tree)?;
|
||||||
|
Ok(Arc::new(LoroTree { tree: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_movable_list_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroMovableList>,
|
||||||
|
) -> LoroResult<Arc<LoroMovableList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroMovableList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_counter_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroCounter>,
|
||||||
|
) -> LoroResult<Arc<LoroCounter>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().counter)?;
|
||||||
|
Ok(Arc::new(LoroCounter { counter: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cursor(&self, pos: u32, side: Side) -> Option<Arc<Cursor>> {
|
||||||
|
self.list
|
||||||
|
.get_cursor(pos as usize, side)
|
||||||
|
.map(|v| Arc::new(v.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroList {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Cursor(loro::cursor::Cursor);
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
pub fn new(id: Option<ID>, container: ContainerID, side: Side, origin_pos: u32) -> Self {
|
||||||
|
Self(loro::cursor::Cursor::new(
|
||||||
|
id,
|
||||||
|
container.into(),
|
||||||
|
side,
|
||||||
|
origin_pos as usize,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Cursor {
|
||||||
|
type Target = loro::cursor::Cursor;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::cursor::Cursor> for Cursor {
|
||||||
|
fn from(c: loro::cursor::Cursor) -> Self {
|
||||||
|
Self(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Cursor> for loro::cursor::Cursor {
|
||||||
|
fn from(c: Cursor) -> Self {
|
||||||
|
c.0
|
||||||
|
}
|
||||||
|
}
|
150
crates/loro-ffi/src/container/map.rs
Normal file
150
crates/loro-ffi/src/container/map.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use loro::LoroResult;
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue, LoroValueLike, ValueOrContainer};
|
||||||
|
|
||||||
|
use super::{LoroCounter, LoroList, LoroMovableList, LoroText, LoroTree};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroMap {
|
||||||
|
pub(crate) map: loro::LoroMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
map: loro::LoroMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.map.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a key-value pair from the map.
|
||||||
|
pub fn delete(&self, key: &str) -> LoroResult<()> {
|
||||||
|
self.map.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the key-value pairs of the map.
|
||||||
|
// pub fn for_each<I>(&self, f: I)
|
||||||
|
// where
|
||||||
|
// I: FnMut(&str, loro::ValueOrContainer),
|
||||||
|
// {
|
||||||
|
// self.map.for_each(f)
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Insert a key-value pair into the map.
|
||||||
|
pub fn insert(&self, key: &str, value: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.map.insert(key, value.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the map.
|
||||||
|
pub fn len(&self) -> u32 {
|
||||||
|
self.map.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ID of the map.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.map.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the map is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.map.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value of the map with the given key.
|
||||||
|
pub fn get(&self, key: &str) -> Option<Arc<dyn ValueOrContainer>> {
|
||||||
|
self.map
|
||||||
|
.get(key)
|
||||||
|
.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_list_container(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
child: Arc<LoroList>,
|
||||||
|
) -> LoroResult<Arc<LoroList>> {
|
||||||
|
let c = self
|
||||||
|
.map
|
||||||
|
.insert_container(key, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_map_container(&self, key: &str, child: Arc<LoroMap>) -> LoroResult<Arc<LoroMap>> {
|
||||||
|
let c = self.map.insert_container(key, child.as_ref().clone().map)?;
|
||||||
|
Ok(Arc::new(LoroMap { map: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_text_container(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
child: Arc<LoroText>,
|
||||||
|
) -> LoroResult<Arc<LoroText>> {
|
||||||
|
let c = self
|
||||||
|
.map
|
||||||
|
.insert_container(key, child.as_ref().clone().text)?;
|
||||||
|
Ok(Arc::new(LoroText { text: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_tree_container(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
child: Arc<LoroTree>,
|
||||||
|
) -> LoroResult<Arc<LoroTree>> {
|
||||||
|
let c = self
|
||||||
|
.map
|
||||||
|
.insert_container(key, child.as_ref().clone().tree)?;
|
||||||
|
Ok(Arc::new(LoroTree { tree: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_movable_list_container(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
child: Arc<LoroMovableList>,
|
||||||
|
) -> LoroResult<Arc<LoroMovableList>> {
|
||||||
|
let c = self
|
||||||
|
.map
|
||||||
|
.insert_container(key, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroMovableList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_counter_container(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
child: Arc<LoroCounter>,
|
||||||
|
) -> LoroResult<Arc<LoroCounter>> {
|
||||||
|
let c = self
|
||||||
|
.map
|
||||||
|
.insert_container(key, child.as_ref().clone().counter)?;
|
||||||
|
Ok(Arc::new(LoroCounter { counter: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shallow value of the map.
|
||||||
|
///
|
||||||
|
/// It will not convert the state of sub-containers, but represent them as [LoroValue::Container].
|
||||||
|
pub fn get_value(&self) -> LoroValue {
|
||||||
|
self.map.get_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the deep value of the map.
|
||||||
|
///
|
||||||
|
/// It will convert the state of sub-containers into a nested JSON value.
|
||||||
|
pub fn get_deep_value(&self) -> LoroValue {
|
||||||
|
self.map.get_deep_value().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroMap {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
257
crates/loro-ffi/src/container/movable_list.rs
Normal file
257
crates/loro-ffi/src/container/movable_list.rs
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use loro::{cursor::Side, LoroResult};
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue, LoroValueLike, ValueOrContainer};
|
||||||
|
|
||||||
|
use super::{Cursor, LoroCounter, LoroList, LoroMap, LoroText, LoroTree};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroMovableList {
|
||||||
|
pub(crate) list: loro::LoroMovableList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroMovableList {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
list: loro::LoroMovableList::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the container id.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.list.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the container is attached to a document
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.list.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a value at the given position.
|
||||||
|
pub fn insert(&self, pos: u32, v: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.list.insert(pos as usize, v.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete values at the given position.
|
||||||
|
#[inline]
|
||||||
|
pub fn delete(&self, pos: u32, len: u32) -> LoroResult<()> {
|
||||||
|
self.list.delete(pos as usize, len as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value at the given position.
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, index: u32) -> Option<Arc<dyn ValueOrContainer>> {
|
||||||
|
self.list
|
||||||
|
.get(index as usize)
|
||||||
|
.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the list.
|
||||||
|
pub fn len(&self) -> u32 {
|
||||||
|
self.list.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the list is empty.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shallow value of the list.
|
||||||
|
///
|
||||||
|
/// It will not convert the state of sub-containers, but represent them as [LoroValue::Container].
|
||||||
|
pub fn get_value(&self) -> LoroValue {
|
||||||
|
self.list.get_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the deep value of the list.
|
||||||
|
///
|
||||||
|
/// It will convert the state of sub-containers into a nested JSON value.
|
||||||
|
pub fn get_deep_value(&self) -> LoroValue {
|
||||||
|
self.list.get_deep_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the last element of the list.
|
||||||
|
#[inline]
|
||||||
|
pub fn pop(&self) -> LoroResult<Option<Arc<dyn ValueOrContainer>>> {
|
||||||
|
self.list
|
||||||
|
.pop()
|
||||||
|
.map(|v| v.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn push(&self, v: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.list.push(v.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a container to the end of the list.
|
||||||
|
// pub fn push_container<C: ContainerTrait>(&self, child: C) -> LoroResult<C> {
|
||||||
|
// let pos = self.list.len();
|
||||||
|
// Ok(C::from_list(
|
||||||
|
// self.list.insert_container(pos, child.to_list())?,
|
||||||
|
// ))
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_list_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroList>,
|
||||||
|
) -> LoroResult<Arc<LoroList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_map_container(&self, pos: u32, child: Arc<LoroMap>) -> LoroResult<Arc<LoroMap>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().map)?;
|
||||||
|
Ok(Arc::new(LoroMap { map: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_text_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroText>,
|
||||||
|
) -> LoroResult<Arc<LoroText>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().text)?;
|
||||||
|
Ok(Arc::new(LoroText { text: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_tree_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroTree>,
|
||||||
|
) -> LoroResult<Arc<LoroTree>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().tree)?;
|
||||||
|
Ok(Arc::new(LoroTree { tree: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_movable_list_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroMovableList>,
|
||||||
|
) -> LoroResult<Arc<LoroMovableList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroMovableList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn insert_counter_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroCounter>,
|
||||||
|
) -> LoroResult<Arc<LoroCounter>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.insert_container(pos as usize, child.as_ref().clone().counter)?;
|
||||||
|
Ok(Arc::new(LoroCounter { counter: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_list_container(&self, pos: u32, child: Arc<LoroList>) -> LoroResult<Arc<LoroList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_map_container(&self, pos: u32, child: Arc<LoroMap>) -> LoroResult<Arc<LoroMap>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().map)?;
|
||||||
|
Ok(Arc::new(LoroMap { map: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_text_container(&self, pos: u32, child: Arc<LoroText>) -> LoroResult<Arc<LoroText>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().text)?;
|
||||||
|
Ok(Arc::new(LoroText { text: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_tree_container(&self, pos: u32, child: Arc<LoroTree>) -> LoroResult<Arc<LoroTree>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().tree)?;
|
||||||
|
Ok(Arc::new(LoroTree { tree: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_movable_list_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroMovableList>,
|
||||||
|
) -> LoroResult<Arc<LoroMovableList>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().list)?;
|
||||||
|
Ok(Arc::new(LoroMovableList { list: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_counter_container(
|
||||||
|
&self,
|
||||||
|
pos: u32,
|
||||||
|
child: Arc<LoroCounter>,
|
||||||
|
) -> LoroResult<Arc<LoroCounter>> {
|
||||||
|
let c = self
|
||||||
|
.list
|
||||||
|
.set_container(pos as usize, child.as_ref().clone().counter)?;
|
||||||
|
Ok(Arc::new(LoroCounter { counter: c }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value at the given position.
|
||||||
|
pub fn set(&self, pos: u32, value: Arc<dyn LoroValueLike>) -> LoroResult<()> {
|
||||||
|
self.list.set(pos as usize, value.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the value at the given position to the given position.
|
||||||
|
pub fn mov(&self, from: u32, to: u32) -> LoroResult<()> {
|
||||||
|
self.list.mov(from as usize, to as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the cursor at the given position.
|
||||||
|
///
|
||||||
|
/// Using "index" to denote cursor positions can be unstable, as positions may
|
||||||
|
/// shift with document edits. To reliably represent a position or range within
|
||||||
|
/// a document, it is more effective to leverage the unique ID of each item/character
|
||||||
|
/// in a List CRDT or Text CRDT.
|
||||||
|
///
|
||||||
|
/// Loro optimizes State metadata by not storing the IDs of deleted elements. This
|
||||||
|
/// approach complicates tracking cursors since they rely on these IDs. The solution
|
||||||
|
/// recalculates position by replaying relevant history to update stable positions
|
||||||
|
/// accurately. To minimize the performance impact of history replay, the system
|
||||||
|
/// updates cursor info to reference only the IDs of currently present elements,
|
||||||
|
/// thereby reducing the need for replay.
|
||||||
|
pub fn get_cursor(&self, pos: u32, side: Side) -> Option<Arc<Cursor>> {
|
||||||
|
self.list
|
||||||
|
.get_cursor(pos as usize, side)
|
||||||
|
.map(|v| Arc::new(v.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroMovableList {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
237
crates/loro-ffi/src/container/text.rs
Normal file
237
crates/loro-ffi/src/container/text.rs
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
|
use loro::{cursor::Side, LoroResult, TextDelta};
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue, LoroValueLike};
|
||||||
|
|
||||||
|
use super::Cursor;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroText {
|
||||||
|
pub(crate) text: loro::LoroText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroText {
|
||||||
|
/// Create a new container that is detached from the document.
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
text: loro::LoroText::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the container is attached to a document
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.text.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [ContainerID] of the text container.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.text.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate each span(internal storage unit) of the text.
|
||||||
|
///
|
||||||
|
/// The callback function will be called for each character in the text.
|
||||||
|
/// If the callback returns `false`, the iteration will stop.
|
||||||
|
// TODO:
|
||||||
|
pub fn iter(&self, callback: impl FnMut(&str) -> bool) {
|
||||||
|
self.text.iter(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a string at the given unicode position.
|
||||||
|
pub fn insert(&self, pos: u32, s: &str) -> LoroResult<()> {
|
||||||
|
self.text.insert(pos as usize, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a string at the given utf-8 position.
|
||||||
|
pub fn insert_utf8(&self, pos: u32, s: &str) -> LoroResult<()> {
|
||||||
|
self.text.insert_utf8(pos as usize, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a range of text at the given unicode position with unicode length.
|
||||||
|
pub fn delete(&self, pos: u32, len: u32) -> LoroResult<()> {
|
||||||
|
self.text.delete(pos as usize, len as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a range of text at the given utf-8 position with utf-8 length.
|
||||||
|
pub fn delete_utf8(&self, pos: u32, len: u32) -> LoroResult<()> {
|
||||||
|
self.text.delete_utf8(pos as usize, len as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a string slice at the given Unicode range
|
||||||
|
pub fn slice(&self, start_index: u32, end_index: u32) -> LoroResult<String> {
|
||||||
|
self.text.slice(start_index as usize, end_index as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the characters at given unicode position.
|
||||||
|
// TODO:
|
||||||
|
pub fn char_at(&self, pos: u32) -> LoroResult<char> {
|
||||||
|
self.text.char_at(pos as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete specified character and insert string at the same position at given unicode position.
|
||||||
|
pub fn splice(&self, pos: u32, len: u32, s: &str) -> LoroResult<String> {
|
||||||
|
self.text.splice(pos as usize, len as usize, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the text container is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.text.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the text container in UTF-8.
|
||||||
|
pub fn len_utf8(&self) -> u32 {
|
||||||
|
self.text.len_utf8() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the text container in Unicode.
|
||||||
|
pub fn len_unicode(&self) -> u32 {
|
||||||
|
self.text.len_unicode() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the text container in UTF-16.
|
||||||
|
pub fn len_utf16(&self) -> u32 {
|
||||||
|
self.text.len_utf16() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the current text based on the provided text.
|
||||||
|
pub fn update(&self, text: &str) {
|
||||||
|
self.text.update(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.
|
||||||
|
// TODO:
|
||||||
|
pub fn apply_delta(&self, delta: &[TextDelta]) -> LoroResult<()> {
|
||||||
|
self.text.apply_delta(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a range of text with a key-value pair.
|
||||||
|
///
|
||||||
|
/// You can use it to create a highlight, make a range of text bold, or add a link to a range of text.
|
||||||
|
///
|
||||||
|
/// You can specify the `expand` option to set the behavior when inserting text at the boundary of the range.
|
||||||
|
///
|
||||||
|
/// - `after`(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
|
||||||
|
/// - `before`: when inserting text right before the given range, the mark will be expanded to include the inserted text
|
||||||
|
/// - `none`: the mark will not be expanded to include the inserted text at the boundaries
|
||||||
|
/// - `both`: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
|
||||||
|
///
|
||||||
|
/// *You should make sure that a key is always associated with the same expand type.*
|
||||||
|
///
|
||||||
|
/// Note: this is not suitable for unmergeable annotations like comments.
|
||||||
|
pub fn mark(
|
||||||
|
&self,
|
||||||
|
from: u32,
|
||||||
|
to: u32,
|
||||||
|
key: &str,
|
||||||
|
value: Arc<dyn LoroValueLike>,
|
||||||
|
) -> LoroResult<()> {
|
||||||
|
self.text
|
||||||
|
.mark(from as usize..to as usize, key, value.as_loro_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unmark a range of text with a key and a value.
|
||||||
|
///
|
||||||
|
/// You can use it to remove highlights, bolds or links
|
||||||
|
///
|
||||||
|
/// You can specify the `expand` option to set the behavior when inserting text at the boundary of the range.
|
||||||
|
///
|
||||||
|
/// **Note: You should specify the same expand type as when you mark the text.**
|
||||||
|
///
|
||||||
|
/// - `after`(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
|
||||||
|
/// - `before`: when inserting text right before the given range, the mark will be expanded to include the inserted text
|
||||||
|
/// - `none`: the mark will not be expanded to include the inserted text at the boundaries
|
||||||
|
/// - `both`: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
|
||||||
|
///
|
||||||
|
/// *You should make sure that a key is always associated with the same expand type.*
|
||||||
|
///
|
||||||
|
/// Note: you cannot delete unmergeable annotations like comments by this method.
|
||||||
|
pub fn unmark(&self, from: u32, to: u32, key: &str) -> LoroResult<()> {
|
||||||
|
self.text.unmark(from as usize..to as usize, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text in [Delta](https://quilljs.com/docs/delta/) format.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// # use loro::{LoroDoc, ToJson, ExpandType};
|
||||||
|
/// # use serde_json::json;
|
||||||
|
///
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let text = doc.get_text("text");
|
||||||
|
/// text.insert(0, "Hello world!").unwrap();
|
||||||
|
/// text.mark(0..5, "bold", true).unwrap();
|
||||||
|
/// assert_eq!(
|
||||||
|
/// text.to_delta().to_json_value(),
|
||||||
|
/// json!([
|
||||||
|
/// { "insert": "Hello", "attributes": {"bold": true} },
|
||||||
|
/// { "insert": " world!" },
|
||||||
|
/// ])
|
||||||
|
/// );
|
||||||
|
/// text.unmark(3..5, "bold").unwrap();
|
||||||
|
/// assert_eq!(
|
||||||
|
/// text.to_delta().to_json_value(),
|
||||||
|
/// json!([
|
||||||
|
/// { "insert": "Hel", "attributes": {"bold": true} },
|
||||||
|
/// { "insert": "lo world!" },
|
||||||
|
/// ])
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn to_delta(&self) -> LoroValue {
|
||||||
|
self.text.to_delta().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the cursor at the given position.
|
||||||
|
///
|
||||||
|
/// Using "index" to denote cursor positions can be unstable, as positions may
|
||||||
|
/// shift with document edits. To reliably represent a position or range within
|
||||||
|
/// a document, it is more effective to leverage the unique ID of each item/character
|
||||||
|
/// in a List CRDT or Text CRDT.
|
||||||
|
///
|
||||||
|
/// Loro optimizes State metadata by not storing the IDs of deleted elements. This
|
||||||
|
/// approach complicates tracking cursors since they rely on these IDs. The solution
|
||||||
|
/// recalculates position by replaying relevant history to update stable positions
|
||||||
|
/// accurately. To minimize the performance impact of history replay, the system
|
||||||
|
/// updates cursor info to reference only the IDs of currently present elements,
|
||||||
|
/// thereby reducing the need for replay.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use loro::{LoroDoc, ToJson};
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let text = &doc.get_text("text");
|
||||||
|
/// text.insert(0, "01234").unwrap();
|
||||||
|
/// let pos = text.get_cursor(5, Default::default()).unwrap();
|
||||||
|
/// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5);
|
||||||
|
/// text.insert(0, "01234").unwrap();
|
||||||
|
/// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 10);
|
||||||
|
/// text.delete(0, 10).unwrap();
|
||||||
|
/// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 0);
|
||||||
|
/// text.insert(0, "01234").unwrap();
|
||||||
|
/// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5);
|
||||||
|
/// ```
|
||||||
|
pub fn get_cursor(&self, pos: u32, side: Side) -> Option<Arc<Cursor>> {
|
||||||
|
self.text
|
||||||
|
.get_cursor(pos as usize, side)
|
||||||
|
.map(|v| Arc::new(v.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LoroText {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.text.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroText {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
261
crates/loro-ffi/src/container/tree.rs
Normal file
261
crates/loro-ffi/src/container/tree.rs
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use loro::{LoroError, LoroResult, LoroTreeError, TreeID};
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue};
|
||||||
|
|
||||||
|
use super::LoroMap;
|
||||||
|
|
||||||
|
pub enum TreeParentId {
|
||||||
|
Node { id: TreeID },
|
||||||
|
Root,
|
||||||
|
Deleted,
|
||||||
|
Unexist,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroTree {
|
||||||
|
pub(crate) tree: loro::LoroTree,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroTree {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tree: loro::LoroTree::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the container is attached to a document
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.tree.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new tree node and return the [`TreeID`].
|
||||||
|
///
|
||||||
|
/// If the `parent` is `None`, the created node is the root of a tree.
|
||||||
|
/// Otherwise, the created node is a child of the parent tree node.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use loro::LoroDoc;
|
||||||
|
///
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let tree = doc.get_tree("tree");
|
||||||
|
/// // create a root
|
||||||
|
/// let root = tree.create(None).unwrap();
|
||||||
|
/// // create a new child
|
||||||
|
/// let child = tree.create(root).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn create(&self, parent: TreeParentId) -> LoroResult<TreeID> {
|
||||||
|
self.tree.create(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new tree node at the given index and return the [`TreeID`].
|
||||||
|
///
|
||||||
|
/// If the `parent` is `None`, the created node is the root of a tree.
|
||||||
|
/// If the `index` is greater than the number of children of the parent, error will be returned.
|
||||||
|
pub fn create_at(&self, parent: TreeParentId, index: u32) -> LoroResult<TreeID> {
|
||||||
|
self.tree.create_at(parent, index as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roots(&self) -> Vec<TreeID> {
|
||||||
|
self.tree.roots()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the `target` node to be a child of the `parent` node.
|
||||||
|
///
|
||||||
|
/// If the `parent` is `None`, the `target` node will be a root.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use loro::LoroDoc;
|
||||||
|
///
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let tree = doc.get_tree("tree");
|
||||||
|
/// let root = tree.create(None).unwrap();
|
||||||
|
/// let root2 = tree.create(None).unwrap();
|
||||||
|
/// // move `root2` to be a child of `root`.
|
||||||
|
/// tree.mov(root2, root).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn mov(&self, target: TreeID, parent: TreeParentId) -> LoroResult<()> {
|
||||||
|
self.tree.mov(target, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the `target` node to be a child of the `parent` node at the given index.
|
||||||
|
/// If the `parent` is `None`, the `target` node will be a root.
|
||||||
|
pub fn mov_to(&self, target: TreeID, parent: TreeParentId, to: u32) -> LoroResult<()> {
|
||||||
|
self.tree.mov_to(target, parent, to as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the `target` node to be a child after the `after` node with the same parent.
|
||||||
|
pub fn mov_after(&self, target: TreeID, after: TreeID) -> LoroResult<()> {
|
||||||
|
self.tree.mov_after(target, after)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the `target` node to be a child before the `before` node with the same parent.
|
||||||
|
pub fn mov_before(&self, target: TreeID, before: TreeID) -> LoroResult<()> {
|
||||||
|
self.tree.mov_before(target, before)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a tree node.
|
||||||
|
///
|
||||||
|
/// Note: If the deleted node has children, the children do not appear in the state
|
||||||
|
/// rather than actually being deleted.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use loro::LoroDoc;
|
||||||
|
///
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let tree = doc.get_tree("tree");
|
||||||
|
/// let root = tree.create(None).unwrap();
|
||||||
|
/// tree.delete(root).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn delete(&self, target: TreeID) -> LoroResult<()> {
|
||||||
|
self.tree.delete(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the associated metadata map handler of a tree node.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use loro::LoroDoc;
|
||||||
|
///
|
||||||
|
/// let doc = LoroDoc::new();
|
||||||
|
/// let tree = doc.get_tree("tree");
|
||||||
|
/// let root = tree.create(None).unwrap();
|
||||||
|
/// let root_meta = tree.get_meta(root).unwrap();
|
||||||
|
/// root_meta.insert("color", "red");
|
||||||
|
/// ```
|
||||||
|
pub fn get_meta(&self, target: TreeID) -> LoroResult<Arc<LoroMap>> {
|
||||||
|
self.tree
|
||||||
|
.get_meta(target)
|
||||||
|
.map(|h| Arc::new(LoroMap { map: h }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the parent of target node.
|
||||||
|
///
|
||||||
|
/// - If the target node does not exist, throws Error.
|
||||||
|
/// - If the target node is a root node, return `None`.
|
||||||
|
pub fn parent(&self, target: TreeID) -> LoroResult<TreeParentId> {
|
||||||
|
if let Some(p) = self.tree.parent(target) {
|
||||||
|
Ok(p.into())
|
||||||
|
} else {
|
||||||
|
Err(LoroError::TreeError(LoroTreeError::TreeNodeNotExist(
|
||||||
|
target,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return whether target node exists.
|
||||||
|
pub fn contains(&self, target: TreeID) -> bool {
|
||||||
|
self.tree.contains(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return whether target node is deleted.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - If the target node does not exist, return `LoroTreeError::TreeNodeNotExist`.
|
||||||
|
pub fn is_node_deleted(&self, target: TreeID) -> LoroResult<bool> {
|
||||||
|
self.tree.is_node_deleted(&target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return all nodes, including deleted nodes
|
||||||
|
pub fn nodes(&self) -> Vec<TreeID> {
|
||||||
|
self.tree.nodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return all children of the target node.
|
||||||
|
///
|
||||||
|
/// If the parent node does not exist, return `None`.
|
||||||
|
pub fn children(&self, parent: TreeParentId) -> Option<Vec<TreeID>> {
|
||||||
|
self.tree.children(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the number of children of the target node.
|
||||||
|
pub fn children_num(&self, parent: TreeParentId) -> Option<u32> {
|
||||||
|
self.tree.children_num(parent).map(|v| v as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return container id of the tree.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.tree.id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the fractional index of the target node with hex format.
|
||||||
|
pub fn fractional_index(&self, target: TreeID) -> Option<String> {
|
||||||
|
self.tree.fractional_index(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the flat array of the forest.
|
||||||
|
///
|
||||||
|
/// Note: the metadata will be not resolved. So if you don't only care about hierarchy
|
||||||
|
/// but also the metadata, you should use [TreeHandler::get_value_with_meta()].
|
||||||
|
pub fn get_value(&self) -> LoroValue {
|
||||||
|
self.tree.get_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the flat array of the forest, each node is with metadata.
|
||||||
|
pub fn get_value_with_meta(&self) -> LoroValue {
|
||||||
|
self.tree.get_value_with_meta().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the fractional index is enabled.
|
||||||
|
pub fn is_fractional_index_enabled(&self) -> bool {
|
||||||
|
self.tree.is_fractional_index_enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable fractional index for Tree Position.
|
||||||
|
///
|
||||||
|
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
|
||||||
|
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
|
||||||
|
///
|
||||||
|
/// Generally speaking, jitter will affect the growth rate of document size.
|
||||||
|
/// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size)
|
||||||
|
#[inline]
|
||||||
|
pub fn enable_fractional_index(&self, jitter: u8) {
|
||||||
|
self.tree.enable_fractional_index(jitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable the fractional index generation for Tree Position when
|
||||||
|
/// you don't need the Tree's siblings to be sorted. The fractional index will be always default.
|
||||||
|
#[inline]
|
||||||
|
pub fn disable_fractional_index(&self) {
|
||||||
|
self.tree.disable_fractional_index();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroTree {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::TreeParentId> for TreeParentId {
|
||||||
|
fn from(value: loro::TreeParentId) -> Self {
|
||||||
|
match value {
|
||||||
|
loro::TreeParentId::Node(id) => TreeParentId::Node { id },
|
||||||
|
loro::TreeParentId::Root => TreeParentId::Root,
|
||||||
|
loro::TreeParentId::Deleted => TreeParentId::Deleted,
|
||||||
|
loro::TreeParentId::Unexist => TreeParentId::Unexist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TreeParentId> for loro::TreeParentId {
|
||||||
|
fn from(value: TreeParentId) -> Self {
|
||||||
|
match value {
|
||||||
|
TreeParentId::Node { id } => loro::TreeParentId::Node(id),
|
||||||
|
TreeParentId::Root => loro::TreeParentId::Root,
|
||||||
|
TreeParentId::Deleted => loro::TreeParentId::Deleted,
|
||||||
|
TreeParentId::Unexist => loro::TreeParentId::Unexist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
crates/loro-ffi/src/container/unknown.rs
Normal file
12
crates/loro-ffi/src/container/unknown.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use crate::ContainerID;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LoroUnknown {
|
||||||
|
pub(crate) unknown: loro::LoroUnknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroUnknown {
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.unknown.id().into()
|
||||||
|
}
|
||||||
|
}
|
726
crates/loro-ffi/src/doc.rs
Normal file
726
crates/loro-ffi/src/doc.rs
Normal file
|
@ -0,0 +1,726 @@
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
cmp::Ordering,
|
||||||
|
ops::Deref,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use loro::{
|
||||||
|
cursor::CannotFindRelativePosition, DocAnalysis, FrontiersNotIncluded, IdSpan, JsonPathError,
|
||||||
|
JsonSchema, Lamport, LoroDoc as InnerLoroDoc, LoroError, LoroResult, PeerID, SubID, Timestamp,
|
||||||
|
ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::{DiffEvent, Subscriber},
|
||||||
|
AbsolutePosition, Configure, ContainerID, ContainerIdLike, Cursor, Frontiers, Index,
|
||||||
|
LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, LoroValue, StyleConfigMap,
|
||||||
|
ValueOrContainer, VersionVector,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct LoroDoc {
|
||||||
|
doc: InnerLoroDoc,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoroDoc {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
doc: InnerLoroDoc::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fork(&self) -> Arc<Self> {
|
||||||
|
let doc = self.doc.fork();
|
||||||
|
Arc::new(LoroDoc { doc })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the configurations of the document.
|
||||||
|
#[inline]
|
||||||
|
pub fn config(&self) -> Arc<Configure> {
|
||||||
|
Arc::new(self.doc.config().clone().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get `Change` at the given id.
|
||||||
|
///
|
||||||
|
/// `Change` is a grouped continuous operations that share the same id, timestamp, commit message.
|
||||||
|
///
|
||||||
|
/// - The id of the `Change` is the id of its first op.
|
||||||
|
/// - The second op's id is `{ peer: change.id.peer, counter: change.id.counter + 1 }`
|
||||||
|
///
|
||||||
|
/// The same applies on `Lamport`:
|
||||||
|
///
|
||||||
|
/// - The lamport of the `Change` is the lamport of its first op.
|
||||||
|
/// - The second op's lamport is `change.lamport + 1`
|
||||||
|
///
|
||||||
|
/// The length of the `Change` is how many operations it contains
|
||||||
|
#[inline]
|
||||||
|
pub fn get_change(&self, id: ID) -> Option<ChangeMeta> {
|
||||||
|
self.doc.get_change(id).map(|x| x.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes the metadata for an imported blob from the provided bytes.
|
||||||
|
#[inline]
|
||||||
|
pub fn decode_import_blob_meta(&self, bytes: &[u8]) -> LoroResult<ImportBlobMetadata> {
|
||||||
|
let s = InnerLoroDoc::decode_import_blob_meta(bytes)?;
|
||||||
|
Ok(s.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether to record the timestamp of each change. Default is `false`.
|
||||||
|
///
|
||||||
|
/// If enabled, the Unix timestamp will be recorded for each change automatically.
|
||||||
|
///
|
||||||
|
/// You can set each timestamp manually when committing a change.
|
||||||
|
///
|
||||||
|
/// NOTE: Timestamps are forced to be in ascending order.
|
||||||
|
/// If you commit a new change with a timestamp that is less than the existing one,
|
||||||
|
/// the largest existing timestamp will be used instead.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_record_timestamp(&self, record: bool) {
|
||||||
|
self.doc.set_record_timestamp(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the interval of mergeable changes, in milliseconds.
|
||||||
|
///
|
||||||
|
/// If two continuous local changes are within the interval, they will be merged into one change.
|
||||||
|
/// The default value is 1000 seconds.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_change_merge_interval(&self, interval: i64) {
|
||||||
|
self.doc.set_change_merge_interval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the rich text format configuration of the document.
|
||||||
|
///
|
||||||
|
/// You need to config it if you use rich text `mark` method.
|
||||||
|
/// Specifically, you need to config the `expand` property of each style.
|
||||||
|
///
|
||||||
|
/// Expand is used to specify the behavior of expanding when new text is inserted at the
|
||||||
|
/// beginning or end of the style.
|
||||||
|
#[inline]
|
||||||
|
pub fn config_text_style(&self, text_style: Arc<StyleConfigMap>) {
|
||||||
|
self.doc
|
||||||
|
.config_text_style(Arc::try_unwrap(text_style).unwrap().into_loro())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach the document state to the latest known version.
|
||||||
|
///
|
||||||
|
/// > The document becomes detached during a `checkout` operation.
|
||||||
|
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
|
||||||
|
/// > In a detached state, the document is not editable, and any `import` operations will be
|
||||||
|
/// > recorded in the `OpLog` without being applied to the `DocState`.
|
||||||
|
#[inline]
|
||||||
|
pub fn attach(&self) {
|
||||||
|
self.doc.attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkout the `DocState` to a specific version.
|
||||||
|
///
|
||||||
|
/// > The document becomes detached during a `checkout` operation.
|
||||||
|
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
|
||||||
|
/// > In a detached state, the document is not editable, and any `import` operations will be
|
||||||
|
/// > recorded in the `OpLog` without being applied to the `DocState`.
|
||||||
|
///
|
||||||
|
/// You should call `attach` to attach the `DocState` to the latest version of `OpLog`.
|
||||||
|
#[inline]
|
||||||
|
pub fn checkout(&self, frontiers: &Frontiers) -> LoroResult<()> {
|
||||||
|
self.doc.checkout(&frontiers.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkout the `DocState` to the latest version.
|
||||||
|
///
|
||||||
|
/// > The document becomes detached during a `checkout` operation.
|
||||||
|
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
|
||||||
|
/// > In a detached state, the document is not editable, and any `import` operations will be
|
||||||
|
/// > recorded in the `OpLog` without being applied to the `DocState`.
|
||||||
|
///
|
||||||
|
/// This has the same effect as `attach`.
|
||||||
|
#[inline]
|
||||||
|
pub fn checkout_to_latest(&self) {
|
||||||
|
self.doc.checkout_to_latest()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare the frontiers with the current OpLog's version.
|
||||||
|
///
|
||||||
|
/// If `other` contains any version that's not contained in the current OpLog, return [Ordering::Less].
|
||||||
|
#[inline]
|
||||||
|
pub fn cmp_with_frontiers(&self, other: &Frontiers) -> Ordering {
|
||||||
|
self.doc.cmp_with_frontiers(&other.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
pub fn cmp_frontiers(
|
||||||
|
&self,
|
||||||
|
a: &Frontiers,
|
||||||
|
b: &Frontiers,
|
||||||
|
) -> Result<Option<Ordering>, FrontiersNotIncluded> {
|
||||||
|
self.doc.cmp_frontiers(&a.into(), &b.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force the document enter the detached mode.
|
||||||
|
///
|
||||||
|
/// In this mode, when you importing new updates, the [loro_internal::DocState] will not be changed.
|
||||||
|
///
|
||||||
|
/// Learn more at https://loro.dev/docs/advanced/doc_state_and_oplog#attacheddetached-status
|
||||||
|
#[inline]
|
||||||
|
pub fn detach(&self) {
|
||||||
|
self.doc.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a batch of updates/snapshot.
|
||||||
|
///
|
||||||
|
/// The data can be in arbitrary order. The import result will be the same.
|
||||||
|
#[inline]
|
||||||
|
pub fn import_batch(&self, bytes: &[Vec<u8>]) -> LoroResult<()> {
|
||||||
|
self.doc.import_batch(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_movable_list(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroMovableList> {
|
||||||
|
Arc::new(LoroMovableList {
|
||||||
|
list: self.doc.get_movable_list(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::MovableList),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_list(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroList> {
|
||||||
|
Arc::new(LoroList {
|
||||||
|
list: self.doc.get_list(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::List),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_map(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroMap> {
|
||||||
|
Arc::new(LoroMap {
|
||||||
|
map: self.doc.get_map(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::Map),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroText> {
|
||||||
|
Arc::new(LoroText {
|
||||||
|
text: self.doc.get_text(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::Text),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tree(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroTree> {
|
||||||
|
Arc::new(LoroTree {
|
||||||
|
tree: self.doc.get_tree(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::Tree),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_counter(&self, id: Arc<dyn ContainerIdLike>) -> Arc<LoroCounter> {
|
||||||
|
Arc::new(LoroCounter {
|
||||||
|
counter: self.doc.get_counter(loro::ContainerID::from(
|
||||||
|
id.as_container_id(crate::ContainerType::Counter),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit the cumulative auto commit transaction.
|
||||||
|
///
|
||||||
|
/// There is a transaction behind every operation.
|
||||||
|
/// The events will be emitted after a transaction is committed. A transaction is committed when:
|
||||||
|
///
|
||||||
|
/// - `doc.commit()` is called.
|
||||||
|
/// - `doc.exportFrom(version)` is called.
|
||||||
|
/// - `doc.import(data)` is called.
|
||||||
|
/// - `doc.checkout(version)` is called.
|
||||||
|
#[inline]
|
||||||
|
pub fn commit(&self) {
|
||||||
|
self.doc.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_with(&self, options: CommitOptions) {
|
||||||
|
self.doc.commit_with(options.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set commit message for the current uncommitted changes
|
||||||
|
pub fn set_next_commit_message(&self, msg: &str) {
|
||||||
|
self.doc.set_next_commit_message(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the document is in detached mode, where the [loro_internal::DocState] is not
|
||||||
|
/// synchronized with the latest version of the [loro_internal::OpLog].
|
||||||
|
#[inline]
|
||||||
|
pub fn is_detached(&self) -> bool {
|
||||||
|
self.doc.is_detached()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import updates/snapshot exported by [`LoroDoc::export_snapshot`] or [`LoroDoc::export_from`].
|
||||||
|
#[inline]
|
||||||
|
pub fn import(&self, bytes: &[u8]) -> Result<(), LoroError> {
|
||||||
|
self.doc.import_with(bytes, "".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import updates/snapshot exported by [`LoroDoc::export_snapshot`] or [`LoroDoc::export_from`].
|
||||||
|
///
|
||||||
|
/// It marks the import with a custom `origin` string. It can be used to track the import source
|
||||||
|
/// in the generated events.
|
||||||
|
#[inline]
|
||||||
|
pub fn import_with(&self, bytes: &[u8], origin: &str) -> Result<(), LoroError> {
|
||||||
|
self.doc.import_with(bytes, origin.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_json_updates(&self, json: &str) -> Result<(), LoroError> {
|
||||||
|
self.doc.import_json_updates(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export the current state with json-string format of the document.
|
||||||
|
#[inline]
|
||||||
|
pub fn export_json_updates(&self, start_vv: &VersionVector, end_vv: &VersionVector) -> String {
|
||||||
|
let json = self
|
||||||
|
.doc
|
||||||
|
.export_json_updates(&start_vv.into(), &end_vv.into());
|
||||||
|
serde_json::to_string(&json).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export all the ops not included in the given `VersionVector`
|
||||||
|
#[inline]
|
||||||
|
pub fn export_from(&self, vv: &VersionVector) -> Vec<u8> {
|
||||||
|
self.doc.export_from(&vv.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export the current state and history of the document.
|
||||||
|
#[inline]
|
||||||
|
pub fn export_snapshot(&self) -> Vec<u8> {
|
||||||
|
self.doc.export_snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frontiers_to_vv(&self, frontiers: &Frontiers) -> Option<Arc<VersionVector>> {
|
||||||
|
self.doc
|
||||||
|
.frontiers_to_vv(&frontiers.into())
|
||||||
|
.map(|v| Arc::new(v.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vv_to_frontiers(&self, vv: &VersionVector) -> Arc<Frontiers> {
|
||||||
|
Arc::new(self.doc.vv_to_frontiers(&vv.into()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: with oplog
|
||||||
|
// TODO: with state
|
||||||
|
|
||||||
|
pub fn oplog_vv(&self) -> Arc<VersionVector> {
|
||||||
|
Arc::new(self.doc.oplog_vv().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_vv(&self) -> Arc<VersionVector> {
|
||||||
|
Arc::new(self.doc.state_vv().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `VersionVector` of trimmed history
|
||||||
|
///
|
||||||
|
/// The ops included by the trimmed history are not in the doc.
|
||||||
|
#[inline]
|
||||||
|
pub fn trimmed_vv(&self) -> Arc<VersionVector> {
|
||||||
|
Arc::new(loro::VersionVector::from_im_vv(&self.doc.trimmed_vv()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total number of operations in the `OpLog`
|
||||||
|
#[inline]
|
||||||
|
pub fn len_ops(&self) -> u64 {
|
||||||
|
self.doc.len_ops() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the total number of changes in the `OpLog`
|
||||||
|
#[inline]
|
||||||
|
pub fn len_changes(&self) -> u64 {
|
||||||
|
self.doc.len_changes() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shallow value of the document.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_value(&self) -> LoroValue {
|
||||||
|
self.doc.get_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_deep_value(&self) -> LoroValue {
|
||||||
|
self.doc.get_deep_value().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current state with container id of the doc
|
||||||
|
pub fn get_deep_value_with_id(&self) -> LoroValue {
|
||||||
|
self.doc.get_deep_value_with_id().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oplog_frontiers(&self) -> Arc<Frontiers> {
|
||||||
|
Arc::new(self.doc.oplog_frontiers().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_frontiers(&self) -> Arc<Frontiers> {
|
||||||
|
Arc::new(self.doc.state_frontiers().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the PeerID
|
||||||
|
#[inline]
|
||||||
|
pub fn peer_id(&self) -> PeerID {
|
||||||
|
self.doc.peer_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the PeerID
|
||||||
|
///
|
||||||
|
/// NOTE: You need ot make sure there is no chance two peer have the same PeerID.
|
||||||
|
/// If it happens, the document will be corrupted.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_peer_id(&self, peer: PeerID) -> LoroResult<()> {
|
||||||
|
self.doc.set_peer_id(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self, container_id: &ContainerID, subscriber: Arc<dyn Subscriber>) -> SubID {
|
||||||
|
self.doc.subscribe(
|
||||||
|
&(container_id.into()),
|
||||||
|
Arc::new(move |e| {
|
||||||
|
subscriber.on_diff(DiffEvent::from(e));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_root(&self, subscriber: Arc<dyn Subscriber>) -> SubID {
|
||||||
|
// self.doc.subscribe_root(callback)
|
||||||
|
self.doc.subscribe_root(Arc::new(move |e| {
|
||||||
|
subscriber.on_diff(DiffEvent::from(e));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a subscription by subscription id.
|
||||||
|
pub fn unsubscribe(&self, id: SubID) {
|
||||||
|
self.doc.unsubscribe(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe the local update of the document.
|
||||||
|
pub fn subscribe_local_update(
|
||||||
|
&self,
|
||||||
|
callback: Arc<dyn LocalUpdateCallback>,
|
||||||
|
) -> Arc<Subscription> {
|
||||||
|
let s = self.doc.subscribe_local_update(Box::new(move |update| {
|
||||||
|
// TODO: should it be cloned?
|
||||||
|
callback.on_local_update(update.to_vec());
|
||||||
|
}));
|
||||||
|
Arc::new(Subscription(Arc::new(Mutex::new(s))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate the size of the document states in memory.
|
||||||
|
#[inline]
|
||||||
|
pub fn log_estimate_size(&self) {
|
||||||
|
self.doc.log_estimate_size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the correctness of the document state by comparing it with the state
|
||||||
|
/// calculated by applying all the history.
|
||||||
|
#[inline]
|
||||||
|
pub fn check_state_correctness_slow(&self) {
|
||||||
|
self.doc.check_state_correctness_slow()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_path(&self, path: &[Index]) -> Option<Arc<dyn ValueOrContainer>> {
|
||||||
|
self.doc
|
||||||
|
.get_by_path(&path.iter().map(|v| v.clone().into()).collect::<Vec<_>>())
|
||||||
|
.map(|x| Arc::new(x) as Arc<dyn ValueOrContainer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_str_path(&self, path: &str) -> Option<Arc<dyn ValueOrContainer>> {
|
||||||
|
self.doc
|
||||||
|
.get_by_str_path(path)
|
||||||
|
.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cursor_pos(
|
||||||
|
&self,
|
||||||
|
cursor: &Cursor,
|
||||||
|
) -> Result<PosQueryResult, CannotFindRelativePosition> {
|
||||||
|
let loro::cursor::PosQueryResult { update, current } = self.doc.get_cursor_pos(cursor)?;
|
||||||
|
Ok(PosQueryResult {
|
||||||
|
current: AbsolutePosition {
|
||||||
|
pos: current.pos as u32,
|
||||||
|
side: current.side,
|
||||||
|
},
|
||||||
|
update: update.map(|x| Arc::new(x.into())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the history cache is built.
|
||||||
|
#[inline]
|
||||||
|
pub fn has_history_cache(&self) -> bool {
|
||||||
|
self.doc.has_history_cache()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free the history cache that is used for making checkout faster.
|
||||||
|
///
|
||||||
|
/// If you use checkout that switching to an old/concurrent version, the history cache will be built.
|
||||||
|
/// You can free it by calling this method.
|
||||||
|
#[inline]
|
||||||
|
pub fn free_history_cache(&self) {
|
||||||
|
self.doc.free_history_cache()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free the cached diff calculator that is used for checkout.
|
||||||
|
#[inline]
|
||||||
|
pub fn free_diff_calculator(&self) {
|
||||||
|
self.doc.free_diff_calculator()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encoded all ops and history cache to bytes and store them in the kv store.
|
||||||
|
///
|
||||||
|
/// The parsed ops will be dropped
|
||||||
|
#[inline]
|
||||||
|
pub fn compact_change_store(&self) {
|
||||||
|
self.doc.compact_change_store()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: https://github.com/mozilla/uniffi-rs/issues/1372
|
||||||
|
/// Export the document in the given mode.
|
||||||
|
// pub fn export(&self, mode: ExportMode) -> Vec<u8> {
|
||||||
|
// self.doc.export(mode.into())
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn export_updates_in_range(&self, spans: &[IdSpan]) -> Vec<u8> {
|
||||||
|
self.doc.export(loro::ExportMode::UpdatesInRange {
|
||||||
|
spans: Cow::Borrowed(spans),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_gc_snapshot(&self, frontiers: &Frontiers) -> Vec<u8> {
|
||||||
|
self.doc
|
||||||
|
.export(loro::ExportMode::GcSnapshot(Cow::Owned(frontiers.into())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_state_only(&self, frontiers: Option<Arc<Frontiers>>) -> Vec<u8> {
|
||||||
|
self.doc
|
||||||
|
.export(loro::ExportMode::StateOnly(frontiers.map(|x| {
|
||||||
|
let a = Arc::try_unwrap(x).unwrap();
|
||||||
|
Cow::Owned(loro::Frontiers::from(a))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: impl
|
||||||
|
/// Analyze the container info of the doc
|
||||||
|
///
|
||||||
|
/// This is used for development and debugging. It can be slow.
|
||||||
|
pub fn analyze(&self) -> DocAnalysis {
|
||||||
|
self.doc.analyze()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path from the root to the container
|
||||||
|
pub fn get_path_to_container(&self, id: &ContainerID) -> Option<Vec<ContainerPath>> {
|
||||||
|
self.doc.get_path_to_container(&id.into()).map(|x| {
|
||||||
|
x.into_iter()
|
||||||
|
.map(|(id, idx)| ContainerPath {
|
||||||
|
id: id.into(),
|
||||||
|
path: (&idx).into(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a JSONPath expression on the document and return matching values or handlers.
|
||||||
|
///
|
||||||
|
/// This method allows querying the document structure using JSONPath syntax.
|
||||||
|
/// It returns a vector of `ValueOrHandler` which can represent either primitive values
|
||||||
|
/// or container handlers, depending on what the JSONPath expression matches.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path` - A string slice containing the JSONPath expression to evaluate.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A `Result` containing either:
|
||||||
|
/// - `Ok(Vec<ValueOrHandler>)`: A vector of matching values or handlers.
|
||||||
|
/// - `Err(String)`: An error message if the JSONPath expression is invalid or evaluation fails.
|
||||||
|
#[inline]
|
||||||
|
pub fn jsonpath(&self, path: &str) -> Result<Vec<Arc<dyn ValueOrContainer>>, JsonPathError> {
|
||||||
|
self.doc.jsonpath(path).map(|vec| {
|
||||||
|
vec.into_iter()
|
||||||
|
.map(|v| Arc::new(v) as Arc<dyn ValueOrContainer>)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoroDoc {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for LoroDoc {
|
||||||
|
type Target = InnerLoroDoc;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangeMeta {
|
||||||
|
/// Lamport timestamp of the Change
|
||||||
|
pub lamport: Lamport,
|
||||||
|
/// The first Op id of the Change
|
||||||
|
pub id: ID,
|
||||||
|
/// [Unix time](https://en.wikipedia.org/wiki/Unix_time)
|
||||||
|
/// It is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970.
|
||||||
|
pub timestamp: Timestamp,
|
||||||
|
/// The commit message of the change
|
||||||
|
pub message: Option<String>,
|
||||||
|
/// The dependencies of the first op of the change
|
||||||
|
pub deps: Arc<Frontiers>,
|
||||||
|
/// The total op num inside this change
|
||||||
|
pub len: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::ChangeMeta> for ChangeMeta {
|
||||||
|
fn from(value: loro::ChangeMeta) -> Self {
|
||||||
|
Self {
|
||||||
|
lamport: value.lamport,
|
||||||
|
id: value.id,
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
message: value.message.map(|x| (*x).to_string()),
|
||||||
|
deps: Arc::new(value.deps.into()),
|
||||||
|
len: value.len as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImportBlobMetadata {
|
||||||
|
/// The partial start version vector.
|
||||||
|
///
|
||||||
|
/// Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
|
||||||
|
/// However, it does not constitute a complete version vector, as it only contains counters
|
||||||
|
/// from peers included within the import blob.
|
||||||
|
pub partial_start_vv: Arc<VersionVector>,
|
||||||
|
/// The partial end version vector.
|
||||||
|
///
|
||||||
|
/// Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
|
||||||
|
/// However, it does not constitute a complete version vector, as it only contains counters
|
||||||
|
/// from peers included within the import blob.
|
||||||
|
pub partial_end_vv: Arc<VersionVector>,
|
||||||
|
pub start_timestamp: i64,
|
||||||
|
pub start_frontiers: Arc<Frontiers>,
|
||||||
|
pub end_timestamp: i64,
|
||||||
|
pub change_num: u32,
|
||||||
|
pub is_snapshot: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::ImportBlobMetadata> for ImportBlobMetadata {
|
||||||
|
fn from(value: loro::ImportBlobMetadata) -> Self {
|
||||||
|
Self {
|
||||||
|
partial_start_vv: Arc::new(value.partial_start_vv.into()),
|
||||||
|
partial_end_vv: Arc::new(value.partial_end_vv.into()),
|
||||||
|
start_timestamp: value.start_timestamp,
|
||||||
|
start_frontiers: Arc::new(value.start_frontiers.into()),
|
||||||
|
end_timestamp: value.end_timestamp,
|
||||||
|
change_num: value.change_num,
|
||||||
|
is_snapshot: value.is_snapshot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CommitOptions {
|
||||||
|
pub origin: Option<String>,
|
||||||
|
pub immediate_renew: bool,
|
||||||
|
pub timestamp: Option<Timestamp>,
|
||||||
|
pub commit_msg: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CommitOptions> for loro::CommitOptions {
|
||||||
|
fn from(value: CommitOptions) -> Self {
|
||||||
|
loro::CommitOptions {
|
||||||
|
origin: value.origin.map(|x| x.into()),
|
||||||
|
immediate_renew: value.immediate_renew,
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
commit_msg: value.commit_msg.map(|x| x.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait JsonSchemaLike {
|
||||||
|
fn into_json_schema(&self) -> LoroResult<JsonSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TryInto<JsonSchema> + Clone> JsonSchemaLike for T {
|
||||||
|
fn into_json_schema(&self) -> LoroResult<JsonSchema> {
|
||||||
|
self.clone()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| LoroError::InvalidJsonSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LocalUpdateCallback: Sync + Send {
|
||||||
|
fn on_local_update(&self, update: Vec<u8>);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Unsubscriber: Sync + Send {
|
||||||
|
fn on_unsubscribe(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to a subscription created by GPUI. When dropped, the subscription
|
||||||
|
/// is cancelled and the callback will no longer be invoked.
|
||||||
|
pub struct Subscription(Arc<Mutex<loro::Subscription>>);
|
||||||
|
|
||||||
|
impl Subscription {
|
||||||
|
pub fn new(unsubscribe: Arc<dyn Unsubscriber>) -> Self {
|
||||||
|
Self(Arc::new(Mutex::new(loro::Subscription::new(move || {
|
||||||
|
unsubscribe.on_unsubscribe()
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detach(self: Arc<Self>) {
|
||||||
|
let s = Arc::try_unwrap(self)
|
||||||
|
.map_err(|_| "Arc::try_unwrap Subscription failed")
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
let s = Arc::try_unwrap(s)
|
||||||
|
.map_err(|_| "Arc::try_unwrap Subscription failed")
|
||||||
|
.unwrap();
|
||||||
|
s.into_inner().unwrap().detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for Subscription {}
|
||||||
|
unsafe impl Sync for Subscription {}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Subscription {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("Subscription")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PosQueryResult {
|
||||||
|
pub update: Option<Arc<Cursor>>,
|
||||||
|
pub current: AbsolutePosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ExportMode {
|
||||||
|
Snapshot,
|
||||||
|
Updates { from: VersionVector },
|
||||||
|
UpdatesInRange { spans: Vec<IdSpan> },
|
||||||
|
GcSnapshot { frontiers: Frontiers },
|
||||||
|
StateOnly { frontiers: Option<Frontiers> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExportMode> for loro::ExportMode<'_> {
|
||||||
|
fn from(value: ExportMode) -> Self {
|
||||||
|
match value {
|
||||||
|
ExportMode::Snapshot => loro::ExportMode::Snapshot,
|
||||||
|
ExportMode::Updates { from } => loro::ExportMode::Updates {
|
||||||
|
from: Cow::Owned(from.into()),
|
||||||
|
},
|
||||||
|
ExportMode::UpdatesInRange { spans } => loro::ExportMode::UpdatesInRange {
|
||||||
|
spans: Cow::Owned(spans),
|
||||||
|
},
|
||||||
|
ExportMode::GcSnapshot { frontiers } => {
|
||||||
|
loro::ExportMode::GcSnapshot(Cow::Owned(frontiers.into()))
|
||||||
|
}
|
||||||
|
ExportMode::StateOnly { frontiers } => {
|
||||||
|
loro::ExportMode::StateOnly(frontiers.map(|x| Cow::Owned(x.into())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContainerPath {
|
||||||
|
pub id: ContainerID,
|
||||||
|
pub path: Index,
|
||||||
|
}
|
309
crates/loro-ffi/src/event.rs
Normal file
309
crates/loro-ffi/src/event.rs
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use loro::{EventTriggerKind, TreeID};
|
||||||
|
|
||||||
|
use crate::{ContainerID, LoroValue, TreeParentId, ValueOrContainer};
|
||||||
|
|
||||||
|
pub trait Subscriber: Sync + Send {
|
||||||
|
fn on_diff(&self, diff: DiffEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiffEvent {
|
||||||
|
/// How the event is triggered.
|
||||||
|
pub triggered_by: EventTriggerKind,
|
||||||
|
/// The origin of the event.
|
||||||
|
pub origin: String,
|
||||||
|
/// The current receiver of the event.
|
||||||
|
pub current_target: Option<ContainerID>,
|
||||||
|
/// The diffs of the event.
|
||||||
|
pub events: Vec<ContainerDiff>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<loro::event::DiffEvent<'a>> for DiffEvent {
|
||||||
|
fn from(diff_event: loro::event::DiffEvent) -> Self {
|
||||||
|
Self {
|
||||||
|
triggered_by: diff_event.triggered_by,
|
||||||
|
origin: diff_event.origin.to_string(),
|
||||||
|
current_target: diff_event.current_target.map(|v| v.into()),
|
||||||
|
events: diff_event.events.iter().map(ContainerDiff::from).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PathItem {
|
||||||
|
pub container: ContainerID,
|
||||||
|
pub index: Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A diff of a container.
|
||||||
|
pub struct ContainerDiff {
|
||||||
|
/// The target container id of the diff.
|
||||||
|
pub target: ContainerID,
|
||||||
|
/// The path of the diff.
|
||||||
|
pub path: Vec<PathItem>,
|
||||||
|
/// Whether the diff is from unknown container.
|
||||||
|
pub is_unknown: bool,
|
||||||
|
/// The diff
|
||||||
|
pub diff: Diff,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Index {
|
||||||
|
Key { key: String },
|
||||||
|
Seq { index: u32 },
|
||||||
|
Node { target: TreeID },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Diff {
|
||||||
|
/// A list diff.
|
||||||
|
List { diff: Vec<ListDiffItem> },
|
||||||
|
/// A text diff.
|
||||||
|
Text { diff: Vec<TextDelta> },
|
||||||
|
/// A map diff.
|
||||||
|
Map { diff: MapDelta },
|
||||||
|
/// A tree diff.
|
||||||
|
Tree { diff: TreeDiff },
|
||||||
|
/// A counter diff.
|
||||||
|
Counter { diff: f64 },
|
||||||
|
/// An unknown diff.
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TextDelta {
|
||||||
|
Retain {
|
||||||
|
retain: u32,
|
||||||
|
attributes: Option<HashMap<String, LoroValue>>,
|
||||||
|
},
|
||||||
|
Insert {
|
||||||
|
insert: String,
|
||||||
|
attributes: Option<HashMap<String, LoroValue>>,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
delete: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ListDiffItem {
|
||||||
|
/// Insert a new element into the list.
|
||||||
|
Insert {
|
||||||
|
/// The new elements to insert.
|
||||||
|
insert: Vec<Arc<dyn ValueOrContainer>>,
|
||||||
|
/// Whether the new elements are created by moving
|
||||||
|
is_move: bool,
|
||||||
|
},
|
||||||
|
/// Delete n elements from the list at the current index.
|
||||||
|
Delete {
|
||||||
|
/// The number of elements to delete.
|
||||||
|
delete: u32,
|
||||||
|
},
|
||||||
|
/// Retain n elements in the list.
|
||||||
|
///
|
||||||
|
/// This is used to keep the current index unchanged.
|
||||||
|
Retain {
|
||||||
|
/// The number of elements to retain.
|
||||||
|
retain: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MapDelta {
|
||||||
|
/// All the updated keys and their new values.
|
||||||
|
pub updated: HashMap<String, Option<Arc<dyn ValueOrContainer>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreeDiff {
|
||||||
|
pub diff: Vec<TreeDiffItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreeDiffItem {
|
||||||
|
pub target: TreeID,
|
||||||
|
pub action: TreeExternalDiff,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TreeExternalDiff {
|
||||||
|
Create {
|
||||||
|
parent: TreeParentId,
|
||||||
|
index: u32,
|
||||||
|
fractional_index: String,
|
||||||
|
},
|
||||||
|
Move {
|
||||||
|
parent: TreeParentId,
|
||||||
|
index: u32,
|
||||||
|
fractional_index: String,
|
||||||
|
old_parent: TreeParentId,
|
||||||
|
old_index: u32,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
old_parent: TreeParentId,
|
||||||
|
old_index: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> From<&'b loro::event::ContainerDiff<'a>> for ContainerDiff {
|
||||||
|
fn from(value: &loro::event::ContainerDiff<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
target: value.target.into(),
|
||||||
|
path: value
|
||||||
|
.path
|
||||||
|
.iter()
|
||||||
|
.map(|(id, index)| PathItem {
|
||||||
|
container: id.into(),
|
||||||
|
index: index.into(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
is_unknown: value.is_unknown,
|
||||||
|
diff: (&value.diff).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a loro::Index> for Index {
|
||||||
|
fn from(value: &loro::Index) -> Self {
|
||||||
|
match value {
|
||||||
|
loro::Index::Key(key) => Index::Key {
|
||||||
|
key: key.to_string(),
|
||||||
|
},
|
||||||
|
loro::Index::Seq(index) => Index::Seq {
|
||||||
|
index: *index as u32,
|
||||||
|
},
|
||||||
|
loro::Index::Node(target) => Index::Node { target: *target },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Index> for loro::Index {
|
||||||
|
fn from(value: Index) -> loro::Index {
|
||||||
|
match value {
|
||||||
|
Index::Key { key } => loro::Index::Key(key.into()),
|
||||||
|
Index::Seq { index } => loro::Index::Seq(index as usize),
|
||||||
|
Index::Node { target } => loro::Index::Node(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> From<&'b loro::event::Diff<'a>> for Diff {
|
||||||
|
fn from(value: &loro::event::Diff) -> Self {
|
||||||
|
match value {
|
||||||
|
loro::event::Diff::List(l) => {
|
||||||
|
let mut ans = Vec::with_capacity(l.len());
|
||||||
|
for item in l.iter() {
|
||||||
|
match item {
|
||||||
|
loro::event::ListDiffItem::Insert { insert, is_move } => {
|
||||||
|
let mut new_insert = Vec::with_capacity(insert.len());
|
||||||
|
for v in insert.iter() {
|
||||||
|
new_insert.push(Arc::new(v.clone()) as Arc<dyn ValueOrContainer>);
|
||||||
|
}
|
||||||
|
ans.push(ListDiffItem::Insert {
|
||||||
|
insert: new_insert,
|
||||||
|
is_move: *is_move,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loro::event::ListDiffItem::Delete { delete } => {
|
||||||
|
ans.push(ListDiffItem::Delete {
|
||||||
|
delete: *delete as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loro::event::ListDiffItem::Retain { retain } => {
|
||||||
|
ans.push(ListDiffItem::Retain {
|
||||||
|
retain: *retain as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Diff::List { diff: ans }
|
||||||
|
}
|
||||||
|
loro::event::Diff::Text(t) => {
|
||||||
|
let mut ans = Vec::new();
|
||||||
|
for item in t.iter() {
|
||||||
|
match item {
|
||||||
|
loro::TextDelta::Retain { retain, attributes } => {
|
||||||
|
ans.push(TextDelta::Retain {
|
||||||
|
retain: *retain as u32,
|
||||||
|
attributes: attributes.as_ref().map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.clone().into()))
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loro::TextDelta::Insert { insert, attributes } => {
|
||||||
|
ans.push(TextDelta::Insert {
|
||||||
|
insert: insert.to_string(),
|
||||||
|
attributes: attributes.as_ref().map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.clone().into()))
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loro::TextDelta::Delete { delete } => {
|
||||||
|
ans.push(TextDelta::Delete {
|
||||||
|
delete: *delete as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Diff::Text { diff: ans }
|
||||||
|
}
|
||||||
|
loro::event::Diff::Map(m) => {
|
||||||
|
let mut updated = HashMap::new();
|
||||||
|
for (key, value) in m.updated.iter() {
|
||||||
|
updated.insert(
|
||||||
|
key.to_string(),
|
||||||
|
value
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| Arc::new(v.clone()) as Arc<dyn ValueOrContainer>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Diff::Map {
|
||||||
|
diff: MapDelta { updated },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loro::event::Diff::Tree(t) => {
|
||||||
|
let mut diff = Vec::new();
|
||||||
|
for item in t.iter() {
|
||||||
|
diff.push(TreeDiffItem {
|
||||||
|
target: item.target,
|
||||||
|
action: match &item.action {
|
||||||
|
loro::TreeExternalDiff::Create {
|
||||||
|
parent,
|
||||||
|
index,
|
||||||
|
position,
|
||||||
|
} => TreeExternalDiff::Create {
|
||||||
|
parent: (*parent).into(),
|
||||||
|
index: *index as u32,
|
||||||
|
fractional_index: position.to_string(),
|
||||||
|
},
|
||||||
|
loro::TreeExternalDiff::Move {
|
||||||
|
parent,
|
||||||
|
index,
|
||||||
|
position,
|
||||||
|
old_parent,
|
||||||
|
old_index,
|
||||||
|
} => TreeExternalDiff::Move {
|
||||||
|
parent: (*parent).into(),
|
||||||
|
index: *index as u32,
|
||||||
|
fractional_index: position.to_string(),
|
||||||
|
old_parent: (*old_parent).into(),
|
||||||
|
old_index: *old_index as u32,
|
||||||
|
},
|
||||||
|
loro::TreeExternalDiff::Delete {
|
||||||
|
old_parent,
|
||||||
|
old_index,
|
||||||
|
} => TreeExternalDiff::Delete {
|
||||||
|
old_parent: (*old_parent).into(),
|
||||||
|
old_index: *old_index as u32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Diff::Tree {
|
||||||
|
diff: TreeDiff { diff },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loro::event::Diff::Counter(c) => Diff::Counter { diff: *c },
|
||||||
|
loro::event::Diff::Unknown => Diff::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,59 +1,124 @@
|
||||||
#![allow(clippy::missing_safety_doc)]
|
mod value;
|
||||||
|
|
||||||
use std::ffi::{c_char, CStr, CString};
|
use loro::Container;
|
||||||
|
pub use loro::{
|
||||||
|
cursor::Side, undo::UndoOrRedo, CannotFindRelativePosition, Counter, CounterSpan,
|
||||||
|
EventTriggerKind, ExpandType, FractionalIndex, IdLp, IdSpan, JsonChange, JsonFutureOp,
|
||||||
|
JsonFutureOpWrapper, JsonListOp, JsonMapOp, JsonMovableListOp, JsonOp, JsonOpContent,
|
||||||
|
JsonPathError, JsonSchema, JsonTextOp, JsonTreeOp, Lamport, LoroError, PeerID, StyleConfig,
|
||||||
|
SubID, TreeID, ID,
|
||||||
|
};
|
||||||
|
pub use std::cmp::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
pub use value::{ContainerID, ContainerType, LoroValue, LoroValueLike};
|
||||||
|
mod doc;
|
||||||
|
pub use doc::{
|
||||||
|
ChangeMeta, CommitOptions, ContainerPath, ExportMode, ImportBlobMetadata, JsonSchemaLike,
|
||||||
|
LocalUpdateCallback, LoroDoc, PosQueryResult, Subscription, Unsubscriber,
|
||||||
|
};
|
||||||
|
mod container;
|
||||||
|
pub use container::{
|
||||||
|
ContainerIdLike, Cursor, LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree,
|
||||||
|
LoroUnknown, TreeParentId,
|
||||||
|
};
|
||||||
|
mod event;
|
||||||
|
pub use event::{
|
||||||
|
ContainerDiff, Diff, DiffEvent, Index, ListDiffItem, MapDelta, PathItem, Subscriber, TextDelta,
|
||||||
|
TreeDiff, TreeDiffItem, TreeExternalDiff,
|
||||||
|
};
|
||||||
|
mod undo;
|
||||||
|
pub use undo::{AbsolutePosition, CursorWithPos, OnPop, OnPush, UndoItemMeta, UndoManager};
|
||||||
|
mod config;
|
||||||
|
pub use config::{Configure, StyleConfigMap};
|
||||||
|
mod version;
|
||||||
|
pub use version::{Frontiers, VersionVector, VersionVectorDiff};
|
||||||
|
mod awareness;
|
||||||
|
pub use awareness::{Awareness, AwarenessPeerUpdate, PeerInfo};
|
||||||
|
|
||||||
use loro_internal::{LoroDoc, TextHandler};
|
// https://github.com/mozilla/uniffi-rs/issues/1372
|
||||||
|
pub trait ValueOrContainer: Send + Sync {
|
||||||
/// create Loro with a random unique client id
|
fn is_value(&self) -> bool;
|
||||||
#[no_mangle]
|
fn is_container(&self) -> bool;
|
||||||
pub extern "C" fn loro_new() -> *mut LoroDoc {
|
fn as_value(&self) -> Option<LoroValue>;
|
||||||
Box::into_raw(Box::default())
|
fn as_container(&self) -> Option<ContainerID>;
|
||||||
|
fn as_loro_list(&self) -> Option<Arc<LoroList>>;
|
||||||
|
fn as_loro_text(&self) -> Option<Arc<LoroText>>;
|
||||||
|
fn as_loro_map(&self) -> Option<Arc<LoroMap>>;
|
||||||
|
fn as_loro_movable_list(&self) -> Option<Arc<LoroMovableList>>;
|
||||||
|
fn as_loro_tree(&self) -> Option<Arc<LoroTree>>;
|
||||||
|
fn as_loro_counter(&self) -> Option<Arc<LoroCounter>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Release all memory of Loro
|
impl ValueOrContainer for loro::ValueOrContainer {
|
||||||
#[no_mangle]
|
fn is_value(&self) -> bool {
|
||||||
pub unsafe extern "C" fn loro_free(loro: *mut LoroDoc) {
|
loro::ValueOrContainer::is_value(self)
|
||||||
if !loro.is_null() {
|
}
|
||||||
drop(Box::from_raw(loro));
|
|
||||||
|
fn is_container(&self) -> bool {
|
||||||
|
loro::ValueOrContainer::is_container(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_value(&self) -> Option<LoroValue> {
|
||||||
|
loro::ValueOrContainer::as_value(self)
|
||||||
|
.cloned()
|
||||||
|
.map(LoroValue::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_container(&self) -> Option<ContainerID> {
|
||||||
|
loro::ValueOrContainer::as_container(self).map(|c| c.id().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_list(&self) -> Option<Arc<LoroList>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::List(list)) => {
|
||||||
|
Some(Arc::new(LoroList { list: list.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_text(&self) -> Option<Arc<LoroText>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::Text(c)) => {
|
||||||
|
Some(Arc::new(LoroText { text: c.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_map(&self) -> Option<Arc<LoroMap>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::Map(c)) => {
|
||||||
|
Some(Arc::new(LoroMap { map: c.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_movable_list(&self) -> Option<Arc<LoroMovableList>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::MovableList(c)) => {
|
||||||
|
Some(Arc::new(LoroMovableList { list: c.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_tree(&self) -> Option<Arc<LoroTree>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::Tree(c)) => {
|
||||||
|
Some(Arc::new(LoroTree { tree: c.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_loro_counter(&self) -> Option<Arc<LoroCounter>> {
|
||||||
|
match self {
|
||||||
|
loro::ValueOrContainer::Container(Container::Counter(c)) => {
|
||||||
|
Some(Arc::new(LoroCounter { counter: c.clone() }))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn loro_get_text(loro: *mut LoroDoc, id: *const c_char) -> *mut TextHandler {
|
|
||||||
assert!(!loro.is_null());
|
|
||||||
assert!(!id.is_null());
|
|
||||||
let id = CStr::from_ptr(id).to_str().unwrap();
|
|
||||||
let text = loro.as_mut().unwrap().get_text(id);
|
|
||||||
Box::into_raw(Box::new(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn text_free(text: *mut TextHandler) {
|
|
||||||
if !text.is_null() {
|
|
||||||
drop(Box::from_raw(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn text_insert(
|
|
||||||
text: *mut TextHandler,
|
|
||||||
ctx: *const LoroDoc,
|
|
||||||
pos: usize,
|
|
||||||
value: *const c_char,
|
|
||||||
) {
|
|
||||||
assert!(!text.is_null());
|
|
||||||
assert!(!ctx.is_null());
|
|
||||||
let text = text.as_mut().unwrap();
|
|
||||||
let ctx = ctx.as_ref().unwrap();
|
|
||||||
let value = CStr::from_ptr(value).to_str().unwrap();
|
|
||||||
let mut txn = ctx.txn().unwrap();
|
|
||||||
text.insert_with_txn(&mut txn, pos, value).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn text_value(text: *mut TextHandler) -> *mut c_char {
|
|
||||||
assert!(!text.is_null());
|
|
||||||
let text = text.as_mut().unwrap();
|
|
||||||
let value = text.get_value().as_string().unwrap().to_string();
|
|
||||||
CString::new(value).unwrap().into_raw()
|
|
||||||
}
|
|
||||||
|
|
167
crates/loro-ffi/src/undo.rs
Normal file
167
crates/loro-ffi/src/undo.rs
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use loro::LoroResult;
|
||||||
|
|
||||||
|
use crate::{Cursor, LoroDoc, LoroValue, Side};
|
||||||
|
|
||||||
|
pub struct UndoManager(RwLock<loro::UndoManager>);
|
||||||
|
|
||||||
|
impl UndoManager {
|
||||||
|
/// Create a new UndoManager.
|
||||||
|
pub fn new(doc: &LoroDoc) -> Self {
|
||||||
|
Self(RwLock::new(loro::UndoManager::new(doc)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undo the last change made by the peer.
|
||||||
|
pub fn undo(&self, doc: &LoroDoc) -> LoroResult<bool> {
|
||||||
|
self.0.write().unwrap().undo(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redo the last change made by the peer.
|
||||||
|
pub fn redo(&self, doc: &LoroDoc) -> LoroResult<bool> {
|
||||||
|
self.0.write().unwrap().redo(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a new checkpoint.
|
||||||
|
pub fn record_new_checkpoint(&self, doc: &LoroDoc) -> LoroResult<()> {
|
||||||
|
self.0.write().unwrap().record_new_checkpoint(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the undo manager can undo.
|
||||||
|
pub fn can_undo(&self) -> bool {
|
||||||
|
self.0.read().unwrap().can_undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the undo manager can redo.
|
||||||
|
pub fn can_redo(&self) -> bool {
|
||||||
|
self.0.read().unwrap().can_redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If a local event's origin matches the given prefix, it will not be recorded in the
|
||||||
|
/// undo stack.
|
||||||
|
pub fn add_exclude_origin_prefix(&self, prefix: &str) {
|
||||||
|
self.0.write().unwrap().add_exclude_origin_prefix(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the maximum number of undo steps. The default value is 100.
|
||||||
|
pub fn set_max_undo_steps(&self, size: u32) {
|
||||||
|
self.0.write().unwrap().set_max_undo_steps(size as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the merge interval in ms. The default value is 0, which means no merge.
|
||||||
|
pub fn set_merge_interval(&self, interval: i64) {
|
||||||
|
self.0.write().unwrap().set_merge_interval(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the listener for push events.
|
||||||
|
/// The listener will be called when a new undo/redo item is pushed into the stack.
|
||||||
|
pub fn set_on_push(&self, on_push: Option<Arc<dyn OnPush>>) {
|
||||||
|
let on_push = on_push.map(|x| {
|
||||||
|
Box::new(move |u, c| loro::undo::UndoItemMeta::from(x.on_push(u, c)))
|
||||||
|
as loro::undo::OnPush
|
||||||
|
});
|
||||||
|
self.0.write().unwrap().set_on_push(on_push)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the listener for pop events.
|
||||||
|
/// The listener will be called when an undo/redo item is popped from the stack.
|
||||||
|
pub fn set_on_pop(&self, on_pop: Option<Arc<dyn OnPop>>) {
|
||||||
|
let on_pop = on_pop.map(|x| {
|
||||||
|
Box::new(move |u, c, m| (x.on_pop(u, c, UndoItemMeta::from(m)))) as loro::undo::OnPop
|
||||||
|
});
|
||||||
|
|
||||||
|
self.0.write().unwrap().set_on_pop(on_pop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OnPush: Send + Sync {
|
||||||
|
fn on_push(
|
||||||
|
&self,
|
||||||
|
undo_or_redo: loro::undo::UndoOrRedo,
|
||||||
|
couter_span: loro::CounterSpan,
|
||||||
|
) -> UndoItemMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OnPop: Send + Sync {
|
||||||
|
fn on_pop(
|
||||||
|
&self,
|
||||||
|
undo_or_redo: loro::undo::UndoOrRedo,
|
||||||
|
couter_span: loro::CounterSpan,
|
||||||
|
undo_meta: UndoItemMeta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UndoItemMeta {
|
||||||
|
pub value: LoroValue,
|
||||||
|
pub cursors: Vec<CursorWithPos>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::undo::UndoItemMeta> for UndoItemMeta {
|
||||||
|
fn from(meta: loro::undo::UndoItemMeta) -> Self {
|
||||||
|
Self {
|
||||||
|
value: meta.value.into(),
|
||||||
|
cursors: meta
|
||||||
|
.cursors
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| CursorWithPos {
|
||||||
|
cursor: Arc::new(c.cursor.into()),
|
||||||
|
pos: AbsolutePosition {
|
||||||
|
pos: c.pos.pos as u32,
|
||||||
|
side: c.pos.side,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a UndoItemMeta> for loro::undo::UndoItemMeta {
|
||||||
|
fn from(meta: &UndoItemMeta) -> Self {
|
||||||
|
loro::undo::UndoItemMeta {
|
||||||
|
value: (&meta.value).into(),
|
||||||
|
cursors: meta
|
||||||
|
.cursors
|
||||||
|
.iter()
|
||||||
|
.map(|c| loro::undo::CursorWithPos {
|
||||||
|
cursor: c.cursor.as_ref().clone().into(),
|
||||||
|
pos: loro::cursor::AbsolutePosition {
|
||||||
|
pos: c.pos.pos as usize,
|
||||||
|
side: c.pos.side,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UndoItemMeta> for loro::undo::UndoItemMeta {
|
||||||
|
fn from(meta: UndoItemMeta) -> Self {
|
||||||
|
loro::undo::UndoItemMeta {
|
||||||
|
value: (meta.value).into(),
|
||||||
|
cursors: meta
|
||||||
|
.cursors
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| loro::undo::CursorWithPos {
|
||||||
|
cursor: c.cursor.as_ref().clone().into(),
|
||||||
|
pos: loro::cursor::AbsolutePosition {
|
||||||
|
pos: c.pos.pos as usize,
|
||||||
|
side: c.pos.side,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CursorWithPos {
|
||||||
|
pub cursor: Arc<Cursor>,
|
||||||
|
pub pos: AbsolutePosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct AbsolutePosition {
|
||||||
|
pub pos: u32,
|
||||||
|
pub side: Side,
|
||||||
|
}
|
234
crates/loro-ffi/src/value.rs
Normal file
234
crates/loro-ffi/src/value.rs
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use loro::{Counter, PeerID};
|
||||||
|
|
||||||
|
pub trait LoroValueLike: Sync + Send {
|
||||||
|
fn as_loro_value(&self) -> crate::LoroValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum ContainerType {
|
||||||
|
Text,
|
||||||
|
Map,
|
||||||
|
List,
|
||||||
|
MovableList,
|
||||||
|
Tree,
|
||||||
|
Counter,
|
||||||
|
Unknown { kind: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ContainerID {
|
||||||
|
Root {
|
||||||
|
name: String,
|
||||||
|
container_type: ContainerType,
|
||||||
|
},
|
||||||
|
Normal {
|
||||||
|
peer: PeerID,
|
||||||
|
counter: Counter,
|
||||||
|
container_type: ContainerType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LoroValue {
|
||||||
|
Null,
|
||||||
|
Bool { value: bool },
|
||||||
|
Double { value: f64 },
|
||||||
|
I64 { value: i64 },
|
||||||
|
Binary { value: Vec<u8> },
|
||||||
|
String { value: String },
|
||||||
|
List { value: Vec<LoroValue> },
|
||||||
|
Map { value: HashMap<String, LoroValue> },
|
||||||
|
Container { value: ContainerID },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LoroValue> for loro::LoroValue {
|
||||||
|
fn from(value: LoroValue) -> loro::LoroValue {
|
||||||
|
match value {
|
||||||
|
LoroValue::Null => loro::LoroValue::Null,
|
||||||
|
LoroValue::Bool { value } => loro::LoroValue::Bool(value),
|
||||||
|
LoroValue::Double { value } => loro::LoroValue::Double(value),
|
||||||
|
LoroValue::I64 { value } => loro::LoroValue::I64(value),
|
||||||
|
LoroValue::Binary { value } => loro::LoroValue::Binary(Arc::new(value)),
|
||||||
|
LoroValue::String { value } => loro::LoroValue::String(Arc::new(value)),
|
||||||
|
LoroValue::List { value } => {
|
||||||
|
loro::LoroValue::List(Arc::new(value.into_iter().map(Into::into).collect()))
|
||||||
|
}
|
||||||
|
LoroValue::Map { value } => loro::LoroValue::Map(Arc::new(
|
||||||
|
value.into_iter().map(|(k, v)| (k, v.into())).collect(),
|
||||||
|
)),
|
||||||
|
LoroValue::Container { value } => loro::LoroValue::Container(value.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a LoroValue> for loro::LoroValue {
|
||||||
|
fn from(value: &LoroValue) -> loro::LoroValue {
|
||||||
|
match value {
|
||||||
|
LoroValue::Null => loro::LoroValue::Null,
|
||||||
|
LoroValue::Bool { value } => loro::LoroValue::Bool(*value),
|
||||||
|
LoroValue::Double { value } => loro::LoroValue::Double(*value),
|
||||||
|
LoroValue::I64 { value } => loro::LoroValue::I64(*value),
|
||||||
|
LoroValue::Binary { value } => loro::LoroValue::Binary(Arc::new(value.clone())),
|
||||||
|
LoroValue::String { value } => loro::LoroValue::String(Arc::new(value.clone())),
|
||||||
|
LoroValue::List { value } => {
|
||||||
|
loro::LoroValue::List(Arc::new(value.iter().map(Into::into).collect()))
|
||||||
|
}
|
||||||
|
LoroValue::Map { value } => loro::LoroValue::Map(Arc::new(
|
||||||
|
value.iter().map(|(k, v)| (k.clone(), v.into())).collect(),
|
||||||
|
)),
|
||||||
|
LoroValue::Container { value } => loro::LoroValue::Container(value.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::LoroValue> for LoroValue {
|
||||||
|
fn from(value: loro::LoroValue) -> LoroValue {
|
||||||
|
match value {
|
||||||
|
loro::LoroValue::Null => LoroValue::Null,
|
||||||
|
loro::LoroValue::Bool(value) => LoroValue::Bool { value },
|
||||||
|
loro::LoroValue::Double(value) => LoroValue::Double { value },
|
||||||
|
loro::LoroValue::I64(value) => LoroValue::I64 { value },
|
||||||
|
loro::LoroValue::Binary(value) => LoroValue::Binary {
|
||||||
|
value: value.to_vec(),
|
||||||
|
},
|
||||||
|
loro::LoroValue::String(value) => LoroValue::String {
|
||||||
|
value: value.to_string(),
|
||||||
|
},
|
||||||
|
loro::LoroValue::List(value) => LoroValue::List {
|
||||||
|
value: (*value).clone().into_iter().map(Into::into).collect(),
|
||||||
|
},
|
||||||
|
loro::LoroValue::Map(value) => LoroValue::Map {
|
||||||
|
value: (*value)
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, v.into()))
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
loro::LoroValue::Container(value) => LoroValue::Container {
|
||||||
|
value: value.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ContainerType> for loro::ContainerType {
|
||||||
|
fn from(value: ContainerType) -> loro::ContainerType {
|
||||||
|
match value {
|
||||||
|
ContainerType::Text => loro::ContainerType::Text,
|
||||||
|
ContainerType::Map => loro::ContainerType::Map,
|
||||||
|
ContainerType::List => loro::ContainerType::List,
|
||||||
|
ContainerType::MovableList => loro::ContainerType::MovableList,
|
||||||
|
ContainerType::Tree => loro::ContainerType::Tree,
|
||||||
|
ContainerType::Counter => loro::ContainerType::Counter,
|
||||||
|
ContainerType::Unknown { kind } => loro::ContainerType::Unknown(kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::ContainerType> for ContainerType {
|
||||||
|
fn from(value: loro::ContainerType) -> ContainerType {
|
||||||
|
match value {
|
||||||
|
loro::ContainerType::Text => ContainerType::Text,
|
||||||
|
loro::ContainerType::Map => ContainerType::Map,
|
||||||
|
loro::ContainerType::List => ContainerType::List,
|
||||||
|
loro::ContainerType::MovableList => ContainerType::MovableList,
|
||||||
|
loro::ContainerType::Tree => ContainerType::Tree,
|
||||||
|
loro::ContainerType::Counter => ContainerType::Counter,
|
||||||
|
loro::ContainerType::Unknown(kind) => ContainerType::Unknown { kind },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ContainerID> for loro::ContainerID {
|
||||||
|
fn from(value: ContainerID) -> loro::ContainerID {
|
||||||
|
match value {
|
||||||
|
ContainerID::Root {
|
||||||
|
name,
|
||||||
|
container_type,
|
||||||
|
} => loro::ContainerID::Root {
|
||||||
|
name: name.into(),
|
||||||
|
container_type: container_type.into(),
|
||||||
|
},
|
||||||
|
ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type,
|
||||||
|
} => loro::ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type: container_type.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ContainerID> for loro::ContainerID {
|
||||||
|
fn from(value: &ContainerID) -> loro::ContainerID {
|
||||||
|
match value {
|
||||||
|
ContainerID::Root {
|
||||||
|
name,
|
||||||
|
container_type,
|
||||||
|
} => loro::ContainerID::Root {
|
||||||
|
name: name.clone().into(),
|
||||||
|
container_type: (*container_type).into(),
|
||||||
|
},
|
||||||
|
ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type,
|
||||||
|
} => loro::ContainerID::Normal {
|
||||||
|
peer: *peer,
|
||||||
|
counter: *counter,
|
||||||
|
container_type: (*container_type).into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::ContainerID> for ContainerID {
|
||||||
|
fn from(value: loro::ContainerID) -> ContainerID {
|
||||||
|
match value {
|
||||||
|
loro::ContainerID::Root {
|
||||||
|
name,
|
||||||
|
container_type,
|
||||||
|
} => ContainerID::Root {
|
||||||
|
name: name.to_string(),
|
||||||
|
container_type: container_type.into(),
|
||||||
|
},
|
||||||
|
loro::ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type,
|
||||||
|
} => ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type: container_type.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a loro::ContainerID> for ContainerID {
|
||||||
|
fn from(value: &loro::ContainerID) -> ContainerID {
|
||||||
|
match value {
|
||||||
|
loro::ContainerID::Root {
|
||||||
|
name,
|
||||||
|
container_type,
|
||||||
|
} => ContainerID::Root {
|
||||||
|
name: name.to_string(),
|
||||||
|
container_type: (*container_type).into(),
|
||||||
|
},
|
||||||
|
loro::ContainerID::Normal {
|
||||||
|
peer,
|
||||||
|
counter,
|
||||||
|
container_type,
|
||||||
|
} => ContainerID::Normal {
|
||||||
|
peer: *peer,
|
||||||
|
counter: *counter,
|
||||||
|
container_type: (*container_type).into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
156
crates/loro-ffi/src/version.rs
Normal file
156
crates/loro-ffi/src/version.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use std::{cmp::Ordering, collections::HashMap, sync::RwLock};
|
||||||
|
|
||||||
|
use loro::{CounterSpan, IdSpan, LoroResult, PeerID, ID};
|
||||||
|
|
||||||
|
pub struct VersionVector(RwLock<loro::VersionVector>);
|
||||||
|
|
||||||
|
impl VersionVector {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(RwLock::new(loro::VersionVector::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff(&self, rhs: &Self) -> VersionVectorDiff {
|
||||||
|
self.0.read().unwrap().diff(&rhs.0.read().unwrap()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last(&self, peer: PeerID) -> Option<i32> {
|
||||||
|
self.0.read().unwrap().get_last(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_last(&self, id: ID) {
|
||||||
|
self.0.write().unwrap().set_last(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_end(&self, id: ID) {
|
||||||
|
self.0.write().unwrap().set_end(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_missing_span(&self, target: &Self) -> Vec<IdSpan> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get_missing_span(&target.0.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&self, other: &VersionVector) {
|
||||||
|
self.0.write().unwrap().merge(&other.0.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_vv(&self, other: &VersionVector) -> bool {
|
||||||
|
self.0.read().unwrap().includes_vv(&other.0.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_id(&self, id: ID) -> bool {
|
||||||
|
self.0.read().unwrap().includes_id(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intersect_span(&self, target: IdSpan) -> Option<CounterSpan> {
|
||||||
|
self.0.read().unwrap().intersect_span(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_to_include_vv(&self, other: &VersionVector) {
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.extend_to_include_vv(other.0.read().unwrap().iter());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn partial_cmp(&self, other: &VersionVector) -> Option<Ordering> {
|
||||||
|
self.0.read().unwrap().partial_cmp(&other.0.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eq(&self, other: &VersionVector) -> bool {
|
||||||
|
self.0.read().unwrap().eq(&other.0.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
self.0.read().unwrap().encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(bytes: &[u8]) -> LoroResult<Self> {
|
||||||
|
let ans = Self(RwLock::new(loro::VersionVector::decode(bytes)?));
|
||||||
|
Ok(ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Frontiers(loro::Frontiers);
|
||||||
|
|
||||||
|
impl Frontiers {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(loro::Frontiers::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eq(&self, other: &Frontiers) -> bool {
|
||||||
|
self.0.eq(&other.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_id(id: ID) -> Self {
|
||||||
|
Self(loro::Frontiers::from(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_ids(ids: Vec<ID>) -> Self {
|
||||||
|
Self(loro::Frontiers::from(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
self.0.encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(bytes: &[u8]) -> LoroResult<Self> {
|
||||||
|
let ans = Self(loro::Frontiers::decode(bytes)?);
|
||||||
|
Ok(ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VersionVectorDiff {
|
||||||
|
/// need to add these spans to move from right to left
|
||||||
|
pub left: HashMap<PeerID, CounterSpan>,
|
||||||
|
/// need to add these spans to move from left to right
|
||||||
|
pub right: HashMap<PeerID, CounterSpan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::VersionVectorDiff> for VersionVectorDiff {
|
||||||
|
fn from(value: loro::VersionVectorDiff) -> Self {
|
||||||
|
Self {
|
||||||
|
left: value.left.into_iter().collect(),
|
||||||
|
right: value.right.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VersionVector> for loro::VersionVector {
|
||||||
|
fn from(value: VersionVector) -> Self {
|
||||||
|
value.0.into_inner().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&VersionVector> for loro::VersionVector {
|
||||||
|
fn from(value: &VersionVector) -> Self {
|
||||||
|
value.0.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::VersionVector> for VersionVector {
|
||||||
|
fn from(value: loro::VersionVector) -> Self {
|
||||||
|
Self(RwLock::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<loro::Frontiers> for Frontiers {
|
||||||
|
fn from(value: loro::Frontiers) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Frontiers> for loro::Frontiers {
|
||||||
|
fn from(value: Frontiers) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Frontiers> for loro::Frontiers {
|
||||||
|
fn from(value: &Frontiers) -> Self {
|
||||||
|
value.0.clone()
|
||||||
|
}
|
||||||
|
}
|
|
@ -93,7 +93,7 @@ impl StyleConfigMap {
|
||||||
expand: ExpandType::None,
|
expand: ExpandType::None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
map.map.insert(
|
map.map.insert(
|
||||||
"code".into(),
|
"code".into(),
|
||||||
StyleConfig {
|
StyleConfig {
|
||||||
|
|
|
@ -239,18 +239,6 @@ impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_retain(&self) -> bool {
|
|
||||||
matches!(self, Self::Retain { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_insert(&self) -> bool {
|
|
||||||
matches!(self, Self::Insert { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_delete(&self) -> bool {
|
|
||||||
matches!(self, Self::Delete { .. })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -222,7 +222,7 @@ fn encode_changes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
json::ListOp::Insert {
|
json::ListOp::Insert {
|
||||||
pos: *pos,
|
pos: *pos as u32,
|
||||||
value: value.into(),
|
value: value.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,8 +230,8 @@ fn encode_changes(
|
||||||
id_start,
|
id_start,
|
||||||
span: DeleteSpan { pos, signed_len },
|
span: DeleteSpan { pos, signed_len },
|
||||||
}) => json::ListOp::Delete {
|
}) => json::ListOp::Delete {
|
||||||
pos: *pos,
|
pos: *pos as i32,
|
||||||
len: *signed_len,
|
len: *signed_len as i32,
|
||||||
start_id: register_id(id_start, peer_register),
|
start_id: register_id(id_start, peer_register),
|
||||||
},
|
},
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
@ -251,7 +251,7 @@ fn encode_changes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
json::MovableListOp::Insert {
|
json::MovableListOp::Insert {
|
||||||
pos: *pos,
|
pos: *pos as u32,
|
||||||
value: value.into(),
|
value: value.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,8 +259,8 @@ fn encode_changes(
|
||||||
id_start,
|
id_start,
|
||||||
span: DeleteSpan { pos, signed_len },
|
span: DeleteSpan { pos, signed_len },
|
||||||
}) => json::MovableListOp::Delete {
|
}) => json::MovableListOp::Delete {
|
||||||
pos: *pos,
|
pos: *pos as i32,
|
||||||
len: *signed_len,
|
len: *signed_len as i32,
|
||||||
start_id: register_id(id_start, peer_register),
|
start_id: register_id(id_start, peer_register),
|
||||||
},
|
},
|
||||||
InnerListOp::Move {
|
InnerListOp::Move {
|
||||||
|
@ -309,8 +309,8 @@ fn encode_changes(
|
||||||
id_start,
|
id_start,
|
||||||
span: DeleteSpan { pos, signed_len },
|
span: DeleteSpan { pos, signed_len },
|
||||||
}) => json::TextOp::Delete {
|
}) => json::TextOp::Delete {
|
||||||
pos: *pos,
|
pos: *pos as i32,
|
||||||
len: *signed_len,
|
len: *signed_len as i32,
|
||||||
start_id: register_id(id_start, peer_register),
|
start_id: register_id(id_start, peer_register),
|
||||||
},
|
},
|
||||||
InnerListOp::StyleStart {
|
InnerListOp::StyleStart {
|
||||||
|
@ -496,8 +496,8 @@ fn decode_op(op: json::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> LoroRes
|
||||||
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
||||||
id_start,
|
id_start,
|
||||||
span: DeleteSpan {
|
span: DeleteSpan {
|
||||||
pos,
|
pos: pos as isize,
|
||||||
signed_len: len,
|
signed_len: len as isize,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -532,15 +532,15 @@ fn decode_op(op: json::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> LoroRes
|
||||||
let range = arena.alloc_values(values.iter().cloned());
|
let range = arena.alloc_values(values.iter().cloned());
|
||||||
InnerContent::List(InnerListOp::Insert {
|
InnerContent::List(InnerListOp::Insert {
|
||||||
slice: SliceRange::new(range.start as u32..range.end as u32),
|
slice: SliceRange::new(range.start as u32..range.end as u32),
|
||||||
pos,
|
pos: pos as usize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
json::ListOp::Delete { pos, len, start_id } => {
|
json::ListOp::Delete { pos, len, start_id } => {
|
||||||
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
||||||
id_start: convert_id(&start_id, peers),
|
id_start: convert_id(&start_id, peers),
|
||||||
span: DeleteSpan {
|
span: DeleteSpan {
|
||||||
pos,
|
pos: pos as isize,
|
||||||
signed_len: len,
|
signed_len: len as isize,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -561,15 +561,15 @@ fn decode_op(op: json::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> LoroRes
|
||||||
let range = arena.alloc_values(values.iter().cloned());
|
let range = arena.alloc_values(values.iter().cloned());
|
||||||
InnerContent::List(InnerListOp::Insert {
|
InnerContent::List(InnerListOp::Insert {
|
||||||
slice: SliceRange::new(range.start as u32..range.end as u32),
|
slice: SliceRange::new(range.start as u32..range.end as u32),
|
||||||
pos,
|
pos: pos as usize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
json::MovableListOp::Delete { pos, len, start_id } => {
|
json::MovableListOp::Delete { pos, len, start_id } => {
|
||||||
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
|
||||||
id_start: convert_id(&start_id, peers),
|
id_start: convert_id(&start_id, peers),
|
||||||
span: DeleteSpan {
|
span: DeleteSpan {
|
||||||
pos,
|
pos: pos as isize,
|
||||||
signed_len: len,
|
signed_len: len as isize,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -704,7 +704,6 @@ pub mod json {
|
||||||
use fractional_index::FractionalIndex;
|
use fractional_index::FractionalIndex;
|
||||||
use loro_common::{ContainerID, IdLp, Lamport, LoroValue, PeerID, TreeID, ID};
|
use loro_common::{ContainerID, IdLp, Lamport, LoroValue, PeerID, TreeID, ID};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::{encoding::OwnedValue, version::Frontiers};
|
use crate::{encoding::OwnedValue, version::Frontiers};
|
||||||
|
|
||||||
|
@ -724,7 +723,7 @@ pub mod json {
|
||||||
pub id: ID,
|
pub id: ID,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
#[serde(with = "self::serde_impl::deps")]
|
#[serde(with = "self::serde_impl::deps")]
|
||||||
pub deps: SmallVec<[ID; 2]>,
|
pub deps: Vec<ID>,
|
||||||
pub lamport: Lamport,
|
pub lamport: Lamport,
|
||||||
pub msg: Option<String>,
|
pub msg: Option<String>,
|
||||||
pub ops: Vec<JsonOp>,
|
pub ops: Vec<JsonOp>,
|
||||||
|
@ -760,12 +759,12 @@ pub mod json {
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum ListOp {
|
pub enum ListOp {
|
||||||
Insert {
|
Insert {
|
||||||
pos: usize,
|
pos: u32,
|
||||||
value: LoroValue,
|
value: LoroValue,
|
||||||
},
|
},
|
||||||
Delete {
|
Delete {
|
||||||
pos: isize,
|
pos: i32,
|
||||||
len: isize,
|
len: i32,
|
||||||
#[serde(with = "self::serde_impl::id")]
|
#[serde(with = "self::serde_impl::id")]
|
||||||
start_id: ID,
|
start_id: ID,
|
||||||
},
|
},
|
||||||
|
@ -775,12 +774,12 @@ pub mod json {
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum MovableListOp {
|
pub enum MovableListOp {
|
||||||
Insert {
|
Insert {
|
||||||
pos: usize,
|
pos: u32,
|
||||||
value: LoroValue,
|
value: LoroValue,
|
||||||
},
|
},
|
||||||
Delete {
|
Delete {
|
||||||
pos: isize,
|
pos: i32,
|
||||||
len: isize,
|
len: i32,
|
||||||
#[serde(with = "self::serde_impl::id")]
|
#[serde(with = "self::serde_impl::id")]
|
||||||
start_id: ID,
|
start_id: ID,
|
||||||
},
|
},
|
||||||
|
@ -812,8 +811,8 @@ pub mod json {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
Delete {
|
Delete {
|
||||||
pos: isize,
|
pos: i32,
|
||||||
len: isize,
|
len: i32,
|
||||||
#[serde(with = "self::serde_impl::id")]
|
#[serde(with = "self::serde_impl::id")]
|
||||||
start_id: ID,
|
start_id: ID,
|
||||||
},
|
},
|
||||||
|
@ -939,11 +938,11 @@ pub mod json {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "counter")]
|
#[cfg(feature = "counter")]
|
||||||
ContainerType::Counter => {
|
ContainerType::Counter => {
|
||||||
let (_key, v) =
|
let (_key, value) =
|
||||||
map.next_entry::<String, OwnedValue>()?.unwrap();
|
map.next_entry::<String, OwnedValue>()?.unwrap();
|
||||||
super::JsonOpContent::Future(super::FutureOpWrapper {
|
super::JsonOpContent::Future(super::FutureOpWrapper {
|
||||||
prop: 0,
|
prop: 0,
|
||||||
value: super::FutureOp::Counter(v),
|
value: super::FutureOp::Counter(value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
@ -1038,7 +1037,7 @@ pub mod json {
|
||||||
s.collect_seq(deps.iter().map(|x| x.to_string()))
|
s.collect_seq(deps.iter().map(|x| x.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize<'de, 'a, D>(d: D) -> Result<smallvec::SmallVec<[ID; 2]>, D::Error>
|
pub fn deserialize<'de, 'a, D>(d: D) -> Result<Vec<ID>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
pub use loro_common::{LoroError, LoroResult};
|
|
|
@ -68,14 +68,12 @@ pub mod delta;
|
||||||
pub use loro_delta;
|
pub use loro_delta;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
|
||||||
pub use error::{LoroError, LoroResult};
|
|
||||||
pub mod estimated_size;
|
pub mod estimated_size;
|
||||||
pub(crate) mod history_cache;
|
pub(crate) mod history_cache;
|
||||||
pub(crate) mod macros;
|
pub(crate) mod macros;
|
||||||
pub(crate) mod state;
|
pub(crate) mod state;
|
||||||
pub mod undo;
|
pub mod undo;
|
||||||
pub(crate) mod value;
|
pub(crate) mod value;
|
||||||
pub(crate) use id::{PeerID, ID};
|
|
||||||
|
|
||||||
// TODO: rename as Key?
|
// TODO: rename as Key?
|
||||||
pub(crate) use loro_common::InternalString;
|
pub(crate) use loro_common::InternalString;
|
||||||
|
@ -84,6 +82,10 @@ pub use container::ContainerType;
|
||||||
pub use encoding::json_schema::json;
|
pub use encoding::json_schema::json;
|
||||||
pub use fractional_index::FractionalIndex;
|
pub use fractional_index::FractionalIndex;
|
||||||
pub use loro_common::{loro_value, to_value};
|
pub use loro_common::{loro_value, to_value};
|
||||||
|
pub use loro_common::{
|
||||||
|
Counter, CounterSpan, IdLp, IdSpan, Lamport, LoroError, LoroResult, LoroTreeError, PeerID,
|
||||||
|
TreeID, ID,
|
||||||
|
};
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
pub use value::wasm;
|
pub use value::wasm;
|
||||||
pub use value::{ApplyDiff, LoroValue, ToJson};
|
pub use value::{ApplyDiff, LoroValue, ToJson};
|
||||||
|
|
|
@ -1472,7 +1472,7 @@ impl LoroDoc {
|
||||||
ExportMode::Snapshot => export_fast_snapshot(self),
|
ExportMode::Snapshot => export_fast_snapshot(self),
|
||||||
ExportMode::Updates { from } => export_fast_updates(self, &from),
|
ExportMode::Updates { from } => export_fast_updates(self, &from),
|
||||||
ExportMode::UpdatesInRange { spans } => {
|
ExportMode::UpdatesInRange { spans } => {
|
||||||
export_fast_updates_in_range(&self.oplog.try_lock().unwrap(), &spans)
|
export_fast_updates_in_range(&self.oplog.try_lock().unwrap(), spans.as_ref())
|
||||||
}
|
}
|
||||||
ExportMode::GcSnapshot(f) => export_gc_snapshot(self, &f),
|
ExportMode::GcSnapshot(f) => export_gc_snapshot(self, &f),
|
||||||
ExportMode::StateOnly(f) => match f {
|
ExportMode::StateOnly(f) => match f {
|
||||||
|
|
|
@ -73,11 +73,11 @@ pub(crate) struct NodePosition {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumAsInner, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumAsInner, Serialize)]
|
||||||
pub enum TreeParentId {
|
pub enum TreeParentId {
|
||||||
Node(TreeID),
|
Node(TreeID),
|
||||||
|
Root,
|
||||||
|
Deleted,
|
||||||
// We use `Unexist` as the old parent of a new node created
|
// We use `Unexist` as the old parent of a new node created
|
||||||
// so we can infer the retreat internal diff is `Uncreate`
|
// so we can infer the retreat internal diff is `Uncreate`
|
||||||
Unexist,
|
Unexist,
|
||||||
Deleted,
|
|
||||||
Root,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<TreeID>> for TreeParentId {
|
impl From<Option<TreeID>> for TreeParentId {
|
||||||
|
|
|
@ -19,9 +19,8 @@ loro-common = { path = "../loro-common", version = "0.16.2", features = ["serde_
|
||||||
loro-kv-store = { path = "../kv-store", version = "0.16.2" }
|
loro-kv-store = { path = "../kv-store", version = "0.16.2" }
|
||||||
delta = { path = "../delta", package = "loro-delta", version = "0.16.2" }
|
delta = { path = "../delta", package = "loro-delta", version = "0.16.2" }
|
||||||
generic-btree = { version = "^0.10.5" }
|
generic-btree = { version = "^0.10.5" }
|
||||||
enum-as-inner = "0.6.0"
|
enum-as-inner = { workspace = true }
|
||||||
either = "1.9.0"
|
tracing = { workspace = true }
|
||||||
tracing = "0.1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use loro_internal::{
|
use loro_internal::{
|
||||||
container::ContainerID, handler::counter::CounterHandler, HandlerTrait, LoroResult, LoroValue,
|
container::ContainerID, handler::counter::CounterHandler, HandlerTrait, LoroResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Container, ContainerTrait, SealedTrait};
|
use crate::{Container, ContainerTrait, SealedTrait};
|
||||||
|
@ -40,8 +40,8 @@ impl LoroCounter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current value of the counter.
|
/// Get the current value of the counter.
|
||||||
pub fn get_value(&self) -> LoroValue {
|
pub fn get_value(&self) -> f64 {
|
||||||
self.handler.get_value()
|
self.handler.get_value().into_double().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current value of the counter
|
/// Get the current value of the counter
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![warn(missing_debug_implementations)]
|
#![warn(missing_debug_implementations)]
|
||||||
use either::Either;
|
|
||||||
use event::{DiffEvent, Subscriber};
|
use event::{DiffEvent, Subscriber};
|
||||||
use loro_internal::container::IntoContainerId;
|
pub use loro_internal::cursor::CannotFindRelativePosition;
|
||||||
use loro_internal::cursor::CannotFindRelativePosition;
|
|
||||||
use loro_internal::cursor::Cursor;
|
use loro_internal::cursor::Cursor;
|
||||||
use loro_internal::cursor::PosQueryResult;
|
use loro_internal::cursor::PosQueryResult;
|
||||||
use loro_internal::cursor::Side;
|
use loro_internal::cursor::Side;
|
||||||
use loro_internal::encoding::ImportBlobMetadata;
|
|
||||||
use loro_internal::handler::HandlerTrait;
|
use loro_internal::handler::HandlerTrait;
|
||||||
use loro_internal::handler::ValueOrHandler;
|
use loro_internal::handler::ValueOrHandler;
|
||||||
use loro_internal::loro_common::LoroTreeError;
|
|
||||||
use loro_internal::undo::{OnPop, OnPush};
|
use loro_internal::undo::{OnPop, OnPush};
|
||||||
use loro_internal::version::ImVersionVector;
|
pub use loro_internal::version::ImVersionVector;
|
||||||
use loro_internal::DocState;
|
use loro_internal::DocState;
|
||||||
use loro_internal::FractionalIndex;
|
|
||||||
use loro_internal::LoroDoc as InnerLoroDoc;
|
use loro_internal::LoroDoc as InnerLoroDoc;
|
||||||
use loro_internal::OpLog;
|
use loro_internal::OpLog;
|
||||||
use loro_internal::{
|
use loro_internal::{
|
||||||
|
@ -35,31 +30,38 @@ pub use loro_internal::subscription::PeerIdUpdateCallback;
|
||||||
pub use loro_internal::ChangeMeta;
|
pub use loro_internal::ChangeMeta;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub use loro_internal::awareness;
|
pub use loro_internal::awareness;
|
||||||
|
pub use loro_internal::change::Timestamp;
|
||||||
pub use loro_internal::configure::Configure;
|
pub use loro_internal::configure::Configure;
|
||||||
pub use loro_internal::configure::StyleConfigMap;
|
pub use loro_internal::configure::{StyleConfig, StyleConfigMap};
|
||||||
pub use loro_internal::container::richtext::ExpandType;
|
pub use loro_internal::container::richtext::ExpandType;
|
||||||
pub use loro_internal::container::{ContainerID, ContainerType};
|
pub use loro_internal::container::{ContainerID, ContainerType, IntoContainerId};
|
||||||
pub use loro_internal::cursor;
|
pub use loro_internal::cursor;
|
||||||
pub use loro_internal::delta::{TreeDeltaItem, TreeDiff, TreeExternalDiff};
|
pub use loro_internal::delta::{TreeDeltaItem, TreeDiff, TreeDiffItem, TreeExternalDiff};
|
||||||
pub use loro_internal::encoding::ExportMode;
|
pub use loro_internal::encoding::ExportMode;
|
||||||
pub use loro_internal::event::Index;
|
pub use loro_internal::encoding::ImportBlobMetadata;
|
||||||
|
pub use loro_internal::event::{EventTriggerKind, Index};
|
||||||
pub use loro_internal::handler::TextDelta;
|
pub use loro_internal::handler::TextDelta;
|
||||||
pub use loro_internal::id::{PeerID, TreeID, ID};
|
|
||||||
pub use loro_internal::json;
|
pub use loro_internal::json;
|
||||||
pub use loro_internal::json::JsonSchema;
|
pub use loro_internal::json::{
|
||||||
|
FutureOp as JsonFutureOp, FutureOpWrapper as JsonFutureOpWrapper, JsonChange, JsonOp,
|
||||||
|
JsonOpContent, JsonSchema, ListOp as JsonListOp, MapOp as JsonMapOp,
|
||||||
|
MovableListOp as JsonMovableListOp, TextOp as JsonTextOp, TreeOp as JsonTreeOp,
|
||||||
|
};
|
||||||
pub use loro_internal::kv_store::{KvStore, MemKvStore};
|
pub use loro_internal::kv_store::{KvStore, MemKvStore};
|
||||||
pub use loro_internal::loro::CommitOptions;
|
pub use loro_internal::loro::CommitOptions;
|
||||||
pub use loro_internal::loro::DocAnalysis;
|
pub use loro_internal::loro::DocAnalysis;
|
||||||
pub use loro_internal::oplog::FrontiersNotIncluded;
|
pub use loro_internal::oplog::FrontiersNotIncluded;
|
||||||
pub use loro_internal::subscription::SubID;
|
pub use loro_internal::subscription::SubID;
|
||||||
pub use loro_internal::undo;
|
pub use loro_internal::undo;
|
||||||
pub use loro_internal::version::{Frontiers, VersionVector};
|
pub use loro_internal::version::{Frontiers, VersionVector, VersionVectorDiff};
|
||||||
pub use loro_internal::ApplyDiff;
|
pub use loro_internal::ApplyDiff;
|
||||||
pub use loro_internal::Subscription;
|
pub use loro_internal::Subscription;
|
||||||
pub use loro_internal::TreeParentId;
|
|
||||||
pub use loro_internal::UndoManager as InnerUndoManager;
|
pub use loro_internal::UndoManager as InnerUndoManager;
|
||||||
pub use loro_internal::{loro_value, to_value};
|
pub use loro_internal::{loro_value, to_value};
|
||||||
pub use loro_internal::{LoroError, LoroResult, LoroValue, ToJson};
|
pub use loro_internal::{
|
||||||
|
Counter, CounterSpan, FractionalIndex, IdLp, IdSpan, Lamport, PeerID, TreeID, TreeParentId, ID,
|
||||||
|
};
|
||||||
|
pub use loro_internal::{LoroError, LoroResult, LoroTreeError, LoroValue, ToJson};
|
||||||
pub use loro_kv_store as kv_store;
|
pub use loro_kv_store as kv_store;
|
||||||
|
|
||||||
#[cfg(feature = "jsonpath")]
|
#[cfg(feature = "jsonpath")]
|
||||||
|
@ -274,7 +276,7 @@ impl LoroDoc {
|
||||||
///
|
///
|
||||||
/// Learn more at https://loro.dev/docs/advanced/doc_state_and_oplog#attacheddetached-status
|
/// Learn more at https://loro.dev/docs/advanced/doc_state_and_oplog#attacheddetached-status
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn detach(&mut self) {
|
pub fn detach(&self) {
|
||||||
self.doc.detach()
|
self.doc.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,7 +758,8 @@ impl LoroDoc {
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use loro::LoroDoc;
|
/// # use loro::{LoroDoc, ToJson};
|
||||||
|
///
|
||||||
/// let doc = LoroDoc::new();
|
/// let doc = LoroDoc::new();
|
||||||
/// let map = doc.get_map("users");
|
/// let map = doc.get_map("users");
|
||||||
/// map.insert("alice", 30).unwrap();
|
/// map.insert("alice", 30).unwrap();
|
||||||
|
@ -764,7 +767,7 @@ impl LoroDoc {
|
||||||
///
|
///
|
||||||
/// let result = doc.jsonpath("$.users.alice").unwrap();
|
/// let result = doc.jsonpath("$.users.alice").unwrap();
|
||||||
/// assert_eq!(result.len(), 1);
|
/// assert_eq!(result.len(), 1);
|
||||||
/// assert_eq!(result[0].to_json_value(), serde_json::json!(30));
|
/// assert_eq!(result[0].as_value().unwrap().to_json_value(), serde_json::json!(30));
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(feature = "jsonpath")]
|
#[cfg(feature = "jsonpath")]
|
||||||
|
@ -923,10 +926,12 @@ impl LoroList {
|
||||||
|
|
||||||
/// Get the value at the given position.
|
/// Get the value at the given position.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get(&self, index: usize) -> Option<Either<LoroValue, Container>> {
|
pub fn get(&self, index: usize) -> Option<ValueOrContainer> {
|
||||||
match self.handler.get_(index) {
|
match self.handler.get_(index) {
|
||||||
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
|
Some(ValueOrHandler::Handler(c)) => {
|
||||||
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
|
Some(ValueOrContainer::Container(Container::from_handler(c)))
|
||||||
|
}
|
||||||
|
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -973,11 +978,13 @@ impl LoroList {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the elements of the list.
|
/// Iterate over the elements of the list.
|
||||||
pub fn for_each<I>(&self, f: I)
|
pub fn for_each<I>(&self, mut f: I)
|
||||||
where
|
where
|
||||||
I: FnMut((usize, ValueOrHandler)),
|
I: FnMut((usize, ValueOrContainer)),
|
||||||
{
|
{
|
||||||
self.handler.for_each(f)
|
self.handler.for_each(&mut |(index, v)| {
|
||||||
|
f((index, ValueOrContainer::from(v)));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the length of the list.
|
/// Get the length of the list.
|
||||||
|
@ -1202,11 +1209,13 @@ impl LoroMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the value of the map with the given key.
|
/// Get the value of the map with the given key.
|
||||||
pub fn get(&self, key: &str) -> Option<Either<LoroValue, Container>> {
|
pub fn get(&self, key: &str) -> Option<ValueOrContainer> {
|
||||||
match self.handler.get_(key) {
|
match self.handler.get_(key) {
|
||||||
None => None,
|
None => None,
|
||||||
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
|
Some(ValueOrHandler::Handler(c)) => {
|
||||||
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
|
Some(ValueOrContainer::Container(Container::from_handler(c)))
|
||||||
|
}
|
||||||
|
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1795,8 +1804,8 @@ impl LoroTree {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - If the target node does not exist, return `LoroTreeError::TreeNodeNotExist`.
|
/// - If the target node does not exist, return `LoroTreeError::TreeNodeNotExist`.
|
||||||
pub fn is_node_deleted(&self, target: TreeID) -> LoroResult<bool> {
|
pub fn is_node_deleted(&self, target: &TreeID) -> LoroResult<bool> {
|
||||||
self.handler.is_node_deleted(&target)
|
self.handler.is_node_deleted(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return all nodes, including deleted nodes
|
/// Return all nodes, including deleted nodes
|
||||||
|
@ -1845,7 +1854,7 @@ impl LoroTree {
|
||||||
.map(|x| x.to_string())
|
.map(|x| x.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the flat array of the forest.
|
/// Return the hierarchy array of the forest.
|
||||||
///
|
///
|
||||||
/// Note: the metadata will be not resolved. So if you don't only care about hierarchy
|
/// Note: the metadata will be not resolved. So if you don't only care about hierarchy
|
||||||
/// but also the metadata, you should use [TreeHandler::get_value_with_meta()].
|
/// but also the metadata, you should use [TreeHandler::get_value_with_meta()].
|
||||||
|
@ -1853,7 +1862,7 @@ impl LoroTree {
|
||||||
self.handler.get_value()
|
self.handler.get_value()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the flat array of the forest, each node is with metadata.
|
/// Return the hierarchy array of the forest, each node is with metadata.
|
||||||
pub fn get_value_with_meta(&self) -> LoroValue {
|
pub fn get_value_with_meta(&self) -> LoroValue {
|
||||||
self.handler.get_deep_value()
|
self.handler.get_deep_value()
|
||||||
}
|
}
|
||||||
|
@ -1964,6 +1973,14 @@ impl LoroMovableList {
|
||||||
self.handler.id().clone()
|
self.handler.id().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the container is attached to a document
|
||||||
|
///
|
||||||
|
/// The edits on a detached container will not be persisted.
|
||||||
|
/// To attach the container to the document, please insert it into an attached container.
|
||||||
|
pub fn is_attached(&self) -> bool {
|
||||||
|
self.handler.is_attached()
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a value at the given position.
|
/// Insert a value at the given position.
|
||||||
pub fn insert(&self, pos: usize, v: impl Into<LoroValue>) -> LoroResult<()> {
|
pub fn insert(&self, pos: usize, v: impl Into<LoroValue>) -> LoroResult<()> {
|
||||||
self.handler.insert(pos, v)
|
self.handler.insert(pos, v)
|
||||||
|
@ -1975,10 +1992,12 @@ impl LoroMovableList {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the value at the given position.
|
/// Get the value at the given position.
|
||||||
pub fn get(&self, index: usize) -> Option<Either<LoroValue, Container>> {
|
pub fn get(&self, index: usize) -> Option<ValueOrContainer> {
|
||||||
match self.handler.get_(index) {
|
match self.handler.get_(index) {
|
||||||
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
|
Some(ValueOrHandler::Handler(c)) => {
|
||||||
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
|
Some(ValueOrContainer::Container(Container::from_handler(c)))
|
||||||
|
}
|
||||||
|
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2009,10 +2028,12 @@ impl LoroMovableList {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop the last element of the list.
|
/// Pop the last element of the list.
|
||||||
pub fn pop(&self) -> LoroResult<Option<Either<LoroValue, Container>>> {
|
pub fn pop(&self) -> LoroResult<Option<ValueOrContainer>> {
|
||||||
Ok(match self.handler.pop_()? {
|
Ok(match self.handler.pop_()? {
|
||||||
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
|
Some(ValueOrHandler::Handler(c)) => {
|
||||||
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
|
Some(ValueOrContainer::Container(Container::from_handler(c)))
|
||||||
|
}
|
||||||
|
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
|
||||||
None => None,
|
None => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2148,6 +2169,13 @@ pub struct LoroUnknown {
|
||||||
handler: InnerUnknownHandler,
|
handler: InnerUnknownHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LoroUnknown {
|
||||||
|
/// Get the container id.
|
||||||
|
pub fn id(&self) -> ContainerID {
|
||||||
|
self.handler.id().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SealedTrait for LoroUnknown {}
|
impl SealedTrait for LoroUnknown {}
|
||||||
impl ContainerTrait for LoroUnknown {
|
impl ContainerTrait for LoroUnknown {
|
||||||
type Handler = InnerUnknownHandler;
|
type Handler = InnerUnknownHandler;
|
||||||
|
@ -2360,7 +2388,7 @@ impl ValueOrContainer {
|
||||||
Container::Tree(c) => c.get_value(),
|
Container::Tree(c) => c.get_value(),
|
||||||
Container::MovableList(c) => c.get_deep_value(),
|
Container::MovableList(c) => c.get_deep_value(),
|
||||||
#[cfg(feature = "counter")]
|
#[cfg(feature = "counter")]
|
||||||
Container::Counter(c) => c.get_value(),
|
Container::Counter(c) => c.get_value().into(),
|
||||||
Container::Unknown(_) => LoroValue::Null,
|
Container::Unknown(_) => LoroValue::Null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue