mirror of
https://github.com/hexedtech/codemp.git
synced 2024-12-23 05:14:54 +01:00
feat: Merge branch 'config' into dev
This commit is contained in:
commit
081a72d733
46 changed files with 697 additions and 401 deletions
34
.github/workflows/javascript.yml
vendored
34
.github/workflows/javascript.yml
vendored
|
@ -31,13 +31,13 @@ jobs:
|
|||
with:
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
working-directory: dist/js/publish
|
||||
- run: npx napi build --cargo-cwd=../../.. --platform --release --features=js --strip
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- run: npx napi build --cargo-cwd=../.. --platform --release --features=js --strip
|
||||
working-directory: dist/js
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codemp-js-${{ matrix.platform.target }}
|
||||
path: dist/js/publish/codemp.*.node
|
||||
path: dist/js/codemp.*.node
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -56,32 +56,34 @@ jobs:
|
|||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm install
|
||||
working-directory: dist/js/publish
|
||||
- run: npx napi build --cargo-cwd=../../.. --platform --features=js
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- run: npx napi build --cargo-cwd=../.. --platform --features=js
|
||||
working-directory: dist/js
|
||||
- run: rm *.node
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- run: npx napi create-npm-dir -t .; tree
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist/js/publish/artifacts
|
||||
path: dist/js/artifacts
|
||||
pattern: codemp-js-*
|
||||
- run: npx napi artifacts; tree
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- run: npx napi prepublish -t . --skip-gh-release
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: rm -rf *.node artifacts node_modules
|
||||
working-directory: dist/js/publish
|
||||
- run: rm -rf *.node artifacts node_modules npm
|
||||
working-directory: dist/js
|
||||
- run: cp ../../README.md .
|
||||
working-directory: dist/js
|
||||
# TODO this is a bit awful, but napi just appends the platform triplet to the resulting package name
|
||||
# however we want '@codemp/native-...' and 'codemp' (because otherwise it gets flagged as spam)
|
||||
# so we just sed out before releasing. this is really ugly but if it works right now i'll just
|
||||
# take it and think again about it later
|
||||
- run: sed -i 's/"@codemp\/native"/"codemp"/' package.json
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
- run: npm publish
|
||||
working-directory: dist/js/publish
|
||||
working-directory: dist/js
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
|||
- luajit # should we test with lua54 instead?
|
||||
- java
|
||||
- js
|
||||
- python
|
||||
- py
|
||||
toolchain:
|
||||
- stable
|
||||
# - beta
|
||||
|
|
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -228,7 +228,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "codemp"
|
||||
version = "0.7.0-beta.3"
|
||||
version = "0.7.0-beta.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codemp-proto",
|
||||
|
@ -242,6 +242,7 @@ dependencies = [
|
|||
"napi-derive",
|
||||
"pyo3",
|
||||
"pyo3-build-config 0.19.2",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
@ -388,6 +389,16 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
|
@ -862,11 +873,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5a52f529509c236114a5cf5bb3c0c06ff0695ad45d718256930ec2416edf3817"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"erased-serde",
|
||||
"mlua-sys",
|
||||
"mlua_derive",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde-value",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1020,6 +1034,15 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -1521,18 +1544,28 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.205"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.205"
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
|
||||
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||
dependencies = [
|
||||
"ordered-float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1939,6 +1972,12 @@ dependencies = [
|
|||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
|
@ -1970,6 +2009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -11,7 +11,7 @@ authors = [
|
|||
]
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
version = "0.7.0-beta.3"
|
||||
version = "0.7.0-beta.4"
|
||||
exclude = ["dist/*"]
|
||||
|
||||
[lib]
|
||||
|
@ -43,7 +43,7 @@ lazy_static = { version = "1.4", optional = true }
|
|||
jni = { version = "0.21", features = ["invocation"], optional = true }
|
||||
|
||||
# glue (lua)
|
||||
mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send"], optional = true }
|
||||
mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send", "serialize"], optional = true }
|
||||
|
||||
# glue (js)
|
||||
napi = { version = "2.16", features = ["full"], optional = true }
|
||||
|
@ -54,6 +54,7 @@ pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"], optiona
|
|||
|
||||
# extra
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
# glue (js)
|
||||
|
@ -65,18 +66,15 @@ pyo3-build-config = { version = "0.19", optional = true }
|
|||
default = []
|
||||
# extra
|
||||
async-trait = ["dep:async-trait"]
|
||||
serialize = ["dep:serde", "uuid/serde"]
|
||||
# ffi
|
||||
rust = [] # used for ci matrix
|
||||
java = ["lazy_static", "jni", "tracing-subscriber"]
|
||||
js = ["napi-build", "tracing-subscriber", "napi", "napi-derive"]
|
||||
python = ["pyo3", "tracing-subscriber", "pyo3-build-config"]
|
||||
lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static"]
|
||||
lua54 = ["lua", "mlua-codemp-patch/lua54"]
|
||||
lua53 = ["lua", "mlua-codemp-patch/lua53"]
|
||||
lua52 = ["lua", "mlua-codemp-patch/lua52"]
|
||||
lua51 = ["lua", "mlua-codemp-patch/lua51"]
|
||||
luajit = ["lua", "mlua-codemp-patch/luajit"]
|
||||
py = ["pyo3", "tracing-subscriber", "pyo3-build-config"]
|
||||
lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/lua54"]
|
||||
luajit = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/luajit"]
|
||||
|
||||
|
||||
[package.metadata.docs.rs] # enabled features when building on docs.rs
|
||||
features = ["lua", "java", "js", "python"]
|
||||
features = ["serialize"]
|
||||
|
|
4
build.rs
4
build.rs
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "js")]
|
||||
extern crate napi_build;
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[cfg(feature = "py")]
|
||||
extern crate pyo3_build_config;
|
||||
|
||||
/// The main method of the buildscript, required by some glue modules.
|
||||
|
@ -11,7 +11,7 @@ fn main() {
|
|||
napi_build::setup();
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[cfg(feature = "py")]
|
||||
{
|
||||
pyo3_build_config::add_extension_module_link_args();
|
||||
}
|
||||
|
|
46
dist/README.md
vendored
Normal file
46
dist/README.md
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Compiling and Distributing FFI-compatible binaries
|
||||
`codemp` aims to target as many platforms as possible, while remaining maintainable and performant.
|
||||
|
||||
To guarantee this, it can compile to a bare rust lib but also 4 different FFI-compatible shared objects: JavaScript, Python, Lua, Java.
|
||||
|
||||
> We also plan to offer bare C bindings for every other language which can do C interop, but it's not our top priority right now.
|
||||
|
||||
To compile the bare FFI-compatible shared object, just `cargo build --release --features=<lang>`, replacing `<lang>` with either `js`, `py`, `java`, `lua` or `luajit`.
|
||||
In most languages, just importing the resulting shared object will work, however refer to each language's section below for more in-depth information.
|
||||
|
||||
## JavaScript
|
||||
To build a npm package, `napi-cli` must first be installed: `npm install napi-cli`.
|
||||
|
||||
You can then `npx napi build` in the project root to compile the native extension and create the type annotations (`index.d.ts`).
|
||||
A package.json is provided for publishing, but will require some tweaking.
|
||||
|
||||
## Python
|
||||
To distribute the native extension we can leverage python wheels. It will be necessary to build the relevant wheels with [`maturin`](https://github.com/PyO3/maturin).
|
||||
After installing with `pip install maturin`, run `maturin build` to obtain an `import`able package and installable wheels.
|
||||
|
||||
## Lua
|
||||
Built Lua bindings are valid lua modules and require no extra steps to be used.
|
||||
|
||||
## Java
|
||||
`codemp`'s Java bindings are implemented using the [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/).
|
||||
|
||||
On the Rust side, all Java-related code is gated behind the `java` feature, and is implemented using [`jni`](https://github.com/jni-rs/jni-rs) crate.
|
||||
|
||||
Unlike other supported languages, Java is statically typed and requires knowing all foreign function types at compile time.
|
||||
This means that, to use `codemp` through the JNI, all functions who need to be called must also be declared on the Java side, marked as `native`.
|
||||
|
||||
Thus, we also provide pre-made Java glue code, wrapping all native calls and defining classes to hold `codemp` types.
|
||||
|
||||
The Java bindings have no known major quirk. However, here are a list of facts that are useful to know when developing with these:
|
||||
|
||||
* Memory management is entirely delegated to the JVM's garbage collector.
|
||||
* A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually.
|
||||
* Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`.
|
||||
* `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
||||
|
||||
### Using
|
||||
`codemp` **will be available soon** as an artifact on [Maven Central](https://mvnrepository.com)
|
||||
|
||||
### Building
|
||||
This is a [Gradle](https://gradle.org/) project: building requires having both Gradle and Cargo installed, as well as the JDK (any non-abandoned version).
|
||||
Once you have all the requirements, building is as simple as running `gradle build`: the output is going to be a JAR under `build/libs`, which you can import into your classpath with your IDE of choice.
|
17
dist/java/README.md
vendored
17
dist/java/README.md
vendored
|
@ -1,17 +0,0 @@
|
|||
# Java bindings
|
||||
`codemp`'s Java bindings are implemented using the [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/).
|
||||
|
||||
On the Rust side, all Java-related code is gated behind the `java` feature, and is implemented using[`jni-rs`](https://github.com/jni-rs/jni-rs).
|
||||
Unlike other languages, Java requires glue code on both sides: as a result, a Java component is necessary.
|
||||
|
||||
## Building
|
||||
This is a [Gradle](https://gradle.org/) project: building requires having both Gradle and Cargo installed, as well as the JDK (any non-abandoned version).
|
||||
Once you have all the requirements, building is as simple as running `gradle build`: the output is going to be a JAR under `build/libs`, which you can import into your classpath with your IDE of choice.
|
||||
|
||||
## Development
|
||||
The Java bindings have no known major quirk. However, here are a list of facts that are useful to know when developing with these:
|
||||
|
||||
* Memory management is entirely delegated to the JVM's garbage collector.
|
||||
* A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually.
|
||||
* Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`.
|
||||
* `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
6
dist/java/src/mp/code/BufferController.java
vendored
6
dist/java/src/mp/code/BufferController.java
vendored
|
@ -1,5 +1,6 @@
|
|||
package mp.code;
|
||||
|
||||
import mp.code.data.Callback;
|
||||
import mp.code.data.Cursor;
|
||||
import mp.code.data.TextChange;
|
||||
import mp.code.exceptions.ControllerException;
|
||||
|
@ -38,6 +39,11 @@ public class BufferController {
|
|||
send(this.ptr, change);
|
||||
}
|
||||
|
||||
private static native void callback(long self, Callback<BufferController> cb);
|
||||
public void callback(Callback<BufferController> cb) {
|
||||
callback(this.ptr, cb);
|
||||
}
|
||||
|
||||
private static native void free(long self);
|
||||
@Override
|
||||
protected void finalize() {
|
||||
|
|
20
dist/java/src/mp/code/Client.java
vendored
20
dist/java/src/mp/code/Client.java
vendored
|
@ -1,6 +1,7 @@
|
|||
package mp.code;
|
||||
|
||||
import cz.adamh.utils.NativeUtils;
|
||||
import mp.code.data.User;
|
||||
import mp.code.exceptions.ConnectionException;
|
||||
import mp.code.exceptions.ConnectionRemoteException;
|
||||
|
||||
|
@ -10,14 +11,16 @@ import java.util.Optional;
|
|||
public class Client {
|
||||
private final long ptr;
|
||||
|
||||
public static native Client connect(String url, String username, String password) throws ConnectionException;
|
||||
public static native Client connect(String username, String password) throws ConnectionException;
|
||||
public static native Client connectToServer(String username, String password, String host, int port, boolean tls) throws ConnectionException;
|
||||
|
||||
Client(long ptr) {
|
||||
this.ptr = ptr;
|
||||
}
|
||||
|
||||
private static native String get_url(long self);
|
||||
public String getUrl() {
|
||||
return get_url(this.ptr);
|
||||
private static native User get_user(long self);
|
||||
public User getUser() {
|
||||
return get_user(this.ptr);
|
||||
}
|
||||
|
||||
private static native Workspace join_workspace(long self, String id) throws ConnectionException;
|
||||
|
@ -45,6 +48,11 @@ public class Client {
|
|||
return list_workspaces(this.ptr, owned, invited);
|
||||
}
|
||||
|
||||
private static native String[] active_workspaces(long self);
|
||||
public String[] activeWorkspaces() {
|
||||
return active_workspaces(this.ptr);
|
||||
}
|
||||
|
||||
private static native boolean leave_workspace(long self, String id);
|
||||
public boolean leaveWorkspace(String id) {
|
||||
return leave_workspace(this.ptr, id);
|
||||
|
@ -55,9 +63,9 @@ public class Client {
|
|||
return Optional.ofNullable(get_workspace(this.ptr, workspace));
|
||||
}
|
||||
|
||||
private static native void refresh_native(long self) throws ConnectionRemoteException;
|
||||
private static native void refresh(long self) throws ConnectionRemoteException;
|
||||
public void refresh() throws ConnectionRemoteException {
|
||||
refresh_native(this.ptr);
|
||||
refresh(this.ptr);
|
||||
}
|
||||
|
||||
private static native void free(long self);
|
||||
|
|
6
dist/java/src/mp/code/CursorController.java
vendored
6
dist/java/src/mp/code/CursorController.java
vendored
|
@ -1,5 +1,6 @@
|
|||
package mp.code;
|
||||
|
||||
import mp.code.data.Callback;
|
||||
import mp.code.data.Cursor;
|
||||
import mp.code.exceptions.ControllerException;
|
||||
|
||||
|
@ -27,6 +28,11 @@ public class CursorController {
|
|||
send(this.ptr, cursor);
|
||||
}
|
||||
|
||||
private static native void callback(long self, Callback<CursorController> cb);
|
||||
public void callback(Callback<CursorController> cb) {
|
||||
callback(this.ptr, cb);
|
||||
}
|
||||
|
||||
private static native void free(long self);
|
||||
@Override
|
||||
protected void finalize() {
|
||||
|
|
6
dist/java/src/mp/code/Workspace.java
vendored
6
dist/java/src/mp/code/Workspace.java
vendored
|
@ -30,9 +30,9 @@ public class Workspace {
|
|||
return Optional.ofNullable(get_buffer(this.ptr, path));
|
||||
}
|
||||
|
||||
private static native String[] get_file_tree(long self, String filter);
|
||||
public String[] getFileTree(Optional<String> filter) {
|
||||
return get_file_tree(this.ptr, filter.orElse(null));
|
||||
private static native String[] get_file_tree(long self, String filter, boolean strict);
|
||||
public String[] getFileTree(Optional<String> filter, boolean strict) {
|
||||
return get_file_tree(this.ptr, filter.orElse(null), strict);
|
||||
}
|
||||
|
||||
private static native void create_buffer(String path) throws ConnectionRemoteException;
|
||||
|
|
6
dist/java/src/mp/code/data/Callback.java
vendored
Normal file
6
dist/java/src/mp/code/data/Callback.java
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
package mp.code.data;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Callback<T> {
|
||||
void invoke(T controller);
|
||||
}
|
13
dist/java/src/mp/code/data/User.java
vendored
Normal file
13
dist/java/src/mp/code/data/User.java
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
package mp.code.data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class User {
|
||||
public final UUID id;
|
||||
public final String name;
|
||||
|
||||
public User(UUID id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
18
dist/js/README.md
vendored
18
dist/js/README.md
vendored
|
@ -1,18 +0,0 @@
|
|||
# JavaScript bindings
|
||||
NodeJS allows directly `require`ing properly formed shared objects, so the glue can live mostly on the Rust side.
|
||||
|
||||
Our JavaScript glue is built with [`napi`](https://napi.rs).
|
||||
|
||||
To get a usable shared object just `cargo build --release --features=js`, however preparing a proper javascript package to be included as dependency requires more steps.
|
||||
|
||||
## `npm`
|
||||
|
||||
`codemp` is directly available on `npm` as [`codemp`](https://npmjs.org/package/codemp).
|
||||
|
||||
## Building
|
||||
|
||||
To build a node package, `napi-cli` must first be installed: `npm install napi-cli`.
|
||||
|
||||
You can then `npx napi build` in the project root to compile the native extension and create the type annotations (`index.d.ts`).
|
||||
A package.json is provided for publishing, but will require some tweaking.
|
||||
|
1
dist/js/build.sh
vendored
1
dist/js/build.sh
vendored
|
@ -1 +0,0 @@
|
|||
npx napi build --cargo-cwd ../..
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@codemp/native",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.10",
|
||||
"description": "code multiplexer -- javascript bindings",
|
||||
"keywords": [
|
||||
"codemp",
|
||||
|
@ -35,8 +35,8 @@
|
|||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@codemp/native-win32-x64-msvc": "0.0.4",
|
||||
"@codemp/native-darwin-arm64": "0.0.4",
|
||||
"@codemp/native-linux-x64-gnu": "0.0.4"
|
||||
"@codemp/native-win32-x64-msvc": "0.0.10",
|
||||
"@codemp/native-darwin-arm64": "0.0.10",
|
||||
"@codemp/native-linux-x64-gnu": "0.0.10"
|
||||
}
|
||||
}
|
89
dist/js/publish/README.md
vendored
89
dist/js/publish/README.md
vendored
|
@ -1,89 +0,0 @@
|
|||
[![codemp](https://code.mp/static/banner.png)](https://code.mp)
|
||||
|
||||
[![Actions Status](https://github.com/hexedtech/codemp/actions/workflows/ci.yml/badge.svg)](https://github.com/hexedtech/codemp/actions)
|
||||
[![docs.rs](https://img.shields.io/docsrs/codemp)](https://docs.rs/codemp/0.7.0-beta.4/codemp/)
|
||||
[![Crates.io Version](https://img.shields.io/crates/v/codemp)](https://crates.io/crates/codemp)
|
||||
[![NPM Version](https://img.shields.io/npm/v/codemp)](https://npmjs.org/package/codemp)
|
||||
[![PyPI - Version](https://img.shields.io/pypi/v/codemp)](https://pypi.org/project/codemp)
|
||||
[![Crates.io License](https://img.shields.io/crates/l/codemp)](https://github.com/hexedtech/codemp/blob/dev/LICENSE)
|
||||
[![Gitter](https://img.shields.io/gitter/room/hexedtech/codemp)](https://gitter.im/hexedtech/codemp)
|
||||
|
||||
> `codemp` is a **collaborative** text editing solution to work remotely.
|
||||
|
||||
It seamlessly integrates in your editor providing remote cursors and instant text synchronization,
|
||||
as well as a remote virtual workspace for you and your team.
|
||||
|
||||
> `codemp` is build with state-of-the-art CRDT technology, guaranteeing eventual consistency.
|
||||
|
||||
This means everyone in a workspace will always be working on the exact same file _eventually_:
|
||||
even under unreliable networks or constrained resources, the underlying CRDT will always reach a
|
||||
convergent state across all users. Even with this baseline, `codemp`'s protocol is optimized for speed
|
||||
and low network footprint, meaning even slow connections can provide stable real-time editing.
|
||||
|
||||
The full documentation is available on [docs.rs](https://docs.rs/codemp/0.7.0-beta.4/codemp/).
|
||||
|
||||
# Usage
|
||||
`codemp` is primarily used as a plugin in your editor of choice.
|
||||
|
||||
## Installation
|
||||
> [!IMPORTANT]
|
||||
> The editor plugins are in active development. Expect frequent changes.
|
||||
|
||||
`codemp` is available as a plugin for a growing number of text editors. Currently we support:
|
||||
- [NeoVim](https://github.com/hexedtech/codemp-nvim)
|
||||
- [VSCode](https://github.com/hexedtech/codemp-vscode)
|
||||
- [Sublime Text](https://github.com/hexedtech/codemp-sublime)
|
||||
<!-- - [IntelliJ Platform](https://github.com/hexedtech/codemp-intellij) -->
|
||||
|
||||
## Registration
|
||||
The `codemp` protocol is [openly available](https://github.com/hexedtech/codemp-proto/) and servers may be freely developed with it.
|
||||
|
||||
A reference instance is provided by hexed.technology at [code.mp](https://code.mp). You may create an account for it [here](https://code.mp/register).
|
||||
During the initial closed beta, registrations will require an invite code. Get in contact if interested.
|
||||
|
||||
An open beta is going to follow with free access to a single workspace per user.
|
||||
After such period, [code.mp](https://code.mp) will switch to a subscription-based model.
|
||||
|
||||
# Development
|
||||
This is the main client library for `codemp`. It provides a batteries-included fully-featured `Client`, managed by the library itself, and exposes a number of functions to interact with it. The host program can obtain a `Client` handle by connecting, and from that reference can retrieve every other necessary component.
|
||||
|
||||
`codemp` is primarily a rlib and can be used as such, but is also available in other languages via FFI.
|
||||
|
||||
Adding a dependency on `codemp` is **easy**:
|
||||
|
||||
### From Rust
|
||||
Just `cargo add codemp` and check the docs for some examples.
|
||||
|
||||
### From supported languages
|
||||
We provide first-class bindings for:
|
||||
- [JavaScript](./dist/js/README.md): available from `npm` as [`codemp`](https://npmjs.org/package/codemp)
|
||||
- [Python](./dist/lua/README.md): available from `PyPI` as [`codemp`](https://pypi.org/project/codemp)
|
||||
- [Lua](./dist/lua/README.md): run `cargo build --features=lua`
|
||||
- [Java](./dist/java/README.md): run `gradle build` in `dist/java/` (requires Gradle)
|
||||
|
||||
As a design philosophy, our binding APIs attempt to perfectly mimic their Rust counterparts, so the main documentation can still be referenced as source of truth.
|
||||
Refer to specific language documentation for specifics, differences and quirks.
|
||||
|
||||
### From other languages
|
||||
> [!IMPORTANT]
|
||||
> The common C bindings are not available yet!
|
||||
|
||||
Any other language with C FFI capabilities will be able to use `codemp` via its bare C bindings.
|
||||
This may be more complex and may require wrapping the native calls underneath.
|
||||
|
||||
# Get in Touch
|
||||
We love to hear back from users! Be it to give feedback, propose new features or highlight bugs, don't hesitate to reach out!
|
||||
|
||||
## Contacts
|
||||
We have a public [Gitter](https://gitter.im) room available on [gitter.im/hexedtech/codemp](https://gitter.im/hexedtech/codemp).
|
||||
It's possible to freely browse the room history, but to send new messages it will be necessary to sign in with your GitHub account.
|
||||
|
||||
If you have a [Matrix](https://matrix.org) account, you can join the gitter room directly at [#hexedtech_codemp:gitter.im](https://matrix.to/#/#hexedtech_codemp:gitter.im)
|
||||
|
||||
## Contributing
|
||||
If you find bugs or would like to see new features implemented, be sure to open an issue on this repository.
|
||||
|
||||
> [!WARNING]
|
||||
> The CLA necessary for code contributions is not yet available!
|
||||
|
||||
In case you wish to contribute code, that's great! We love external contributions, but we require you to sign our CLA first (available soon).
|
19
dist/lua/README.md
vendored
19
dist/lua/README.md
vendored
|
@ -1,19 +0,0 @@
|
|||
# Lua bindings
|
||||
Lua allows directly `require`ing properly constructed shared objects, so glue code can live completely on the Rust side.
|
||||
|
||||
The Lua-compatible wrappers are built with [`mlua`](https://github.com/mlua-rs/mlua).
|
||||
|
||||
To build, just `cargo build --release --features=lua` and rename the resulting `libcodemp.so` / `codemp.dll` / `codemp.dylib` in `codemp_native.so/dll/dylib`.
|
||||
This is important because Lua looks up the constructor symbol based on filename.
|
||||
|
||||
Type hints are provided in `annotations.lua`, just include them in your language server: `---@module 'annotations'`.
|
||||
|
||||
## LuaRocks
|
||||
`codemp` is available as a rock on [LuaRocks](https://luarocks.org/modules/alemi/codemp)
|
||||
|
||||
## Manual bundling
|
||||
LuaRocks compiles from source, which only works if have the rust toolchain available. To provide a reasonable NeoVim experience, we provide pre-built binaries.
|
||||
|
||||
> Download latest build and annotations from [here](https://codemp.dev/releases/lua/)
|
||||
|
||||
You will need a loader file to provide annotations: you can use provided `codemp.lua`
|
39
dist/lua/annotations.lua
vendored
39
dist/lua/annotations.lua
vendored
|
@ -147,10 +147,11 @@ function Workspace:attach_buffer(path) end
|
|||
---detach from an active buffer, closing all streams. returns false if buffer was no longer active
|
||||
function Workspace:detach_buffer(path) end
|
||||
|
||||
---@param filter? string only return elements starting with given filter
|
||||
---@param filter? string apply a filter to the return elements
|
||||
---@param strict boolean whether to strictly match or just check whether it starts with it
|
||||
---@return string[]
|
||||
---return the list of available buffers in this workspace, as relative paths from workspace root
|
||||
function Workspace:filetree(filter) end
|
||||
function Workspace:filetree(filter, strict) end
|
||||
|
||||
---@return NilPromise
|
||||
---@async
|
||||
|
@ -181,21 +182,19 @@ function Workspace:event() end
|
|||
---handle to a remote buffer, for async send/recv operations
|
||||
local BufferController = {}
|
||||
|
||||
---@class (exact) TextChange
|
||||
---@class TextChange
|
||||
---@field content string text content of change
|
||||
---@field first integer start index of change
|
||||
---@field last integer end index of change
|
||||
---@field hash integer? optional hash of text buffer after this change, for sync checks
|
||||
---@field apply fun(self: TextChange, other: string): string apply this text change to a string
|
||||
|
||||
---@param first integer change start index
|
||||
---@param last integer change end index
|
||||
---@param content string change content
|
||||
---@param change TextChange text change to broadcast
|
||||
---@return NilPromise
|
||||
---@async
|
||||
---@nodiscard
|
||||
---update buffer with a text change; note that to delete content should be empty but not span, while to insert span should be empty but not content (can insert and delete at the same time)
|
||||
function BufferController:send(first, last, content) end
|
||||
function BufferController:send(change) end
|
||||
|
||||
---@return MaybeTextChangePromise
|
||||
---@async
|
||||
|
@ -239,28 +238,24 @@ function BufferController:content() end
|
|||
---handle to a workspace's cursor channel, allowing send/recv operations
|
||||
local CursorController = {}
|
||||
|
||||
---@class (exact) RowCol
|
||||
---@class RowCol
|
||||
---@field row integer row number
|
||||
---@field col integer column number
|
||||
---row and column tuple
|
||||
|
||||
---@class (exact) Cursor
|
||||
---@class Cursor
|
||||
---@field user string? id of user owning this cursor
|
||||
---@field buffer string relative path ("name") of buffer on which this cursor is
|
||||
---@field start RowCol cursor start position
|
||||
---@field finish RowCol cursor end position
|
||||
---a cursor position
|
||||
|
||||
---@param buffer string buffer relative path ("name") to send this cursor on
|
||||
---@param start_row integer cursor start row
|
||||
---@param start_col integer cursor start col
|
||||
---@param end_row integer cursor end row
|
||||
---@param end_col integer cursor end col
|
||||
---@param cursor Cursor cursor event to broadcast
|
||||
---@return NilPromise
|
||||
---@async
|
||||
---@nodiscard
|
||||
---update cursor position by sending a cursor event to server
|
||||
function CursorController:send(buffer, start_row, start_col, end_row, end_col) end
|
||||
function CursorController:send(cursor) end
|
||||
|
||||
|
||||
---@return MaybeCursorPromise
|
||||
|
@ -294,18 +289,24 @@ function CursorController:callback(cb) end
|
|||
|
||||
|
||||
|
||||
|
||||
---@class Config
|
||||
---@field username string user identifier used to register, possibly your email
|
||||
---@field password string user password chosen upon registration
|
||||
---@field host string | nil address of server to connect to, default api.code.mp
|
||||
---@field port integer | nil port to connect to, default 50053
|
||||
---@field tls boolean | nil enable or disable tls, default true
|
||||
|
||||
---@class (exact) Codemp
|
||||
---the codemp shared library
|
||||
local Codemp = {}
|
||||
|
||||
---@param host string server host to connect to
|
||||
---@param username string username used to log in (usually email)
|
||||
---@param password string password used to log in
|
||||
---@param config Config configuration for
|
||||
---@return ClientPromise
|
||||
---@async
|
||||
---@nodiscard
|
||||
---connect to codemp server, authenticate and return client
|
||||
function Codemp.connect(host, username, password) end
|
||||
function Codemp.connect(config) end
|
||||
|
||||
---@return function | nil
|
||||
---@nodiscard
|
||||
|
|
14
dist/py/README.md
vendored
14
dist/py/README.md
vendored
|
@ -1,14 +0,0 @@
|
|||
# Python bindings
|
||||
Python allows directly `import`ing properly formed shared objects, so the glue can live mostly on the Rust side.
|
||||
|
||||
Our Python glue is built with [`PyO3`](https://pyo3.rs).
|
||||
|
||||
To get a usable shared object just `cargo build --release --features=python`, however preparing a proper python package to be included as dependency requires more steps.
|
||||
|
||||
## `PyPI`
|
||||
|
||||
`codemp` is directly available on `PyPI` as [`codemp`](https://pypi.org/project/codemp).
|
||||
|
||||
## Building
|
||||
To distribute the native extension we can leverage python wheels. It will be necessary to build the relevant wheels with [`maturin`](https://github.com/PyO3/maturin).
|
||||
After installing with `pip install maturin`, run `maturin build` to obtain an `import`able package.
|
14
dist/py/src/codemp/codemp.pyi
vendored
14
dist/py/src/codemp/codemp.pyi
vendored
|
@ -7,10 +7,20 @@ class Driver:
|
|||
"""
|
||||
def stop(self) -> None: ...
|
||||
|
||||
def get_default_config() -> Config: ...
|
||||
class Config:
|
||||
"""
|
||||
Configuration data structure for codemp clients
|
||||
"""
|
||||
username: str
|
||||
password: str
|
||||
host: Optional[str]
|
||||
port: Optional[int]
|
||||
tls: Optional[bool]
|
||||
|
||||
def init() -> Driver: ...
|
||||
def set_logger(logger_cb: Callable[[str], None], debug: bool) -> bool: ...
|
||||
def connect(host: str, username: str, password: str) -> Promise[Client]: ...
|
||||
def connect(config: Config) -> Promise[Client]: ...
|
||||
|
||||
class Promise[T]:
|
||||
"""
|
||||
|
@ -57,7 +67,7 @@ class Workspace:
|
|||
def cursor(self) -> CursorController: ...
|
||||
def buffer_by_name(self, path: str) -> Optional[BufferController]: ...
|
||||
def buffer_list(self) -> list[str]: ...
|
||||
def filetree(self, filter: Optional[str]) -> list[str]: ...
|
||||
def filetree(self, filter: Optional[str], strict: bool) -> list[str]: ...
|
||||
|
||||
class TextChange:
|
||||
"""
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
///
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass(get_all))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TextChange {
|
||||
/// Range start of text change, as char indexes in buffer previous state.
|
||||
pub start: u32,
|
||||
|
@ -41,7 +42,7 @@ impl TextChange {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "python", pyo3::pymethods)]
|
||||
#[cfg_attr(feature = "py", pyo3::pymethods)]
|
||||
impl TextChange {
|
||||
/// Returns true if this [`TextChange`] deletes existing text.
|
||||
///
|
||||
|
|
63
src/api/config.rs
Normal file
63
src/api/config.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
//! # Config
|
||||
//! Data structure defining clients configuration
|
||||
|
||||
/// Configuration struct for `codemp` client
|
||||
///
|
||||
/// username and password are required fields, while everything else is optional
|
||||
///
|
||||
/// host, port and tls affect all connections to all grpc services
|
||||
/// resulting endpoint is composed like this:
|
||||
/// http{tls?'s':''}://{host}:{port}
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Config {
|
||||
/// user identifier used to register, possibly your email
|
||||
pub username: String,
|
||||
/// user password chosen upon registration
|
||||
pub password: String,
|
||||
/// address of server to connect to, default api.code.mp
|
||||
pub host: Option<String>,
|
||||
/// port to connect to, default 50053
|
||||
pub port: Option<u16>,
|
||||
/// enable or disable tls, default true
|
||||
pub tls: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// construct a new Config object, with given username and password
|
||||
pub fn new(username: String, password: String) -> Self {
|
||||
Self {
|
||||
username,
|
||||
password,
|
||||
host: None,
|
||||
port: None,
|
||||
tls: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn host(&self) -> &str {
|
||||
self.host.as_deref().unwrap_or("api.code.mp")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn port(&self) -> u16 {
|
||||
self.port.unwrap_or(50053)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn tls(&self) -> bool {
|
||||
self.tls.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint(&self) -> String {
|
||||
format!(
|
||||
"{}://{}:{}",
|
||||
if self.tls() { "https" } else { "http" },
|
||||
self.host(),
|
||||
self.port()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
//! ### Cursor
|
||||
//! Represents the position of a remote user's cursor.
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[cfg(feature = "py")]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// User cursor position in a buffer
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "python", pyclass)]
|
||||
// #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))]
|
||||
#[cfg_attr(feature = "py", pyclass)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))]
|
||||
pub struct Cursor {
|
||||
/// Cursor start position in buffer, as 0-indexed row-column tuple.
|
||||
pub start: (i32, i32),
|
||||
|
|
|
@ -4,7 +4,8 @@ use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner;
|
|||
|
||||
/// Event in a [crate::Workspace].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Event {
|
||||
/// Fired when the file tree changes.
|
||||
/// Contains the modified buffer path (deleted, created or renamed).
|
||||
|
|
|
@ -7,6 +7,9 @@ pub mod controller;
|
|||
/// a generic representation of a text change
|
||||
pub mod change;
|
||||
|
||||
/// client configuration
|
||||
pub mod config;
|
||||
|
||||
/// representation for an user's cursor
|
||||
pub mod cursor;
|
||||
|
||||
|
@ -18,6 +21,7 @@ pub mod user;
|
|||
|
||||
pub use controller::Controller;
|
||||
pub use change::TextChange;
|
||||
pub use config::Config;
|
||||
pub use cursor::Cursor;
|
||||
pub use event::Event;
|
||||
pub use user::User;
|
||||
|
|
|
@ -6,6 +6,7 @@ use uuid::Uuid;
|
|||
|
||||
/// Represents a service user
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct User {
|
||||
/// User unique identifier, should never change.
|
||||
pub id: Uuid,
|
||||
|
|
|
@ -18,7 +18,7 @@ use super::worker::DeltaRequest;
|
|||
/// Each buffer controller internally tracks the last acknowledged state, remaining always in sync
|
||||
/// with the server while allowing to procedurally receive changes while still sending new ones.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi)]
|
||||
pub struct BufferController(pub(crate) Arc<BufferControllerInner>);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ use codemp_proto::{
|
|||
common::{Empty, Token}, session::{session_client::SessionClient, InviteRequest, WorkspaceRequest},
|
||||
};
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[cfg(feature = "py")]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// A `codemp` client handle.
|
||||
|
@ -22,13 +22,13 @@ use pyo3::prelude::*;
|
|||
/// A new [`Client`] can be obtained with [`Client::connect`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "python", pyclass)]
|
||||
#[cfg_attr(feature = "py", pyclass)]
|
||||
pub struct Client(Arc<ClientInner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientInner {
|
||||
user: User,
|
||||
host: String,
|
||||
config: crate::api::Config,
|
||||
workspaces: DashMap<String, Workspace>,
|
||||
auth: AuthClient<Channel>,
|
||||
session: SessionClient<InterceptedService<Channel, network::SessionInterceptor>>,
|
||||
|
@ -37,39 +37,29 @@ struct ClientInner {
|
|||
|
||||
impl Client {
|
||||
/// Connect to the server, authenticate and instantiate a new [`Client`].
|
||||
pub async fn connect(
|
||||
host: impl AsRef<str>,
|
||||
username: impl AsRef<str>,
|
||||
password: impl AsRef<str>,
|
||||
) -> ConnectionResult<Self> {
|
||||
let host = if host.as_ref().starts_with("http") {
|
||||
host.as_ref().to_string()
|
||||
} else {
|
||||
format!("https://{}", host.as_ref())
|
||||
};
|
||||
|
||||
let channel = Endpoint::from_shared(host.clone())?.connect().await?;
|
||||
pub async fn connect(config: crate::api::Config) -> ConnectionResult<Self> {
|
||||
// TODO move these two into network.rs
|
||||
let channel = Endpoint::from_shared(config.endpoint())?.connect().await?;
|
||||
let mut auth = AuthClient::new(channel.clone());
|
||||
|
||||
let resp = auth.login(LoginRequest {
|
||||
username: username.as_ref().to_string(),
|
||||
password: password.as_ref().to_string(),
|
||||
username: config.username.clone(),
|
||||
password: config.password.clone(),
|
||||
})
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
let claims = InternallyMutable::new(resp.token);
|
||||
|
||||
// TODO move this one into network.rs
|
||||
let session = SessionClient::with_interceptor(
|
||||
channel, network::SessionInterceptor(claims.channel())
|
||||
);
|
||||
|
||||
Ok(Client(Arc::new(ClientInner {
|
||||
host,
|
||||
user: resp.user.into(),
|
||||
workspaces: DashMap::default(),
|
||||
claims,
|
||||
auth, session,
|
||||
claims, auth, session, config
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -139,7 +129,7 @@ impl Client {
|
|||
let ws = Workspace::try_new(
|
||||
workspace.as_ref().to_string(),
|
||||
self.0.user.clone(),
|
||||
&self.0.host,
|
||||
self.0.config.clone(),
|
||||
token,
|
||||
self.0.claims.channel(),
|
||||
)
|
||||
|
|
|
@ -12,7 +12,7 @@ use codemp_proto::{cursor::{CursorPosition, RowCol}, files::BufferNode};
|
|||
///
|
||||
/// An unique [CursorController] exists for each active [crate::Workspace].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi)]
|
||||
pub struct CursorController(pub(crate) Arc<CursorControllerInner>);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/// This currently wraps an [http code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status),
|
||||
/// returned as procedure status.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("server rejected procedure with error code: {0}")]
|
||||
#[error("server rejected procedure with error code: {0:?}")]
|
||||
pub struct RemoteError(#[from] tonic::Status);
|
||||
|
||||
/// Wraps [std::result::Result] with a [RemoteError].
|
||||
|
@ -16,11 +16,11 @@ pub type RemoteResult<T> = std::result::Result<T, RemoteError>;
|
|||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConnectionError {
|
||||
/// Underlying [`tonic::transport::Error`].
|
||||
#[error("transport error: {0}")]
|
||||
#[error("transport error: {0:?}")]
|
||||
Transport(#[from] tonic::transport::Error),
|
||||
|
||||
/// Error from the remote server, see [`RemoteError`].
|
||||
#[error("server rejected connection attempt: {0}")]
|
||||
#[error("server rejected connection attempt: {0:?}")]
|
||||
Remote(#[from] RemoteError),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use jni::{objects::{JClass, JObject, JValueGen}, sys::{jlong, jobject, jstring}, JNIEnv};
|
||||
|
||||
use crate::api::Controller;
|
||||
use crate::{api::Controller, ffi::java::handle_callback};
|
||||
|
||||
use super::{JExceptable, RT};
|
||||
|
||||
|
@ -87,3 +87,70 @@ fn recv_jni(env: &mut JNIEnv, change: Option<crate::api::TextChange>) -> jobject
|
|||
}
|
||||
}.as_raw()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_BufferController_callback<'local>(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass<'local>,
|
||||
self_ptr: jlong,
|
||||
cb: JObject<'local>,
|
||||
) {
|
||||
handle_callback!("mp/code/BufferController", env, self_ptr, cb, crate::buffer::Controller);
|
||||
}
|
||||
|
||||
/// Receive from Java, converts and sends a [crate::api::TextChange].
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_BufferController_send<'local>(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass<'local>,
|
||||
self_ptr: jlong,
|
||||
input: JObject<'local>,
|
||||
) {
|
||||
let Ok(start) = env.get_field(&input, "start", "I")
|
||||
.and_then(|sr| sr.i())
|
||||
.jexcept(&mut env)
|
||||
.try_into()
|
||||
else {
|
||||
env.throw_new("java/lang/IllegalArgumentException", "Start index cannot be negative!")
|
||||
.expect("Failed to throw exception!");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(end) = env.get_field(&input, "end", "I")
|
||||
.and_then(|er| er.i())
|
||||
.jexcept(&mut env)
|
||||
.try_into()
|
||||
else {
|
||||
env.throw_new("java/lang/IllegalArgumentException", "End index cannot be negative!")
|
||||
.expect("Failed to throw exception!");
|
||||
return;
|
||||
};
|
||||
|
||||
let content = env.get_field(&input, "content", "Ljava/lang/String;")
|
||||
.and_then(|b| b.l())
|
||||
.map(|b| b.into())
|
||||
.jexcept(&mut env);
|
||||
let content = env.get_string(&content)
|
||||
.map(|b| b.into())
|
||||
.jexcept(&mut env);
|
||||
|
||||
let hash = env.get_field(&input, "hash", "Ljava/util/OptionalLong")
|
||||
.and_then(|hash| hash.l())
|
||||
.and_then(|hash| {
|
||||
if env.call_method(&hash, "isPresent", "()Z", &[]).and_then(|r| r.z()).jexcept(&mut env) {
|
||||
env.call_method(&hash, "getAsLong", "()J", &[])
|
||||
.and_then(|r| r.j())
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}).jexcept(&mut env);
|
||||
|
||||
let controller = unsafe { Box::leak(Box::from_raw(self_ptr as *mut crate::buffer::Controller)) };
|
||||
RT.block_on(controller.send(crate::api::TextChange {
|
||||
start,
|
||||
end,
|
||||
content,
|
||||
hash,
|
||||
})).jexcept(&mut env);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,69 @@
|
|||
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jlong, jobject, jobjectArray}, JNIEnv};
|
||||
use crate::{client::Client, Workspace};
|
||||
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jint, jlong, jobject, jobjectArray}, JNIEnv};
|
||||
use crate::{api::Config, client::Client, Workspace};
|
||||
|
||||
use super::{JExceptable, RT};
|
||||
use super::{JExceptable, JObjectify, RT};
|
||||
|
||||
/// Connect to a given URL and return a [Client] to interact with that server.
|
||||
/// Connect using the given credentials to the default server, and return a [Client] to interact with it.
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_Client_connect<'local>(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass<'local>,
|
||||
url: JString<'local>,
|
||||
user: JString<'local>,
|
||||
pwd: JString<'local>
|
||||
) -> jobject {
|
||||
let url: String = env.get_string(&url)
|
||||
let username: String = env.get_string(&user)
|
||||
.map(|s| s.into())
|
||||
.jexcept(&mut env);
|
||||
let user: String = env.get_string(&user)
|
||||
let password: String = env.get_string(&pwd)
|
||||
.map(|s| s.into())
|
||||
.jexcept(&mut env);
|
||||
let pwd: String = env.get_string(&pwd)
|
||||
connect_internal(env, Config {
|
||||
username,
|
||||
password,
|
||||
host: None,
|
||||
port: None,
|
||||
tls: None
|
||||
})
|
||||
}
|
||||
|
||||
/// Connect to a given URL and return a [Client] to interact with that server.
|
||||
#[no_mangle]
|
||||
#[allow(non_snake_case)]
|
||||
pub extern "system" fn Java_mp_code_Client_connectToServer<'local>(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass<'local>,
|
||||
user: JString<'local>,
|
||||
pwd: JString<'local>,
|
||||
host: JString<'local>,
|
||||
port: jint,
|
||||
tls: jboolean
|
||||
) -> jobject {
|
||||
let username: String = env.get_string(&user)
|
||||
.map(|s| s.into())
|
||||
.jexcept(&mut env);
|
||||
RT.block_on(crate::Client::connect(&url, &user, &pwd))
|
||||
let password: String = env.get_string(&pwd)
|
||||
.map(|s| s.into())
|
||||
.jexcept(&mut env);
|
||||
let host: String = env.get_string(&host)
|
||||
.map(|s| s.into())
|
||||
.jexcept(&mut env);
|
||||
|
||||
if port < 0 {
|
||||
env.throw_new("mp/code/exceptions/JNIException", "Negative port number!")
|
||||
.jexcept(&mut env);
|
||||
}
|
||||
|
||||
connect_internal(env, Config {
|
||||
username,
|
||||
password,
|
||||
host: Some(host),
|
||||
port: Some(port as u16),
|
||||
tls: Some(tls != 0),
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_internal(mut env: JNIEnv, config: Config) -> jobject {
|
||||
RT.block_on(Client::connect(config))
|
||||
.map(|client| Box::into_raw(Box::new(client)) as jlong)
|
||||
.map(|ptr| {
|
||||
env.find_class("mp/code/Client")
|
||||
|
@ -30,6 +72,20 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>(
|
|||
}).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
/// Gets the current [crate::api::User].
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_Client_get_1user(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
self_ptr: jlong
|
||||
) -> jobject {
|
||||
let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) };
|
||||
client.user().clone()
|
||||
.jobjectify(&mut env)
|
||||
.jexcept(&mut env)
|
||||
.as_raw()
|
||||
}
|
||||
|
||||
/// Join a [Workspace] and return a pointer to it.
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>(
|
||||
|
@ -120,16 +176,34 @@ pub extern "system" fn Java_mp_code_Client_list_1workspaces<'local>(
|
|||
let list = RT
|
||||
.block_on(client.list_workspaces(owned != 0, invited != 0))
|
||||
.jexcept(&mut env);
|
||||
|
||||
env.find_class("java/lang/String")
|
||||
.and_then(|class| env.new_object_array(list.len() as i32, class, JObject::null()))
|
||||
.map(|arr| {
|
||||
.inspect(|arr| {
|
||||
for (idx, path) in list.iter().enumerate() {
|
||||
env.new_string(path)
|
||||
.and_then(|path| env.set_object_array_element(&arr, idx as i32, path))
|
||||
.and_then(|path| env.set_object_array_element(arr, idx as i32, path))
|
||||
.jexcept(&mut env)
|
||||
}
|
||||
}).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
/// List available workspaces.
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_Client_active_1workspaces<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_class: JClass<'local>,
|
||||
self_ptr: jlong
|
||||
) -> jobjectArray {
|
||||
let client = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Client)) };
|
||||
let list = client.active_workspaces();
|
||||
env.find_class("java/lang/String")
|
||||
.and_then(|class| env.new_object_array(list.len() as i32, class, JObject::null()))
|
||||
.inspect(|arr| {
|
||||
for (idx, path) in list.iter().enumerate() {
|
||||
env.new_string(path)
|
||||
.and_then(|path| env.set_object_array_element(arr, idx as i32, path))
|
||||
.jexcept(&mut env)
|
||||
}
|
||||
arr
|
||||
}).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv};
|
||||
use crate::api::Controller;
|
||||
|
||||
use super::{JExceptable, RT};
|
||||
use super::{handle_callback, JExceptable, RT};
|
||||
|
||||
/// Try to fetch a [crate::api::Cursor], or returns null if there's nothing.
|
||||
#[no_mangle]
|
||||
|
@ -56,6 +56,16 @@ fn jni_recv(env: &mut JNIEnv, cursor: Option<crate::api::Cursor>) -> jobject {
|
|||
}.as_raw()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_CursorController_callback<'local>(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass<'local>,
|
||||
self_ptr: jlong,
|
||||
cb: JObject<'local>,
|
||||
) {
|
||||
handle_callback!("mp/code/CursorController", env, self_ptr, cb, crate::cursor::Controller);
|
||||
}
|
||||
|
||||
/// Receive from Java, converts and sends a [crate::api::Cursor].
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_mp_code_CursorController_send<'local>(
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
//! ### java
|
||||
//! Since for java it is necessary to deal with the JNI and no complete FFI library is available,
|
||||
//! java glue directly writes JNI functions leveraging [jni] rust bindings.
|
||||
//!
|
||||
//! To have a runnable `jar`, some extra Java code must be compiled (available under `dist/java`)
|
||||
//! and bundled together with the shared object. Such extra wrapper provides classes and methods
|
||||
//! loading the native extension and invoking the underlying native functions.
|
||||
|
||||
pub mod client;
|
||||
pub mod workspace;
|
||||
pub mod cursor;
|
||||
|
@ -113,7 +105,7 @@ impl<T> JExceptable<T> for Result<T, uuid::Error> where T: Default {
|
|||
/// This is essentially the same as [TryInto], but that can't be emplemented on non-local types.
|
||||
pub(crate) trait JObjectify<'local> {
|
||||
/// The error type, likely to be [jni::errors::Error].
|
||||
type Error;
|
||||
type Error: std::fmt::Debug;
|
||||
|
||||
/// Attempt to convert the given object to a [jni::objects::JObject].
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error>;
|
||||
|
@ -121,12 +113,97 @@ pub(crate) trait JObjectify<'local> {
|
|||
|
||||
impl<'local> JObjectify<'local> for uuid::Uuid {
|
||||
type Error = jni::errors::Error;
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, jni::errors::Error> {
|
||||
env.find_class("java/util/UUID").and_then(|class| {
|
||||
let (msb, lsb) = self.as_u64_pair();
|
||||
let msb = i64::from_ne_bytes(msb.to_ne_bytes());
|
||||
let lsb = i64::from_ne_bytes(lsb.to_ne_bytes());
|
||||
env.new_object(&class, "(JJ)V", &[jni::objects::JValueGen::Long(msb), jni::objects::JValueGen::Long(lsb)])
|
||||
})
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error> {
|
||||
let class = env.find_class("java/util/UUID")?;
|
||||
let (msb, lsb) = self.as_u64_pair();
|
||||
let msb = i64::from_ne_bytes(msb.to_ne_bytes());
|
||||
let lsb = i64::from_ne_bytes(lsb.to_ne_bytes());
|
||||
env.new_object(&class, "(JJ)V", &[jni::objects::JValueGen::Long(msb), jni::objects::JValueGen::Long(lsb)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'local> JObjectify<'local> for crate::api::User {
|
||||
type Error = jni::errors::Error;
|
||||
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error> {
|
||||
let id_field = self.id.jobjectify(env)?;
|
||||
let name_field = env.new_string(self.name)?;
|
||||
let class = env.find_class("mp/code/data/User")?;
|
||||
env.new_object(
|
||||
&class,
|
||||
"(Ljava/util/UUID;Ljava/lang/String;)V",
|
||||
&[
|
||||
jni::objects::JValueGen::Object(&id_field),
|
||||
jni::objects::JValueGen::Object(&name_field)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'local> JObjectify<'local> for crate::cursor::Controller {
|
||||
type Error = jni::errors::Error;
|
||||
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error> {
|
||||
let class = env.find_class("mp/code/CursorController")?;
|
||||
env.new_object(
|
||||
class,
|
||||
"(J)V",
|
||||
&[
|
||||
jni::objects::JValueGen::Long(Box::into_raw(Box::new(&self)) as jni::sys::jlong)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'local> JObjectify<'local> for crate::buffer::Controller {
|
||||
type Error = jni::errors::Error;
|
||||
|
||||
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error> {
|
||||
let class = env.find_class("mp/code/BufferController")?;
|
||||
env.new_object(
|
||||
class,
|
||||
"(J)V",
|
||||
&[
|
||||
jni::objects::JValueGen::Long(Box::into_raw(Box::new(&self)) as jni::sys::jlong)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! handle_callback {
|
||||
($jtype:literal, $env:ident, $self_ptr:ident, $cb:ident, $t:ty) => {
|
||||
let controller = unsafe { Box::leak(Box::from_raw($self_ptr as *mut $t)) };
|
||||
|
||||
let Ok(jvm) = $env.get_java_vm() else {
|
||||
$env.throw_new("mp/code/exceptions/JNIException", "Failed to get JVM reference!")
|
||||
.expect("Failed to throw exception!");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(cb_ref) = $env.new_global_ref($cb) else {
|
||||
$env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!")
|
||||
.expect("Failed to throw exception!");
|
||||
return;
|
||||
};
|
||||
controller.callback(move |controller: $t| {
|
||||
use std::ops::DerefMut;
|
||||
use crate::ffi::java::JObjectify;
|
||||
let mut guard = jvm.attach_current_thread().unwrap();
|
||||
let jcontroller = match controller.jobjectify(guard.deref_mut()) {
|
||||
Err(e) => return tracing::error!("could not convert callback argument: {e:?}"),
|
||||
Ok(x) => x,
|
||||
};
|
||||
let sig = format!("(L{};)V", $jtype);
|
||||
if let Err(e) = guard.call_method(&cb_ref,
|
||||
"invoke",
|
||||
&sig,
|
||||
&[jni::objects::JValueGen::Object(&jcontroller)]
|
||||
) {
|
||||
tracing::error!("error invoking callback: {e:?}");
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use handle_callback;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject, jobjectArray, jstring}, JNIEnv};
|
||||
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jlong, jobject, jobjectArray, jstring}, JNIEnv};
|
||||
use crate::Workspace;
|
||||
|
||||
use super::{JExceptable, JObjectify, RT};
|
||||
|
@ -22,9 +22,7 @@ pub extern "system" fn Java_mp_code_Workspace_get_1cursor<'local>(
|
|||
self_ptr: jlong
|
||||
) -> jobject {
|
||||
let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) };
|
||||
env.find_class("mp/code/CursorController").and_then(|class|
|
||||
env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(workspace.cursor())) as jlong)])
|
||||
).jexcept(&mut env).as_raw()
|
||||
workspace.cursor().jobjectify(&mut env).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
/// Get a buffer controller by name and returns a pointer to it.
|
||||
|
@ -39,12 +37,10 @@ pub extern "system" fn Java_mp_code_Workspace_get_1buffer<'local>(
|
|||
let path = unsafe { env.get_string_unchecked(&input) }
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.jexcept(&mut env);
|
||||
|
||||
workspace.buffer_by_name(&path).map(|buf| {
|
||||
env.find_class("mp/code/BufferController").and_then(|class|
|
||||
env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(buf)) as jlong)])
|
||||
).jexcept(&mut env)
|
||||
}).unwrap_or_default().as_raw()
|
||||
workspace.buffer_by_name(&path)
|
||||
.map(|buf| buf.jobjectify(&mut env).jexcept(&mut env))
|
||||
.unwrap_or_default()
|
||||
.as_raw()
|
||||
}
|
||||
|
||||
/// Create a new buffer.
|
||||
|
@ -69,7 +65,8 @@ pub extern "system" fn Java_mp_code_Workspace_get_1file_1tree(
|
|||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
self_ptr: jlong,
|
||||
filter: JString
|
||||
filter: JString,
|
||||
strict: jboolean
|
||||
) -> jobjectArray {
|
||||
let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) };
|
||||
let filter: Option<String> = if filter.is_null() {
|
||||
|
@ -82,16 +79,15 @@ pub extern "system" fn Java_mp_code_Workspace_get_1file_1tree(
|
|||
)
|
||||
};
|
||||
|
||||
let file_tree = workspace.filetree(filter.as_deref());
|
||||
let file_tree = workspace.filetree(filter.as_deref(), strict != 0);
|
||||
env.find_class("java/lang/String")
|
||||
.and_then(|class| env.new_object_array(file_tree.len() as i32, class, JObject::null()))
|
||||
.map(|arr| {
|
||||
.inspect(|arr| {
|
||||
for (idx, path) in file_tree.iter().enumerate() {
|
||||
env.new_string(path)
|
||||
.and_then(|path| env.set_object_array_element(&arr, idx as i32, path))
|
||||
.and_then(|path| env.set_object_array_element(arr, idx as i32, path))
|
||||
.jexcept(&mut env)
|
||||
}
|
||||
arr
|
||||
}).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
|
@ -108,12 +104,9 @@ pub extern "system" fn Java_mp_code_Workspace_attach_1to_1buffer<'local>(
|
|||
.map(|path| path.to_string_lossy().to_string())
|
||||
.jexcept(&mut env);
|
||||
RT.block_on(workspace.attach(&path))
|
||||
.map(|buffer| Box::into_raw(Box::new(buffer)) as jlong)
|
||||
.map(|ptr| {
|
||||
env.find_class("mp/code/BufferController")
|
||||
.and_then(|class| env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]))
|
||||
.jexcept(&mut env)
|
||||
}).jexcept(&mut env).as_raw()
|
||||
.map(|buffer| buffer.jobjectify(&mut env).jexcept(&mut env))
|
||||
.jexcept(&mut env)
|
||||
.as_raw()
|
||||
}
|
||||
|
||||
/// Detach from a buffer.
|
||||
|
@ -180,13 +173,12 @@ pub extern "system" fn Java_mp_code_Workspace_list_1buffer_1users<'local>(
|
|||
|
||||
env.find_class("java/util/UUID")
|
||||
.and_then(|class| env.new_object_array(users.len() as i32, &class, JObject::null()))
|
||||
.map(|arr| {
|
||||
.inspect(|arr| {
|
||||
for (idx, user) in users.iter().enumerate() {
|
||||
user.id.jobjectify(&mut env)
|
||||
.and_then(|id| env.set_object_array_element(&arr, idx as i32, id))
|
||||
.and_then(|id| env.set_object_array_element(arr, idx as i32, id))
|
||||
.jexcept(&mut env);
|
||||
}
|
||||
arr
|
||||
}).jexcept(&mut env).as_raw()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,8 @@ use crate::{Client, Workspace};
|
|||
|
||||
#[napi]
|
||||
/// connect to codemp servers and return a client session
|
||||
pub async fn connect(addr: Option<String>, username: String, password: String) -> napi::Result<crate::Client>{
|
||||
let client = crate::Client::connect(addr.as_deref().unwrap_or("http://code.mp:50053"), username, password)
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
pub async fn connect(config: crate::api::Config) -> napi::Result<crate::Client>{
|
||||
Ok(crate::Client::connect(config).await?)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
//! ### javascript
|
||||
//! Using [napi] it's possible to map perfectly the entirety of `codemp` API.
|
||||
//! Async operations run on a dedicated [tokio] runtime and the result is sent back to main thread
|
||||
|
||||
pub mod client;
|
||||
pub mod workspace;
|
||||
pub mod cursor;
|
||||
|
|
|
@ -12,8 +12,8 @@ impl Workspace {
|
|||
}
|
||||
|
||||
#[napi(js_name = "filetree")]
|
||||
pub fn js_filetree(&self, filter: Option<&str>) -> Vec<String> {
|
||||
self.filetree(filter)
|
||||
pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
|
||||
self.filetree(filter, strict)
|
||||
}
|
||||
|
||||
#[napi(js_name = "cursor")]
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
//! ### Lua
|
||||
//! Using [mlua] it's possible to map almost perfectly the entirety of `codemp` API.
|
||||
//! Notable outliers are functions that receive `codemp` objects: these instead receive arguments
|
||||
//! to build the object instead (such as [`crate::api::Controller::send`])
|
||||
//!
|
||||
//! Note that async operations are carried out on a [tokio] current_thread runtime, so it is
|
||||
//! necessary to drive it. A separate driver thread can be spawned with `spawn_runtime_driver`
|
||||
//! function.
|
||||
//!
|
||||
//! To work with callbacks, the main Lua thread must periodically stop and poll for callbacks via
|
||||
//! `poll_callback`, otherwise those will never run. This is necessary to allow safe concurrent
|
||||
//! access to the global Lua state, so minimize callback execution time as much as possible.
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
@ -173,6 +160,18 @@ macro_rules! a_sync {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! from_lua_serde {
|
||||
($($t:ty)*) => {
|
||||
$(
|
||||
impl FromLua for $t {
|
||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<$t> {
|
||||
lua.from_value(value)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
fn spawn_runtime_driver(_: &Lua, ():()) -> LuaResult<Driver> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let handle = std::thread::spawn(move || tokio().block_on(async move {
|
||||
|
@ -254,19 +253,19 @@ impl LuaUserData for CodempClient {
|
|||
impl LuaUserData for CodempWorkspace {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
|
||||
methods.add_method("create_buffer", |_, this, (name,):(String,)|
|
||||
methods.add_method("create", |_, this, (name,):(String,)|
|
||||
a_sync! { this => Ok(this.create(&name).await?) }
|
||||
);
|
||||
|
||||
methods.add_method("attach_buffer", |_, this, (name,):(String,)|
|
||||
methods.add_method("attach", |_, this, (name,):(String,)|
|
||||
a_sync! { this => Ok(this.attach(&name).await?) }
|
||||
);
|
||||
|
||||
methods.add_method("detach_buffer", |_, this, (name,):(String,)|
|
||||
methods.add_method("detach", |_, this, (name,):(String,)|
|
||||
Ok(matches!(this.detach(&name), DetachResult::Detaching | DetachResult::AlreadyDetached))
|
||||
);
|
||||
|
||||
methods.add_method("delete_buffer", |_, this, (name,):(String,)|
|
||||
methods.add_method("delete", |_, this, (name,):(String,)|
|
||||
a_sync! { this => Ok(this.delete(&name).await?) }
|
||||
);
|
||||
|
||||
|
@ -295,8 +294,8 @@ impl LuaUserData for CodempWorkspace {
|
|||
// Ok(())
|
||||
// });
|
||||
|
||||
methods.add_method("filetree", |_, this, (filter,):(Option<String>,)|
|
||||
Ok(this.filetree(filter.as_deref()))
|
||||
methods.add_method("filetree", |_, this, (filter, strict,):(Option<String>, bool,)|
|
||||
Ok(this.filetree(filter.as_deref(), strict))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -308,6 +307,7 @@ impl LuaUserData for CodempWorkspace {
|
|||
}
|
||||
}
|
||||
|
||||
from_lua_serde! { CodempEvent }
|
||||
impl LuaUserData for CodempEvent {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
|
||||
|
@ -325,12 +325,13 @@ impl LuaUserData for CodempEvent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl LuaUserData for CodempCursorController {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
|
||||
|
||||
methods.add_method("send", |_, this, (buffer, start_row, start_col, end_row, end_col):(String, i32, i32, i32, i32)|
|
||||
a_sync! { this => Ok(this.send(CodempCursor { buffer, start: (start_row, start_col), end: (end_row, end_col), user: None }).await?) }
|
||||
methods.add_method("send", |_, this, (cursor,):(CodempCursor,)|
|
||||
a_sync! { this => Ok(this.send(cursor).await?) }
|
||||
);
|
||||
methods.add_method("try_recv", |_, this, ()|
|
||||
a_sync! { this => Ok(this.try_recv().await?) }
|
||||
|
@ -348,6 +349,7 @@ impl LuaUserData for CodempCursorController {
|
|||
}
|
||||
}
|
||||
|
||||
from_lua_serde! { CodempCursor }
|
||||
impl LuaUserData for Cursor {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
|
||||
|
@ -388,17 +390,8 @@ impl LuaUserData for CodempBufferController {
|
|||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
|
||||
|
||||
methods.add_method("send", |_, this, (start, end, content, hash): (usize, usize, String, Option<i64>)|
|
||||
a_sync! { this => Ok(
|
||||
this.send(
|
||||
CodempTextChange {
|
||||
start: start as u32,
|
||||
end: end as u32,
|
||||
content,
|
||||
hash,
|
||||
}
|
||||
).await?
|
||||
)}
|
||||
methods.add_method("send", |_, this, (change,): (CodempTextChange,)|
|
||||
a_sync! { this => Ok(this.send(change).await?)}
|
||||
);
|
||||
|
||||
methods.add_method("try_recv", |_, this, ()| a_sync! { this => Ok(this.try_recv().await?) });
|
||||
|
@ -417,6 +410,7 @@ impl LuaUserData for CodempBufferController {
|
|||
}
|
||||
}
|
||||
|
||||
from_lua_serde! { CodempTextChange }
|
||||
impl LuaUserData for CodempTextChange {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("content", |_, this| Ok(this.content.clone()));
|
||||
|
@ -431,7 +425,16 @@ impl LuaUserData for CodempTextChange {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
from_lua_serde! { CodempConfig }
|
||||
impl LuaUserData for CodempConfig {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("username", |_, this| Ok(this.username.clone()));
|
||||
fields.add_field_method_get("password", |_, this| Ok(this.password.clone()));
|
||||
fields.add_field_method_get("host", |_, this| Ok(this.host.clone()));
|
||||
fields.add_field_method_get("port", |_, this| Ok(this.port));
|
||||
fields.add_field_method_get("tls", |_, this| Ok(this.tls));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LuaLoggerProducer(mpsc::UnboundedSender<String>);
|
||||
|
@ -534,8 +537,8 @@ fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> {
|
|||
let exports = lua.create_table()?;
|
||||
|
||||
// entrypoint
|
||||
exports.set("connect", lua.create_function(|_, (host, username, password):(String,String,String)|
|
||||
a_sync! { => Ok(CodempClient::connect(host, username, password).await?) }
|
||||
exports.set("connect", lua.create_function(|_, (config,):(CodempConfig,)|
|
||||
a_sync! { => Ok(CodempClient::connect(config).await?) }
|
||||
)?)?;
|
||||
|
||||
// utils
|
||||
|
|
|
@ -1,9 +1,47 @@
|
|||
//! ### FFI
|
||||
//! The glue code for FFI (Foreign Function Interface) in various languages, each gated behind
|
||||
//! a feature flag.
|
||||
//! # Foreign Function Interface
|
||||
//! `codemp` aims to be available as a library from as many programming languages as possible.
|
||||
//! To achieve this, we rely on Foreign Function Interface.
|
||||
//!
|
||||
//! ## JavaScript
|
||||
//! Our JavaScript glue is built with [`napi`](https://napi.rs).
|
||||
//!
|
||||
//! For all except Java, the resulting shared object is ready to use, but external packages are
|
||||
//! available to simplify dependency management and provide type hints in editor.
|
||||
//! All async operations are handled on a separate tokio runtime, automatically managed by `napi`.
|
||||
//! Callbacks are safely scheduled to be called on the main loop thread.
|
||||
//!
|
||||
//! ## Python
|
||||
//! Our Python glue is built with [`PyO3`](https://pyo3.rs).
|
||||
//!
|
||||
//! All async operations return a `Promise`, which can we `.wait()`-ed to block and get the return
|
||||
//! value. The `Future` itself is run on a `tokio` runtime in a dedicated thread, which must be
|
||||
//! stared with `codemp.init()` before doing any async operations.
|
||||
//!
|
||||
//! ## Lua
|
||||
//! Our Lua glue is built with [`mlua`](https://github.com/mlua-rs/mlua).
|
||||
//!
|
||||
//! Lua bindings run all async code on a current thread tokio runtime, which should be driven with
|
||||
//! a dedicated thread.
|
||||
//!
|
||||
//! All async functions will return a `Promise`, which can be `:await()`-ed to block and get the
|
||||
//! return value.
|
||||
//!
|
||||
//! Note as Lua uses filename to locate entrypoint symbol, so shared object can't just have any name.
|
||||
//! Accepted filenames are `libcodemp.___`, `codemp.___`, `codemp_native.___`, `codemp_lua.___` (extension depends on your platform: `so` on linux, `dll` on windows, `dylib` on macos).
|
||||
//! Type hints are provided in `dist/lua/annotations.lua`, just include them in your language server: `---@module 'annotations'`.
|
||||
//!
|
||||
//! `codemp` is available as a rock on [LuaRocks](https://luarocks.org/modules/alemi/codemp),
|
||||
//! however LuaRocks compiles from source and will require having `cargo` installed.
|
||||
//! We provide pre-built binaries at [codemp.dev/releases/lua](https://codemp.dev/releases/lua/).
|
||||
//! **Please do not rely on this link, as our built binaries will likely move somewhere else soon!**.
|
||||
//!
|
||||
//! ## Java
|
||||
//! Our Java glue is built with [`jni`](https://github.com/jni-rs/jni-rs).
|
||||
//!
|
||||
//! Memory management is entirely delegated to the JVM's garbage collector.
|
||||
//! A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually.
|
||||
//!
|
||||
//! Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`.
|
||||
//! `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
||||
//!
|
||||
|
||||
/// java bindings, built with [jni]
|
||||
#[cfg(feature = "java")]
|
||||
|
@ -18,5 +56,5 @@ pub mod lua;
|
|||
pub mod js;
|
||||
|
||||
/// python bindings, built with [pyo3]
|
||||
#[cfg(feature = "python")]
|
||||
#[cfg(feature = "py")]
|
||||
pub mod python;
|
||||
|
|
|
@ -5,25 +5,13 @@ use pyo3::prelude::*;
|
|||
|
||||
#[pymethods]
|
||||
impl Client {
|
||||
#[new]
|
||||
fn __new__(
|
||||
host: String,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> crate::errors::ConnectionResult<Self> {
|
||||
super::tokio().block_on(Client::connect(host, username, password))
|
||||
}
|
||||
|
||||
// #[pyo3(name = "join_workspace")]
|
||||
// async fn pyjoin_workspace(&self, workspace: String) -> JoinHandle<crate::Result<Workspace>> {
|
||||
// tracing::info!("attempting to join the workspace {}", workspace);
|
||||
|
||||
// let this = self.clone();
|
||||
// async {
|
||||
// tokio()
|
||||
// .spawn(async move { this.join_workspace(workspace).await })
|
||||
// .await
|
||||
// }
|
||||
// #[new]
|
||||
// fn __new__(
|
||||
// host: String,
|
||||
// username: String,
|
||||
// password: String,
|
||||
// ) -> crate::errors::ConnectionResult<Self> {
|
||||
// super::tokio().block_on(Client::connect(host, username, password))
|
||||
// }
|
||||
|
||||
#[pyo3(name = "join_workspace")]
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
//! ### python
|
||||
//! Using [pyo3] it's possible to map perfectly the entirety of `codemp` API.
|
||||
//! Async operations run on a dedicated [tokio] runtime
|
||||
|
||||
pub mod client;
|
||||
pub mod controllers;
|
||||
pub mod workspace;
|
||||
|
@ -150,8 +146,18 @@ fn init() -> PyResult<Driver> {
|
|||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn connect(host: String, username: String, password: String) -> PyResult<Promise> {
|
||||
a_sync!(Client::connect(host, username, password).await)
|
||||
fn get_default_config() -> crate::api::Config {
|
||||
let mut conf = crate::api::Config::new("".to_string(), "".to_string());
|
||||
conf.host = Some(conf.host().to_string());
|
||||
conf.port = Some(conf.port());
|
||||
conf.tls = Some(false);
|
||||
conf
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn connect(py: Python, config: Py<crate::api::Config>) -> PyResult<Promise> {
|
||||
let conf: crate::api::Config = config.extract(py)?;
|
||||
a_sync!(Client::connect(conf).await)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
|
@ -222,6 +228,7 @@ impl IntoPy<PyObject> for crate::api::User {
|
|||
#[pymodule]
|
||||
fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(init, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(get_default_config, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(connect, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(set_logger, m)?)?;
|
||||
m.add_class::<Driver>()?;
|
||||
|
|
|
@ -82,8 +82,8 @@ impl Workspace {
|
|||
}
|
||||
|
||||
#[pyo3(name = "filetree")]
|
||||
#[pyo3(signature = (filter=None))]
|
||||
fn pyfiletree(&self, filter: Option<&str>) -> Vec<String> {
|
||||
self.filetree(filter)
|
||||
#[pyo3(signature = (filter=None, strict=false))]
|
||||
fn pyfiletree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
|
||||
self.filetree(filter, strict)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ pub use crate::api::{
|
|||
Cursor as CodempCursor,
|
||||
User as CodempUser,
|
||||
Event as CodempEvent,
|
||||
Config as CodempConfig,
|
||||
};
|
||||
|
||||
pub use crate::{
|
||||
|
|
|
@ -33,7 +33,7 @@ use uuid::Uuid;
|
|||
use napi_derive::napi;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "js", napi)]
|
||||
pub struct Workspace(Arc<WorkspaceInner>);
|
||||
|
||||
|
@ -54,12 +54,12 @@ impl Workspace {
|
|||
pub(crate) async fn try_new(
|
||||
name: String,
|
||||
user: User,
|
||||
dest: &str,
|
||||
config: crate::api::Config,
|
||||
token: Token,
|
||||
claims: tokio::sync::watch::Receiver<codemp_proto::common::Token>, // TODO ughh receiving this
|
||||
) -> ConnectionResult<Self> {
|
||||
let workspace_claim = InternallyMutable::new(token);
|
||||
let services = Services::try_new(dest, claims, workspace_claim.channel()).await?;
|
||||
let services = Services::try_new(&config.endpoint(), claims, workspace_claim.channel()).await?;
|
||||
let ws_stream = services.ws().attach(Empty {}).await?.into_inner();
|
||||
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
|
@ -279,10 +279,17 @@ impl Workspace {
|
|||
}
|
||||
|
||||
/// Get the filetree as it is currently cached.
|
||||
/// A filter may be applied, and it may be strict (equality check) or not (starts_with check).
|
||||
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
|
||||
pub fn filetree(&self, filter: Option<&str>) -> Vec<String> {
|
||||
pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
|
||||
self.0.filetree.iter()
|
||||
.filter(|f| filter.map_or(true, |flt| f.starts_with(flt)))
|
||||
.filter(|f| filter.map_or(true, |flt| {
|
||||
if strict {
|
||||
f.as_str() == flt
|
||||
} else {
|
||||
f.starts_with(flt)
|
||||
}
|
||||
}))
|
||||
.map(|f| f.clone())
|
||||
.collect()
|
||||
}
|
||||
|
@ -357,8 +364,8 @@ impl Drop for WorkspaceInner {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
|
||||
#[cfg_attr(feature = "python", derive(PartialEq))]
|
||||
#[cfg_attr(feature = "py", pyo3::pyclass(eq, eq_int))]
|
||||
#[cfg_attr(feature = "py", derive(PartialEq))]
|
||||
pub enum DetachResult {
|
||||
NotAttached,
|
||||
Detaching,
|
||||
|
|
Loading…
Reference in a new issue