feat: Merge branch 'config' into dev

This commit is contained in:
əlemi 2024-09-15 02:04:57 +02:00
commit 081a72d733
Signed by: alemi
GPG key ID: A4895B84D311642C
46 changed files with 697 additions and 401 deletions

View file

@ -31,13 +31,13 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- run: npm install - run: npm install
working-directory: dist/js/publish working-directory: dist/js
- run: npx napi build --cargo-cwd=../../.. --platform --release --features=js --strip - run: npx napi build --cargo-cwd=../.. --platform --release --features=js --strip
working-directory: dist/js/publish working-directory: dist/js
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: codemp-js-${{ matrix.platform.target }} name: codemp-js-${{ matrix.platform.target }}
path: dist/js/publish/codemp.*.node path: dist/js/codemp.*.node
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -56,32 +56,34 @@ jobs:
node-version: '20' node-version: '20'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: npm install - run: npm install
working-directory: dist/js/publish working-directory: dist/js
- run: npx napi build --cargo-cwd=../../.. --platform --features=js - run: npx napi build --cargo-cwd=../.. --platform --features=js
working-directory: dist/js/publish working-directory: dist/js
- run: rm *.node - run: rm *.node
working-directory: dist/js/publish working-directory: dist/js
- run: npx napi create-npm-dir -t .; tree - run: npx napi create-npm-dir -t .; tree
working-directory: dist/js/publish working-directory: dist/js
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
path: dist/js/publish/artifacts path: dist/js/artifacts
pattern: codemp-js-* pattern: codemp-js-*
- run: npx napi artifacts; tree - run: npx napi artifacts; tree
working-directory: dist/js/publish working-directory: dist/js
- run: npx napi prepublish -t . --skip-gh-release - run: npx napi prepublish -t . --skip-gh-release
working-directory: dist/js/publish working-directory: dist/js
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: rm -rf *.node artifacts node_modules - run: rm -rf *.node artifacts node_modules npm
working-directory: dist/js/publish 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 # 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) # 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 # 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 # take it and think again about it later
- run: sed -i 's/"@codemp\/native"/"codemp"/' package.json - run: sed -i 's/"@codemp\/native"/"codemp"/' package.json
working-directory: dist/js/publish working-directory: dist/js
- run: npm publish - run: npm publish
working-directory: dist/js/publish working-directory: dist/js
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -22,7 +22,7 @@ jobs:
- luajit # should we test with lua54 instead? - luajit # should we test with lua54 instead?
- java - java
- js - js
- python - py
toolchain: toolchain:
- stable - stable
# - beta # - beta

52
Cargo.lock generated
View file

@ -228,7 +228,7 @@ dependencies = [
[[package]] [[package]]
name = "codemp" name = "codemp"
version = "0.7.0-beta.3" version = "0.7.0-beta.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"codemp-proto", "codemp-proto",
@ -242,6 +242,7 @@ dependencies = [
"napi-derive", "napi-derive",
"pyo3", "pyo3",
"pyo3-build-config 0.19.2", "pyo3-build-config 0.19.2",
"serde",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@ -388,6 +389,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@ -862,11 +873,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a52f529509c236114a5cf5bb3c0c06ff0695ad45d718256930ec2416edf3817" checksum = "5a52f529509c236114a5cf5bb3c0c06ff0695ad45d718256930ec2416edf3817"
dependencies = [ dependencies = [
"bstr", "bstr",
"erased-serde",
"mlua-sys", "mlua-sys",
"mlua_derive", "mlua_derive",
"num-traits", "num-traits",
"parking_lot", "parking_lot",
"rustc-hash", "rustc-hash",
"serde",
"serde-value",
] ]
[[package]] [[package]]
@ -1020,6 +1034,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 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]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1521,18 +1544,28 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.205" version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde-value"
version = "1.0.205" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1939,6 +1972,12 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "typeid"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
@ -1970,6 +2009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde",
] ]
[[package]] [[package]]

View file

