diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index a5c704b..24e9e4e 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -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 }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e204dc..1485c9d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - luajit # should we test with lua54 instead? - java - js - - python + - py toolchain: - stable # - beta diff --git a/Cargo.lock b/Cargo.lock index cd9e00b..5261b34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 3f467df..4ca0a86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ ] license = "GPL-3.0-only" edition = "2021" -version = "0.7.0-beta.3" +version = "0.7.0-beta.4" exclude = ["dist/*"] [lib] @@ -43,7 +43,7 @@ lazy_static = { version = "1.4", optional = true } jni = { version = "0.21", features = ["invocation"], optional = true } # glue (lua) -mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send"], optional = true } +mlua-codemp-patch = { version = "0.10.0-beta.2", features = ["module", "send", "serialize"], optional = true } # glue (js) napi = { version = "2.16", features = ["full"], optional = true } @@ -54,6 +54,7 @@ pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"], optiona # extra async-trait = { version = "0.1", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } [build-dependencies] # glue (js) @@ -65,18 +66,15 @@ pyo3-build-config = { version = "0.19", optional = true } default = [] # extra async-trait = ["dep:async-trait"] +serialize = ["dep:serde", "uuid/serde"] # ffi rust = [] # used for ci matrix java = ["lazy_static", "jni", "tracing-subscriber"] js = ["napi-build", "tracing-subscriber", "napi", "napi-derive"] -python = ["pyo3", "tracing-subscriber", "pyo3-build-config"] -lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static"] -lua54 = ["lua", "mlua-codemp-patch/lua54"] -lua53 = ["lua", "mlua-codemp-patch/lua53"] -lua52 = ["lua", "mlua-codemp-patch/lua52"] -lua51 = ["lua", "mlua-codemp-patch/lua51"] -luajit = ["lua", "mlua-codemp-patch/luajit"] +py = ["pyo3", "tracing-subscriber", "pyo3-build-config"] +lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/lua54"] +luajit = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize", "mlua-codemp-patch/luajit"] [package.metadata.docs.rs] # enabled features when building on docs.rs -features = ["lua", "java", "js", "python"] +features = ["serialize"] diff --git a/build.rs b/build.rs index 9522fcb..fa74299 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ #[cfg(feature = "js")] extern crate napi_build; -#[cfg(feature = "python")] +#[cfg(feature = "py")] extern crate pyo3_build_config; /// The main method of the buildscript, required by some glue modules. @@ -11,7 +11,7 @@ fn main() { napi_build::setup(); } - #[cfg(feature = "python")] + #[cfg(feature = "py")] { pyo3_build_config::add_extension_module_link_args(); } diff --git a/dist/README.md b/dist/README.md new file mode 100644 index 0000000..317e696 --- /dev/null +++ b/dist/README.md @@ -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=`, replacing `` 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. diff --git a/dist/java/README.md b/dist/java/README.md deleted file mode 100644 index 6cf16e0..0000000 --- a/dist/java/README.md +++ /dev/null @@ -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. diff --git a/dist/java/src/mp/code/BufferController.java b/dist/java/src/mp/code/BufferController.java index 818bed6..32f2704 100644 --- a/dist/java/src/mp/code/BufferController.java +++ b/dist/java/src/mp/code/BufferController.java @@ -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 cb); + public void callback(Callback cb) { + callback(this.ptr, cb); + } + private static native void free(long self); @Override protected void finalize() { diff --git a/dist/java/src/mp/code/Client.java b/dist/java/src/mp/code/Client.java index d96bc06..acd65c6 100644 --- a/dist/java/src/mp/code/Client.java +++ b/dist/java/src/mp/code/Client.java @@ -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); diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index fb93983..850fcae 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -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 cb); + public void callback(Callback cb) { + callback(this.ptr, cb); + } + private static native void free(long self); @Override protected void finalize() { diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index ddc971a..3b5da95 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -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 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 filter, boolean strict) { + return get_file_tree(this.ptr, filter.orElse(null), strict); } private static native void create_buffer(String path) throws ConnectionRemoteException; diff --git a/dist/java/src/mp/code/data/Callback.java b/dist/java/src/mp/code/data/Callback.java new file mode 100644 index 0000000..c436f67 --- /dev/null +++ b/dist/java/src/mp/code/data/Callback.java @@ -0,0 +1,6 @@ +package mp.code.data; + +@FunctionalInterface +public interface Callback { + void invoke(T controller); +} diff --git a/dist/java/src/mp/code/data/User.java b/dist/java/src/mp/code/data/User.java new file mode 100644 index 0000000..0c672bb --- /dev/null +++ b/dist/java/src/mp/code/data/User.java @@ -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; + } +} diff --git a/dist/js/README.md b/dist/js/README.md deleted file mode 100644 index 570e186..0000000 --- a/dist/js/README.md +++ /dev/null @@ -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. - diff --git a/dist/js/build.sh b/dist/js/build.sh deleted file mode 100644 index ed9c465..0000000 --- a/dist/js/build.sh +++ /dev/null @@ -1 +0,0 @@ -npx napi build --cargo-cwd ../.. \ No newline at end of file diff --git a/dist/js/publish/package.json b/dist/js/package.json similarity index 84% rename from dist/js/publish/package.json rename to dist/js/package.json index 8aa2929..df3adae 100644 --- a/dist/js/publish/package.json +++ b/dist/js/package.json @@ -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" } } diff --git a/dist/js/publish/README.md b/dist/js/publish/README.md deleted file mode 100644 index cff94db..0000000 --- a/dist/js/publish/README.md +++ /dev/null @@ -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) - - -## 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). diff --git a/dist/lua/README.md b/dist/lua/README.md deleted file mode 100644 index c7342a7..0000000 --- a/dist/lua/README.md +++ /dev/null @@ -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` diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 6408412..8e19337 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -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 diff --git a/dist/py/README.md b/dist/py/README.md deleted file mode 100644 index a0e19f3..0000000 --- a/dist/py/README.md +++ /dev/null @@ -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. diff --git a/dist/py/src/codemp/codemp.pyi b/dist/py/src/codemp/codemp.pyi index dc9a974..d7eebc8 100644 --- a/dist/py/src/codemp/codemp.pyi +++ b/dist/py/src/codemp/codemp.pyi @@ -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: """ diff --git a/src/api/change.rs b/src/api/change.rs index 6fc8188..6e0aa1c 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -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. /// diff --git a/src/api/config.rs b/src/api/config.rs new file mode 100644 index 0000000..8a70a10 --- /dev/null +++ b/src/api/config.rs @@ -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, + /// port to connect to, default 50053 + pub port: Option, + /// enable or disable tls, default true + pub tls: Option, +} + +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() + ) + } +} diff --git a/src/api/cursor.rs b/src/api/cursor.rs index dc7467f..06b6b94 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -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), diff --git a/src/api/event.rs b/src/api/event.rs index c6159c4..26e87df 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -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). diff --git a/src/api/mod.rs b/src/api/mod.rs index 8097b14..2e5f00d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -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; diff --git a/src/api/user.rs b/src/api/user.rs index a00ae3a..2b610f9 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -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, diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index b91016b..a690b05 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -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); diff --git a/src/client.rs b/src/client.rs index 2bdc60b..bd425d9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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); #[derive(Debug)] struct ClientInner { user: User, - host: String, + config: crate::api::Config, workspaces: DashMap, auth: AuthClient, session: SessionClient>, @@ -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, - username: impl AsRef, - password: impl AsRef, - ) -> ConnectionResult { - 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 { + // 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(), ) diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 780590e..7bb1789 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -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); diff --git a/src/errors.rs b/src/errors.rs index e817631..500d8ce 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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 = std::result::Result; #[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), } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 57dbb5a..8315feb 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -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) -> 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); +} diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 43ef790..c0dbd5b 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -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() } diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 2472407..31f43a5 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -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) -> 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>( diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 7c0ca05..3bcb617 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -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 JExceptable for Result 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, 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::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, 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, 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, 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, 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; + diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 14e57ef..540580f 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -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 = 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() } diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index d82e8f3..8f5efab 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -3,11 +3,8 @@ use crate::{Client, Workspace}; #[napi] /// connect to codemp servers and return a client session -pub async fn connect(addr: Option, username: String, password: String) -> napi::Result{ - 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{ + Ok(crate::Client::connect(config).await?) } #[napi] diff --git a/src/ffi/js/mod.rs b/src/ffi/js/mod.rs index bdad225..e1b6ddf 100644 --- a/src/ffi/js/mod.rs +++ b/src/ffi/js/mod.rs @@ -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; diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index 0711beb..a470c2e 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -12,8 +12,8 @@ impl Workspace { } #[napi(js_name = "filetree")] - pub fn js_filetree(&self, filter: Option<&str>) -> Vec { - self.filetree(filter) + pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec { + self.filetree(filter, strict) } #[napi(js_name = "cursor")] diff --git a/src/ffi/lua.rs b/src/ffi/lua.rs index 4bdc0a5..4a80bab 100644 --- a/src/ffi/lua.rs +++ b/src/ffi/lua.rs @@ -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 { 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>(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,)| - Ok(this.filetree(filter.as_deref())) + methods.add_method("filetree", |_, this, (filter, strict,):(Option, 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>(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>(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>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); @@ -388,17 +390,8 @@ impl LuaUserData for CodempBufferController { fn add_methods>(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)| - 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>(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>(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); @@ -534,8 +537,8 @@ fn entrypoint(lua: &Lua) -> LuaResult { 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 diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 241bdee..f9c4ae4 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -1,9 +1,47 @@ -//! ### FFI -//! The glue code for FFI (Foreign Function Interface) in various languages, each gated behind -//! a feature flag. +//! # Foreign Function Interface +//! `codemp` aims to be available as a library from as many programming languages as possible. +//! To achieve this, we rely on Foreign Function Interface. +//! +//! ## JavaScript +//! Our JavaScript glue is built with [`napi`](https://napi.rs). //! -//! For all except Java, the resulting shared object is ready to use, but external packages are -//! available to simplify dependency management and provide type hints in editor. +//! All async operations are handled on a separate tokio runtime, automatically managed by `napi`. +//! Callbacks are safely scheduled to be called on the main loop thread. +//! +//! ## Python +//! Our Python glue is built with [`PyO3`](https://pyo3.rs). +//! +//! All async operations return a `Promise`, which can we `.wait()`-ed to block and get the return +//! value. The `Future` itself is run on a `tokio` runtime in a dedicated thread, which must be +//! stared with `codemp.init()` before doing any async operations. +//! +//! ## Lua +//! Our Lua glue is built with [`mlua`](https://github.com/mlua-rs/mlua). +//! +//! Lua bindings run all async code on a current thread tokio runtime, which should be driven with +//! a dedicated thread. +//! +//! All async functions will return a `Promise`, which can be `:await()`-ed to block and get the +//! return value. +//! +//! Note as Lua uses filename to locate entrypoint symbol, so shared object can't just have any name. +//! Accepted filenames are `libcodemp.___`, `codemp.___`, `codemp_native.___`, `codemp_lua.___` (extension depends on your platform: `so` on linux, `dll` on windows, `dylib` on macos). +//! Type hints are provided in `dist/lua/annotations.lua`, just include them in your language server: `---@module 'annotations'`. +//! +//! `codemp` is available as a rock on [LuaRocks](https://luarocks.org/modules/alemi/codemp), +//! however LuaRocks compiles from source and will require having `cargo` installed. +//! We provide pre-built binaries at [codemp.dev/releases/lua](https://codemp.dev/releases/lua/). +//! **Please do not rely on this link, as our built binaries will likely move somewhere else soon!**. +//! +//! ## Java +//! Our Java glue is built with [`jni`](https://github.com/jni-rs/jni-rs). +//! +//! Memory management is entirely delegated to the JVM's garbage collector. +//! A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually. +//! +//! Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`. +//! `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug. +//! /// java bindings, built with [jni] #[cfg(feature = "java")] @@ -18,5 +56,5 @@ pub mod lua; pub mod js; /// python bindings, built with [pyo3] -#[cfg(feature = "python")] +#[cfg(feature = "py")] pub mod python; diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index 93afba7..ffeb3a6 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -5,25 +5,13 @@ use pyo3::prelude::*; #[pymethods] impl Client { - #[new] - fn __new__( - host: String, - username: String, - password: String, - ) -> crate::errors::ConnectionResult { - super::tokio().block_on(Client::connect(host, username, password)) - } - - // #[pyo3(name = "join_workspace")] - // async fn pyjoin_workspace(&self, workspace: String) -> JoinHandle> { - // 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 { + // super::tokio().block_on(Client::connect(host, username, password)) // } #[pyo3(name = "join_workspace")] diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 8268d93..496f159 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -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 { } #[pyfunction] -fn connect(host: String, username: String, password: String) -> PyResult { - 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) -> PyResult { + let conf: crate::api::Config = config.extract(py)?; + a_sync!(Client::connect(conf).await) } #[pyfunction] @@ -222,6 +228,7 @@ impl IntoPy 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::()?; diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index c9b12e8..1a10f4d 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -82,8 +82,8 @@ impl Workspace { } #[pyo3(name = "filetree")] - #[pyo3(signature = (filter=None))] - fn pyfiletree(&self, filter: Option<&str>) -> Vec { - self.filetree(filter) + #[pyo3(signature = (filter=None, strict=false))] + fn pyfiletree(&self, filter: Option<&str>, strict: bool) -> Vec { + self.filetree(filter, strict) } } diff --git a/src/prelude.rs b/src/prelude.rs index 834bc36..a2fd045 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -7,6 +7,7 @@ pub use crate::api::{ Cursor as CodempCursor, User as CodempUser, Event as CodempEvent, + Config as CodempConfig, }; pub use crate::{ diff --git a/src/workspace.rs b/src/workspace.rs index 6c4dc97..1977f63 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -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); @@ -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, // TODO ughh receiving this ) -> ConnectionResult { 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 { + pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec { 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,