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

View file

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

52
Cargo.lock generated
View file

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

View file

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

View file

@ -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
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;
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() {

View file

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

View file

@ -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() {

View file

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

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

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

View file

@ -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
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
//! 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),

View file

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

View file

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

View file

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

View file

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

View file

@ -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(),
)

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

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

View file

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

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

View file

@ -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).
//!
//! 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.
//!
//! 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.
/// 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;

View file

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

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 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>()?;

View file

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

View file

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

View file

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