@ -11,7 +11,7 @@ authors = [
] ]
license = "GPL-3.0-only" license = "GPL-3.0-only"
edition = "2021" edition = "2021"
version = "0.7.0-beta.3" version = "0.7.0-beta.4"
exclude = ["dist/*"] exclude = ["dist/*"]
[lib] [lib]
@ -43,7 +43,7 @@ lazy_static = { version = "1.4", optional = true }
jni = { version = "0.21", features = ["invocation"], optional = true } jni = { version = "0.21", features = ["invocation"], optional = true }
# glue (lua) # 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) # glue (js)
napi = { version = "2.16", features = ["full"], optional = true } napi = { version = "2.16", features = ["full"], optional = true }
@ -54,6 +54,7 @@ pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"], optiona
# extra # extra
async-trait = { version = "0.1", optional = true } async-trait = { version = "0.1", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
[build-dependencies] [build-dependencies]
# glue (js) # glue (js)
@ -65,18 +66,15 @@ pyo3-build-config = { version = "0.19", optional = true }
default = [] default = []
# extra # extra
async-trait = ["dep:async-trait"] async-trait = ["dep:async-trait"]
serialize = ["dep:serde", "uuid/serde"]
# ffi # ffi
rust = [] # used for ci matrix rust = [] # used for ci matrix
java = ["lazy_static", "jni", "tracing-subscriber"] java = ["lazy_static", "jni", "tracing-subscriber"]
js = ["napi-build", "tracing-subscriber", "napi", "napi-derive"] js = ["napi-build", "tracing-subscriber", "napi", "napi-derive"]
python = ["pyo3", "tracing-subscriber", "pyo3-build-config"] py = ["pyo3", "tracing-subscriber", "pyo3-build-config"]
lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static"] lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/lua54"]
lua54 = ["lua", "mlua-codemp-patch/lua54"] luajit = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/luajit"]
lua53 = ["lua", "mlua-codemp-patch/lua53"]
lua52 = ["lua", "mlua-codemp-patch/lua52"]
lua51 = ["lua", "mlua-codemp-patch/lua51"]
luajit = ["lua", "mlua-codemp-patch/luajit"]
[package.metadata.docs.rs] # enabled features when building on docs.rs [package.metadata.docs.rs] # enabled features when building on docs.rs
features = ["lua", "java", "js", "python"] features = ["serialize"]

View file

@ -1,7 +1,7 @@
#[cfg(feature = "js")] #[cfg(feature = "js")]
extern crate napi_build; extern crate napi_build;
#[cfg(feature = "python")] #[cfg(feature = "py")]
extern crate pyo3_build_config; extern crate pyo3_build_config;
/// The main method of the buildscript, required by some glue modules. /// The main method of the buildscript, required by some glue modules.
@ -11,7 +11,7 @@ fn main() {
napi_build::setup(); napi_build::setup();
} }
#[cfg(feature = "python")] #[cfg(feature = "py")]
{ {
pyo3_build_config::add_extension_module_link_args(); pyo3_build_config::add_extension_module_link_args();
} }

46
dist/README.md vendored Normal file
View 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
View file

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

View file

