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:
Leon Zhao 2024-09-29 07:41:59 +08:00 committed by GitHub
parent 5e3f269c8c
commit 4414053a82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 3234 additions and 698 deletions

17
Cargo.lock generated
View file

@ -572,9 +572,9 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "either"
version = "1.9.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "embedded-io"
@ -694,7 +694,7 @@ dependencies = [
"ctor 0.2.6",
"dev-utils",
"ensure-cov",
"enum-as-inner 0.5.1",
"enum-as-inner 0.6.0",
"enum_dispatch",
"fxhash",
"itertools 0.12.1",
@ -1052,7 +1052,6 @@ dependencies = [
"anyhow",
"ctor 0.2.6",
"dev-utils",
"either",
"enum-as-inner 0.6.0",
"generic-btree",
"loro-common 0.16.12",
@ -1156,6 +1155,14 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "loro-ffi"
version = "0.16.2"
dependencies = [
"loro 0.16.12",
"serde_json",
]
[[package]]
name = "loro-internal"
version = "0.16.2"
@ -1243,7 +1250,7 @@ dependencies = [
"dhat",
"either",
"ensure-cov",
"enum-as-inner 0.5.1",
"enum-as-inner 0.6.0",
"enum_dispatch",
"fxhash",
"generic-btree",

View file

@ -12,12 +12,13 @@ members = [
"crates/dev-utils",
"crates/delta",
"crates/kv-store",
"crates/loro-ffi",
]
resolver = "2"
[workspace.dependencies]
enum_dispatch = "0.3.11"
enum-as-inner = "0.5.1"
enum-as-inner = "0.6.0"
fxhash = "0.2.1"
tracing = { version = "0.1" }
serde_columnar = { version = "0.3.10" }
@ -30,3 +31,4 @@ bytes = "1"
once_cell = "1.18.0"
xxhash-rust = { version = "0.8.12", features = ["xxh32"] }
ensure-cov = "0.1.0"
either = "1.13.0"

View file

@ -93,9 +93,27 @@ impl ActorTrait for DrawActor {
};
let map = self.doc.get_map(id);
let pos_map = map.get("pos").unwrap().unwrap_right().into_map().unwrap();
let x = pos_map.get("x").unwrap().unwrap_left().into_i64().unwrap();
let y = pos_map.get("y").unwrap().unwrap_left().into_i64().unwrap();
let pos_map = map
.get("pos")
.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
.insert("x", x.overflowing_add(relative_to.x as i64).0)
.unwrap();

View file

@ -62,7 +62,17 @@ impl ActorTrait for CounterActor {
let counter = loro.get_counter("counter");
let result = counter.get_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;
// snapshot to snapshot

View file

@ -155,7 +155,7 @@ impl Actionable for TreeAction {
let nodes = tree
.nodes()
.into_iter()
.filter(|x| !tree.is_node_deleted(*x).unwrap())
.filter(|x| !tree.is_node_deleted(x).unwrap())
.collect::<Vec<_>>();
let node_num = nodes.len();
let TreeAction { target, action } = self;

View file

@ -164,7 +164,7 @@ impl OneDocFuzzer {
let nodes = tree
.nodes()
.into_iter()
.filter(|x| !tree.is_node_deleted(*x).unwrap())
.filter(|x| !tree.is_node_deleted(x).unwrap())
.collect::<Vec<_>>();
let node_num = nodes.len();
let crate::container::TreeAction { target, action } = tree_action;

View file

@ -129,7 +129,7 @@ fn snapshot_from_016_can_be_imported_in_cur_version() {
.get_map("map")
.get("new_key")
.unwrap()
.left()
.into_value()
.unwrap(),
loro::LoroValue::String(Arc::new("new_value".into()))
);

View file

@ -1,20 +1,12 @@
[package]
name = "loro-ffi"
version = "0.1.0"
version = "0.16.2"
edition = "2021"
build = "build.rs"
license = "MIT"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
loro-internal = { path = "../loro-internal" }
[lib]
crate-type = ["staticlib", "cdylib"]
name = "loro"
[build-dependencies]
cbindgen = "0.24.3"
loro = { path = "../loro", features = ["counter","jsonpath"] }
serde_json = {workspace = true}

View file

@ -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
```

View file

@ -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");
}

View file

@ -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"

View file

@ -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

View file

@ -1,5 +0,0 @@
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build

View file

@ -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")
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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" "$@"

View file

@ -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

View file

@ -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")

View file

@ -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);
}

View file

@ -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);
}

View 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,
}
}
}

View 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)
}
}

View 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,
}
}
}

View 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()
}
}

View 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
}
}

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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,
}
}
}

View 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
View 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,
}

View 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,
}
}
}

View file

@ -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};
/// create Loro with a random unique client id
#[no_mangle]
pub extern "C" fn loro_new() -> *mut LoroDoc {
Box::into_raw(Box::default())
// https://github.com/mozilla/uniffi-rs/issues/1372
pub trait ValueOrContainer: Send + Sync {
fn is_value(&self) -> bool;
fn is_container(&self) -> bool;
fn as_value(&self) -> Option<LoroValue>;
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
#[no_mangle]
pub unsafe extern "C" fn loro_free(loro: *mut LoroDoc) {
if !loro.is_null() {
drop(Box::from_raw(loro));
impl ValueOrContainer for loro::ValueOrContainer {
fn is_value(&self) -> bool {
loro::ValueOrContainer::is_value(self)
}
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
View 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,
}

View 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(),
},
}
}
}

View 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()
}
}

View file

@ -93,7 +93,7 @@ impl StyleConfigMap {
expand: ExpandType::None,
},
);
map.map.insert(
"code".into(),
StyleConfig {

View file

@ -239,18 +239,6 @@ impl<Value: DeltaValue, M: Meta> DeltaItem<Value, M> {
_ => 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)]

View file

@ -222,7 +222,7 @@ fn encode_changes(
}
});
json::ListOp::Insert {
pos: *pos,
pos: *pos as u32,
value: value.into(),
}
}
@ -230,8 +230,8 @@ fn encode_changes(
id_start,
span: DeleteSpan { pos, signed_len },
}) => json::ListOp::Delete {
pos: *pos,
len: *signed_len,
pos: *pos as i32,
len: *signed_len as i32,
start_id: register_id(id_start, peer_register),
},
_ => unreachable!(),
@ -251,7 +251,7 @@ fn encode_changes(
}
});
json::MovableListOp::Insert {
pos: *pos,
pos: *pos as u32,
value: value.into(),
}
}
@ -259,8 +259,8 @@ fn encode_changes(
id_start,
span: DeleteSpan { pos, signed_len },
}) => json::MovableListOp::Delete {
pos: *pos,
len: *signed_len,
pos: *pos as i32,
len: *signed_len as i32,
start_id: register_id(id_start, peer_register),
},
InnerListOp::Move {
@ -309,8 +309,8 @@ fn encode_changes(
id_start,
span: DeleteSpan { pos, signed_len },
}) => json::TextOp::Delete {
pos: *pos,
len: *signed_len,
pos: *pos as i32,
len: *signed_len as i32,
start_id: register_id(id_start, peer_register),
},
InnerListOp::StyleStart {
@ -496,8 +496,8 @@ fn decode_op(op: json::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> LoroRes
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
id_start,
span: DeleteSpan {
pos,
signed_len: len,
pos: pos as isize,
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());
InnerContent::List(InnerListOp::Insert {
slice: SliceRange::new(range.start as u32..range.end as u32),
pos,
pos: pos as usize,
})
}
json::ListOp::Delete { pos, len, start_id } => {
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
id_start: convert_id(&start_id, peers),
span: DeleteSpan {
pos,
signed_len: len,
pos: pos as isize,
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());
InnerContent::List(InnerListOp::Insert {
slice: SliceRange::new(range.start as u32..range.end as u32),
pos,
pos: pos as usize,
})
}
json::MovableListOp::Delete { pos, len, start_id } => {
InnerContent::List(InnerListOp::Delete(DeleteSpanWithId {
id_start: convert_id(&start_id, peers),
span: DeleteSpan {
pos,
signed_len: len,
pos: pos as isize,
signed_len: len as isize,
},
}))
}
@ -704,7 +704,6 @@ pub mod json {
use fractional_index::FractionalIndex;
use loro_common::{ContainerID, IdLp, Lamport, LoroValue, PeerID, TreeID, ID};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::{encoding::OwnedValue, version::Frontiers};
@ -724,7 +723,7 @@ pub mod json {
pub id: ID,
pub timestamp: i64,
#[serde(with = "self::serde_impl::deps")]
pub deps: SmallVec<[ID; 2]>,
pub deps: Vec<ID>,
pub lamport: Lamport,
pub msg: Option<String>,
pub ops: Vec<JsonOp>,
@ -760,12 +759,12 @@ pub mod json {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ListOp {
Insert {
pos: usize,
pos: u32,
value: LoroValue,
},
Delete {
pos: isize,
len: isize,
pos: i32,
len: i32,
#[serde(with = "self::serde_impl::id")]
start_id: ID,
},
@ -775,12 +774,12 @@ pub mod json {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MovableListOp {
Insert {
pos: usize,
pos: u32,
value: LoroValue,
},
Delete {
pos: isize,
len: isize,
pos: i32,
len: i32,
#[serde(with = "self::serde_impl::id")]
start_id: ID,
},
@ -812,8 +811,8 @@ pub mod json {
text: String,
},
Delete {
pos: isize,
len: isize,
pos: i32,
len: i32,
#[serde(with = "self::serde_impl::id")]
start_id: ID,
},
@ -939,11 +938,11 @@ pub mod json {
}
#[cfg(feature = "counter")]
ContainerType::Counter => {
let (_key, v) =
let (_key, value) =
map.next_entry::<String, OwnedValue>()?.unwrap();
super::JsonOpContent::Future(super::FutureOpWrapper {
prop: 0,
value: super::FutureOp::Counter(v),
value: super::FutureOp::Counter(value),
})
}
_ => unreachable!(),
@ -1038,7 +1037,7 @@ pub mod json {
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
D: Deserializer<'de>,
{

View file

@ -1 +0,0 @@
pub use loro_common::{LoroError, LoroResult};

View file

@ -68,14 +68,12 @@ pub mod delta;
pub use loro_delta;
pub mod event;
pub use error::{LoroError, LoroResult};
pub mod estimated_size;
pub(crate) mod history_cache;
pub(crate) mod macros;
pub(crate) mod state;
pub mod undo;
pub(crate) mod value;
pub(crate) use id::{PeerID, ID};
// TODO: rename as Key?
pub(crate) use loro_common::InternalString;
@ -84,6 +82,10 @@ pub use container::ContainerType;
pub use encoding::json_schema::json;
pub use fractional_index::FractionalIndex;
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")]
pub use value::wasm;
pub use value::{ApplyDiff, LoroValue, ToJson};

View file

@ -1472,7 +1472,7 @@ impl LoroDoc {
ExportMode::Snapshot => export_fast_snapshot(self),
ExportMode::Updates { from } => export_fast_updates(self, &from),
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::StateOnly(f) => match f {

View file

@ -73,11 +73,11 @@ pub(crate) struct NodePosition {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumAsInner, Serialize)]
pub enum TreeParentId {
Node(TreeID),
Root,
Deleted,
// We use `Unexist` as the old parent of a new node created
// so we can infer the retreat internal diff is `Uncreate`
Unexist,
Deleted,
Root,
}
impl From<Option<TreeID>> for TreeParentId {

View file

@ -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" }
delta = { path = "../delta", package = "loro-delta", version = "0.16.2" }
generic-btree = { version = "^0.10.5" }
enum-as-inner = "0.6.0"
either = "1.9.0"
tracing = "0.1"
enum-as-inner = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
serde_json = "1.0.87"

View file

@ -1,5 +1,5 @@
use loro_internal::{
container::ContainerID, handler::counter::CounterHandler, HandlerTrait, LoroResult, LoroValue,
container::ContainerID, handler::counter::CounterHandler, HandlerTrait, LoroResult,
};
use crate::{Container, ContainerTrait, SealedTrait};
@ -40,8 +40,8 @@ impl LoroCounter {
}
/// Get the current value of the counter.
pub fn get_value(&self) -> LoroValue {
self.handler.get_value()
pub fn get_value(&self) -> f64 {
self.handler.get_value().into_double().unwrap()
}
/// Get the current value of the counter

View file

@ -1,21 +1,16 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
use either::Either;
use event::{DiffEvent, Subscriber};
use loro_internal::container::IntoContainerId;
use loro_internal::cursor::CannotFindRelativePosition;
pub use loro_internal::cursor::CannotFindRelativePosition;
use loro_internal::cursor::Cursor;
use loro_internal::cursor::PosQueryResult;
use loro_internal::cursor::Side;
use loro_internal::encoding::ImportBlobMetadata;
use loro_internal::handler::HandlerTrait;
use loro_internal::handler::ValueOrHandler;
use loro_internal::loro_common::LoroTreeError;
use loro_internal::undo::{OnPop, OnPush};
use loro_internal::version::ImVersionVector;
pub use loro_internal::version::ImVersionVector;
use loro_internal::DocState;
use loro_internal::FractionalIndex;
use loro_internal::LoroDoc as InnerLoroDoc;
use loro_internal::OpLog;
use loro_internal::{
@ -35,31 +30,38 @@ pub use loro_internal::subscription::PeerIdUpdateCallback;
pub use loro_internal::ChangeMeta;
pub mod event;
pub use loro_internal::awareness;
pub use loro_internal::change::Timestamp;
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::{ContainerID, ContainerType};
pub use loro_internal::container::{ContainerID, ContainerType, IntoContainerId};
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::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::id::{PeerID, TreeID, ID};
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::loro::CommitOptions;
pub use loro_internal::loro::DocAnalysis;
pub use loro_internal::oplog::FrontiersNotIncluded;
pub use loro_internal::subscription::SubID;
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::Subscription;
pub use loro_internal::TreeParentId;
pub use loro_internal::UndoManager as InnerUndoManager;
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;
#[cfg(feature = "jsonpath")]
@ -274,7 +276,7 @@ impl LoroDoc {
///
/// Learn more at https://loro.dev/docs/advanced/doc_state_and_oplog#attacheddetached-status
#[inline]
pub fn detach(&mut self) {
pub fn detach(&self) {
self.doc.detach()
}
@ -756,7 +758,8 @@ impl LoroDoc {
/// # Example
///
/// ```
/// # use loro::LoroDoc;
/// # use loro::{LoroDoc, ToJson};
///
/// let doc = LoroDoc::new();
/// let map = doc.get_map("users");
/// map.insert("alice", 30).unwrap();
@ -764,7 +767,7 @@ impl LoroDoc {
///
/// let result = doc.jsonpath("$.users.alice").unwrap();
/// 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]
#[cfg(feature = "jsonpath")]
@ -923,10 +926,12 @@ impl LoroList {
/// Get the value at the given position.
#[inline]
pub fn get(&self, index: usize) -> Option<Either<LoroValue, Container>> {
pub fn get(&self, index: usize) -> Option<ValueOrContainer> {
match self.handler.get_(index) {
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
Some(ValueOrHandler::Handler(c)) => {
Some(ValueOrContainer::Container(Container::from_handler(c)))
}
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
None => None,
}
}
@ -973,11 +978,13 @@ impl LoroList {
}
/// 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
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.
@ -1202,11 +1209,13 @@ impl LoroMap {
}
/// 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) {
None => None,
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
Some(ValueOrHandler::Handler(c)) => {
Some(ValueOrContainer::Container(Container::from_handler(c)))
}
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
}
}
@ -1795,8 +1804,8 @@ impl LoroTree {
/// # Errors
///
/// - If the target node does not exist, return `LoroTreeError::TreeNodeNotExist`.
pub fn is_node_deleted(&self, target: TreeID) -> LoroResult<bool> {
self.handler.is_node_deleted(&target)
pub fn is_node_deleted(&self, target: &TreeID) -> LoroResult<bool> {
self.handler.is_node_deleted(target)
}
/// Return all nodes, including deleted nodes
@ -1845,7 +1854,7 @@ impl LoroTree {
.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
/// but also the metadata, you should use [TreeHandler::get_value_with_meta()].
@ -1853,7 +1862,7 @@ impl LoroTree {
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 {
self.handler.get_deep_value()
}
@ -1964,6 +1973,14 @@ impl LoroMovableList {
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.
pub fn insert(&self, pos: usize, v: impl Into<LoroValue>) -> LoroResult<()> {
self.handler.insert(pos, v)
@ -1975,10 +1992,12 @@ impl LoroMovableList {
}
/// 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) {
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
Some(ValueOrHandler::Handler(c)) => {
Some(ValueOrContainer::Container(Container::from_handler(c)))
}
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
None => None,
}
}
@ -2009,10 +2028,12 @@ impl LoroMovableList {
}
/// 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_()? {
Some(ValueOrHandler::Handler(c)) => Some(Either::Right(c.into())),
Some(ValueOrHandler::Value(v)) => Some(Either::Left(v)),
Some(ValueOrHandler::Handler(c)) => {
Some(ValueOrContainer::Container(Container::from_handler(c)))
}
Some(ValueOrHandler::Value(v)) => Some(ValueOrContainer::Value(v)),
None => None,
})
}
@ -2148,6 +2169,13 @@ pub struct LoroUnknown {
handler: InnerUnknownHandler,
}
impl LoroUnknown {
/// Get the container id.
pub fn id(&self) -> ContainerID {
self.handler.id().clone()
}
}
impl SealedTrait for LoroUnknown {}
impl ContainerTrait for LoroUnknown {
type Handler = InnerUnknownHandler;
@ -2360,7 +2388,7 @@ impl ValueOrContainer {
Container::Tree(c) => c.get_value(),
Container::MovableList(c) => c.get_deep_value(),
#[cfg(feature = "counter")]
Container::Counter(c) => c.get_value(),
Container::Counter(c) => c.get_value().into(),
Container::Unknown(_) => LoroValue::Null,
},
}