@ -1,5 +1,6 @@
package mp.code; package mp.code;
import mp.code.data.Callback;
import mp.code.data.Cursor; import mp.code.data.Cursor;
import mp.code.data.TextChange; import mp.code.data.TextChange;
import mp.code.exceptions.ControllerException; import mp.code.exceptions.ControllerException;
@ -38,6 +39,11 @@ public class BufferController {
send(this.ptr, change); 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); private static native void free(long self);
@Override @Override
protected void finalize() { protected void finalize() {

View file

@ -1,6 +1,7 @@
package mp.code; package mp.code;
import cz.adamh.utils.NativeUtils; import cz.adamh.utils.NativeUtils;
import mp.code.data.User;
import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionException;
import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ConnectionRemoteException;
@ -10,14 +11,16 @@ import java.util.Optional;
public class Client { public class Client {
private final long ptr; 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) { Client(long ptr) {
this.ptr = ptr; this.ptr = ptr;
} }
private static native String get_url(long self); private static native User get_user(long self);
public String getUrl() { public User getUser() {
return get_url(this.ptr); return get_user(this.ptr);
} }
private static native Workspace join_workspace(long self, String id) throws ConnectionException; 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); 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); private static native boolean leave_workspace(long self, String id);
public boolean leaveWorkspace(String id) { public boolean leaveWorkspace(String id) {
return leave_workspace(this.ptr, id); return leave_workspace(this.ptr, id);
@ -55,9 +63,9 @@ public class Client {
return Optional.ofNullable(get_workspace(this.ptr, workspace)); 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 { public void refresh() throws ConnectionRemoteException {
refresh_native(this.ptr); refresh(this.ptr);
} }
private static native void free(long self); private static native void free(long self);

View file

@ -1,5 +1,6 @@
package mp.code; package mp.code;
import mp.code.data.Callback;
import mp.code.data.Cursor; import mp.code.data.Cursor;
import mp.code.exceptions.ControllerException; import mp.code.exceptions.ControllerException;
@ -27,6 +28,11 @@ public class CursorController {
send(this.ptr, cursor); 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); private static native void free(long self);
@Override @Override
protected void finalize() { protected void finalize() {

View file

@ -30,9 +30,9 @@ public class Workspace {
return Optional.ofNullable(get_buffer(this.ptr, path)); return Optional.ofNullable(get_buffer(this.ptr, path));
} }
private static native String[] get_file_tree(long self, String filter); private static native String[] get_file_tree(long self, String filter, boolean strict);
public String[] getFileTree(Optional<String> filter) { public String[] getFileTree(Optional<String> filter, boolean strict) {
return get_file_tree(this.ptr, filter.orElse(null)); return get_file_tree(this.ptr, filter.orElse(null), strict);
} }
private static native void create_buffer(String path) throws ConnectionRemoteException; private static native void create_buffer(String path) throws ConnectionRemoteException;

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

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

@ -1 +0,0 @@
npx napi build --cargo-cwd ../..

View file

@ -1,6 +1,6 @@
{ {
"name": "@codemp/native", "name": "@codemp/native",
"version": "0.0.9", "version": "0.0.10",
"description": "code multiplexer -- javascript bindings", "description": "code multiplexer -- javascript bindings",
"keywords": [ "keywords": [
"codemp", "codemp",
@ -35,8 +35,8 @@
} }
}, },
"optionalDependencies": { "optionalDependencies": {
"@codemp/native-win32-x64-msvc": "0.0.4", "@codemp/native-win32-x64-msvc": "0.0.10",
"@codemp/native-darwin-arm64": "0.0.4", "@codemp/native-darwin-arm64": "0.0.10",
"@codemp/native-linux-x64-gnu": "0.0.4" "@codemp/native-linux-x64-gnu": "0.0.10"
} }
} }

View file

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

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

View file

@ -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 ---detach from an active buffer, closing all streams. returns false if buffer was no longer active
function Workspace:detach_buffer(path) end 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 string[]
---return the list of available buffers in this workspace, as relative paths from workspace root ---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 ---@return NilPromise
---@async ---@async
@ -181,21 +182,19 @@ function Workspace:event() end
---handle to a remote buffer, for async send/recv operations ---handle to a remote buffer, for async send/recv operations
local BufferController = {} local BufferController = {}
---@class (exact) TextChange ---@class TextChange
---@field content string text content of change ---@field content string text content of change
---@field first integer start index of change ---@field first integer start index of change
---@field last integer end 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 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 ---@field apply fun(self: TextChange, other: string): string apply this text change to a string
---@param first integer change start index ---@param change TextChange text change to broadcast
---@param last integer change end index
---@param content string change content
---@return NilPromise ---@return NilPromise
---@async ---@async
---@nodiscard ---@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) ---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 ---@return MaybeTextChangePromise
---@async ---@async
@ -239,28 +238,24 @@ function BufferController:content() end
---handle to a workspace's cursor channel, allowing send/recv operations ---handle to a workspace's cursor channel, allowing send/recv operations
local CursorController = {} local CursorController = {}
---@class (exact) RowCol ---@class RowCol
---@field row integer row number ---@field row integer row number
---@field col integer column number ---@field col integer column number
---row and column tuple ---row and column tuple
---@class (exact) Cursor ---@class Cursor
---@field user string? id of user owning this cursor ---@field user string? id of user owning this cursor
---@field buffer string relative path ("name") of buffer on which this cursor is ---@field buffer string relative path ("name") of buffer on which this cursor is
---@field start RowCol cursor start position ---@field start RowCol cursor start position
---@field finish RowCol cursor end position ---@field finish RowCol cursor end position
---a cursor position ---a cursor position
---@param buffer string buffer relative path ("name") to send this cursor on ---@param cursor Cursor cursor event to broadcast
---@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
---@return NilPromise ---@return NilPromise
---@async ---@async
---@nodiscard ---@nodiscard
---update cursor position by sending a cursor event to server ---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 ---@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 ---@class (exact) Codemp
---the codemp shared library ---the codemp shared library
local Codemp = {} local Codemp = {}
---@param host string server host to connect to ---@param config Config configuration for
---@param username string username used to log in (usually email)
---@param password string password used to log in
---@return ClientPromise ---@return ClientPromise
---@async ---@async
---@nodiscard ---@nodiscard
---connect to codemp server, authenticate and return client ---connect to codemp server, authenticate and return client
function Codemp.connect(host, username, password) end function Codemp.connect(config) end
---@return function | nil ---@return function | nil
---@nodiscard ---@nodiscard

14
dist/py/README.md vendored
View file

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

View file

@ -7,10 +7,20 @@ class Driver:
""" """
def stop(self) -> None: ... 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 init() -> Driver: ...
def set_logger(logger_cb: Callable[[str], None], debug: bool) -> bool: ... 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]: class Promise[T]:
""" """
@ -57,7 +67,7 @@ class Workspace:
def cursor(self) -> CursorController: ... def cursor(self) -> CursorController: ...
def buffer_by_name(self, path: str) -> Optional[BufferController]: ... def buffer_by_name(self, path: str) -> Optional[BufferController]: ...
def buffer_list(self) -> list[str]: ... 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: class TextChange:
""" """

View file

@ -22,7 +22,8 @@
/// ///
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "js", napi_derive::napi(object))] #[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 { pub struct TextChange {
/// Range start of text change, as char indexes in buffer previous state. /// Range start of text change, as char indexes in buffer previous state.
pub start: u32, pub start: u32,
@ -41,7 +42,7 @@ impl TextChange {
} }
} }
#[cfg_attr(feature = "python", pyo3::pymethods)] #[cfg_attr(feature = "py", pyo3::pymethods)]
impl TextChange { impl TextChange {
/// Returns true if this [`TextChange`] deletes existing text. /// Returns true if this [`TextChange`] deletes existing text.
/// ///

63
src/api/config.rs Normal file
View 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()
)
}
}

View file

@ -1,13 +1,14 @@
//! ### Cursor //! ### Cursor
//! Represents the position of a remote user's cursor. //! Represents the position of a remote user's cursor.
#[cfg(feature = "python")] #[cfg(feature = "py")]
use pyo3::prelude::*; use pyo3::prelude::*;
/// User cursor position in a buffer /// User cursor position in a buffer
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "py", pyclass)]
// #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))]
pub struct Cursor { pub struct Cursor {
/// Cursor start position in buffer, as 0-indexed row-column tuple. /// Cursor start position in buffer, as 0-indexed row-column tuple.
pub start: (i32, i32), pub start: (i32, i32),

View file

@ -4,7 +4,8 @@ use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner;
/// Event in a [crate::Workspace]. /// Event in a [crate::Workspace].
#[derive(Debug, Clone)] #[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 { pub enum Event {
/// Fired when the file tree changes. /// Fired when the file tree changes.
/// Contains the modified buffer path (deleted, created or renamed). /// Contains the modified buffer path (deleted, created or renamed).

View file

@ -7,6 +7,9 @@ pub mod controller;
/// a generic representation of a text change /// a generic representation of a text change
pub mod change; pub mod change;
/// client configuration
pub mod config;
/// representation for an user's cursor /// representation for an user's cursor
pub mod cursor; pub mod cursor;
@ -18,6 +21,7 @@ pub mod user;
pub use controller::Controller; pub use controller::Controller;
pub use change::TextChange; pub use change::TextChange;
pub use config::Config;
pub use cursor::Cursor; pub use cursor::Cursor;
pub use event::Event; pub use event::Event;
pub use user::User; pub use user::User;

View file

@ -6,6 +6,7 @@ use uuid::Uuid;
/// Represents a service user /// Represents a service user
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct User { pub struct User {
/// User unique identifier, should never change. /// User unique identifier, should never change.
pub id: Uuid, pub id: Uuid,

View file

@ -18,7 +18,7 @@ use super::worker::DeltaRequest;
/// Each buffer controller internally tracks the last acknowledged state, remaining always in sync /// 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. /// with the server while allowing to procedurally receive changes while still sending new ones.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "py", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi)] #[cfg_attr(feature = "js", napi_derive::napi)]
pub struct BufferController(pub(crate) Arc<BufferControllerInner>); pub struct BufferController(pub(crate) Arc<BufferControllerInner>);

View file

@ -12,7 +12,7 @@ use codemp_proto::{
common::{Empty, Token}, session::{session_client::SessionClient, InviteRequest, WorkspaceRequest}, common::{Empty, Token}, session::{session_client::SessionClient, InviteRequest, WorkspaceRequest},
}; };
#[cfg(feature = "python")] #[cfg(feature = "py")]
use pyo3::prelude::*; use pyo3::prelude::*;
/// A `codemp` client handle. /// A `codemp` client handle.
@ -22,13 +22,13 @@ use pyo3::prelude::*;
/// A new [`Client`] can be obtained with [`Client::connect`]. /// A new [`Client`] can be obtained with [`Client::connect`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "js", napi_derive::napi)] #[cfg_attr(feature = "js", napi_derive::napi)]
#[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "py", pyclass)]
pub struct Client(Arc<ClientInner>); pub struct Client(Arc<ClientInner>);
#[derive(Debug)] #[derive(Debug)]
struct ClientInner { struct ClientInner {
user: User, user: User,
host: String, config: crate::api::Config,
workspaces: DashMap<String, Workspace>, workspaces: DashMap<String, Workspace>,
auth: AuthClient<Channel>, auth: AuthClient<Channel>,
session: SessionClient<InterceptedService<Channel, network::SessionInterceptor>>, session: SessionClient<InterceptedService<Channel, network::SessionInterceptor>>,
@ -37,39 +37,29 @@ struct ClientInner {
impl Client { impl Client {
/// Connect to the server, authenticate and instantiate a new [`Client`]. /// Connect to the server, authenticate and instantiate a new [`Client`].
pub async fn connect( pub async fn connect(config: crate::api::Config) -> ConnectionResult<Self> {
host: impl AsRef<str>, // TODO move these two into network.rs
username: impl AsRef<str>, let channel = Endpoint::from_shared(config.endpoint())?.connect().await?;
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?;
let mut auth = AuthClient::new(channel.clone()); let mut auth = AuthClient::new(channel.clone());
let resp = auth.login(LoginRequest { let resp = auth.login(LoginRequest {
username: username.as_ref().to_string(), username: config.username.clone(),
password: password.as_ref().to_string(), password: config.password.clone(),
}) })
.await? .await?
.into_inner(); .into_inner();
let claims = InternallyMutable::new(resp.token); let claims = InternallyMutable::new(resp.token);
// TODO move this one into network.rs
let session = SessionClient::with_interceptor( let session = SessionClient::with_interceptor(
channel, network::SessionInterceptor(claims.channel()) channel, network::SessionInterceptor(claims.channel())
); );
Ok(Client(Arc::new(ClientInner { Ok(Client(Arc::new(ClientInner {
host,
user: resp.user.into(), user: resp.user.into(),
workspaces: DashMap::default(), workspaces: DashMap::default(),
claims, claims, auth, session, config
auth, session,
}))) })))
} }
@ -139,7 +129,7 @@ impl Client {
let ws = Workspace::try_new( let ws = Workspace::try_new(
workspace.as_ref().to_string(), workspace.as_ref().to_string(),
self.0.user.clone(), self.0.user.clone(),
&self.0.host, self.0.config.clone(),
token, token,
self.0.claims.channel(), self.0.claims.channel(),
) )

View file

@ -12,7 +12,7 @@ use codemp_proto::{cursor::{CursorPosition, RowCol}, files::BufferNode};
/// ///
/// An unique [CursorController] exists for each active [crate::Workspace]. /// An unique [CursorController] exists for each active [crate::Workspace].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "py", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi)] #[cfg_attr(feature = "js", napi_derive::napi)]
pub struct CursorController(pub(crate) Arc<CursorControllerInner>); pub struct CursorController(pub(crate) Arc<CursorControllerInner>);

View file

@ -6,7 +6,7 @@
/// This currently wraps an [http code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), /// This currently wraps an [http code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status),
/// returned as procedure status. /// returned as procedure status.
#[derive(Debug, thiserror::Error)] #[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); pub struct RemoteError(#[from] tonic::Status);
/// Wraps [std::result::Result] with a [RemoteError]. /// Wraps [std::result::Result] with a [RemoteError].
@ -16,11 +16,11 @@ pub type RemoteResult<T> = std::result::Result<T, RemoteError>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ConnectionError { pub enum ConnectionError {
/// Underlying [`tonic::transport::Error`]. /// Underlying [`tonic::transport::Error`].
#[error("transport error: {0}")] #[error("transport error: {0:?}")]
Transport(#[from] tonic::transport::Error), Transport(#[from] tonic::transport::Error),
/// Error from the remote server, see [`RemoteError`]. /// Error from the remote server, see [`RemoteError`].
#[error("server rejected connection attempt: {0}")] #[error("server rejected connection attempt: {0:?}")]
Remote(#[from] RemoteError), Remote(#[from] RemoteError),
} }

View file

@ -1,6 +1,6 @@
use jni::{objects::{JClass, JObject, JValueGen}, sys::{jlong, jobject, jstring}, JNIEnv}; 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}; use super::{JExceptable, RT};
@ -87,3 +87,70 @@ fn recv_jni(env: &mut JNIEnv, change: Option<crate::api::TextChange>) -> jobject
} }
}.as_raw() }.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);
}

View file

@ -1,27 +1,69 @@
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jlong, jobject, jobjectArray}, JNIEnv}; use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jboolean, jint, jlong, jobject, jobjectArray}, JNIEnv};
use crate::{client::Client, Workspace}; 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] #[no_mangle]
pub extern "system" fn Java_mp_code_Client_connect<'local>( pub extern "system" fn Java_mp_code_Client_connect<'local>(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass<'local>, _class: JClass<'local>,
url: JString<'local>,
user: JString<'local>, user: JString<'local>,
pwd: JString<'local> pwd: JString<'local>
) -> jobject { ) -> jobject {
let url: String = env.get_string(&url) let username: String = env.get_string(&user)
.map(|s| s.into()) .map(|s| s.into())
.jexcept(&mut env); .jexcept(&mut env);
let user: String = env.get_string(&user) let password: String = env.get_string(&pwd)
.map(|s| s.into()) .map(|s| s.into())
.jexcept(&mut env); .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()) .map(|s| s.into())
.jexcept(&mut env); .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(|client| Box::into_raw(Box::new(client)) as jlong)
.map(|ptr| { .map(|ptr| {
env.find_class("mp/code/Client") 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() }).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. /// Join a [Workspace] and return a pointer to it.
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_mp_code_Client_join_1workspace<'local>( 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 let list = RT
.block_on(client.list_workspaces(owned != 0, invited != 0)) .block_on(client.list_workspaces(owned != 0, invited != 0))
.jexcept(&mut env); .jexcept(&mut env);
env.find_class("java/lang/String") env.find_class("java/lang/String")
.and_then(|class| env.new_object_array(list.len() as i32, class, JObject::null())) .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() { for (idx, path) in list.iter().enumerate() {
env.new_string(path) 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) .jexcept(&mut env)
} }
arr
}).jexcept(&mut env).as_raw() }).jexcept(&mut env).as_raw()
} }

View file

@ -1,7 +1,7 @@
use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv}; use jni::{objects::{JClass, JObject, JString, JValueGen}, sys::{jlong, jobject}, JNIEnv};
use crate::api::Controller; 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. /// Try to fetch a [crate::api::Cursor], or returns null if there's nothing.
#[no_mangle] #[no_mangle]
@ -56,6 +56,16 @@ fn jni_recv(env: &mut JNIEnv, cursor: Option<crate::api::Cursor>) -> jobject {
}.as_raw() }.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]. /// Receive from Java, converts and sends a [crate::api::Cursor].
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_mp_code_CursorController_send<'local>( pub extern "system" fn Java_mp_code_CursorController_send<'local>(

View file

@ -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 client;
pub mod workspace; pub mod workspace;
pub mod cursor; 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. /// This is essentially the same as [TryInto], but that can't be emplemented on non-local types.
pub(crate) trait JObjectify<'local> { pub(crate) trait JObjectify<'local> {
/// The error type, likely to be [jni::errors::Error]. /// 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]. /// 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>; 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 { impl<'local> JObjectify<'local> for uuid::Uuid {
type Error = jni::errors::Error; type Error = jni::errors::Error;
fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, jni::errors::Error> { fn jobjectify(self, env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JObject<'local>, Self::Error> {
env.find_class("java/util/UUID").and_then(|class| { let class = env.find_class("java/util/UUID")?;
let (msb, lsb) = self.as_u64_pair(); let (msb, lsb) = self.as_u64_pair();
let msb = i64::from_ne_bytes(msb.to_ne_bytes()); let msb = i64::from_ne_bytes(msb.to_ne_bytes());
let lsb = i64::from_ne_bytes(lsb.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)]) 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;

View file

@ -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 crate::Workspace;
use super::{JExceptable, JObjectify, RT}; use super::{JExceptable, JObjectify, RT};
@ -22,9 +22,7 @@ pub extern "system" fn Java_mp_code_Workspace_get_1cursor<'local>(
self_ptr: jlong self_ptr: jlong
) -> jobject { ) -> jobject {
let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) };
env.find_class("mp/code/CursorController").and_then(|class| workspace.cursor().jobjectify(&mut env).jexcept(&mut env).as_raw()
env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(workspace.cursor())) as jlong)])
).jexcept(&mut env).as_raw()
} }
/// Get a buffer controller by name and returns a pointer to it. /// 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) } let path = unsafe { env.get_string_unchecked(&input) }
.map(|path| path.to_string_lossy().to_string()) .map(|path| path.to_string_lossy().to_string())
.jexcept(&mut env); .jexcept(&mut env);
workspace.buffer_by_name(&path)
workspace.buffer_by_name(&path).map(|buf| { .map(|buf| buf.jobjectify(&mut env).jexcept(&mut env))
env.find_class("mp/code/BufferController").and_then(|class| .unwrap_or_default()
env.new_object(class, "(J)V", &[JValueGen::Long(Box::into_raw(Box::new(buf)) as jlong)]) .as_raw()
).jexcept(&mut env)
}).unwrap_or_default().as_raw()
} }
/// Create a new buffer. /// Create a new buffer.
@ -69,7 +65,8 @@ pub extern "system" fn Java_mp_code_Workspace_get_1file_1tree(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
self_ptr: jlong, self_ptr: jlong,
filter: JString filter: JString,
strict: jboolean
) -> jobjectArray { ) -> jobjectArray {
let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) }; let workspace = unsafe { Box::leak(Box::from_raw(self_ptr as *mut Workspace)) };
let filter: Option<String> = if filter.is_null() { 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") env.find_class("java/lang/String")
.and_then(|class| env.new_object_array(file_tree.len() as i32, class, JObject::null())) .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() { for (idx, path) in file_tree.iter().enumerate() {
env.new_string(path) 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)
} }
arr
}).jexcept(&mut env).as_raw() }).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()) .map(|path| path.to_string_lossy().to_string())
.jexcept(&mut env); .jexcept(&mut env);
RT.block_on(workspace.attach(&path)) RT.block_on(workspace.attach(&path))
.map(|buffer| Box::into_raw(Box::new(buffer)) as jlong) .map(|buffer| buffer.jobjectify(&mut env).jexcept(&mut env))
.map(|ptr| { .jexcept(&mut env)
env.find_class("mp/code/BufferController") .as_raw()
.and_then(|class| env.new_object(class, "(J)V", &[JValueGen::Long(ptr)]))
.jexcept(&mut env)
}).jexcept(&mut env).as_raw()
} }
/// Detach from a buffer. /// 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") env.find_class("java/util/UUID")
.and_then(|class| env.new_object_array(users.len() as i32, &class, JObject::null())) .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() { for (idx, user) in users.iter().enumerate() {
user.id.jobjectify(&mut env) 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); .jexcept(&mut env);
} }
arr
}).jexcept(&mut env).as_raw() }).jexcept(&mut env).as_raw()
} }

View file

@ -3,11 +3,8 @@ use crate::{Client, Workspace};
#[napi] #[napi]
/// connect to codemp servers and return a client session /// connect to codemp servers and return a client session
pub async fn connect(addr: Option<String>, username: String, password: String) -> napi::Result<crate::Client>{ pub async fn connect(config: crate::api::Config) -> napi::Result<crate::Client>{
let client = crate::Client::connect(addr.as_deref().unwrap_or("http://code.mp:50053"), username, password) Ok(crate::Client::connect(config).await?)
.await?;
Ok(client)
} }
#[napi] #[napi]

View file

@ -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 client;
pub mod workspace; pub mod workspace;
pub mod cursor; pub mod cursor;

View file

@ -12,8 +12,8 @@ impl Workspace {
} }
#[napi(js_name = "filetree")] #[napi(js_name = "filetree")]
pub fn js_filetree(&self, filter: Option<&str>) -> Vec<String> { pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.filetree(filter) self.filetree(filter, strict)
} }
#[napi(js_name = "cursor")] #[napi(js_name = "cursor")]

View file

@ -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::io::Write;
use std::sync::Mutex; 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> { fn spawn_runtime_driver(_: &Lua, ():()) -> LuaResult<Driver> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = std::thread::spawn(move || tokio().block_on(async move { let handle = std::thread::spawn(move || tokio().block_on(async move {
@ -254,19 +253,19 @@ impl LuaUserData for CodempClient {
impl LuaUserData for CodempWorkspace { impl LuaUserData for CodempWorkspace {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); 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?) } 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?) } 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)) 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?) } a_sync! { this => Ok(this.delete(&name).await?) }
); );
@ -295,8 +294,8 @@ impl LuaUserData for CodempWorkspace {
// Ok(()) // Ok(())
// }); // });
methods.add_method("filetree", |_, this, (filter,):(Option<String>,)| methods.add_method("filetree", |_, this, (filter, strict,):(Option<String>, bool,)|
Ok(this.filetree(filter.as_deref())) Ok(this.filetree(filter.as_deref(), strict))
); );
} }
@ -308,6 +307,7 @@ impl LuaUserData for CodempWorkspace {
} }
} }
from_lua_serde! { CodempEvent }
impl LuaUserData for CodempEvent { impl LuaUserData for CodempEvent {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
@ -325,12 +325,13 @@ impl LuaUserData for CodempEvent {
} }
} }
impl LuaUserData for CodempCursorController { impl LuaUserData for CodempCursorController {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); 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)| methods.add_method("send", |_, this, (cursor,):(CodempCursor,)|
a_sync! { this => Ok(this.send(CodempCursor { buffer, start: (start_row, start_col), end: (end_row, end_col), user: None }).await?) } a_sync! { this => Ok(this.send(cursor).await?) }
); );
methods.add_method("try_recv", |_, this, ()| methods.add_method("try_recv", |_, this, ()|
a_sync! { this => Ok(this.try_recv().await?) } a_sync! { this => Ok(this.try_recv().await?) }
@ -348,6 +349,7 @@ impl LuaUserData for CodempCursorController {
} }
} }
from_lua_serde! { CodempCursor }
impl LuaUserData for Cursor { impl LuaUserData for Cursor {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); 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) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_method("send", |_, this, (start, end, content, hash): (usize, usize, String, Option<i64>)| methods.add_method("send", |_, this, (change,): (CodempTextChange,)|
a_sync! { this => Ok( a_sync! { this => Ok(this.send(change).await?)}
this.send(
CodempTextChange {
start: start as u32,
end: end as u32,
content,
hash,
}
).await?
)}
); );
methods.add_method("try_recv", |_, this, ()| a_sync! { this => Ok(this.try_recv().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 { impl LuaUserData for CodempTextChange {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("content", |_, this| Ok(this.content.clone())); 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)] #[derive(Debug, Clone)]
struct LuaLoggerProducer(mpsc::UnboundedSender<String>); struct LuaLoggerProducer(mpsc::UnboundedSender<String>);
@ -534,8 +537,8 @@ fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?; let exports = lua.create_table()?;
// entrypoint // entrypoint
exports.set("connect", lua.create_function(|_, (host, username, password):(String,String,String)| exports.set("connect", lua.create_function(|_, (config,):(CodempConfig,)|
a_sync! { => Ok(CodempClient::connect(host, username, password).await?) } a_sync! { => Ok(CodempClient::connect(config).await?) }
)?)?; )?)?;
// utils // utils

View file

@ -1,9 +1,47 @@
//! ### FFI //! # Foreign Function Interface
//! The glue code for FFI (Foreign Function Interface) in various languages, each gated behind //! `codemp` aims to be available as a library from as many programming languages as possible.
//! a feature flag. //! 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 //! All async operations are handled on a separate tokio runtime, automatically managed by `napi`.
//! available to simplify dependency management and provide type hints in editor. //! 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] /// java bindings, built with [jni]
#[cfg(feature = "java")] #[cfg(feature = "java")]
@ -18,5 +56,5 @@ pub mod lua;
pub mod js; pub mod js;
/// python bindings, built with [pyo3] /// python bindings, built with [pyo3]
#[cfg(feature = "python")] #[cfg(feature = "py")]
pub mod python; pub mod python;

View file

@ -5,25 +5,13 @@ use pyo3::prelude::*;
#[pymethods] #[pymethods]
impl Client { impl Client {
#[new] // #[new]
fn __new__( // fn __new__(
host: String, // host: String,
username: String, // username: String,
password: String, // password: String,
) -> crate::errors::ConnectionResult<Self> { // ) -> crate::errors::ConnectionResult<Self> {
super::tokio().block_on(Client::connect(host, username, password)) // 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
// }
// } // }
#[pyo3(name = "join_workspace")] #[pyo3(name = "join_workspace")]

View file

@ -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 client;
pub mod controllers; pub mod controllers;
pub mod workspace; pub mod workspace;
@ -150,8 +146,18 @@ fn init() -> PyResult<Driver> {
} }
#[pyfunction] #[pyfunction]
fn connect(host: String, username: String, password: String) -> PyResult<Promise> { fn get_default_config() -> crate::api::Config {
a_sync!(Client::connect(host, username, password).await) 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] #[pyfunction]
@ -222,6 +228,7 @@ impl IntoPy<PyObject> for crate::api::User {
#[pymodule] #[pymodule]
fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> { fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(init, m)?)?; 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!(connect, m)?)?;
m.add_function(wrap_pyfunction!(set_logger, m)?)?; m.add_function(wrap_pyfunction!(set_logger, m)?)?;
m.add_class::<Driver>()?; m.add_class::<Driver>()?;

View file

@ -82,8 +82,8 @@ impl Workspace {
} }
#[pyo3(name = "filetree")] #[pyo3(name = "filetree")]
#[pyo3(signature = (filter=None))] #[pyo3(signature = (filter=None, strict=false))]
fn pyfiletree(&self, filter: Option<&str>) -> Vec<String> { fn pyfiletree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.filetree(filter) self.filetree(filter, strict)
} }
} }

View file

@ -7,6 +7,7 @@ pub use crate::api::{
Cursor as CodempCursor, Cursor as CodempCursor,
User as CodempUser, User as CodempUser,
Event as CodempEvent, Event as CodempEvent,
Config as CodempConfig,
}; };
pub use crate::{ pub use crate::{

View file

@ -33,7 +33,7 @@ use uuid::Uuid;
use napi_derive::napi; use napi_derive::napi;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "py", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi)] #[cfg_attr(feature = "js", napi)]
pub struct Workspace(Arc<WorkspaceInner>); pub struct Workspace(Arc<WorkspaceInner>);
@ -54,12 +54,12 @@ impl Workspace {
pub(crate) async fn try_new( pub(crate) async fn try_new(
name: String, name: String,
user: User, user: User,
dest: &str, config: crate::api::Config,
token: Token, token: Token,
claims: tokio::sync::watch::Receiver<codemp_proto::common::Token>, // TODO ughh receiving this claims: tokio::sync::watch::Receiver<codemp_proto::common::Token>, // TODO ughh receiving this
) -> ConnectionResult<Self> { ) -> ConnectionResult<Self> {
let workspace_claim = InternallyMutable::new(token); 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 ws_stream = services.ws().attach(Empty {}).await?.into_inner();
let (tx, rx) = mpsc::channel(128); let (tx, rx) = mpsc::channel(128);
@ -279,10 +279,17 @@ impl Workspace {
} }
/// Get the filetree as it is currently cached. /// 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 // #[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() 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()) .map(|f| f.clone())
.collect() .collect()
} }
@ -357,8 +364,8 @@ impl Drop for WorkspaceInner {
} }
} }
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))] #[cfg_attr(feature = "py", pyo3::pyclass(eq, eq_int))]
#[cfg_attr(feature = "python", derive(PartialEq))] #[cfg_attr(feature = "py", derive(PartialEq))]
pub enum DetachResult { pub enum DetachResult {
NotAttached, NotAttached,
Detaching, Detaching,