From 6511f1749da0b2b81fa05b0dcacb55bf65723cc8 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 30 Jul 2025 20:54:14 +0200 Subject: [PATCH 001/121] chore: bump dependencies (easy ones) --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57cc3281..2c5a3cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ authors = [ "cschen " ] license = "GPL-3.0-only" -edition = "2021" +edition = "2024" version = "0.8.5" exclude = ["dist/*"] @@ -26,10 +26,10 @@ thiserror = "2.0" diamond-types = "1.0" # proto codemp-proto = "0.7" -uuid = { version = "1.13", features = ["v4"] } -tonic = { version = "0.12", features = ["tls", "tls-roots"] } +uuid = { version = "1.17", features = ["v4"] } +tonic = { version = "0.14", features = ["tls-native-roots"] } # api -tokio = { version = "1.43", features = ["macros", "rt-multi-thread", "sync"] } +tokio = { version = "1.47", features = ["macros", "rt-multi-thread", "sync"] } xxhash-rust = { version = "0.8", features = ["xxh3"] } # client tokio-stream = "0.1" From 514a08037549e4fd24d0140464c61a03a0055712 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 30 Jul 2025 20:54:28 +0200 Subject: [PATCH 002/121] chore: bump ffi deps (this is nasty) --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c5a3cb8..2f97c783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,14 +43,14 @@ jni = { version = "0.21", features = ["invocation"], optional = true } jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } # glue (lua) -mlua = { version = "0.10", features = ["module", "serialize", "error-send"], optional = true } +mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } # glue (js) -napi = { version = "2.16", features = ["full"], optional = true } -napi-derive = { version="2.16", optional = true} +napi = { version = "3.1", features = ["full"], optional = true } +napi-derive = { version="3.1", optional = true} # glue (python) -pyo3 = { version = "0.23", features = ["extension-module", "multiple-pymethods"], optional = true} +pyo3 = { version = "0.25", features = ["extension-module", "multiple-pymethods"], optional = true} # extra async-trait = { version = "0.1", optional = true } @@ -58,9 +58,9 @@ serde = { version = "1.0", features = ["derive"], optional = true } [build-dependencies] # glue (js) -napi-build = { version = "2.1", optional = true } +napi-build = { version = "2.2", optional = true } # glue (python) -pyo3-build-config = { version = "0.23", optional = true } +pyo3-build-config = { version = "0.25", optional = true } [features] default = ["lua-jit", "py-abi3"] From 1f0feaa2e75f64dbc51de65fca7c558601d2c263 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 30 Jul 2025 20:54:40 +0200 Subject: [PATCH 003/121] chore: update lockfile --- Cargo.lock | 825 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 537 insertions(+), 288 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e7f4c38..84c2422d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-stream" @@ -66,18 +66,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -88,9 +88,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -99,14 +99,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes", "futures-util", "http", "http-body", "http-body-util", "itoa", - "matchit", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core 0.5.2", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -139,11 +165,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -162,15 +207,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "serde", @@ -178,27 +223,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.14" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] @@ -211,22 +250,22 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -246,10 +285,10 @@ dependencies = [ "pyo3", "pyo3-build-config", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", - "tonic", + "tonic 0.14.0", "tracing", "tracing-subscriber", "uuid", @@ -263,7 +302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69a4d042572106ec01a58676fd57ebfab11b653b26fa46c887580097d90eb191" dependencies = [ "prost", - "tonic", + "tonic 0.12.3", "tonic-build", "uuid", ] @@ -291,18 +330,18 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" dependencies = [ "unicode-segmentation", ] [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -337,14 +376,20 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "ctor" -version = "0.2.9" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" dependencies = [ - "quote", - "syn 2.0.98", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + [[package]] name = "dashmap" version = "6.1.0" @@ -378,11 +423,26 @@ dependencies = [ "str_indices 0.3.2", ] +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" @@ -401,9 +461,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" dependencies = [ "serde", "typeid", @@ -411,12 +471,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -478,25 +538,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -513,9 +573,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" -version = "0.4.7" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -523,7 +583,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -544,9 +604,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -556,9 +616,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -577,12 +637,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -590,9 +650,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -642,18 +702,20 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -661,14 +723,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -694,29 +757,40 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inventory" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b12ebb6799019b044deaf431eadfe23245b259bba5a2c0796acec3943a3cdb" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "itertools" version = "0.14.0" @@ -728,9 +802,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "java-locator" @@ -784,7 +858,7 @@ checksum = "609491ce00edcf12946945a514d033bf6e8bfbab02c6a25a46ed8cd4749707da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -815,9 +889,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -831,25 +905,25 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -857,9 +931,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lz4_flex" @@ -876,11 +950,17 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -899,29 +979,29 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "mlua" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b" +checksum = "de25fc513588ac1273aa8c6dc0fffee6d32c12f38dc75f5cdc74547121a107ef" dependencies = [ "bstr", "either", @@ -931,15 +1011,16 @@ dependencies = [ "num-traits", "parking_lot", "rustc-hash", + "rustversion", "serde", "serde-value", ] [[package]] name = "mlua-sys" -version = "0.6.7" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594" +checksum = "bcdf7c9e260ca82aaa32ac11148941952b856bb8c69aa5a9e65962f21fcb8637" dependencies = [ "cc", "cfg-if", @@ -948,34 +1029,35 @@ dependencies = [ [[package]] name = "mlua_derive" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870d71c172fcf491c6b5fb4c04160619a2ee3e5a42a1402269c66bcbf1dd4deb" +checksum = "465bddde514c4eb3b50b543250e97c1d4b284fa3ef7dc0ba2992c77545dbceb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "napi" -version = "2.16.16" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839ae2ee5e62c6348669c50098b187c08115bd3cced658c9c0bf945fca0fec83" +checksum = "afaf586c21f260e9dc327ae3585fc6efcbb24a416d5151da38bbd35a1f2663c8" dependencies = [ "bitflags", "chrono", "ctor", "encoding_rs", - "napi-derive", + "napi-build", "napi-sys", - "once_cell", + "nohash-hasher", + "rustc-hash", "serde", "serde_json", "tokio", @@ -983,48 +1065,52 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.4" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19" +checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14" [[package]] name = "napi-derive" -version = "2.16.13" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +checksum = "7e6d190d5e09d449b2b38127cdcdb7aed860599e492a15c73f977d5d87df69a5" dependencies = [ - "cfg-if", "convert_case", + "ctor", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "napi-derive-backend" -version = "1.0.75" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +checksum = "15158ced16693eaa0c709e4c9768ca08eb56325691e68510db8440d27ccd41d1" dependencies = [ "convert_case", - "once_cell", "proc-macro2", "quote", - "regex", "semver", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "napi-sys" -version = "2.4.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" dependencies = [ - "libloading 0.8.6", + "libloading 0.8.8", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1076,9 +1162,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl-probe" @@ -1103,9 +1189,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1113,9 +1199,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1137,27 +1223,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.10.0", ] [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1174,33 +1260,33 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.29" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1215,9 +1301,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1248,7 +1334,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.98", + "syn 2.0.104", "tempfile", ] @@ -1262,7 +1348,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1276,11 +1362,10 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.23.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" dependencies = [ - "cfg-if", "indoc", "inventory", "libc", @@ -1295,9 +1380,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.23.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" dependencies = [ "once_cell", "target-lexicon", @@ -1305,9 +1390,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.23.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" dependencies = [ "libc", "pyo3-build-config", @@ -1315,38 +1400,44 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.23.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "pyo3-macros-backend" -version = "0.23.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1374,14 +1465,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -1417,13 +1508,13 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" -version = "0.17.9" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -1440,9 +1531,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1452,26 +1543,25 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1491,25 +1581,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -1518,15 +1602,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1577,15 +1661,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -1602,20 +1686,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -1640,18 +1724,15 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smartstring" @@ -1666,14 +1747,24 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1711,9 +1802,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1728,19 +1819,18 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.17.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1757,11 +1847,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -1772,44 +1862,45 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tokio" -version = "1.43.0" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1820,14 +1911,14 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -1846,9 +1937,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -1859,9 +1950,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" @@ -1869,7 +1960,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.10.0", "toml_datetime", "winnow", ] @@ -1882,7 +1973,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.7.9", "base64", "bytes", "h2", @@ -1895,13 +1986,41 @@ dependencies = [ "percent-encoding", "pin-project", "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308e1db96abdccdf0a9150fb69112bf6ea72640e0bd834ef0c4a618ccc8c8ddc" +dependencies = [ + "async-trait", + "axum 0.8.4", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", "rustls-native-certs", - "rustls-pemfile", - "socket2", + "socket2 0.6.0", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", - "tower 0.4.13", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -1918,7 +2037,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1949,10 +2068,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.10.0", "pin-project-lite", + "slab", "sync_wrapper", + "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1980,20 +2104,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2042,15 +2166,15 @@ dependencies = [ [[package]] name = "typeid" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -2060,9 +2184,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unindent" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "untrusted" @@ -2072,12 +2196,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.13.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -2113,15 +2239,15 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -2148,7 +2274,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -2170,7 +2296,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2217,11 +2343,61 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", ] [[package]] @@ -2251,6 +2427,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -2275,13 +2460,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2294,6 +2496,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2306,6 +2514,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2318,12 +2532,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2336,6 +2562,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2348,6 +2580,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2360,6 +2598,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2372,6 +2616,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -2383,9 +2633,9 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -2398,23 +2648,22 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] From 5b54251e59d494d06bea890c8c1ad4e2b538f394 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 30 Jul 2025 21:06:11 +0200 Subject: [PATCH 004/121] chore: cargo fmt --- src/api/config.rs | 11 +++++++--- src/buffer/controller.rs | 2 +- src/buffer/worker.rs | 4 ++-- src/client.rs | 4 ++-- src/cursor/controller.rs | 2 +- src/cursor/worker.rs | 41 +++++++++++++++++------------------ src/ffi/java/buffer.rs | 2 +- src/ffi/java/client.rs | 2 +- src/ffi/java/cursor.rs | 2 +- src/ffi/java/workspace.rs | 6 ++--- src/ffi/js/workspace.rs | 4 ++-- src/ffi/lua/buffer.rs | 6 ++++- src/ffi/lua/cursor.rs | 5 ++++- src/ffi/lua/ext/a_sync.rs | 10 ++++----- src/ffi/lua/ext/callback.rs | 10 +++++---- src/ffi/lua/ext/log.rs | 2 +- src/ffi/python/client.rs | 2 +- src/ffi/python/controllers.rs | 4 ++-- src/ffi/python/mod.rs | 2 +- src/ffi/python/workspace.rs | 4 ++-- src/network.rs | 2 +- src/tests/client.rs | 8 ++++--- src/tests/server.rs | 10 +++++---- src/workspace.rs | 19 +++++++++++----- 24 files changed, 94 insertions(+), 70 deletions(-) diff --git a/src/api/config.rs b/src/api/config.rs index 8cfea00c..09e15707 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -68,8 +68,9 @@ impl Config { impl std::fmt::Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if f.alternate() { - write!(f, -r#"""Config {{ + write!( + f, + r#"""Config {{ username: {}, password: ********, host: {:#?}, @@ -79,7 +80,11 @@ r#"""Config {{ self.username, self.host, self.port, self.tls ) } else { - write!(f, "Config {{ username: {}, password: ********, host: {:?}, port: {:?}, tls: {:?} }}", self.username, self.host, self.port, self.tls) + write!( + f, + "Config {{ username: {}, password: ********, host: {:?}, port: {:?}, tls: {:?} }}", + self.username, self.host, self.port, self.tls + ) } } } diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index 9f1a9d13..13b5005e 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -6,9 +6,9 @@ use std::sync::Arc; use diamond_types::LocalVersion; use tokio::sync::{mpsc, oneshot, watch}; -use crate::api::controller::{AsyncReceiver, AsyncSender, Controller, ControllerCallback}; use crate::api::BufferUpdate; use crate::api::TextChange; +use crate::api::controller::{AsyncReceiver, AsyncSender, Controller, ControllerCallback}; use crate::errors::ControllerResult; use crate::ext::IgnorableError; diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index 9b6b434a..5dcd9be0 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -1,15 +1,15 @@ use std::sync::Arc; +use diamond_types::LocalVersion; use diamond_types::list::encoding::ENCODE_PATCH; use diamond_types::list::{Branch, OpLog}; -use diamond_types::LocalVersion; use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; use uuid::Uuid; -use crate::api::controller::ControllerCallback; use crate::api::BufferUpdate; use crate::api::TextChange; +use crate::api::controller::ControllerCallback; use crate::ext::IgnorableError; use codemp_proto::buffer::{BufferEvent, Operation}; diff --git a/src/client.rs b/src/client.rs index 0df972c2..7e29637f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,9 +17,9 @@ use crate::{ workspace::Workspace, }; use codemp_proto::{ - auth::{auth_client::AuthClient, LoginRequest}, + auth::{LoginRequest, auth_client::AuthClient}, common::{Empty, Token}, - session::{session_client::SessionClient, InviteRequest, WorkspaceRequest}, + session::{InviteRequest, WorkspaceRequest, session_client::SessionClient}, }; #[cfg(feature = "py")] diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index d0c544c5..48525070 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -7,8 +7,8 @@ use tokio::sync::{mpsc, oneshot, watch}; use crate::{ api::{ - controller::{AsyncReceiver, AsyncSender, ControllerCallback}, Controller, Cursor, Selection, + controller::{AsyncReceiver, AsyncSender, ControllerCallback}, }, errors::ControllerResult, }; diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index 2e6e62ea..c4b85f72 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -5,7 +5,7 @@ use tonic::Streaming; use uuid::Uuid; use crate::{ - api::{controller::ControllerCallback, Cursor, Selection, User}, + api::{Cursor, Selection, User, controller::ControllerCallback}, ext::IgnorableError, }; use codemp_proto::cursor::{CursorEvent, CursorPosition}; @@ -27,26 +27,25 @@ struct CursorWorker { impl CursorWorker { #[tracing::instrument(skip(self, tx))] fn handle_recv(&mut self, tx: oneshot::Sender>) { - tx.send( - self.store.pop_front().and_then(|event| { - let user_id = Uuid::from(event.user); - if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { - Some(Cursor { - user: user_name, - sel: Selection { - buffer: event.position.buffer.path, - start_row: event.position.start.row, - start_col: event.position.start.col, - end_row: event.position.end.row, - end_col: event.position.end.col - } - }) - } else { - tracing::warn!("received cursor for unknown user {user_id}"); - None - } - }) - ).unwrap_or_warn("client gave up receiving!"); + tx.send(self.store.pop_front().and_then(|event| { + let user_id = Uuid::from(event.user); + if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { + Some(Cursor { + user: user_name, + sel: Selection { + buffer: event.position.buffer.path, + start_row: event.position.start.row, + start_col: event.position.start.col, + end_row: event.position.end.row, + end_col: event.position.end.col, + }, + }) + } else { + tracing::warn!("received cursor for unknown user {user_id}"); + None + } + })) + .unwrap_or_warn("client gave up receiving!"); } } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index edd7b9a3..e30a6718 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -1,4 +1,4 @@ -use jni::{objects::JObject, JNIEnv}; +use jni::{JNIEnv, objects::JObject}; use jni_toolbox::jni; use crate::{ diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 230b26a1..ecd392f6 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,8 +1,8 @@ use crate::{ + Workspace, api::Config, client::Client, errors::{ConnectionError, RemoteError}, - Workspace, }; use jni_toolbox::jni; diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 4457ddcc..60dae442 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -2,7 +2,7 @@ use crate::{ api::{AsyncReceiver, AsyncSender, Cursor, Selection}, errors::ControllerError, }; -use jni::{objects::JObject, JNIEnv}; +use jni::{JNIEnv, objects::JObject}; use jni_toolbox::jni; use super::null_check; diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 3548a926..42174cb2 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,10 +1,10 @@ use crate::{ - api::{controller::AsyncReceiver, User}, + Workspace, + api::{User, controller::AsyncReceiver}, errors::{ConnectionError, ControllerError, RemoteError}, ffi::java::null_check, - Workspace, }; -use jni::{objects::JObject, JNIEnv}; +use jni::{JNIEnv, objects::JObject}; use jni_toolbox::jni; /// Get the workspace id. diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index e862322c..ae0af3d3 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,7 +1,7 @@ +use crate::Workspace; use crate::api::controller::AsyncReceiver; use crate::buffer::controller::BufferController; use crate::cursor::controller::CursorController; -use crate::Workspace; use napi::threadsafe_function::ErrorStrategy::Fatal; use napi::threadsafe_function::{ ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, @@ -121,7 +121,7 @@ impl Workspace { })?; self.callback(move |controller: Workspace| { tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error - // If it blocks the main thread too many time we have to change this + // If it blocks the main thread too many time we have to change this }); Ok(()) diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index d061b9ca..b294108a 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -47,6 +47,10 @@ impl LuaUserData for CodempBufferController { impl CodempBufferController { fn lua_callback_id(&self) -> String { - format!("codemp-buffercontroller({}:{})-callback-registry", self.workspace_id(), self.path()) + format!( + "codemp-buffercontroller({}:{})-callback-registry", + self.workspace_id(), + self.path() + ) } } diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index d590034a..17f18fb4 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -37,6 +37,9 @@ impl LuaUserData for CodempCursorController { impl CodempCursorController { fn lua_callback_id(&self) -> String { - format!("codemp-cursorcontroller({})-callback-registry", self.workspace_id()) + format!( + "codemp-cursorcontroller({})-callback-registry", + self.workspace_id() + ) } } diff --git a/src/ffi/lua/ext/a_sync.rs b/src/ffi/lua/ext/a_sync.rs index 13600dc1..fe68402a 100644 --- a/src/ffi/lua/ext/a_sync.rs +++ b/src/ffi/lua/ext/a_sync.rs @@ -47,12 +47,10 @@ impl LuaUserData for Promise { // TODO: await MUST NOT be used in callbacks!! methods.add_method_mut("await", |_, this, ()| match this.0.take() { None => Err(LuaError::runtime("Promise already awaited")), - Some(x) => Ok( - tokio() - .block_on(x) - .map_err(LuaError::runtime)? - .map_err(LuaError::runtime)? - ), + Some(x) => Ok(tokio() + .block_on(x) + .map_err(LuaError::runtime)? + .map_err(LuaError::runtime)?), }); methods.add_method_mut("cancel", |_, this, ()| match this.0.take() { None => Err(LuaError::runtime("Promise already awaited")), diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index 269fa8f9..9d40a2d0 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -32,7 +32,9 @@ impl CallbackChannel { pub(crate) fn failure(&self, err: impl std::error::Error) { self.tx - .send(LuaCallback::Fail(format!("callback returned error: {err:?}"))) + .send(LuaCallback::Fail(format!( + "callback returned error: {err:?}" + ))) .unwrap_or_warn("error scheduling callback failure") } @@ -51,14 +53,14 @@ impl CallbackChannel { Ok(LuaCallback::Fail(msg)) => { tracing::error!("callback returned error: {msg}"); None - }, + } Ok(LuaCallback::Invoke(key, arg, cleanup)) => { let cb = match lua.named_registry_value::(&key) { Ok(x) => x, Err(e) => { tracing::error!("could not get callback to invoke: {e}"); return None; - }, + } }; if cleanup { if let Err(e) = lua.unset_named_registry_value(&key) { @@ -66,7 +68,7 @@ impl CallbackChannel { } } Some((cb, arg)) - }, + } }, } } diff --git a/src/ffi/lua/ext/log.rs b/src/ffi/lua/ext/log.rs index b9728e98..2509351d 100644 --- a/src/ffi/lua/ext/log.rs +++ b/src/ffi/lua/ext/log.rs @@ -72,7 +72,7 @@ pub(crate) fn setup_tracing( }); } res - }, + } _ => return Err(LuaError::BindError), // TODO full BadArgument type?? }; diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index 90c12a6c..1c32a3a4 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -1,5 +1,5 @@ -use super::a_sync_allow_threads; use super::Client; +use super::a_sync_allow_threads; use crate::api::User; use crate::workspace::Workspace; use pyo3::prelude::*; diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index 3dd17bd5..caae2c9e 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -1,13 +1,13 @@ -use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::TextChange; +use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::{Cursor, Selection}; use crate::buffer::Controller as BufferController; use crate::cursor::Controller as CursorController; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use super::a_sync_allow_threads; use super::Promise; +use super::a_sync_allow_threads; // need to do manually since Controller is a trait implementation #[pymethods] diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index a68e5e6e..b9120da0 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -3,10 +3,10 @@ pub mod controllers; pub mod workspace; use crate::{ + Client, Workspace, api::{BufferUpdate, Config, Cursor, Event, Selection, TextChange, User}, buffer::Controller as BufferController, cursor::Controller as CursorController, - Client, Workspace, }; use pyo3::{ diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index 8d3c5251..cb1d5dc6 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -1,13 +1,13 @@ -use crate::api::controller::AsyncReceiver; use crate::api::User; +use crate::api::controller::AsyncReceiver; use crate::buffer::Controller as BufferController; use crate::cursor::Controller as CursorController; use crate::workspace::Workspace; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use super::a_sync_allow_threads; use super::Promise; +use super::a_sync_allow_threads; #[pymethods] impl Workspace { diff --git a/src/network.rs b/src/network.rs index aaa39c10..1e0bd69d 100644 --- a/src/network.rs +++ b/src/network.rs @@ -3,7 +3,7 @@ use codemp_proto::{ workspace::workspace_client::WorkspaceClient, }; use tonic::{ - service::{interceptor::InterceptedService, Interceptor}, + service::{Interceptor, interceptor::InterceptedService}, transport::{Channel, Endpoint}, }; diff --git a/src/tests/client.rs b/src/tests/client.rs index 6b960a3b..5c7daa6b 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -244,9 +244,11 @@ async fn test_buffer_search() { async move { workspace_alice.create_buffer(&buffer_name).await?; - assert_or_err!(!workspace_alice - .search_buffers(Some(&buffer_name[0..4])) - .is_empty()); + assert_or_err!( + !workspace_alice + .search_buffers(Some(&buffer_name[0..4])) + .is_empty() + ); assert_or_err!(workspace_alice.search_buffers(Some("_")).is_empty()); workspace_alice.delete_buffer(&buffer_name).await?; Ok(()) diff --git a/src/tests/server.rs b/src/tests/server.rs index ebcf4ce0..815d3286 100644 --- a/src/tests/server.rs +++ b/src/tests/server.rs @@ -73,10 +73,12 @@ async fn test_workspace_interactions() { .invite_to_workspace(&workspace_name, &client_bob.current_user().name) .await?; client_bob.attach_workspace(&workspace_name).await?; - assert_or_err!(client_bob - .fetch_joined_workspaces() - .await? - .contains(&workspace_name)); + assert_or_err!( + client_bob + .fetch_joined_workspaces() + .await? + .contains(&workspace_name) + ); assert_or_err!(client_bob.leave_workspace(&workspace_name)); assert_or_err!(client_alice.leave_workspace(&workspace_name)); diff --git a/src/workspace.rs b/src/workspace.rs index 80d2af05..3f930890 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -5,8 +5,8 @@ use crate::{ api::{ - controller::{AsyncReceiver, ControllerCallback}, Event, User, + controller::{AsyncReceiver, ControllerCallback}, }, buffer, cursor, errors::{ConnectionResult, ControllerResult, RemoteResult}, @@ -18,16 +18,19 @@ use codemp_proto::{ common::{Empty, Token}, files::BufferNode, workspace::{ + WorkspaceEvent, workspace_event::{ Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoin, UserLeave, }, - WorkspaceEvent, }, }; use dashmap::{DashMap, DashSet}; use std::sync::{Arc, Weak}; -use tokio::sync::{mpsc::{self, error::TryRecvError}, oneshot, watch}; +use tokio::sync::{ + mpsc::{self, error::TryRecvError}, + oneshot, watch, +}; use tonic::Streaming; use uuid::Uuid; @@ -184,7 +187,8 @@ impl Workspace { ); let stream = self.0.services.buf().attach(req).await?.into_inner(); - let controller = buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, &self.0.name); + let controller = + buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, &self.0.name); self.0.buffers.insert(path.to_string(), controller.clone()); Ok(controller) @@ -345,7 +349,12 @@ struct WorkspaceWorker { impl WorkspaceWorker { #[tracing::instrument(skip(self, stream, weak))] - pub(crate) async fn work(mut self, ws: String, mut stream: Streaming, weak: Weak) { + pub(crate) async fn work( + mut self, + ws: String, + mut stream: Streaming, + weak: Weak, + ) { tracing::debug!("workspace worker starting"); loop { tokio::select! { From f2dd4473e04f6b5cd3108ab56f9a4da1f3c021de Mon Sep 17 00:00:00 2001 From: frelodev Date: Wed, 30 Jul 2025 22:27:50 +0200 Subject: [PATCH 005/121] chore: napi ffi update for bump deps --- src/ffi/js/buffer.rs | 14 ++++---------- src/ffi/js/cursor.rs | 17 ++++------------- src/ffi/js/workspace.rs | 18 ++++++------------ 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 79f10719..5f29caf9 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -2,7 +2,7 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::{BufferUpdate, TextChange}; use crate::buffer::controller::BufferController; use napi::threadsafe_function::{ - ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, + ThreadsafeFunction, ThreadsafeFunctionCallMode, }; use napi_derive::napi; @@ -14,16 +14,10 @@ impl BufferController { js_name = "callback", ts_args_type = "fun: (event: BufferController) => void" )] - pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun - .create_threadsafe_function( - 0, - |ctx: ThreadSafeCallContext| { - Ok(vec![ctx.value]) - }, - )?; + pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: BufferController| { - tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); + tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index eb173ad3..d094644f 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,9 +1,6 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::cursor::controller::CursorController; -use napi::threadsafe_function::ErrorStrategy::Fatal; -use napi::threadsafe_function::{ - ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, -}; +use napi::threadsafe_function::{ ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] @@ -14,16 +11,10 @@ impl CursorController { js_name = "callback", ts_args_type = "fun: (event: CursorController) => void" )] - pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun - .create_threadsafe_function( - 0, - |ctx: ThreadSafeCallContext| { - Ok(vec![ctx.value]) - }, - )?; + pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: CursorController| { - tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); + tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index ae0af3d3..a46e70a1 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -2,10 +2,7 @@ use crate::Workspace; use crate::api::controller::AsyncReceiver; use crate::buffer::controller::BufferController; use crate::cursor::controller::CursorController; -use napi::threadsafe_function::ErrorStrategy::Fatal; -use napi::threadsafe_function::{ - ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, -}; +use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; use super::client::JsUser; @@ -45,8 +42,8 @@ impl Workspace { /// List all available buffers in this workspace #[napi(js_name = "searchBuffers")] - pub fn js_search_buffers(&self, filter: Option<&str>) -> Vec { - self.search_buffers(filter) + pub fn js_search_buffers(&self, filter: Option) -> Vec { + self.search_buffers(filter.as_deref()) } /// List all user names currently in this workspace @@ -114,13 +111,10 @@ impl Workspace { } #[napi(js_name = "callback", ts_args_type = "fun: (event: Workspace) => void")] - pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun - .create_threadsafe_function(0, |ctx: ThreadSafeCallContext| { - Ok(vec![ctx.value]) - })?; + pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: Workspace| { - tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error + tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); From e72495b7c50bf4184cf0ecbc4b91a56946005fc0 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 31 Jul 2025 00:41:17 +0200 Subject: [PATCH 006/121] chore: 1 less binding --- src/ffi/js/buffer.rs | 3 +-- src/ffi/js/cursor.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 5f29caf9..ae3742aa 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -15,9 +15,8 @@ impl BufferController { ts_args_type = "fun: (event: BufferController) => void" )] pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: BufferController| { - tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); + fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index d094644f..fc14ad0b 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -12,9 +12,8 @@ impl CursorController { ts_args_type = "fun: (event: CursorController) => void" )] pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: CursorController| { - tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); + fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); From e27f5bc5ae2f40d30906f93a64d0ff74ff5f2084 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 31 Jul 2025 01:08:59 +0200 Subject: [PATCH 007/121] fix: java glue probably --- src/ffi/java/mod.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 80ac0289..bd88f7ab 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -26,7 +26,7 @@ pub(crate) fn jvm() -> std::sync::Arc { /// Called upon initialisation of the JVM. #[allow(non_snake_case)] -#[no_mangle] +#[unsafe(no_mangle)] pub extern "system" fn JNI_OnLoad(vm: jni::JavaVM, _: *mut std::ffi::c_void) -> jni::sys::jint { unsafe { JVM = Some(std::sync::Arc::new(vm)) }; jni::sys::JNI_VERSION_1_1 @@ -320,23 +320,25 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { config: Self::From, ) -> Result { let username = { - let jfield = env + let jfield: jni::objects::JString<'j> = env .get_field(&config, "username", "Ljava/lang/String;")? - .l()?; + .l()? + .into(); if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Username can never be null!")); } - unsafe { env.get_string_unchecked(&jfield.into()) }?.into() + unsafe { env.get_string_unchecked(&jfield) }?.into() }; let password = { - let jfield = env + let jfield: jni::objects::JString<'j> = env .get_field(&config, "password", "Ljava/lang/String;")? - .l()?; + .l()? + .into(); if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Password can never be null!")); } - unsafe { env.get_string_unchecked(&jfield.into()) }?.into() + unsafe { env.get_string_unchecked(&jfield) }?.into() }; let host = { @@ -346,8 +348,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { let field = env .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])? - .l()?; - Some(unsafe { env.get_string_unchecked(&field.into()) }?.into()) + .l()? + .into(); + Some(unsafe { env.get_string_unchecked(&field) }?.into()) } else { None } @@ -412,13 +415,14 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Selection { let end_col = env.get_field(&cursor, "endCol", "I")?.i()?; let buffer = { - let jfield = env + let jfield: jni::objects::JString<'j> = env .get_field(&cursor, "buffer", "Ljava/lang/String;")? - .l()?; + .l()? + .into(); if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Buffer can never be null!")); } - unsafe { env.get_string_unchecked(&jfield.into()) }?.into() + unsafe { env.get_string_unchecked(&jfield) }?.into() }; Ok(Self { @@ -447,13 +451,14 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange { .clamp(0, u32::MAX.into()) as u32; let content = { - let jfield = env + let jfield: jni::objects::JString<'j> = env .get_field(&change, "content", "Ljava/lang/String;")? - .l()?; + .l()? + .into(); if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Content can never be null!")); } - unsafe { env.get_string_unchecked(&jfield.into()) }?.into() + unsafe { env.get_string_unchecked(&jfield) }?.into() }; Ok(Self { From b918f5a1062e8dc83c5358241ecb890e37b165ad Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 31 Jul 2025 11:20:20 +0200 Subject: [PATCH 008/121] chore: bump proto to v0.8 --- Cargo.lock | 253 +++++++++++++++++------------------------------------ Cargo.toml | 2 +- 2 files changed, 79 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84c2422d..6a4f0b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,28 +47,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "async-trait" version = "0.1.88" @@ -92,47 +70,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.2", + "axum-core", "bytes", "futures-util", "http", "http-body", "http-body-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -140,27 +91,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", + "tower", "tower-layer", "tower-service", ] @@ -288,7 +219,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-stream", - "tonic 0.14.0", + "tonic", "tracing", "tracing-subscriber", "uuid", @@ -297,13 +228,14 @@ dependencies = [ [[package]] name = "codemp-proto" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a4d042572106ec01a58676fd57ebfab11b653b26fa46c887580097d90eb191" +checksum = "5354f0456633a14e5061879875763e5e259b447a30c788b1c4aae08a96f29b4e" dependencies = [ "prost", - "tonic 0.12.3", - "tonic-build", + "tonic", + "tonic-prost", + "tonic-prost-build", "uuid", ] @@ -583,19 +515,13 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -715,7 +641,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -745,16 +671,6 @@ dependencies = [ "cc", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.10.0" @@ -944,12 +860,6 @@ dependencies = [ "twox-hash", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -1223,7 +1133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap", ] [[package]] @@ -1310,9 +1220,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -1320,9 +1230,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck", "itertools", @@ -1333,6 +1243,8 @@ dependencies = [ "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", "syn 2.0.104", "tempfile", @@ -1340,9 +1252,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools", @@ -1353,13 +1265,33 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "pyo3" version = "0.25.1" @@ -1745,16 +1677,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -1898,7 +1820,7 @@ dependencies = [ "mio", "pin-project-lite", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "windows-sys 0.59.0", ] @@ -1960,41 +1882,11 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.10.0", + "indexmap", "toml_datetime", "winnow", ] -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.14.0" @@ -2002,7 +1894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308e1db96abdccdf0a9150fb69112bf6ea72640e0bd834ef0c4a618ccc8c8ddc" dependencies = [ "async-trait", - "axum 0.8.4", + "axum", "base64", "bytes", "h2", @@ -2015,12 +1907,12 @@ dependencies = [ "percent-encoding", "pin-project", "rustls-native-certs", - "socket2 0.6.0", + "socket2", "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -2028,36 +1920,41 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.12.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +checksum = "18262cdd13dec66e8e3f2e3fe535e4b2cc706fab444a7d3678d75d8ac2557329" dependencies = [ "prettyplease", "proc-macro2", - "prost-build", - "prost-types", "quote", "syn 2.0.104", ] [[package]] -name = "tower" -version = "0.4.13" +name = "tonic-prost" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "2d8b5b7a44512c59f5ad45e0c40e53263cbbf4426d74fe6b569e04f1d4206e9c" dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114cca66d757d72422ef8cccf8be3065321860ac9fa4be73aab37a8a20a9a805" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.104", + "tempfile", + "tonic-build", ] [[package]] @@ -2068,7 +1965,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.10.0", + "indexmap", "pin-project-lite", "slab", "sync_wrapper", @@ -2170,6 +2067,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 2f97c783..fd39f307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = "0.7" +codemp-proto = { version = "0.8", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From be0b71da9d23b314a08855aa1dd43dd0e7d13fb9 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 1 Aug 2025 00:56:10 +0200 Subject: [PATCH 009/121] feat: preliminary work for v0.8 proto glue will need a lot of love --- src/api/buffer.rs | 22 ++++++++++++ src/api/cursor.rs | 2 +- src/api/mod.rs | 8 +++++ src/api/workspace.rs | 31 ++++++++++++++++ src/buffer/worker.rs | 2 +- src/client.rs | 84 +++++++++++++++++++++++-------------------- src/cursor/worker.rs | 20 ++++++----- src/tests/fixtures.rs | 30 ++++++++-------- src/workspace.rs | 72 +++++++++++++++++++++---------------- 9 files changed, 177 insertions(+), 94 deletions(-) create mode 100644 src/api/buffer.rs create mode 100644 src/api/workspace.rs diff --git a/src/api/buffer.rs b/src/api/buffer.rs new file mode 100644 index 00000000..9f834045 --- /dev/null +++ b/src/api/buffer.rs @@ -0,0 +1,22 @@ +//! # Buffer +//! TODO TODO TODO + +/// Represents a service buffer +#[derive(Debug, Clone)] +#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct BufferNode { + /// Buffer path, sort of like a UNIX path. + pub path: String, + /// Wether this buffer gets auto-deleted once all users left + pub ephemeral: bool, +} + +impl From for BufferNode { + fn from(value: codemp_proto::files::BufferNode) -> Self { + Self { + path: value.path, + ephemeral: value.ephemeral, + } + } +} diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 42dcad35..63fdf920 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -14,7 +14,7 @@ pub struct Cursor { /// User who sent the cursor. pub user: String, /// The updated cursor selection. - pub sel: Selection, + pub sel: Vec, } /// A cursor selection span. diff --git a/src/api/mod.rs b/src/api/mod.rs index 9e27c22a..43bea9ef 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -13,15 +13,23 @@ pub mod config; /// representation for an user's cursor pub mod cursor; +/// representation of remote buffers +pub mod buffer; + /// live events in workspaces pub mod event; /// data structure for remote users pub mod user; +/// data structure for workspaces +pub mod workspace; + +pub use buffer::BufferNode; pub use change::{BufferUpdate, TextChange}; pub use config::Config; pub use controller::{AsyncReceiver, AsyncSender, Controller}; pub use cursor::{Cursor, Selection}; pub use event::Event; pub use user::User; +pub use workspace::WorkspaceInfo; diff --git a/src/api/workspace.rs b/src/api/workspace.rs new file mode 100644 index 00000000..61e47fc8 --- /dev/null +++ b/src/api/workspace.rs @@ -0,0 +1,31 @@ +//! # Workspace +//! A workspace is a working environment containing many buffers, owned by one user. +//! Many users can be invited and join a workspace, accessing its buffer list and being able to +//! attach to its buffers (depending on permissions) to send changes. Workspaces are namespaced to +//! users, meaning two workspaces with the same name can exist, but one user can own only one +//! workspace with a given name. + +use uuid::Uuid; + +/// Represents a service workspace +#[derive(Debug, Clone)] +#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct WorkspaceInfo { + /// Workspace unique identifier, should never change. + pub id: Uuid, + /// Workspace name, cannot change and is unique per owner. + pub name: String, + /// Workspace owning user + pub owner: super::User, +} + +impl From for WorkspaceInfo { + fn from(value: codemp_proto::common::WorkspaceInfo) -> Self { + Self { + id: Uuid::from(value.id), + name: value.name, + owner: super::User::from(value.owner), + } + } +} diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index 5dcd9be0..67ec5463 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -41,7 +41,7 @@ impl BufferController { path: &str, tx: mpsc::Sender, rx: Streaming, - workspace_id: &str, + workspace_id: Uuid, ) -> Self { let init = diamond_types::LocalVersion::default(); diff --git a/src/client.rs b/src/client.rs index 7e29637f..a4762dbc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,8 +18,10 @@ use crate::{ }; use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, - common::{Empty, Token}, - session::{InviteRequest, WorkspaceRequest, session_client::SessionClient}, + common::{Empty, Identifier, Token}, + session::{ + InviteRequest, OwnedWorkspaceRequest, WorkspaceRequest, session_client::SessionClient, + }, }; #[cfg(feature = "py")] @@ -39,7 +41,7 @@ pub struct Client(Arc); struct ClientInner { user: Arc, config: crate::api::Config, - workspaces: DashMap, + workspaces: DashMap, auth: AuthClient, session: SessionClient>, claims: InternallyMutable, @@ -91,15 +93,20 @@ impl Client { } /// Attempt to create a new workspace with given name. - pub async fn create_workspace(&self, name: impl AsRef) -> RemoteResult<()> { - self.0 + pub async fn create_workspace( + &self, + name: impl AsRef, + ) -> RemoteResult { + let info = self + .0 .session .clone() - .create_workspace(WorkspaceRequest { - workspace: name.as_ref().to_string(), + .create_workspace(OwnedWorkspaceRequest { + name: name.as_ref().to_string(), }) - .await?; - Ok(()) + .await? + .into_inner(); + Ok(crate::api::WorkspaceInfo::from(info)) } /// Delete an existing workspace if possible. @@ -107,8 +114,8 @@ impl Client { self.0 .session .clone() - .delete_workspace(WorkspaceRequest { - workspace: name.as_ref().to_string(), + .delete_workspace(OwnedWorkspaceRequest { + name: name.as_ref().to_string(), }) .await?; Ok(()) @@ -132,43 +139,44 @@ impl Client { } /// Fetch the names of all workspaces owned by the current user. - pub async fn fetch_owned_workspaces(&self) -> RemoteResult> { - self.fetch_workspaces(true).await + pub async fn fetch_owned_workspaces(&self) -> RemoteResult> { + Ok(self + .0 + .session + .clone() + .fetch_owned_workspaces(Empty {}) + .await? + .into_inner() + .owned + .into_iter() + .map(|x| crate::api::WorkspaceInfo::from(x)) + .collect()) } /// Fetch the names of all workspaces the current user has joined. - pub async fn fetch_joined_workspaces(&self) -> RemoteResult> { - self.fetch_workspaces(false).await - } - - async fn fetch_workspaces(&self, owned: bool) -> RemoteResult> { - let workspaces = self + pub async fn fetch_joined_workspaces(&self) -> RemoteResult> { + Ok(self .0 .session .clone() - .list_workspaces(Empty {}) + .fetch_invited_workspaces(Empty {}) .await? - .into_inner(); - - if owned { - Ok(workspaces.owned) - } else { - Ok(workspaces.invited) - } + .into_inner() + .invited + .into_iter() + .map(|x| crate::api::WorkspaceInfo::from(x)) + .collect()) } /// Join and return a [`Workspace`]. #[tracing::instrument(skip(self, workspace), fields(ws = workspace.as_ref()))] - pub async fn attach_workspace( - &self, - workspace: impl AsRef, - ) -> ConnectionResult { + pub async fn attach_workspace(&self, workspace: uuid::Uuid) -> ConnectionResult { let token = self .0 .session .clone() .access_workspace(WorkspaceRequest { - workspace: workspace.as_ref().to_string(), + id: Identifier::from(workspace), }) .await? .into_inner(); @@ -182,24 +190,22 @@ impl Client { ) .await?; - self.0 - .workspaces - .insert(workspace.as_ref().to_string(), ws.clone()); + self.0.workspaces.insert(workspace, ws.clone()); Ok(ws) } /// Leave the [`Workspace`] with the given name. - pub fn leave_workspace(&self, id: &str) -> bool { - match self.0.workspaces.remove(id) { + pub fn leave_workspace(&self, id: uuid::Uuid) -> bool { + match self.0.workspaces.remove(&id) { None => true, Some(x) => x.1.consume(), } } /// Gets a [`Workspace`] handle by name. - pub fn get_workspace(&self, id: &str) -> Option { - self.0.workspaces.get(id).map(|x| x.clone()) + pub fn get_workspace(&self, id: uuid::Uuid) -> Option { + self.0.workspaces.get(&id).map(|x| x.clone()) } /// Get the names of all active [`Workspace`]s. diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index c4b85f72..d86e8c22 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -32,13 +32,17 @@ impl CursorWorker { if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { Some(Cursor { user: user_name, - sel: Selection { - buffer: event.position.buffer.path, - start_row: event.position.start.row, - start_col: event.position.start.col, - end_row: event.position.end.row, - end_col: event.position.end.col, - }, + sel: event + .position + .into_iter() + .map(|x| Selection { + buffer: x.buffer.path, + start_row: x.start.row, + start_col: x.start.col, + end_row: x.end.row, + end_col: x.end.col, + }) + .collect(), }) } else { tracing::warn!("received cursor for unknown user {user_id}"); @@ -54,7 +58,7 @@ impl CursorController { user_map: Arc>, tx: mpsc::Sender, rx: Streaming, - workspace_id: &str, + workspace_id: Uuid, ) -> Self { // TODO we should tweak the channel buffer size to better propagate backpressure let (op_tx, op_rx) = mpsc::unbounded_channel(); diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index b52712cb..ec390351 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -109,14 +109,14 @@ impl WorkspaceFixture { impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { let client = ClientFixture::of(&self.user).setup().await?; - client.create_workspace(&self.workspace).await?; - let workspace = client.attach_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(&self.workspace).await?; + let workspace = client.attach_workspace(ws_info.id).await?; Ok((client, workspace)) } async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { - if let Some((client, _workspace)) = resource { - client.leave_workspace(&self.workspace); + if let Some((client, workspace)) = resource { + client.leave_workspace(workspace.id()); if let Err(e) = client.delete_workspace(&self.workspace).await { eprintln!("could not delete workspace: {e}"); } @@ -152,12 +152,12 @@ impl ) .setup() .await?; - client.create_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(&self.workspace).await?; client .invite_to_workspace(&self.workspace, invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(&self.workspace).await?; - let invitee_workspace = invitee_client.attach_workspace(&self.workspace).await?; + let workspace = client.attach_workspace(ws_info.id).await?; + let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; Ok((client, workspace, invitee_client, invitee_workspace)) } @@ -170,8 +170,8 @@ impl crate::Workspace, )>, ) { - if let Some((client, _, _, _)) = resource { - client.leave_workspace(&self.workspace); + if let Some((client, ws, _, _)) = resource { + client.leave_workspace(ws.id()); if let Err(e) = client.delete_workspace(&self.workspace).await { eprintln!("could not delete workspace: {e}"); } @@ -247,16 +247,16 @@ impl ) .setup() .await?; - client.create_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(&self.workspace).await?; client .invite_to_workspace(&self.workspace, invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(&self.workspace).await?; - workspace.create_buffer(&self.buffer).await?; + let workspace = client.attach_workspace(ws_info.id).await?; + workspace.create_buffer(&self.buffer, false).await?; let buffer = workspace.attach_buffer(&self.buffer).await?; - let invitee_workspace = invitee_client.attach_workspace(&self.workspace).await?; + let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; let invitee_buffer = invitee_workspace.attach_buffer(&self.buffer).await?; Ok(( @@ -280,9 +280,9 @@ impl crate::buffer::Controller, )>, ) { - if let Some((client, _, _, _, _, _)) = resource { + if let Some((client, ws, _, _, _, _)) = resource { // buffer deletion is implied in workspace deletion - client.leave_workspace(&self.workspace); + client.leave_workspace(ws.id()); if let Err(e) = client.delete_workspace(&self.workspace).await { eprintln!("could not delete workspace: {e}"); } diff --git a/src/workspace.rs b/src/workspace.rs index 3f930890..87449c07 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -16,7 +16,7 @@ use crate::{ use codemp_proto::{ common::{Empty, Token}, - files::BufferNode, + files::{BufferNode, BufferRequest}, workspace::{ WorkspaceEvent, workspace_event::{ @@ -25,7 +25,7 @@ use codemp_proto::{ }, }; -use dashmap::{DashMap, DashSet}; +use dashmap::DashMap; use std::sync::{Arc, Weak}; use tokio::sync::{ mpsc::{self, error::TryRecvError}, @@ -50,12 +50,12 @@ pub struct Workspace(Arc); #[derive(Debug)] struct WorkspaceInner { - name: String, + id: Uuid, current_user: Arc, cursor: cursor::Controller, buffers: DashMap, services: Services, - filetree: DashSet, + filetree: DashMap, users: Arc>, events: tokio::sync::Mutex>, callback: watch::Sender>>, @@ -87,9 +87,9 @@ impl AsyncReceiver for Workspace { } impl Workspace { - #[tracing::instrument(skip(name, user, token, claims), fields(ws = name))] + #[tracing::instrument(skip(id, user, token, claims), fields(ws = %id))] pub(crate) async fn connect( - name: String, + id: Uuid, user: Arc, config: crate::api::Config, token: Token, @@ -111,14 +111,14 @@ impl Workspace { .into_inner(); let users = Arc::new(DashMap::default()); - let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, &name); + let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id); let ws = Self(Arc::new(WorkspaceInner { - name: name.clone(), + id, current_user: user, cursor: controller, buffers: DashMap::default(), - filetree: DashSet::default(), + filetree: DashMap::default(), users, events: tokio::sync::Mutex::new(ev_rx), services, @@ -136,11 +136,11 @@ impl Workspace { }; let _t = tokio::spawn(async move { - worker.work(name, ws_stream, weak).await; + worker.work(id, ws_stream, weak).await; }); ws.fetch_users().await?; - ws.fetch_buffers().await?; + ws.fetch_buffers("").await?; Ok(ws) } @@ -151,19 +151,26 @@ impl Workspace { } /// Create a new buffer in the current workspace. - pub async fn create_buffer(&self, path: &str) -> RemoteResult<()> { + pub async fn create_buffer(&self, path: &str, ephemeral: bool) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); workspace_client .create_buffer(tonic::Request::new(BufferNode { path: path.to_string(), + ephemeral, })) .await?; // add to filetree - self.0.filetree.insert(path.to_string()); + self.0.filetree.insert( + path.to_string(), + crate::api::BufferNode { + path: path.to_string(), + ephemeral, + }, + ); // fetch buffers - self.fetch_buffers().await?; + self.fetch_buffers("").await?; Ok(()) } @@ -188,7 +195,7 @@ impl Workspace { let stream = self.0.services.buf().attach(req).await?.into_inner(); let controller = - buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, &self.0.name); + buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, self.0.id); self.0.buffers.insert(path.to_string(), controller.clone()); Ok(controller) @@ -214,10 +221,12 @@ impl Workspace { } /// Re-fetch the list of available buffers in the workspace. - pub async fn fetch_buffers(&self) -> RemoteResult> { + pub async fn fetch_buffers(&self, filter: impl AsRef) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let resp = workspace_client - .list_buffers(tonic::Request::new(Empty {})) + .list_buffers(tonic::Request::new(BufferRequest { + path: filter.as_ref().to_string(), + })) .await? .into_inner(); @@ -225,8 +234,10 @@ impl Workspace { self.0.filetree.clear(); for b in resp.buffers { - self.0.filetree.insert(b.path.clone()); - out.push(b.path); + out.push(b.path.clone()); + self.0 + .filetree + .insert(b.path.clone(), crate::api::BufferNode::from(b)); } Ok(out) @@ -258,7 +269,7 @@ impl Workspace { pub async fn fetch_buffer_users(&self, path: &str) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let buffer_users = workspace_client - .list_buffer_users(tonic::Request::new(BufferNode { + .list_buffer_users(tonic::Request::new(BufferRequest { path: path.to_string(), })) .await? @@ -277,7 +288,7 @@ impl Workspace { let mut workspace_client = self.0.services.ws(); workspace_client - .delete_buffer(tonic::Request::new(BufferNode { + .delete_buffer(tonic::Request::new(BufferRequest { path: path.to_string(), })) .await?; @@ -289,8 +300,8 @@ impl Workspace { /// Get the workspace unique id. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 - pub fn id(&self) -> String { - self.0.name.clone() + pub fn id(&self) -> Uuid { + self.0.id } /// Return a handle to the [`cursor::Controller`]. @@ -332,8 +343,8 @@ impl Workspace { .0 .filetree .iter() - .filter(|f| filter.is_none_or(|flt| f.starts_with(flt))) - .map(|f| f.clone()) + .filter(|f| filter.is_none_or(|flt| f.key().starts_with(flt))) + .map(|f| f.key().clone()) .collect::>(); tree.sort(); tree @@ -351,7 +362,7 @@ impl WorkspaceWorker { #[tracing::instrument(skip(self, stream, weak))] pub(crate) async fn work( mut self, - ws: String, + ws: Uuid, mut stream: Streaming, weak: Weak, ) { @@ -384,12 +395,13 @@ impl WorkspaceWorker { inner.users.remove(&user.id.uuid()); } // buffer - WorkspaceEventInner::Create(FileCreate { path }) => { - inner.filetree.insert(path); + WorkspaceEventInner::Create(FileCreate { path, ephemeral }) => { + inner.filetree.insert(path.clone(), crate::api::BufferNode { path, ephemeral }); } WorkspaceEventInner::Rename(FileRename { before, after }) => { - inner.filetree.remove(&before); - inner.filetree.insert(after); + if let Some((_path, node)) = inner.filetree.remove(&before) { + inner.filetree.insert(after.clone(), crate::api::BufferNode { path: after, ephemeral: node.ephemeral }); + } } WorkspaceEventInner::Delete(FileDelete { path }) => { inner.filetree.remove(&path); From 681a88721b77df8b42a16597036268771f9ab4e3 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 1 Aug 2025 18:10:23 +0200 Subject: [PATCH 010/121] chore: fetch -> list, to respect proto --- src/workspace.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/workspace.rs b/src/workspace.rs index 87449c07..4315f77b 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -139,8 +139,8 @@ impl Workspace { worker.work(id, ws_stream, weak).await; }); - ws.fetch_users().await?; - ws.fetch_buffers("").await?; + ws.list_users().await?; + ws.list_buffers("").await?; Ok(ws) } @@ -170,7 +170,7 @@ impl Workspace { ); // fetch buffers - self.fetch_buffers("").await?; + self.list_buffers("").await?; Ok(()) } @@ -221,7 +221,7 @@ impl Workspace { } /// Re-fetch the list of available buffers in the workspace. - pub async fn fetch_buffers(&self, filter: impl AsRef) -> RemoteResult> { + pub async fn list_buffers(&self, filter: impl AsRef) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let resp = workspace_client .list_buffers(tonic::Request::new(BufferRequest { @@ -244,7 +244,7 @@ impl Workspace { } /// Re-fetch the list of all users in the workspace. - pub async fn fetch_users(&self) -> RemoteResult> { + pub async fn list_users(&self) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let users = workspace_client .list_users(tonic::Request::new(Empty {})) @@ -266,7 +266,7 @@ impl Workspace { } /// Fetch a list of the [User]s attached to a specific buffer. - pub async fn fetch_buffer_users(&self, path: &str) -> RemoteResult> { + pub async fn list_buffer_users(&self, path: &str) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let buffer_users = workspace_client .list_buffer_users(tonic::Request::new(BufferRequest { From c403fb53d3cdcb16173a423e4bff043c8ca55e0a Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 1 Aug 2025 18:10:36 +0200 Subject: [PATCH 011/121] chore: new imports in prelude --- src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/prelude.rs b/src/prelude.rs index 5f96584c..0d5a434a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -6,6 +6,7 @@ pub use crate::api::{ BufferUpdate as CodempBufferUpdate, Config as CodempConfig, Controller as CodempController, Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection, TextChange as CodempTextChange, User as CodempUser, + WorkspaceInfo as CodempWorkspaceInfo, BufferNode as CodempBufferNode, }; pub use crate::{ From a140fb65ac2a4bb787414af3c017d3055903ee78 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 1 Aug 2025 18:11:32 +0200 Subject: [PATCH 012/121] feat(lua): updated methods --- src/ffi/lua/buffer.rs | 2 +- src/ffi/lua/client.rs | 11 ++++++++--- src/ffi/lua/ext/callback.rs | 4 ++++ src/ffi/lua/ext/mod.rs | 18 ++++++++++++++++++ src/ffi/lua/workspace.rs | 18 +++++++++--------- 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index b294108a..dff38dc9 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -3,7 +3,7 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempTextChange CodempBufferUpdate } +super::ext::impl_lua_serde! { CodempTextChange CodempBufferUpdate CodempBufferNode } impl LuaUserData for CodempBufferController { fn add_methods>(methods: &mut M) { diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index 4c69c40e..f6928a82 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -25,7 +25,10 @@ impl LuaUserData for CodempClient { methods.add_method( "attach_workspace", - |_, this, (ws,): (String,)| a_sync! { this => this.attach_workspace(ws).await? }, + |_, this, (ws,): (String,)| { + let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; + a_sync! { this => this.attach_workspace(ws_id).await? } + }, ); methods.add_method( @@ -53,11 +56,13 @@ impl LuaUserData for CodempClient { ); methods.add_method("leave_workspace", |_, this, (ws,): (String,)| { - Ok(this.leave_workspace(&ws)) + let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; + Ok(this.leave_workspace(ws_id)) }); methods.add_method("get_workspace", |_, this, (ws,): (String,)| { - Ok(this.get_workspace(&ws)) + let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; + Ok(this.get_workspace(ws_id)) }); } } diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index 9d40a2d0..3f28a8b9 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -123,14 +123,18 @@ callback_args! { CursorController: CodempCursorController, BufferController: CodempBufferController, Workspace: CodempWorkspace, + WorkspaceInfo: CodempWorkspaceInfo, + VecWorkspaceInfo: Vec, Event: CodempEvent, MaybeEvent: Option, Cursor: CodempCursor, MaybeCursor: Option, Selection: CodempSelection, + VecSelection: Vec, MaybeSelection: Option, TextChange: CodempTextChange, MaybeTextChange: Option, BufferUpdate: CodempBufferUpdate, MaybeBufferUpdate: Option, + BufferNode: CodempBufferNode, } diff --git a/src/ffi/lua/ext/mod.rs b/src/ffi/lua/ext/mod.rs index a209c0a1..8062db64 100644 --- a/src/ffi/lua/ext/mod.rs +++ b/src/ffi/lua/ext/mod.rs @@ -5,6 +5,23 @@ pub mod log; pub(crate) use a_sync::tokio; pub(crate) use callback::callback; +pub(crate) fn lua_parse_uuid(uuid: &str, pos: usize, name: &str) -> mlua::Result { + use std::str::FromStr; + match uuid::Uuid::from_str(uuid) { + Ok(x) => Ok(x), + Err(e) => Err(mlua::Error::BadArgument { + pos, + name: Some(name.to_string()), + to: Some("Uuid::from_str".to_string()), + cause: std::sync::Arc::new(mlua::Error::FromLuaConversionError { + from: "string", + to: "Uuid".to_string(), + message: Some(e.to_string()), + }), + }), + } +} + macro_rules! impl_lua_serde { ($($t:ty)*) => { $( @@ -24,3 +41,4 @@ macro_rules! impl_lua_serde { } pub(crate) use impl_lua_serde; + diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index 3ce118e6..c5ac5e97 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -3,7 +3,7 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempEvent } +super::ext::impl_lua_serde! { CodempEvent CodempWorkspaceInfo } impl LuaUserData for CodempWorkspace { fn add_methods>(methods: &mut M) { @@ -12,7 +12,7 @@ impl LuaUserData for CodempWorkspace { }); methods.add_method( "create_buffer", - |_, this, (name,): (String,)| a_sync! { this => this.create_buffer(&name).await? }, + |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(&name, ephemeral).await? }, ); methods.add_method( @@ -34,25 +34,25 @@ impl LuaUserData for CodempWorkspace { }); methods.add_method( - "fetch_buffers", - |_, this, ()| a_sync! { this => this.fetch_buffers().await? }, + "list_buffers", + |_, this, (filter,): (String,)| a_sync! { this => this.list_buffers(filter).await? }, ); methods.add_method( - "fetch_users", - |_, this, ()| a_sync! { this => this.fetch_users().await? }, + "list_users", + |_, this, ()| a_sync! { this => this.list_users().await? }, ); methods.add_method("search_buffers", |_, this, (filter,): (Option,)| { Ok(this.search_buffers(filter.as_deref())) }); - methods.add_method("fetch_buffer_users", |_, this, (path,): (String,)| { + methods.add_method("list_buffer_users", |_, this, (path,): (String,)| { a_sync! { - this => this.fetch_buffer_users(&path).await? + this => this.list_buffer_users(&path).await? } }); - methods.add_method("id", |_, this, ()| Ok(this.id())); + methods.add_method("id", |_, this, ()| Ok(this.id().to_string())); methods.add_method("cursor", |_, this, ()| Ok(this.cursor())); methods.add_method("active_buffers", |_, this, ()| Ok(this.active_buffers())); methods.add_method("user_list", |_, this, ()| Ok(this.user_list())); From 57b934584e1e3f1a04b3c51356e6db6c51bc7fa3 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 2 Aug 2025 13:56:46 +0200 Subject: [PATCH 013/121] fix: some work on lua, client, workspace --- dist/lua/annotations.lua | 62 +++++++++++++++++++++++++++++++++---- src/client.rs | 8 ++--- src/ffi/lua/client.rs | 2 +- src/ffi/lua/ext/callback.rs | 1 + src/workspace.rs | 2 +- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 1b638e10..05726df6 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -169,6 +169,39 @@ function UserListPromise:cancel() end ---invoke callback asynchronously as soon as promise is ready function UserListPromise:and_then(cb) end +---@class (exact) WorkspaceInfoPromise : Promise +local WorkspaceInfoPromise = {} +--- block until promise is ready and return value +--- @return WorkspaceInfo +function WorkspaceInfoPromise:await() end +--- cancel promise execution +function WorkspaceInfoPromise:cancel() end +---@param cb fun(x: WorkspaceInfo) callback to invoke +---invoke callback asynchronously as soon as promise is ready +function WorkspaceInfoPromise:and_then(cb) end + +---@class (exact) WorkspaceInfoListPromise : Promise +local WorkspaceInfoListPromise = {} +--- block until promise is ready and return value +--- @return WorkspaceInfo[] +function WorkspaceInfoListPromise:await() end +--- cancel promise execution +function WorkspaceInfoListPromise:cancel() end +---@param cb fun(x: WorkspaceInfo[]) callback to invoke +---invoke callback asynchronously as soon as promise is ready +function WorkspaceInfoListPromise:and_then(cb) end + +---@class (exact) BufferNodeListPromise : Promise +local BufferNodeListPromise = {} +--- block until promise is ready and return value +--- @return BufferNode[] +function BufferNodeListPromise:await() end +--- cancel promise execution +function BufferNodeListPromise:cancel() end +---@param cb fun(x: BufferNode[]) callback to invoke +---invoke callback asynchronously as soon as promise is ready +function BufferNodeListPromise:and_then(cb) end + -- [[ END ASYNC STUFF ]] @@ -198,7 +231,7 @@ function Client:refresh() end function Client:attach_workspace(ws) end ---@param ws string workspace id to create ----@return NilPromise +---@return WorkspaceInfoPromise ---@async ---@nodiscard ---create a new workspace with given id @@ -223,13 +256,13 @@ function Client:delete_workspace(ws) end ---grant user acccess to workspace function Client:invite_to_workspace(ws, user) end ----@return StringArrayPromise +---@return WorkspaceInfoListPromise ---@async ---@nodiscard ---fetch and list owned workspaces function Client:fetch_owned_workspaces() end ----@return StringArrayPromise +---@return WorkspaceInfoListPromise ---@async ---@nodiscard ---fetch and list joined workspaces @@ -243,10 +276,18 @@ function Client:get_workspace(ws) end ---@class User +---represents a service user and contains all its relevant info ---@field id string user uuid ---@field name string user display name +---@class WorkspaceInfo +---represents informations about a workspace, without having an handle to it +---@field id string +---@field name string +---@field owner User + + ---@class (exact) Workspace ---a joined codemp workspace @@ -265,11 +306,12 @@ function Workspace:active_buffers() end function Workspace:cursor() end ---@param path string relative path ("name") of new buffer +---@param ephemeral boolean wether this buffer is ephemeral (auto deletes) ---@return NilPromise ---@async ---@nodiscard ---create a new empty buffer -function Workspace:create_buffer(path) end +function Workspace:create_buffer(path, ephemeral) end ---@param path string relative path ("name") of buffer to delete ---@return NilPromise @@ -304,11 +346,12 @@ function Workspace:search_buffers(filter) end ---return all names of users currently in this workspace function Workspace:user_list() end ----@return NilPromise +---@param filter string filter buffers we want to fetch relative to this path +---@return BufferNodeListPromise ---@async ---@nodiscard ---force refresh buffer list from workspace -function Workspace:fetch_buffers(path) end +function Workspace:list_buffers(filter) end ---@return NilPromise ---@async @@ -356,6 +399,13 @@ function Workspace:callback(cb) end +---@class BufferNode +---@field id string +---@field name string +---@field owner User + + + ---@class (exact) BufferController ---handle to a remote buffer, for async send/recv operations local BufferController = {} diff --git a/src/client.rs b/src/client.rs index a4762dbc..78f7d130 100644 --- a/src/client.rs +++ b/src/client.rs @@ -169,7 +169,7 @@ impl Client { } /// Join and return a [`Workspace`]. - #[tracing::instrument(skip(self, workspace), fields(ws = workspace.as_ref()))] + #[tracing::instrument(skip(self, workspace), fields(ws = %workspace))] pub async fn attach_workspace(&self, workspace: uuid::Uuid) -> ConnectionResult { let token = self .0 @@ -182,7 +182,7 @@ impl Client { .into_inner(); let ws = Workspace::connect( - workspace.as_ref().to_string(), + workspace, self.0.user.clone(), self.0.config.clone(), token, @@ -209,11 +209,11 @@ impl Client { } /// Get the names of all active [`Workspace`]s. - pub fn active_workspaces(&self) -> Vec { + pub fn active_workspaces(&self) -> Vec { self.0 .workspaces .iter() - .map(|x| x.key().to_string()) + .map(|x| *x.key()) .collect() } diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index f6928a82..5921d869 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -15,7 +15,7 @@ impl LuaUserData for CodempClient { Ok(this.current_user().clone()) }); methods.add_method("active_workspaces", |_, this, ()| { - Ok(this.active_workspaces()) + Ok(this.active_workspaces().into_iter().map(|x| x.to_string()).collect::>()) }); methods.add_method( diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index 3f28a8b9..6426886d 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -137,4 +137,5 @@ callback_args! { BufferUpdate: CodempBufferUpdate, MaybeBufferUpdate: Option, BufferNode: CodempBufferNode, + VecBufferNode: Vec, } diff --git a/src/workspace.rs b/src/workspace.rs index 4315f77b..d85cadac 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -221,7 +221,7 @@ impl Workspace { } /// Re-fetch the list of available buffers in the workspace. - pub async fn list_buffers(&self, filter: impl AsRef) -> RemoteResult> { + pub async fn list_buffers(&self, filter: impl AsRef) -> RemoteResult> { let mut workspace_client = self.0.services.ws(); let resp = workspace_client .list_buffers(tonic::Request::new(BufferRequest { From 43f2febff8ddb572a66cba73188f3ccf42ca6562 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 2 Aug 2025 17:32:48 +0200 Subject: [PATCH 014/121] feat(py): add support for uuid and update workspace and client methods glue --- Cargo.toml | 2 +- dist/py/src/codemp/codemp.pyi | 17 +++++++++-------- src/ffi/python/client.rs | 13 +++++++------ src/ffi/python/controllers.rs | 15 +++++++++------ src/ffi/python/mod.rs | 6 +++--- src/ffi/python/workspace.rs | 18 +++++++++--------- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fd39f307..6a8bd25d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ napi = { version = "3.1", features = ["full"], optional = true } napi-derive = { version="3.1", optional = true} # glue (python) -pyo3 = { version = "0.25", features = ["extension-module", "multiple-pymethods"], optional = true} +pyo3 = { version = "0.25", features = ["extension-module", "multiple-pymethods", "uuid"], optional = true} # extra async-trait = { version = "0.1", optional = true } diff --git a/dist/py/src/codemp/codemp.pyi b/dist/py/src/codemp/codemp.pyi index f383f044..eeda90e4 100644 --- a/dist/py/src/codemp/codemp.pyi +++ b/dist/py/src/codemp/codemp.pyi @@ -1,4 +1,5 @@ from typing import Optional, Callable +from uuid import uuid4, UUID def version() -> str: ... @@ -49,15 +50,15 @@ class Client: Handle to the actual client that manages the session. It manages the connection to a server and joining/creating new workspaces """ - def attach_workspace(self, workspace: str) -> Promise[Workspace]: ... + def attach_workspace(self, workspace: UUID) -> Promise[Workspace]: ... def create_workspace(self, workspace: str) -> Promise[None]: ... def delete_workspace(self, workspace: str) -> Promise[None]: ... def invite_to_workspace(self, workspace: str, username: str) -> Promise[None]: ... def fetch_owned_workspaces(self) -> Promise[list[str]]: ... def fetch_joined_workspaces(self) -> Promise[list[str]]: ... - def leave_workspace(self, workspace: str) -> bool: ... - def get_workspace(self, id: str) -> Workspace: ... - def active_workspaces(self) -> list[str]: ... + def leave_workspace(self, workspace: UUID) -> bool: ... + def get_workspace(self, id: UUID) -> Workspace: ... + def active_workspaces(self) -> list[UUID]: ... def current_user(self) -> User: ... def refresh(self) -> Promise[None]: ... @@ -93,12 +94,12 @@ class Workspace: Handle to a workspace inside codemp. It manages buffers. A cursor is tied to the single workspace. """ - def create_buffer(self, path: str) -> Promise[None]: ... + def create_buffer(self, path: str, ephemeral: str) -> Promise[None]: ... def attach_buffer(self, path: str) -> Promise[BufferController]: ... def detach_buffer(self, path: str) -> bool: ... - def fetch_buffers(self) -> Promise[list[str]]: ... - def fetch_users(self) -> Promise[list[User]]: ... - def fetch_buffer_users(self, path: str) -> Promise[list[User]]: ... + def list_buffers(self, filter: str) -> Promise[list[str]]: ... + def list_users(self) -> Promise[list[User]]: ... + def list_buffer_users(self, path: str) -> Promise[list[User]]: ... def delete_buffer(self, path: str) -> Promise[None]: ... def id(self) -> str: ... def cursor(self) -> CursorController: ... diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index 1c32a3a4..fa28bbb8 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -3,6 +3,7 @@ use super::a_sync_allow_threads; use crate::api::User; use crate::workspace::Workspace; use pyo3::prelude::*; +use uuid::Uuid; #[pymethods] impl Client { @@ -16,7 +17,7 @@ impl Client { // } #[pyo3(name = "attach_workspace")] - fn pyattach_workspace(&self, py: Python<'_>, workspace: String) -> PyResult { + fn pyattach_workspace(&self, py: Python<'_>, workspace: Uuid) -> PyResult { tracing::info!("attempting to join the workspace {}", workspace); let this = self.clone(); a_sync_allow_threads!(py, this.attach_workspace(workspace).await) @@ -70,18 +71,18 @@ impl Client { } #[pyo3(name = "leave_workspace")] - fn pyleave_workspace(&self, id: String) -> bool { - self.leave_workspace(id.as_str()) + fn pyleave_workspace(&self, id: Uuid) -> bool { + self.leave_workspace(id) } // join a workspace #[pyo3(name = "get_workspace")] - fn pyget_workspace(&self, id: String) -> Option { - self.get_workspace(id.as_str()) + fn pyget_workspace(&self, id: Uuid) -> Option { + self.get_workspace(id) } #[pyo3(name = "active_workspaces")] - fn pyactive_workspaces(&self) -> Vec { + fn pyactive_workspaces(&self) -> Vec { self.active_workspaces() } diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index caae2c9e..848ccf2b 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -72,7 +72,7 @@ impl BufferController { } #[pyo3(name = "ack")] - fn pyack(&self, v: Vec) -> () { + fn pyack(&self, v: Vec) { self.ack(v) } @@ -128,18 +128,21 @@ impl BufferController { #[pymethods] impl Cursor { #[getter(start)] - fn pystart(&self) -> (i32, i32) { - (self.sel.start_row, self.sel.start_col) + fn pystart(&self) -> Vec<(i32, i32)> { + self.sel + .iter() + .map(|s| (s.start_row, s.start_col)) + .collect() } #[getter(end)] - fn pyend(&self) -> (i32, i32) { - (self.sel.end_row, self.sel.end_col) + fn pyend(&self) -> Vec<(i32, i32)> { + self.sel.iter().map(|s| (s.end_row, s.end_col)).collect() } #[getter(buffer)] fn pybuffer(&self) -> String { - self.sel.buffer.clone() + self.sel.iter().map(|s| s.buffer.clone()).collect() } #[getter(user)] diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index b9120da0..86563808 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -91,7 +91,7 @@ macro_rules! a_sync { ))) }}; } -pub(crate) use a_sync; +//pub(crate) use a_sync; macro_rules! a_sync_allow_threads { ($py:ident, $x:expr) => {{ @@ -330,10 +330,10 @@ fn set_logger(py: Python, logging_cb: PyObject, debug: bool) -> bool { .with_level(true) .with_target(true) .with_thread_ids(false) - .with_thread_names(false) + .with_thread_names(true) .with_file(false) .with_line_number(false) - .with_source_location(false) + .with_source_location(true) .compact(); let log_subscribed = tracing_subscriber::fmt() diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index cb1d5dc6..bbd040bb 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -13,9 +13,9 @@ use super::a_sync_allow_threads; impl Workspace { // join a workspace #[pyo3(name = "create_buffer")] - fn pycreate_buffer(&self, py: Python, path: String) -> PyResult { + fn pycreate_buffer(&self, py: Python, path: String, ephemeral: bool) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.create_buffer(path.as_str()).await) + a_sync_allow_threads!(py, this.create_buffer(path.as_str(), ephemeral).await) } #[pyo3(name = "attach_buffer")] @@ -30,22 +30,22 @@ impl Workspace { } #[pyo3(name = "fetch_buffers")] - fn pyfetch_buffers(&self, py: Python) -> PyResult { + fn pylist_buffers(&self, py: Python, filter: String) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.fetch_buffers().await) + a_sync_allow_threads!(py, this.list_buffers(filter.as_str()).await) } #[pyo3(name = "fetch_users")] - fn pyfetch_users(&self, py: Python) -> PyResult { + fn pylist_users(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.fetch_users().await) + a_sync_allow_threads!(py, this.list_users().await) } #[pyo3(name = "fetch_buffer_users")] - fn pyfetch_buffer_users(&self, py: Python, path: String) -> PyResult { + fn pylist_buffer_users(&self, py: Python, path: String) -> PyResult { // crate::Result> let this = self.clone(); - a_sync_allow_threads!(py, this.fetch_buffer_users(path.as_str()).await) + a_sync_allow_threads!(py, this.list_buffer_users(path.as_str()).await) } #[pyo3(name = "delete_buffer")] @@ -55,7 +55,7 @@ impl Workspace { } #[pyo3(name = "id")] - fn pyid(&self) -> String { + fn pyid(&self) -> uuid::Uuid { self.id() } From 40d65261814a35fe8cbbe1ee0a5dc498b811dc57 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 11:32:10 +0200 Subject: [PATCH 015/121] chore: latest proto rc --- Cargo.lock | 3 +-- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a4f0b95..165377ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,8 +229,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5354f0456633a14e5061879875763e5e259b447a30c788b1c4aae08a96f29b4e" +source = "git+https://github.com/hexedtech/codemp-proto?rev=23012c1ddd2c488a923d4c71f1d8a1a7a61f7412#23012c1ddd2c488a923d4c71f1d8a1a7a61f7412" dependencies = [ "prost", "tonic", diff --git a/Cargo.toml b/Cargo.toml index 6a8bd25d..e2207364 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { version = "0.8", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "23012c1ddd2c488a923d4c71f1d8a1a7a61f7412", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From ffe00fa43106a2d2be5bfd3d26cb65cceae6c2b7 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 11:32:28 +0200 Subject: [PATCH 016/121] feat: keepalive, buffer join/leave events --- src/workspace.rs | 67 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/workspace.rs b/src/workspace.rs index d85cadac..763f655e 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -18,10 +18,9 @@ use codemp_proto::{ common::{Empty, Token}, files::{BufferNode, BufferRequest}, workspace::{ - WorkspaceEvent, workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoin, UserLeave, - }, + Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoin, UserJoinBuffer, UserJoinWorkspace, UserLeave, UserLeaveBuffer, UserLeaveWorkspace + }, WorkspaceEvent }, }; @@ -56,6 +55,7 @@ struct WorkspaceInner { buffers: DashMap, services: Services, filetree: DashMap, + buffer_users: DashMap>, users: Arc>, events: tokio::sync::Mutex>, callback: watch::Sender>>, @@ -119,6 +119,7 @@ impl Workspace { cursor: controller, buffers: DashMap::default(), filetree: DashMap::default(), + buffer_users: DashMap::default(), users, events: tokio::sync::Mutex::new(ev_rx), services, @@ -178,25 +179,48 @@ impl Workspace { /// Attach to a buffer and return a handle to it. #[tracing::instrument(skip(self))] pub async fn attach_buffer(&self, path: &str) -> ConnectionResult { - let mut worskspace_client = self.0.services.ws(); - let request = tonic::Request::new(BufferNode { - path: path.to_string(), + let mut workspace_client = self.0.services.ws(); + let mut buffer_client = self.0.services.buf(); + let path = path.to_string(); + let request = tonic::Request::new(BufferRequest { + path: path.clone(), }); - let credentials = worskspace_client.access_buffer(request).await?.into_inner(); + let credentials = workspace_client.get_buffer_token(request).await?.into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); - req.metadata_mut().insert( - "buffer", - tonic::metadata::MetadataValue::try_from(credentials.token).map_err(|e| { - tonic::Status::internal(format!("failed representing token to string: {e}")) - })?, - ); - let stream = self.0.services.buf().attach(req).await?.into_inner(); + req.metadata_mut().insert("buffer", crate::ext::token_to_metadata(credentials)?); + let stream = buffer_client.attach(req).await?.into_inner(); let controller = - buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, self.0.id); - self.0.buffers.insert(path.to_string(), controller.clone()); + buffer::Controller::spawn(self.0.current_user.id, &path, tx, stream, self.0.id); + + self.0.buffers.insert(path.clone(), controller.clone()); + + let weak = Arc::downgrade(&controller.0); + tokio::spawn(async move { + let _p = path.clone(); + let fut = async move { + loop { + // TODO either configurable token refresh time or calculate depending on token lifetime + tokio::time::sleep(std::time::Duration::from_secs(20)).await; + if weak.upgrade().is_none() { break }; + let new_credentials = workspace_client.get_buffer_token( + tonic::Request::new(BufferRequest { path: path.clone() }) + ) + .await? + .into_inner(); + let mut request = tonic::Request::new(Empty {}); + request.metadata_mut().insert("buffer", crate::ext::token_to_metadata(new_credentials)?); + buffer_client.keep_alive(request).await?; + } + Ok::<(), tonic::Status>(()) + }; + + if let Err(e) = fut.await { + tracing::error!("error in keepalive task for buffer {_p}: {e}"); + } + }); Ok(controller) } @@ -351,6 +375,7 @@ impl Workspace { } } + struct WorkspaceWorker { callback: watch::Receiver>>, pollers: Vec>, @@ -388,12 +413,18 @@ impl WorkspaceWorker { let update = crate::api::Event::from(&ev); match ev { // user - WorkspaceEventInner::Join(UserJoin { user }) => { + WorkspaceEventInner::WorkspaceJoin(UserJoinWorkspace { user }) => { inner.users.insert(user.id.uuid(), user.into()); } - WorkspaceEventInner::Leave(UserLeave { user }) => { + WorkspaceEventInner::WorkspaceLeave(UserLeaveWorkspace { user }) => { inner.users.remove(&user.id.uuid()); } + WorkspaceEventInner::BufferJoin(UserJoinBuffer { user, buffer }) => { + + }, + WorkspaceEventInner::BufferLeave(UserLeaveBuffer { user, buffer }) => { + + }, // buffer WorkspaceEventInner::Create(FileCreate { path, ephemeral }) => { inner.filetree.insert(path.clone(), crate::api::BufferNode { path, ephemeral }); From 6886436cfce9696befec4a424eaf573ccfe7bc87 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 11:32:48 +0200 Subject: [PATCH 017/121] feat: util for token metadata --- src/ext.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ext.rs b/src/ext.rs index 32a6594f..ab7e57a9 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -115,3 +115,9 @@ where } } } + +pub(crate) fn token_to_metadata(tok: codemp_proto::common::Token) -> tonic::Result> { + tonic::metadata::MetadataValue::try_from(tok.token).map_err(|e| { + tonic::Status::internal(format!("failed representing token to string: {e}")) + }) +} From f9f61442a9950cb62c44ba5aad13969f5380dc1b Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 11:33:04 +0200 Subject: [PATCH 018/121] feat: new cursor struct layout --- src/api/cursor.rs | 4 ++-- src/cursor/controller.rs | 4 ++-- src/cursor/worker.rs | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 63fdf920..a8420d2b 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -13,6 +13,8 @@ use pyo3::prelude::*; pub struct Cursor { /// User who sent the cursor. pub user: String, + /// Path of buffer this cursor is on + pub buffer: String, /// The updated cursor selection. pub sel: Vec, } @@ -32,6 +34,4 @@ pub struct Selection { pub end_row: i32, /// Cursor position final column in buffer. pub end_col: i32, - /// Path of buffer this cursor is on. - pub buffer: String, } diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 48525070..ca00b4f0 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -13,7 +13,7 @@ use crate::{ errors::ControllerResult, }; use codemp_proto::{ - cursor::{CursorPosition, RowCol}, + cursor::{CursorPosition, CursorUpdate, RowCol}, files::BufferNode, }; @@ -33,7 +33,7 @@ impl CursorController { #[derive(Debug)] pub(crate) struct CursorControllerInner { - pub(crate) op: mpsc::UnboundedSender, + pub(crate) op: mpsc::UnboundedSender, pub(crate) stream: mpsc::Sender>>, pub(crate) poll: mpsc::UnboundedSender>, pub(crate) callback: watch::Sender>>, diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index d86e8c22..4dff21b5 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -8,13 +8,13 @@ use crate::{ api::{Cursor, Selection, User, controller::ControllerCallback}, ext::IgnorableError, }; -use codemp_proto::cursor::{CursorEvent, CursorPosition}; +use codemp_proto::cursor::{CursorEvent, CursorUpdate}; use super::controller::{CursorController, CursorControllerInner}; struct CursorWorker { workspace_id: String, - op: mpsc::UnboundedReceiver, + op: mpsc::UnboundedReceiver, map: Arc>, stream: mpsc::Receiver>>, poll: mpsc::UnboundedReceiver>, @@ -32,11 +32,12 @@ impl CursorWorker { if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { Some(Cursor { user: user_name, + buffer: event.position.buffer, sel: event .position + .cursors .into_iter() .map(|x| Selection { - buffer: x.buffer.path, start_row: x.start.row, start_col: x.start.col, end_row: x.end.row, @@ -56,7 +57,7 @@ impl CursorWorker { impl CursorController { pub(crate) fn spawn( user_map: Arc>, - tx: mpsc::Sender, + tx: mpsc::Sender, rx: Streaming, workspace_id: Uuid, ) -> Self { @@ -95,7 +96,7 @@ impl CursorController { #[tracing::instrument(skip(worker, tx, rx), fields(ws = worker.workspace_id))] async fn work( mut worker: CursorWorker, - tx: mpsc::Sender, + tx: mpsc::Sender, mut rx: Streaming, ) { tracing::debug!("starting cursor worker"); From 251d82ee4470e2d34d50d5d46498707d5b046847 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 14:05:00 +0200 Subject: [PATCH 019/121] feat: workspace keepalive task --- src/client.rs | 37 ++++++++++++++++++++++++++++++------- src/network.rs | 6 +++--- src/workspace.rs | 15 +++++++++------ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index 78f7d130..d535227b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -171,26 +171,49 @@ impl Client { /// Join and return a [`Workspace`]. #[tracing::instrument(skip(self, workspace), fields(ws = %workspace))] pub async fn attach_workspace(&self, workspace: uuid::Uuid) -> ConnectionResult { - let token = self - .0 - .session - .clone() - .access_workspace(WorkspaceRequest { + let mut session_client = self.0.session.clone(); + let token = session_client + .get_workspace_token(WorkspaceRequest { id: Identifier::from(workspace), }) .await? .into_inner(); + let workspace_claims = InternallyMutable::new(token); + let ws = Workspace::connect( workspace, self.0.user.clone(), self.0.config.clone(), - token, + workspace_claims.channel(), self.0.claims.channel(), ) .await?; - self.0.workspaces.insert(workspace, ws.clone()); + let mut workspace_client = ws.services().ws(); + + let weak = Arc::downgrade(&ws.0); + tokio::spawn(async move { + let fut = async move { + loop { + // TODO either configurable token refresh time or calculate depending on token lifetime + tokio::time::sleep(std::time::Duration::from_secs(240)).await; + if weak.upgrade().is_none() { break }; + let new_credentials = session_client.get_workspace_token( + tonic::Request::new(WorkspaceRequest { id: Identifier::from(workspace) }) + ) + .await? + .into_inner(); + workspace_claims.set(new_credentials); + workspace_client.keep_alive(tonic::Request::new(Empty {})).await?; + } + Ok::<(), tonic::Status>(()) + }; + + if let Err(e) = fut.await { + tracing::error!("error in keepalive task for workspace {workspace}: {e}"); + } + }); Ok(ws) } diff --git a/src/network.rs b/src/network.rs index 1e0bd69d..c4994abb 100644 --- a/src/network.rs +++ b/src/network.rs @@ -40,10 +40,10 @@ impl Services { let channel = Endpoint::from_shared(dest.to_string())?.connect().await?; let inter = WorkspaceInterceptor { session, workspace }; Ok(Self { - cursor: CursorClient::with_interceptor(channel.clone(), inter.clone()), workspace: WorkspaceClient::with_interceptor(channel.clone(), inter.clone()), - // TODO technically we could keep buffers on separate servers, and thus manage buffer - // connections separately, but for now it's more convenient to bundle them with workspace + // TODO technically we could keep buffers and cursors on separate servers, and thus manage + // their connections separately, but for now it's more convenient to bundle them with workspace + cursor: CursorClient::with_interceptor(channel.clone(), inter.clone()), buffer: BufferClient::with_interceptor(channel.clone(), inter.clone()), }) } diff --git a/src/workspace.rs b/src/workspace.rs index 763f655e..23d42100 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -45,7 +45,7 @@ use napi_derive::napi; #[derive(Debug, Clone)] #[cfg_attr(feature = "py", pyo3::pyclass)] #[cfg_attr(feature = "js", napi)] -pub struct Workspace(Arc); +pub struct Workspace(pub(crate) Arc); #[derive(Debug)] struct WorkspaceInner { @@ -87,17 +87,16 @@ impl AsyncReceiver for Workspace { } impl Workspace { - #[tracing::instrument(skip(id, user, token, claims), fields(ws = %id))] + #[tracing::instrument(skip(id, user, workspace_claim, user_claim), fields(ws = %id))] pub(crate) async fn connect( id: Uuid, user: Arc, config: crate::api::Config, - token: Token, - claims: tokio::sync::watch::Receiver, + workspace_claim: tokio::sync::watch::Receiver, + user_claim: tokio::sync::watch::Receiver, ) -> ConnectionResult { - let workspace_claim = InternallyMutable::new(token); let services = - Services::try_new(&config.endpoint(), claims, workspace_claim.channel()).await?; + Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; let ws_stream = services.ws().attach(Empty {}).await?.into_inner(); let (tx, rx) = mpsc::channel(128); @@ -146,6 +145,10 @@ impl Workspace { Ok(ws) } + pub(crate) fn services(&self) -> &Services { + &self.0.services + } + /// drop arc, return true if was last pub(crate) fn consume(self) -> bool { Arc::into_inner(self.0).is_some() From 6046a26626dc0efbad25145e1fcbe963a6243159 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 16:42:05 +0200 Subject: [PATCH 020/121] fix: update again workspace methods --- src/tests/fixtures.rs | 4 +- src/workspace.rs | 94 ++++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index ec390351..f6ba6887 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -254,10 +254,10 @@ impl let workspace = client.attach_workspace(ws_info.id).await?; workspace.create_buffer(&self.buffer, false).await?; - let buffer = workspace.attach_buffer(&self.buffer).await?; + let buffer = workspace.attach_buffer(self.buffer.clone()).await?; let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; - let invitee_buffer = invitee_workspace.attach_buffer(&self.buffer).await?; + let invitee_buffer = invitee_workspace.attach_buffer(self.buffer.clone()).await?; Ok(( client, diff --git a/src/workspace.rs b/src/workspace.rs index 23d42100..2d7a177d 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -10,16 +10,16 @@ use crate::{ }, buffer, cursor, errors::{ConnectionResult, ControllerResult, RemoteResult}, - ext::{IgnorableError, InternallyMutable}, + ext::IgnorableError, network::Services, }; use codemp_proto::{ - common::{Empty, Token}, + common::Empty, files::{BufferNode, BufferRequest}, workspace::{ workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoin, UserJoinBuffer, UserJoinWorkspace, UserLeave, UserLeaveBuffer, UserLeaveWorkspace + Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace }, WorkspaceEvent }, }; @@ -139,8 +139,12 @@ impl Workspace { worker.work(id, ws_stream, weak).await; }); - ws.list_users().await?; - ws.list_buffers("").await?; + ws.fetch_users().await?; + ws.fetch_buffers("".to_string()).await?; + + for buffer_ref in ws.0.buffers.iter() { + ws.fetch_buffer_users(buffer_ref.key().clone()).await?; + } Ok(ws) } @@ -164,7 +168,7 @@ impl Workspace { })) .await?; - // add to filetree + // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( path.to_string(), crate::api::BufferNode { @@ -173,18 +177,14 @@ impl Workspace { }, ); - // fetch buffers - self.list_buffers("").await?; - Ok(()) } /// Attach to a buffer and return a handle to it. #[tracing::instrument(skip(self))] - pub async fn attach_buffer(&self, path: &str) -> ConnectionResult { + pub async fn attach_buffer(&self, path: String) -> ConnectionResult { let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let path = path.to_string(); let request = tonic::Request::new(BufferRequest { path: path.clone(), }); @@ -248,65 +248,58 @@ impl Workspace { } /// Re-fetch the list of available buffers in the workspace. - pub async fn list_buffers(&self, filter: impl AsRef) -> RemoteResult> { + pub async fn fetch_buffers(&self, path: String) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); let resp = workspace_client - .list_buffers(tonic::Request::new(BufferRequest { - path: filter.as_ref().to_string(), - })) + .fetch_buffers(tonic::Request::new(BufferRequest { path })) .await? .into_inner(); - let mut out = Vec::new(); - self.0.filetree.clear(); for b in resp.buffers { - out.push(b.path.clone()); self.0 .filetree .insert(b.path.clone(), crate::api::BufferNode::from(b)); } - Ok(out) + Ok(()) } /// Re-fetch the list of all users in the workspace. - pub async fn list_users(&self) -> RemoteResult> { - let mut workspace_client = self.0.services.ws(); + pub async fn fetch_users(&self) -> RemoteResult<()> { + let mut workspace_client = self.services().ws(); let users = workspace_client - .list_users(tonic::Request::new(Empty {})) + .fetch_users(tonic::Request::new(Empty {})) .await? .into_inner() .users .into_iter() .map(User::from); - let mut result = Vec::new(); - self.0.users.clear(); for u in users { - self.0.users.insert(u.id, u.clone()); - result.push(u); + self.0.users.insert(u.id, u); } - Ok(result) + Ok(()) } /// Fetch a list of the [User]s attached to a specific buffer. - pub async fn list_buffer_users(&self, path: &str) -> RemoteResult> { - let mut workspace_client = self.0.services.ws(); - let buffer_users = workspace_client - .list_buffer_users(tonic::Request::new(BufferRequest { + pub async fn fetch_buffer_users(&self, path: String) -> RemoteResult<()> { + let users = self.services().ws() + .fetch_buffer_users(tonic::Request::new(BufferRequest { path: path.to_string(), })) .await? .into_inner() .users .into_iter() - .map(|id| id.into()) + .map(|x| Uuid::from(x.id)) .collect(); - Ok(buffer_users) + self.0.buffer_users.insert(path, users); + + Ok(()) } /// Delete a buffer. @@ -353,7 +346,7 @@ impl Workspace { .collect() } - /// Get all names of users currently in this workspace + /// Get all users currently in this workspace pub fn user_list(&self) -> Vec { self.0 .users @@ -362,6 +355,19 @@ impl Workspace { .collect() } + /// Get all users currently attached to specified buffer + pub fn buffer_user_list(&self, path: &str) -> Vec { + let mut out = Vec::new(); + if let Some(buf_ref) = self.0.buffer_users.get(path) { + for uid in buf_ref.value() { + if let Some(user_ref) = self.0.users.get(uid) { + out.push(user_ref.value().clone()); + } + } + } + out + } + /// 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 @@ -423,22 +429,36 @@ impl WorkspaceWorker { inner.users.remove(&user.id.uuid()); } WorkspaceEventInner::BufferJoin(UserJoinBuffer { user, buffer }) => { - + match inner.buffer_users.get_mut(&buffer) { + Some(mut buf_users_ref) => buf_users_ref.push(Uuid::from(user.id)), + None => tracing::warn!("received UserJoinBuffer event for an unknown buffer"), + } }, WorkspaceEventInner::BufferLeave(UserLeaveBuffer { user, buffer }) => { - + match inner.buffer_users.get_mut(&buffer) { + Some(mut buf_users_ref) => buf_users_ref.retain(|x| *x != Uuid::from(user.id)), + None => tracing::warn!("received UserLeaveBuffer event for an unknown buffer"), + } }, // buffer WorkspaceEventInner::Create(FileCreate { path, ephemeral }) => { + inner.buffer_users.insert(path.clone(), Vec::new()); inner.filetree.insert(path.clone(), crate::api::BufferNode { path, ephemeral }); } WorkspaceEventInner::Rename(FileRename { before, after }) => { + if let Some((_path, controller)) = inner.buffers.remove(&before) { + inner.buffers.insert(after.clone(), controller); + } if let Some((_path, node)) = inner.filetree.remove(&before) { - inner.filetree.insert(after.clone(), crate::api::BufferNode { path: after, ephemeral: node.ephemeral }); + inner.filetree.insert(after.clone(), node); + } + if let Some((_path, users)) = inner.buffer_users.remove(&before) { + inner.buffer_users.insert(after, users); } } WorkspaceEventInner::Delete(FileDelete { path }) => { inner.filetree.remove(&path); + inner.buffer_users.remove(&path); let _ = inner.buffers.remove(&path); } } From c64a3dcd1b6bba068205bfd26d32ea99353abc54 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 3 Aug 2025 16:42:18 +0200 Subject: [PATCH 021/121] feat: new cursor structs --- src/api/cursor.rs | 14 +++++++++- src/cursor/controller.rs | 59 ++++++++++++++++++++-------------------- src/cursor/worker.rs | 32 ++++++++++++---------- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/src/api/cursor.rs b/src/api/cursor.rs index a8420d2b..a9eed4e6 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -10,9 +10,21 @@ use pyo3::prelude::*; #[cfg_attr(feature = "py", pyclass(get_all))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] // #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] -pub struct Cursor { +pub struct CursorEvent { /// User who sent the cursor. pub user: String, + /// Cursor position data + pub cursor: Cursor, +} + + +/// An event that occurred about a user's cursor. +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "js", napi_derive::napi(object))] +#[cfg_attr(feature = "py", pyclass(get_all))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] +pub struct Cursor { /// Path of buffer this cursor is on pub buffer: String, /// The updated cursor selection. diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index ca00b4f0..a4bb786a 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -7,15 +7,11 @@ use tokio::sync::{mpsc, oneshot, watch}; use crate::{ api::{ - Controller, Cursor, Selection, - controller::{AsyncReceiver, AsyncSender, ControllerCallback}, + controller::{AsyncReceiver, AsyncSender, ControllerCallback}, cursor::CursorEvent, Controller, Cursor }, errors::ControllerResult, }; -use codemp_proto::{ - cursor::{CursorPosition, CursorUpdate, RowCol}, - files::BufferNode, -}; +use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol}; /// A [Controller] for asynchronously sending and receiving [Cursor] event. /// @@ -34,44 +30,49 @@ impl CursorController { #[derive(Debug)] pub(crate) struct CursorControllerInner { pub(crate) op: mpsc::UnboundedSender, - pub(crate) stream: mpsc::Sender>>, + pub(crate) stream: mpsc::Sender>>, pub(crate) poll: mpsc::UnboundedSender>, pub(crate) callback: watch::Sender>>, pub(crate) workspace_id: String, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl Controller for CursorController {} +impl Controller for CursorController {} #[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl AsyncSender for CursorController { - fn send(&self, mut cursor: Selection) -> ControllerResult<()> { - if cursor.start_row > cursor.end_row - || (cursor.start_row == cursor.end_row && cursor.start_col > cursor.end_col) - { - std::mem::swap(&mut cursor.start_row, &mut cursor.end_row); - std::mem::swap(&mut cursor.start_col, &mut cursor.end_col); +impl AsyncSender for CursorController { + fn send(&self, mut cursor: Cursor) -> ControllerResult<()> { + for sel in cursor.sel.iter_mut() { + if sel.start_row > sel.end_row + || (sel.start_row == sel.end_row && sel.start_col > sel.end_col) + { + std::mem::swap(&mut sel.start_row, &mut sel.end_row); + std::mem::swap(&mut sel.start_col, &mut sel.end_col); + } } - Ok(self.0.op.send(CursorPosition { - buffer: BufferNode { - path: cursor.buffer, - }, - start: RowCol { - row: cursor.start_row, - col: cursor.start_col, - }, - end: RowCol { - row: cursor.end_row, - col: cursor.end_col, - }, + Ok(self.0.op.send(CursorUpdate { + buffer: cursor.buffer, + cursors: cursor.sel + .into_iter() + .map(|x| CursorPosition { + start: RowCol { + row: x.start_row, + col: x.start_col, + }, + end: RowCol { + row: x.end_row, + col: x.end_col, + } + }) + .collect() })?) } } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl AsyncReceiver for CursorController { - async fn try_recv(&self) -> ControllerResult> { +impl AsyncReceiver for CursorController { + async fn try_recv(&self) -> ControllerResult> { let (tx, rx) = oneshot::channel(); self.0.stream.send(tx).await?; Ok(rx.await?) diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index 4dff21b5..368c4352 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -16,7 +16,7 @@ struct CursorWorker { workspace_id: String, op: mpsc::UnboundedReceiver, map: Arc>, - stream: mpsc::Receiver>>, + stream: mpsc::Receiver>>, poll: mpsc::UnboundedReceiver>, pollers: Vec>, store: std::collections::VecDeque, @@ -26,24 +26,26 @@ struct CursorWorker { impl CursorWorker { #[tracing::instrument(skip(self, tx))] - fn handle_recv(&mut self, tx: oneshot::Sender>) { + fn handle_recv(&mut self, tx: oneshot::Sender>) { tx.send(self.store.pop_front().and_then(|event| { let user_id = Uuid::from(event.user); if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { - Some(Cursor { + Some(crate::api::cursor::CursorEvent { user: user_name, - buffer: event.position.buffer, - sel: event - .position - .cursors - .into_iter() - .map(|x| Selection { - start_row: x.start.row, - start_col: x.start.col, - end_row: x.end.row, - end_col: x.end.col, - }) - .collect(), + cursor: Cursor { + buffer: event.position.buffer, + sel: event + .position + .cursors + .into_iter() + .map(|x| Selection { + start_row: x.start.row, + start_col: x.start.col, + end_row: x.end.row, + end_col: x.end.col, + }) + .collect(), + } }) } else { tracing::warn!("received cursor for unknown user {user_id}"); From 00ed6b31fa2b0b3103a8fa9c58de07b01d6cba1b Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 5 Mar 2026 21:33:31 +0100 Subject: [PATCH 022/121] fix: all event types --- src/api/event.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api/event.rs b/src/api/event.rs index d3c16544..b2086d96 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -17,16 +17,22 @@ pub enum Event { UserJoin { name: String }, /// Fired when an user leaves the current workspace. UserLeave { name: String }, + /// Fired when an user joins a buffer. + UserJoinBuffer { name: String, buffer: String }, + /// Fired when an user leaves a buffer. + UserLeaveBuffer { name: String, buffer: String }, } impl From for Event { fn from(event: WorkspaceEventInner) -> Self { match event { - WorkspaceEventInner::Join(e) => Self::UserJoin { name: e.user.name }, - WorkspaceEventInner::Leave(e) => Self::UserLeave { name: e.user.name }, + WorkspaceEventInner::WorkspaceJoin(e) => Self::UserJoin { name: e.user.name }, + WorkspaceEventInner::WorkspaceLeave(e) => Self::UserLeave { name: e.user.name }, WorkspaceEventInner::Create(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Delete(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Rename(e) => Self::FileTreeUpdated { path: e.after }, + WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user.name, buffer: e.buffer }, + WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user.name, buffer: e.buffer }, } } } From 7c0421441aff5f5d2781bedc368bc86740351f16 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 5 Mar 2026 21:33:44 +0100 Subject: [PATCH 023/121] chore!: bump proto and ffi deps breaks everything: now it's all uuids! --- Cargo.lock | 164 ++++++++++++++++++++++++++++++++++------------------- Cargo.toml | 9 +-- 2 files changed, 110 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 165377ec..efbabcb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,7 +196,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -207,7 +207,7 @@ dependencies = [ "codemp-proto", "dashmap", "diamond-types", - "jni", + "jni 0.22.3", "jni-toolbox", "mlua", "napi", @@ -229,7 +229,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=23012c1ddd2c488a923d4c71f1d8a1a7a61f7412#23012c1ddd2c488a923d4c71f1d8a1a7a61f7412" +source = "git+https://github.com/hexedtech/codemp-proto?rev=e000cda350ed277167ce494af91aa5bc5fcd339f#e000cda350ed277167ce494af91aa5bc5fcd339f" dependencies = [ "prost", "tonic", @@ -680,12 +680,6 @@ dependencies = [ "hashbrown 0.15.4", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "inventory" version = "0.3.20" @@ -739,28 +733,77 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "java-locator", - "jni-sys", - "libloading 0.7.4", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295dc9997acda1562fdf8d299f56063c936443b60f078e63a5d8d3c34ef2642b" +dependencies = [ + "cfg-if", + "combine", + "java-locator", + "jni-macros", + "jni-sys 0.4.1", + "libloading", + "log", + "simd_cesu8", + "thiserror 2.0.12", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c3d1da60c95c98847b26b9d45f4360fee718b31de746df016d9cd6de916a7ef" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.104", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "jni-toolbox" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cce03cf89bc32b81de142a323a71e9903ee88127a0e04bbd7f215ab74ab6b10a" dependencies = [ - "jni", + "jni 0.21.1", "jni-toolbox-macro", "uuid", ] @@ -808,16 +851,6 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.8.8" @@ -871,15 +904,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -1011,7 +1035,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d" dependencies = [ - "libloading 0.8.8", + "libloading", ] [[package]] @@ -1293,37 +1317,34 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ - "indoc", "inventory", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", + "uuid", ] [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", "pyo3-build-config", @@ -1331,9 +1352,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1343,9 +1364,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", @@ -1472,6 +1493,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.0.8" @@ -1653,6 +1683,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.10" @@ -1740,9 +1786,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -2084,12 +2130,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "untrusted" version = "0.9.0" @@ -2251,7 +2291,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -2284,13 +2324,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2299,7 +2345,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2375,7 +2421,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index e2207364..a8443373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "23012c1ddd2c488a923d4c71f1d8a1a7a61f7412", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "e000cda350ed277167ce494af91aa5bc5fcd339f", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api @@ -39,7 +39,7 @@ dashmap = "6.1" tracing-subscriber = { version = "0.3", optional = true } # glue (java) -jni = { version = "0.21", features = ["invocation"], optional = true } +jni = { version = "0.22", features = ["invocation"], optional = true } jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } # glue (lua) @@ -50,7 +50,7 @@ napi = { version = "3.1", features = ["full"], optional = true } napi-derive = { version="3.1", optional = true} # glue (python) -pyo3 = { version = "0.25", features = ["extension-module", "multiple-pymethods", "uuid"], optional = true} +pyo3 = { version = "0.28", features = ["extension-module", "multiple-pymethods", "uuid"], optional = true} # extra async-trait = { version = "0.1", optional = true } @@ -60,9 +60,10 @@ serde = { version = "1.0", features = ["derive"], optional = true } # glue (js) napi-build = { version = "2.2", optional = true } # glue (python) -pyo3-build-config = { version = "0.25", optional = true } +pyo3-build-config = { version = "0.28", optional = true } [features] +#default = ["lua-jit", "py-abi3", "lua", "java", "js", "py"] default = ["lua-jit", "py-abi3"] # extra async-trait = ["dep:async-trait"] From 9c77cea0e299115c43be343216d58b48607310c5 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 09:07:12 +0100 Subject: [PATCH 024/121] feat: most API procedures now want UUIDs this is an incomplete update: there are some protocol design flaws which make it very complex or impossible to properly migrate to uuids. so this commit makes it compile, but doesn't make it "work" --- src/client.rs | 22 +++++++--------- src/tests/fixtures.rs | 58 +++++++++++++++++++++---------------------- src/workspace.rs | 41 +++++++++++++----------------- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/client.rs b/src/client.rs index d535227b..723e79d7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, common::{Empty, Identifier, Token}, session::{ - InviteRequest, OwnedWorkspaceRequest, WorkspaceRequest, session_client::SessionClient, + InviteRequest, OwnedWorkspaceRequest, session_client::SessionClient, }, }; @@ -110,13 +110,11 @@ impl Client { } /// Delete an existing workspace if possible. - pub async fn delete_workspace(&self, name: impl AsRef) -> RemoteResult<()> { + pub async fn delete_workspace(&self, id: uuid::Uuid) -> RemoteResult<()> { self.0 .session .clone() - .delete_workspace(OwnedWorkspaceRequest { - name: name.as_ref().to_string(), - }) + .delete_workspace(Identifier::from(id)) .await?; Ok(()) } @@ -124,14 +122,14 @@ impl Client { /// Invite user with given username to the given workspace, if possible. pub async fn invite_to_workspace( &self, - workspace_name: impl AsRef, + workspace_id: uuid::Uuid, user_name: impl AsRef, ) -> RemoteResult<()> { self.0 .session .clone() .invite_to_workspace(InviteRequest { - workspace: workspace_name.as_ref().to_string(), + workspace: Identifier::from(workspace_id), user: user_name.as_ref().to_string(), }) .await?; @@ -149,7 +147,7 @@ impl Client { .into_inner() .owned .into_iter() - .map(|x| crate::api::WorkspaceInfo::from(x)) + .map(crate::api::WorkspaceInfo::from) .collect()) } @@ -164,7 +162,7 @@ impl Client { .into_inner() .invited .into_iter() - .map(|x| crate::api::WorkspaceInfo::from(x)) + .map(crate::api::WorkspaceInfo::from) .collect()) } @@ -173,9 +171,7 @@ impl Client { pub async fn attach_workspace(&self, workspace: uuid::Uuid) -> ConnectionResult { let mut session_client = self.0.session.clone(); let token = session_client - .get_workspace_token(WorkspaceRequest { - id: Identifier::from(workspace), - }) + .get_workspace_token(Identifier::from(workspace)) .await? .into_inner(); @@ -200,7 +196,7 @@ impl Client { tokio::time::sleep(std::time::Duration::from_secs(240)).await; if weak.upgrade().is_none() { break }; let new_credentials = session_client.get_workspace_token( - tonic::Request::new(WorkspaceRequest { id: Identifier::from(workspace) }) + tonic::Request::new(Identifier::from(workspace)) ) .await? .into_inner(); diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index f6ba6887..474c4606 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -77,31 +77,31 @@ impl ScopedFixture for ClientFixture { pub struct WorkspaceFixture { user: String, invitee: Option, - workspace: String, + workspace: uuid::Uuid, } impl WorkspaceFixture { - pub fn of(user: &str, invitee: &str, workspace: &str) -> Self { + pub fn of(user: &str, invitee: &str, workspace: uuid::Uuid) -> Self { Self { user: user.to_string(), invitee: Some(invitee.to_string()), - workspace: workspace.to_string(), + workspace, } } - pub fn one(user: &str, ws: &str) -> Self { + pub fn one(user: &str) -> Self { Self { user: user.to_string(), invitee: None, - workspace: format!("{ws}-{}", uuid::Uuid::new_v4()), + workspace: uuid::Uuid::new_v4(), } } - pub fn two(user: &str, invite: &str, ws: &str) -> Self { + pub fn two(user: &str, invite: &str) -> Self { Self { user: user.to_string(), invitee: Some(invite.to_string()), - workspace: format!("{ws}-{}", uuid::Uuid::new_v4()), + workspace: uuid::Uuid::new_v4(), } } } @@ -109,7 +109,7 @@ impl WorkspaceFixture { impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { let client = ClientFixture::of(&self.user).setup().await?; - let ws_info = client.create_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(self.workspace.to_string()).await?; let workspace = client.attach_workspace(ws_info.id).await?; Ok((client, workspace)) } @@ -117,7 +117,7 @@ impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { if let Some((client, workspace)) = resource { client.leave_workspace(workspace.id()); - if let Err(e) = client.delete_workspace(&self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace).await { eprintln!("could not delete workspace: {e}"); } } @@ -152,9 +152,9 @@ impl ) .setup() .await?; - let ws_info = client.create_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(&self.workspace, invitee_client.current_user().name.clone()) + .invite_to_workspace(self.workspace, invitee_client.current_user().name.clone()) .await?; let workspace = client.attach_workspace(ws_info.id).await?; let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; @@ -172,7 +172,7 @@ impl ) { if let Some((client, ws, _, _)) = resource { client.leave_workspace(ws.id()); - if let Err(e) = client.delete_workspace(&self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace).await { eprintln!("could not delete workspace: {e}"); } } @@ -182,35 +182,35 @@ impl pub struct BufferFixture { user: String, invitee: Option, - workspace: String, - buffer: String, + workspace: uuid::Uuid, + buffer: uuid::Uuid, } impl BufferFixture { - pub fn of(user: &str, invitee: &str, workspace: &str, buffer: &str) -> Self { + pub fn of(user: &str, invitee: &str, workspace: uuid::Uuid, buffer: uuid::Uuid) -> Self { Self { user: user.to_string(), invitee: Some(invitee.to_string()), - workspace: workspace.to_string(), - buffer: buffer.to_string(), + workspace, + buffer, } } - pub fn one(user: &str, ws: &str, buf: &str) -> Self { + pub fn one(user: &str, buf: uuid::Uuid) -> Self { Self { user: user.to_string(), invitee: None, - workspace: format!("{ws}-{}", uuid::Uuid::new_v4()), - buffer: buf.to_string(), + workspace: uuid::Uuid::new_v4(), + buffer: buf, } } - pub fn two(user: &str, invite: &str, ws: &str, buf: &str) -> Self { + pub fn two(user: &str, invite: &str, buf: uuid::Uuid) -> Self { Self { user: user.to_string(), invitee: Some(invite.to_string()), - workspace: format!("{ws}-{}", uuid::Uuid::new_v4()), - buffer: buf.to_string(), + workspace: uuid::Uuid::new_v4(), + buffer: buf, } } } @@ -247,17 +247,17 @@ impl ) .setup() .await?; - let ws_info = client.create_workspace(&self.workspace).await?; + let ws_info = client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(&self.workspace, invitee_client.current_user().name.clone()) + .invite_to_workspace(self.workspace, invitee_client.current_user().name.clone()) .await?; let workspace = client.attach_workspace(ws_info.id).await?; - workspace.create_buffer(&self.buffer, false).await?; - let buffer = workspace.attach_buffer(self.buffer.clone()).await?; + workspace.create_buffer(&self.buffer.to_string(), false).await?; + let buffer = workspace.attach_buffer(self.buffer, &self.buffer.to_string()).await?; let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; - let invitee_buffer = invitee_workspace.attach_buffer(self.buffer.clone()).await?; + let invitee_buffer = invitee_workspace.attach_buffer(self.buffer, &self.buffer.to_string()).await?; Ok(( client, @@ -283,7 +283,7 @@ impl if let Some((client, ws, _, _, _, _)) = resource { // buffer deletion is implied in workspace deletion client.leave_workspace(ws.id()); - if let Err(e) = client.delete_workspace(&self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace).await { eprintln!("could not delete workspace: {e}"); } } diff --git a/src/workspace.rs b/src/workspace.rs index 2d7a177d..81aa8347 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -15,12 +15,12 @@ use crate::{ }; use codemp_proto::{ - common::Empty, + common::{Empty, Identifier}, files::{BufferNode, BufferRequest}, workspace::{ - workspace_event::{ + WorkspaceEvent, workspace_event::{ Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace - }, WorkspaceEvent + } }, }; @@ -48,7 +48,7 @@ use napi_derive::napi; pub struct Workspace(pub(crate) Arc); #[derive(Debug)] -struct WorkspaceInner { +pub(crate) struct WorkspaceInner { id: Uuid, current_user: Arc, cursor: cursor::Controller, @@ -140,7 +140,7 @@ impl Workspace { }); ws.fetch_users().await?; - ws.fetch_buffers("".to_string()).await?; + ws.fetch_buffers().await?; for buffer_ref in ws.0.buffers.iter() { ws.fetch_buffer_users(buffer_ref.key().clone()).await?; @@ -182,13 +182,10 @@ impl Workspace { /// Attach to a buffer and return a handle to it. #[tracing::instrument(skip(self))] - pub async fn attach_buffer(&self, path: String) -> ConnectionResult { + pub async fn attach_buffer(&self, id: uuid::Uuid, path: &str) -> ConnectionResult { let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let request = tonic::Request::new(BufferRequest { - path: path.clone(), - }); - let credentials = workspace_client.get_buffer_token(request).await?.into_inner(); + let credentials = workspace_client.get_buffer_token(Identifier::from(id)).await?.into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); @@ -196,21 +193,19 @@ impl Workspace { let stream = buffer_client.attach(req).await?.into_inner(); let controller = - buffer::Controller::spawn(self.0.current_user.id, &path, tx, stream, self.0.id); + buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, self.0.id); - self.0.buffers.insert(path.clone(), controller.clone()); + self.0.buffers.insert(path.to_string(), controller.clone()); + let path = path.to_string(); let weak = Arc::downgrade(&controller.0); tokio::spawn(async move { - let _p = path.clone(); let fut = async move { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(20)).await; if weak.upgrade().is_none() { break }; - let new_credentials = workspace_client.get_buffer_token( - tonic::Request::new(BufferRequest { path: path.clone() }) - ) + let new_credentials = workspace_client.get_buffer_token(Identifier::from(id)) .await? .into_inner(); let mut request = tonic::Request::new(Empty {}); @@ -221,7 +216,7 @@ impl Workspace { }; if let Err(e) = fut.await { - tracing::error!("error in keepalive task for buffer {_p}: {e}"); + tracing::error!("error in keepalive task for buffer {path}: {e}"); } }); @@ -248,10 +243,10 @@ impl Workspace { } /// Re-fetch the list of available buffers in the workspace. - pub async fn fetch_buffers(&self, path: String) -> RemoteResult<()> { + pub async fn fetch_buffers(&self) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); let resp = workspace_client - .fetch_buffers(tonic::Request::new(BufferRequest { path })) + .fetch_buffers(Empty {}) .await? .into_inner(); @@ -303,14 +298,12 @@ impl Workspace { } /// Delete a buffer. - pub async fn delete_buffer(&self, path: &str) -> RemoteResult<()> { + pub async fn delete_buffer(&self, id: uuid::Uuid, path: &str) -> RemoteResult<()> { self.detach_buffer(path); // just in case let mut workspace_client = self.0.services.ws(); workspace_client - .delete_buffer(tonic::Request::new(BufferRequest { - path: path.to_string(), - })) + .delete_buffer(Identifier::from(id)) .await?; self.0.filetree.remove(path); @@ -369,7 +362,7 @@ 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). + /// A filter may be applied, and it works as a "starts_with" check. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn search_buffers(&self, filter: Option<&str>) -> Vec { let mut tree = self From 8110f425f04666b8dc3bf49c6494b4e8d5f9a069 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 15:19:21 +0100 Subject: [PATCH 025/121] feat: use latest rolled-back proto --- Cargo.lock | 3 +- Cargo.toml | 7 ++- src/api/buffer.rs | 2 +- src/api/event.rs | 8 +-- src/api/mod.rs | 4 +- src/api/user.rs | 55 ++++++++++++------- src/api/workspace.rs | 36 +++++++----- src/buffer/controller.rs | 4 +- src/buffer/worker.rs | 21 ++++--- src/client.rs | 77 +++++++++++--------------- src/cursor/controller.rs | 4 +- src/cursor/worker.rs | 22 ++++---- src/prelude.rs | 4 +- src/tests/client.rs | 115 +++++++++++++++++++++------------------ src/tests/fixtures.rs | 73 ++++++++++++++----------- src/tests/server.rs | 42 +++++++------- src/workspace.rs | 103 ++++++++++++++++------------------- 17 files changed, 303 insertions(+), 277 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efbabcb4..cf9a1719 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,13 +229,12 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=e000cda350ed277167ce494af91aa5bc5fcd339f#e000cda350ed277167ce494af91aa5bc5fcd339f" +source = "git+https://github.com/hexedtech/codemp-proto?rev=c38c582174b1c8845c5e008a07a3d4730070873c#c38c582174b1c8845c5e008a07a3d4730070873c" dependencies = [ "prost", "tonic", "tonic-prost", "tonic-prost-build", - "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a8443373..782e4c52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,11 @@ edition = "2024" version = "0.8.5" exclude = ["dist/*"] +[lints] +rust.unsafe_code = "warn" +clippy.unwrap_used = "warn" +clippy.collapsible_if = "allow" + [lib] name = "codemp" crate-type = ["cdylib", "rlib"] @@ -25,7 +30,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "e000cda350ed277167ce494af91aa5bc5fcd339f", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c38c582174b1c8845c5e008a07a3d4730070873c", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api diff --git a/src/api/buffer.rs b/src/api/buffer.rs index 9f834045..3a04fa79 100644 --- a/src/api/buffer.rs +++ b/src/api/buffer.rs @@ -15,7 +15,7 @@ pub struct BufferNode { impl From for BufferNode { fn from(value: codemp_proto::files::BufferNode) -> Self { Self { - path: value.path, + path: value.path.into(), ephemeral: value.ephemeral, } } diff --git a/src/api/event.rs b/src/api/event.rs index b2086d96..487c14db 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -26,13 +26,13 @@ pub enum Event { impl From for Event { fn from(event: WorkspaceEventInner) -> Self { match event { - WorkspaceEventInner::WorkspaceJoin(e) => Self::UserJoin { name: e.user.name }, - WorkspaceEventInner::WorkspaceLeave(e) => Self::UserLeave { name: e.user.name }, + WorkspaceEventInner::WorkspaceJoin(e) => Self::UserJoin { name: e.user }, + WorkspaceEventInner::WorkspaceLeave(e) => Self::UserLeave { name: e.user }, WorkspaceEventInner::Create(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Delete(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Rename(e) => Self::FileTreeUpdated { path: e.after }, - WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user.name, buffer: e.buffer }, - WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user.name, buffer: e.buffer }, + WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user, buffer: e.buffer }, + WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user, buffer: e.buffer }, } } } diff --git a/src/api/mod.rs b/src/api/mod.rs index 43bea9ef..86076e2b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -31,5 +31,5 @@ pub use config::Config; pub use controller::{AsyncReceiver, AsyncSender, Controller}; pub use cursor::{Cursor, Selection}; pub use event::Event; -pub use user::User; -pub use workspace::WorkspaceInfo; +pub use user::UserInfo; +pub use workspace::WorkspaceIdentifier; diff --git a/src/api/user.rs b/src/api/user.rs index 83c57f7e..1f67ecb5 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -2,53 +2,70 @@ //! An user is identified by an UUID, which should never change. //! Each user has an username, which can change but should be unique. -use uuid::Uuid; - /// Represents a service user #[derive(Debug, Clone)] #[cfg_attr(feature = "py", pyo3::pyclass)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct User { - /// User unique identifier, should never change. - pub id: Uuid, - /// User name, can change but should be unique. +pub struct UserInfo { + /// User name, unique and immutable pub name: String, + /// User display name, can change and be duplicated + pub display_name: Option, + /// User description ("bio"), may contain contacts + pub description: Option, + /// User avatar: a small image some editors can display + pub avatar: Option>, +} + +impl UserInfo { + pub fn default_for(username: String) -> Self { + Self { + name: username, + display_name: None, + description: None, + avatar: None, + } + } } -impl From for User { - fn from(value: codemp_proto::common::User) -> Self { +impl From for UserInfo { + fn from(value: codemp_proto::common::UserInfo) -> Self { Self { - id: value.id.uuid(), name: value.name, + display_name: value.display_name, + description: value.description, + avatar: value.avatar, } } } -impl From for codemp_proto::common::User { - fn from(value: User) -> Self { +impl From for codemp_proto::common::UserInfo { + fn from(value: UserInfo) -> Self { Self { - id: value.id.into(), name: value.name, + display_name: value.display_name, + description: value.description, + avatar: value.avatar, } } } -impl PartialEq for User { +impl PartialEq for UserInfo { fn eq(&self, other: &Self) -> bool { - self.id.eq(&other.id) + self.name.eq(&other.name) } } -impl Eq for User {} +impl Eq for UserInfo {} -impl PartialOrd for User { +impl PartialOrd for UserInfo { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.id.cmp(&other.id)) + Some(self.name.cmp(&other.name)) } } -impl Ord for User { +impl Ord for UserInfo { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) + self.name.cmp(&other.name) } } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 61e47fc8..7fc3f724 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -5,27 +5,37 @@ //! users, meaning two workspaces with the same name can exist, but one user can own only one //! workspace with a given name. -use uuid::Uuid; - /// Represents a service workspace -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "py", pyo3::pyclass)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct WorkspaceInfo { - /// Workspace unique identifier, should never change. - pub id: Uuid, +pub struct WorkspaceIdentifier { /// Workspace name, cannot change and is unique per owner. - pub name: String, + pub workspace: String, /// Workspace owning user - pub owner: super::User, + pub user: String, } -impl From for WorkspaceInfo { - fn from(value: codemp_proto::common::WorkspaceInfo) -> Self { +impl From for WorkspaceIdentifier { + fn from(value: codemp_proto::session::WorkspaceIdentifier) -> Self { Self { - id: Uuid::from(value.id), - name: value.name, - owner: super::User::from(value.owner), + workspace: value.workspace, + user: value.user, } } } + +impl From for codemp_proto::session::WorkspaceIdentifier { + fn from(value: WorkspaceIdentifier) -> Self { + Self { + workspace: value.workspace, + user: value.user, + } + } +} + +impl std::fmt::Display for WorkspaceIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}:{}]", self.user, self.workspace) + } +} diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index 13b5005e..2d7a1d79 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -23,7 +23,7 @@ pub struct BufferController(pub(crate) Arc); impl BufferController { /// Get id of workspace containing this controller - pub fn workspace_id(&self) -> &str { + pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { &self.0.workspace_id } @@ -64,7 +64,7 @@ pub(crate) struct BufferControllerInner { pub(crate) delta_request: mpsc::Sender>>, pub(crate) callback: watch::Sender>>, pub(crate) ack_tx: mpsc::UnboundedSender, - pub(crate) workspace_id: String, + pub(crate) workspace_id: crate::api::WorkspaceIdentifier, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index 67ec5463..a61f08d9 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -5,7 +5,6 @@ use diamond_types::list::encoding::ENCODE_PATCH; use diamond_types::list::{Branch, OpLog}; use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; -use uuid::Uuid; use crate::api::BufferUpdate; use crate::api::TextChange; @@ -19,7 +18,7 @@ use super::controller::{BufferController, BufferControllerInner}; struct BufferWorker { agent_id: u32, path: String, - workspace_id: String, + workspace_id: crate::api::WorkspaceIdentifier, latest_version: watch::Sender, local_version: watch::Sender, ack_rx: mpsc::UnboundedReceiver, @@ -37,11 +36,11 @@ struct BufferWorker { impl BufferController { pub(crate) fn spawn( - user_id: Uuid, - path: &str, + user_name: String, + path: String, tx: mpsc::Sender, rx: Streaming, - workspace_id: Uuid, + workspace_id: crate::api::WorkspaceIdentifier, ) -> Self { let init = diamond_types::LocalVersion::default(); @@ -56,10 +55,10 @@ impl BufferController { let (poller_tx, poller_rx) = mpsc::unbounded_channel(); let mut oplog = OpLog::new(); - let agent_id = oplog.get_or_create_agent_id(&user_id.to_string()); + let agent_id = oplog.get_or_create_agent_id(&user_name); let controller = Arc::new(BufferControllerInner { - path: path.to_string(), + path: path.clone(), latest_version: latest_version_rx, local_version: my_version_rx, ops_in: opin_tx, @@ -68,15 +67,15 @@ impl BufferController { delta_request: recv_tx, callback: cb_tx, ack_tx, - workspace_id: workspace_id.to_string(), + workspace_id: workspace_id.clone(), }); let weak = Arc::downgrade(&controller); let worker = BufferWorker { agent_id, - path: path.to_string(), - workspace_id: workspace_id.to_string(), + path, + workspace_id, latest_version: latest_version_tx, local_version: my_version_tx, ack_rx, @@ -97,7 +96,7 @@ impl BufferController { BufferController(controller) } - #[tracing::instrument(skip(worker, tx, rx), fields(ws = worker.workspace_id, path = worker.path))] + #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id, path = worker.path))] async fn work( mut worker: BufferWorker, tx: mpsc::Sender, diff --git a/src/client.rs b/src/client.rs index 723e79d7..e13dc00f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,7 +10,7 @@ use tonic::{ }; use crate::{ - api::User, + api::UserInfo, errors::{ConnectionResult, RemoteResult}, ext::InternallyMutable, network, @@ -18,9 +18,9 @@ use crate::{ }; use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, - common::{Empty, Identifier, Token}, + common::{Empty, Token}, session::{ - InviteRequest, OwnedWorkspaceRequest, session_client::SessionClient, + InviteRequest, OwnedWorkspaceIdentifier, session_client::SessionClient, }, }; @@ -39,9 +39,9 @@ pub struct Client(Arc); #[derive(Debug)] struct ClientInner { - user: Arc, + user: Arc, config: crate::api::Config, - workspaces: DashMap, + workspaces: DashMap, auth: AuthClient, session: SessionClient>, claims: InternallyMutable, @@ -93,51 +93,41 @@ impl Client { } /// Attempt to create a new workspace with given name. - pub async fn create_workspace( - &self, - name: impl AsRef, - ) -> RemoteResult { - let info = self - .0 + pub async fn create_workspace(&self, name: String) -> RemoteResult<()> { + self.0 .session .clone() - .create_workspace(OwnedWorkspaceRequest { - name: name.as_ref().to_string(), - }) + .create_workspace(OwnedWorkspaceIdentifier { workspace: name }) .await? .into_inner(); - Ok(crate::api::WorkspaceInfo::from(info)) + Ok(()) } /// Delete an existing workspace if possible. - pub async fn delete_workspace(&self, id: uuid::Uuid) -> RemoteResult<()> { + pub async fn delete_workspace(&self, name: String) -> RemoteResult<()> { self.0 .session .clone() - .delete_workspace(Identifier::from(id)) + .delete_workspace(OwnedWorkspaceIdentifier { workspace: name }) .await?; Ok(()) } /// Invite user with given username to the given workspace, if possible. - pub async fn invite_to_workspace( - &self, - workspace_id: uuid::Uuid, - user_name: impl AsRef, - ) -> RemoteResult<()> { + pub async fn invite_to_workspace(&self, workspace_name: String, user_name: String) -> RemoteResult<()> { self.0 .session .clone() .invite_to_workspace(InviteRequest { - workspace: Identifier::from(workspace_id), - user: user_name.as_ref().to_string(), + workspace: workspace_name, + user: user_name, }) .await?; Ok(()) } /// Fetch the names of all workspaces owned by the current user. - pub async fn fetch_owned_workspaces(&self) -> RemoteResult> { + pub async fn fetch_owned_workspaces(&self) -> RemoteResult> { Ok(self .0 .session @@ -145,14 +135,14 @@ impl Client { .fetch_owned_workspaces(Empty {}) .await? .into_inner() - .owned + .workspaces .into_iter() - .map(crate::api::WorkspaceInfo::from) + .map(crate::api::WorkspaceIdentifier::from) .collect()) } /// Fetch the names of all workspaces the current user has joined. - pub async fn fetch_joined_workspaces(&self) -> RemoteResult> { + pub async fn fetch_joined_workspaces(&self) -> RemoteResult> { Ok(self .0 .session @@ -160,44 +150,43 @@ impl Client { .fetch_invited_workspaces(Empty {}) .await? .into_inner() - .invited + .workspaces .into_iter() - .map(crate::api::WorkspaceInfo::from) + .map(crate::api::WorkspaceIdentifier::from) .collect()) } /// Join and return a [`Workspace`]. #[tracing::instrument(skip(self, workspace), fields(ws = %workspace))] - pub async fn attach_workspace(&self, workspace: uuid::Uuid) -> ConnectionResult { + pub async fn attach_workspace(&self, workspace: crate::api::WorkspaceIdentifier) -> ConnectionResult { let mut session_client = self.0.session.clone(); let token = session_client - .get_workspace_token(Identifier::from(workspace)) + .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(workspace.clone())) .await? .into_inner(); let workspace_claims = InternallyMutable::new(token); let ws = Workspace::connect( - workspace, + workspace.clone(), self.0.user.clone(), self.0.config.clone(), workspace_claims.channel(), self.0.claims.channel(), ) .await?; - self.0.workspaces.insert(workspace, ws.clone()); + self.0.workspaces.insert(workspace.clone(), ws.clone()); let mut workspace_client = ws.services().ws(); let weak = Arc::downgrade(&ws.0); tokio::spawn(async move { + let _workspace = workspace.clone(); let fut = async move { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(240)).await; if weak.upgrade().is_none() { break }; - let new_credentials = session_client.get_workspace_token( - tonic::Request::new(Identifier::from(workspace)) - ) + let new_credentials = session_client.get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(_workspace.clone())) .await? .into_inner(); workspace_claims.set(new_credentials); @@ -215,29 +204,29 @@ impl Client { } /// Leave the [`Workspace`] with the given name. - pub fn leave_workspace(&self, id: uuid::Uuid) -> bool { - match self.0.workspaces.remove(&id) { + pub fn leave_workspace(&self, id: &crate::api::WorkspaceIdentifier) -> bool { + match self.0.workspaces.remove(id) { None => true, Some(x) => x.1.consume(), } } /// Gets a [`Workspace`] handle by name. - pub fn get_workspace(&self, id: uuid::Uuid) -> Option { - self.0.workspaces.get(&id).map(|x| x.clone()) + pub fn get_workspace(&self, id: &crate::api::WorkspaceIdentifier) -> Option { + self.0.workspaces.get(id).map(|x| x.clone()) } /// Get the names of all active [`Workspace`]s. - pub fn active_workspaces(&self) -> Vec { + pub fn active_workspaces(&self) -> Vec { self.0 .workspaces .iter() - .map(|x| *x.key()) + .map(|x| x.value().id().clone()) .collect() } /// Get the currently logged in user. - pub fn current_user(&self) -> &User { + pub fn current_user(&self) -> &UserInfo { &self.0.user } } diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index a4bb786a..15990f17 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -22,7 +22,7 @@ use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol}; pub struct CursorController(pub(crate) Arc); impl CursorController { - pub fn workspace_id(&self) -> &str { + pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { &self.0.workspace_id } } @@ -33,7 +33,7 @@ pub(crate) struct CursorControllerInner { pub(crate) stream: mpsc::Sender>>, pub(crate) poll: mpsc::UnboundedSender>, pub(crate) callback: watch::Sender>>, - pub(crate) workspace_id: String, + pub(crate) workspace_id: crate::api::WorkspaceIdentifier, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index 368c4352..c630e0e3 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -2,10 +2,9 @@ use std::sync::Arc; use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; -use uuid::Uuid; use crate::{ - api::{Cursor, Selection, User, controller::ControllerCallback}, + api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, ext::IgnorableError, }; use codemp_proto::cursor::{CursorEvent, CursorUpdate}; @@ -13,9 +12,9 @@ use codemp_proto::cursor::{CursorEvent, CursorUpdate}; use super::controller::{CursorController, CursorControllerInner}; struct CursorWorker { - workspace_id: String, + workspace_id: crate::api::WorkspaceIdentifier, op: mpsc::UnboundedReceiver, - map: Arc>, + map: Arc>, stream: mpsc::Receiver>>, poll: mpsc::UnboundedReceiver>, pollers: Vec>, @@ -28,8 +27,7 @@ impl CursorWorker { #[tracing::instrument(skip(self, tx))] fn handle_recv(&mut self, tx: oneshot::Sender>) { tx.send(self.store.pop_front().and_then(|event| { - let user_id = Uuid::from(event.user); - if let Some(user_name) = self.map.get(&user_id).map(|u| u.name.clone()) { + if let Some(user_name) = self.map.get(&event.user).map(|u| u.name.clone()) { Some(crate::api::cursor::CursorEvent { user: user_name, cursor: Cursor { @@ -48,7 +46,7 @@ impl CursorWorker { } }) } else { - tracing::warn!("received cursor for unknown user {user_id}"); + tracing::warn!("received cursor for unknown user {}", event.user); None } })) @@ -58,10 +56,10 @@ impl CursorWorker { impl CursorController { pub(crate) fn spawn( - user_map: Arc>, + user_map: Arc>, tx: mpsc::Sender, rx: Streaming, - workspace_id: Uuid, + workspace_id: crate::api::WorkspaceIdentifier, ) -> Self { // TODO we should tweak the channel buffer size to better propagate backpressure let (op_tx, op_rx) = mpsc::unbounded_channel(); @@ -73,13 +71,13 @@ impl CursorController { stream: stream_tx, callback: cb_tx, poll: poll_tx, - workspace_id: workspace_id.to_string(), + workspace_id: workspace_id.clone(), }); let weak = Arc::downgrade(&controller); let worker = CursorWorker { - workspace_id: workspace_id.to_string(), + workspace_id, op: op_rx, map: user_map, stream: stream_rx, @@ -95,7 +93,7 @@ impl CursorController { CursorController(controller) } - #[tracing::instrument(skip(worker, tx, rx), fields(ws = worker.workspace_id))] + #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id))] async fn work( mut worker: CursorWorker, tx: mpsc::Sender, diff --git a/src/prelude.rs b/src/prelude.rs index 0d5a434a..ff1a47d4 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,8 +5,8 @@ pub use crate::api::{ AsyncReceiver as CodempAsyncReceiver, AsyncSender as CodempAsyncSender, BufferUpdate as CodempBufferUpdate, Config as CodempConfig, Controller as CodempController, Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection, - TextChange as CodempTextChange, User as CodempUser, - WorkspaceInfo as CodempWorkspaceInfo, BufferNode as CodempBufferNode, + TextChange as CodempTextChange, UserInfo as CodempUserInfo, + WorkspaceIdentifier as CodempWorkspaceIdentifier, BufferNode as CodempBufferNode, }; pub use crate::{ diff --git a/src/tests/client.rs b/src/tests/client.rs index 5c7daa6b..e63d1f13 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -10,19 +10,21 @@ async fn test_workspace_creation_and_deletion() { ClientFixture::of("alice") => |client| { let workspace_name = uuid::Uuid::new_v4().to_string(); - client.create_workspace(&workspace_name).await?; + client.create_workspace(workspace_name.clone()).await?; + + let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name.clone() }; // we can't error, so we return empty vec which will be interpreted as err let workspace_list_before = client.fetch_owned_workspaces().await.unwrap_or_default(); - let res = client.delete_workspace(&workspace_name).await; + let res = client.delete_workspace(workspace_name.clone()).await; // we can and should err here, because empty vec will be counted as success! let workspace_list_after = client.fetch_owned_workspaces().await?; - assert_or_err!(workspace_list_before.contains(&workspace_name)); + assert_or_err!(workspace_list_before.contains(&wsid)); res?; - assert_or_err!(workspace_list_after.contains(&workspace_name) == false); + assert_or_err!(workspace_list_after.contains(&wsid) == false); Ok(()) } @@ -35,12 +37,13 @@ async fn test_attach_and_leave_workspace() { ClientFixture::of("alice") => |client| { let workspace_name = uuid::Uuid::new_v4().to_string(); - client.create_workspace(&workspace_name).await?; + client.create_workspace(workspace_name.clone()).await?; + let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name.clone() }; // leaving a workspace you are not attached to, returns true - let leave_workspace_before = client.leave_workspace(&workspace_name); + let leave_workspace_before = client.leave_workspace(&wsid); - let attach_workspace_that_exists = match client.attach_workspace(&workspace_name).await { + let attach_workspace_that_exists = match client.attach_workspace(wsid.clone()).await { Ok(_) => true, Err(e) => { eprintln!("error attaching to workspace: {e}"); @@ -50,9 +53,9 @@ async fn test_attach_and_leave_workspace() { // leaving a workspace you are attached to, returns true // when there is only one reference to it. - let leave_workspace_after = client.leave_workspace(&workspace_name); + let leave_workspace_after = client.leave_workspace(&wsid); - let _ = client.delete_workspace(&workspace_name).await; + let _ = client.delete_workspace(workspace_name).await; assert_or_err!(leave_workspace_before, "leaving a workspace you are not attached to returned false, should return true."); assert_or_err!(attach_workspace_that_exists, "attaching a workspace that exists failed with error"); @@ -75,31 +78,36 @@ async fn test_invite_user_to_workspace() { .expect("failed setting up bob's client"); let ws_name = uuid::Uuid::new_v4().to_string(); + let wsid = crate::api::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: ws_name.clone() }; + // after this we can't just fail anymore: we need to cleanup, so store errs client_alice - .create_workspace(&ws_name) + .create_workspace(ws_name.clone()) .await .expect("failed creating workspace"); let could_invite = client_alice - .invite_to_workspace(&ws_name, &client_bob.current_user().name) + .invite_to_workspace(ws_name.clone(), client_bob.current_user().name.clone()) .await; let ws_list = client_bob .fetch_joined_workspaces() .await .unwrap_or_default(); // can't fail, empty is err - let could_delete = client_alice.delete_workspace(&ws_name).await; + let could_delete = client_alice.delete_workspace(ws_name.clone()).await; could_invite.expect("could not invite bob"); - assert!(ws_list.contains(&ws_name)); + assert!(ws_list.contains(&wsid)); could_delete.expect("could not delete workspace"); } #[tokio::test] async fn test_workspace_lookup() { super::fixture! { - WorkspaceFixture::one("alice", "test-lookup") => |client, workspace| { - assert_or_err!(client.get_workspace(&workspace.id()).is_some()); - assert_or_err!(client.get_workspace(&uuid::Uuid::new_v4().to_string()).is_none()); + WorkspaceFixture::one("alice") => |client, workspace| { + assert_or_err!(client.get_workspace(workspace.id()).is_some()); + assert_or_err!(client.get_workspace(&crate::api::WorkspaceIdentifier { + user: uuid::Uuid::new_v4().to_string(), + workspace: uuid::Uuid::new_v4().to_string(), + }).is_none()); Ok(()) } } @@ -108,8 +116,8 @@ async fn test_workspace_lookup() { #[tokio::test] async fn test_leave_workspace_with_dangling_ref() { super::fixture! { - WorkspaceFixture::one("alice", "test-dangling-ref") => |client, workspace| { - assert_or_err!(client.leave_workspace(&workspace.id()) == false); + WorkspaceFixture::one("alice") => |client, workspace| { + assert_or_err!(client.leave_workspace(workspace.id()) == false); Ok(()) } } @@ -118,9 +126,9 @@ async fn test_leave_workspace_with_dangling_ref() { #[tokio::test] async fn test_lookup_after_leave() { super::fixture! { - WorkspaceFixture::one("alice", "test-lookup-after-leave") => |client, workspace| { - client.leave_workspace(&workspace.id()); - assert_or_err!(client.get_workspace(&workspace.id()).is_none()); + WorkspaceFixture::one("alice") => |client, workspace| { + client.leave_workspace(workspace.id()); + assert_or_err!(client.get_workspace(workspace.id()).is_none()); Ok(()) } } @@ -131,15 +139,16 @@ async fn test_attach_after_leave() { super::fixture! { ClientFixture::of("alice") => |client| { let ws_name = uuid::Uuid::new_v4().to_string(); - client.create_workspace(&ws_name).await?; + client.create_workspace(ws_name.clone()).await?; + let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: ws_name.clone() }; - let could_attach = client.attach_workspace(&ws_name).await.is_ok(); - let clean_leave = client.leave_workspace(&ws_name); + let could_attach = client.attach_workspace(wsid.clone()).await.is_ok(); + let clean_leave = client.leave_workspace(&wsid); // TODO this is very server specific! disconnect may be instant or caught with next // keepalive, let's arbitrarily say that after 20 seconds we should have been disconnected tokio::time::sleep(std::time::Duration::from_secs(20)).await; - let could_attach_again = client.attach_workspace(&ws_name).await; - let could_delete = client.delete_workspace(&ws_name).await; + let could_attach_again = client.attach_workspace(wsid.clone()).await; + let could_delete = client.delete_workspace(ws_name).await; assert_or_err!(could_attach); assert_or_err!(clean_leave); @@ -154,8 +163,8 @@ async fn test_attach_after_leave() { #[tokio::test] async fn test_active_workspaces() { super::fixture! { - WorkspaceFixture::one("alice", "test-active-workspaces") => |client, workspace| { - assert_or_err!(client.active_workspaces().contains(&workspace.id())); + WorkspaceFixture::one("alice") => |client, workspace| { + assert_or_err!(client.active_workspaces().contains(workspace.id())); Ok(()) } } @@ -164,8 +173,8 @@ async fn test_active_workspaces() { #[tokio::test] async fn test_cant_create_same_workspace_more_than_once() { super::fixture! { - WorkspaceFixture::one("alice", "test-create-multiple-times") => |client, workspace| { - assert_or_err!(client.create_workspace(workspace.id()).await.is_err(), "created same workspace twice"); + WorkspaceFixture::one("alice") => |client, workspace| { + assert_or_err!(client.create_workspace(workspace.id().workspace.clone()).await.is_err(), "created same workspace twice"); Ok(()) } } @@ -176,10 +185,10 @@ async fn test_attaching_to_non_existing_is_error() { super::fixture! { ClientFixture::of("alice") => |client| { let workspace_name = uuid::Uuid::new_v4().to_string(); + let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name }; // we don't create any workspace. - // client.create_workspace(workspace_name).await?; - assert_or_err!(client.attach_workspace(&workspace_name).await.is_err()); + assert_or_err!(client.attach_workspace(wsid).await.is_err()); Ok(()) } } @@ -188,11 +197,11 @@ async fn test_attaching_to_non_existing_is_error() { #[tokio::test] async fn test_deleting_workspace_twice_is_an_error() { super::fixture! { - WorkspaceFixture::one("alice", "test-delete-twice") => |client, workspace| { - let workspace_name = workspace.id(); + WorkspaceFixture::one("alice") => |client, workspace| { + let ws = workspace.id(); - client.delete_workspace(&workspace_name).await?; - assert_or_err!(client.delete_workspace(&workspace_name).await.is_err()); + client.delete_workspace(ws.workspace.clone()).await?; + assert_or_err!(client.delete_workspace(ws.workspace.clone()).await.is_err()); Ok(()) } } @@ -201,8 +210,8 @@ async fn test_deleting_workspace_twice_is_an_error() { #[tokio::test] async fn test_cannot_invite_self() { super::fixture! { - WorkspaceFixture::one("alice", "test-invite-self") => |client, workspace| { - assert_or_err!(client.invite_to_workspace(workspace.id(), &client.current_user().name).await.is_err()); + WorkspaceFixture::one("alice") => |client, workspace| { + assert_or_err!(client.invite_to_workspace(workspace.id().workspace.clone(), client.current_user().name.clone()).await.is_err()); Ok(()) } } @@ -211,8 +220,8 @@ async fn test_cannot_invite_self() { #[tokio::test] async fn test_cannot_invite_to_nonexisting() { super::fixture! { - WorkspaceFixture::two("alice", "bob", "test-invite-self") => |client, _ws, client_bob, _workspace_bob| { - assert_or_err!(client.invite_to_workspace(uuid::Uuid::new_v4().to_string(), &client_bob.current_user().name).await.is_err()); + WorkspaceFixture::two("alice", "bob") => |client, _ws, client_bob, _workspace_bob| { + assert_or_err!(client.invite_to_workspace(uuid::Uuid::new_v4().to_string(), client_bob.current_user().name.clone()).await.is_err()); Ok(()) } } @@ -220,13 +229,13 @@ async fn test_cannot_invite_to_nonexisting() { #[tokio::test] async fn cannot_delete_others_workspaces() { - WorkspaceFixture::two("alice", "bob", "test-cannot-delete-others-workspaces") + WorkspaceFixture::two("alice", "bob") .with(|(_, ws_alice, client_bob, _)| { let ws_alice = ws_alice.clone(); let client_bob = client_bob.clone(); async move { assert_or_err!( - client_bob.delete_workspace(&ws_alice.id()).await.is_err(), + client_bob.delete_workspace(ws_alice.id().workspace.clone()).await.is_err(), "bob was allowed to delete a workspace he didn't own!" ); Ok(()) @@ -237,20 +246,20 @@ async fn cannot_delete_others_workspaces() { #[tokio::test] async fn test_buffer_search() { - WorkspaceFixture::one("alice", "test-buffer-search") + WorkspaceFixture::one("alice") .with(|(_, workspace_alice)| { let buffer_name = uuid::Uuid::new_v4().to_string(); let workspace_alice = workspace_alice.clone(); async move { - workspace_alice.create_buffer(&buffer_name).await?; + workspace_alice.create_buffer(buffer_name.clone(), false).await?; assert_or_err!( !workspace_alice .search_buffers(Some(&buffer_name[0..4])) .is_empty() ); assert_or_err!(workspace_alice.search_buffers(Some("_")).is_empty()); - workspace_alice.delete_buffer(&buffer_name).await?; + workspace_alice.delete_buffer(buffer_name).await?; Ok(()) } }) @@ -259,16 +268,16 @@ async fn test_buffer_search() { #[tokio::test] async fn test_send_operation() { - WorkspaceFixture::two("alice", "bob", "test-send-operation") + WorkspaceFixture::two("alice", "bob") .with(|(_, workspace_alice, _, workspace_bob)| { let buffer_name = uuid::Uuid::new_v4().to_string(); let workspace_alice = workspace_alice.clone(); let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(&buffer_name).await?; - let alice = workspace_alice.attach_buffer(&buffer_name).await?; - let bob = workspace_bob.attach_buffer(&buffer_name).await?; + workspace_alice.create_buffer(buffer_name.clone(), false).await?; + let alice = workspace_alice.attach_buffer(buffer_name.clone()).await?; + let bob = workspace_bob.attach_buffer(buffer_name.clone()).await?; alice.send(crate::api::TextChange { start_idx: 0, @@ -289,16 +298,16 @@ async fn test_send_operation() { #[tokio::test] async fn test_content_converges() { - WorkspaceFixture::two("alice", "bob", "test-content-converges") + WorkspaceFixture::two("alice", "bob") .with(|(_, workspace_alice, _, workspace_bob)| { let buffer_name = uuid::Uuid::new_v4().to_string(); let workspace_alice = workspace_alice.clone(); let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(&buffer_name).await?; - let alice = workspace_alice.attach_buffer(&buffer_name).await?; - let bob = workspace_bob.attach_buffer(&buffer_name).await?; + workspace_alice.create_buffer(buffer_name.clone(), false).await?; + let alice = workspace_alice.attach_buffer(buffer_name.clone()).await?; + let bob = workspace_bob.attach_buffer(buffer_name.clone()).await?; let mut join_set = tokio::task::JoinSet::new(); diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 474c4606..65095187 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -1,5 +1,14 @@ use std::{error::Error, future::Future}; +fn new_ws_id() -> String { + format!("ws-test-{}", uuid::Uuid::new_v4()) +} + +#[deprecated = "need to rethink this API"] +fn ws_id(user: String, workspace: String) -> crate::api::WorkspaceIdentifier { + crate::api::WorkspaceIdentifier { user, workspace } +} + #[allow(async_fn_in_trait)] pub trait ScopedFixture { async fn setup(&mut self) -> Result>; @@ -77,15 +86,15 @@ impl ScopedFixture for ClientFixture { pub struct WorkspaceFixture { user: String, invitee: Option, - workspace: uuid::Uuid, + workspace: String, } impl WorkspaceFixture { - pub fn of(user: &str, invitee: &str, workspace: uuid::Uuid) -> Self { + pub fn of(user: &str, invitee: &str, workspace: &str) -> Self { Self { user: user.to_string(), invitee: Some(invitee.to_string()), - workspace, + workspace: workspace.to_string(), } } @@ -93,7 +102,7 @@ impl WorkspaceFixture { Self { user: user.to_string(), invitee: None, - workspace: uuid::Uuid::new_v4(), + workspace: new_ws_id(), } } @@ -101,7 +110,7 @@ impl WorkspaceFixture { Self { user: user.to_string(), invitee: Some(invite.to_string()), - workspace: uuid::Uuid::new_v4(), + workspace: new_ws_id(), } } } @@ -109,15 +118,15 @@ impl WorkspaceFixture { impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { let client = ClientFixture::of(&self.user).setup().await?; - let ws_info = client.create_workspace(self.workspace.to_string()).await?; - let workspace = client.attach_workspace(ws_info.id).await?; + client.create_workspace(self.workspace.to_string()).await?; + let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; Ok((client, workspace)) } async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { if let Some((client, workspace)) = resource { client.leave_workspace(workspace.id()); - if let Err(e) = client.delete_workspace(self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } } @@ -152,12 +161,12 @@ impl ) .setup() .await?; - let ws_info = client.create_workspace(self.workspace.to_string()).await?; + client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(self.workspace, invitee_client.current_user().name.clone()) + .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(ws_info.id).await?; - let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; + let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; + let invitee_workspace = invitee_client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; Ok((client, workspace, invitee_client, invitee_workspace)) } @@ -172,7 +181,7 @@ impl ) { if let Some((client, ws, _, _)) = resource { client.leave_workspace(ws.id()); - if let Err(e) = client.delete_workspace(self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } } @@ -182,35 +191,35 @@ impl pub struct BufferFixture { user: String, invitee: Option, - workspace: uuid::Uuid, - buffer: uuid::Uuid, + workspace: String, + buffer: String, } impl BufferFixture { - pub fn of(user: &str, invitee: &str, workspace: uuid::Uuid, buffer: uuid::Uuid) -> Self { + pub fn of(user: &str, invitee: &str, workspace: &str, buffer: &str) -> Self { Self { user: user.to_string(), invitee: Some(invitee.to_string()), - workspace, - buffer, + workspace: workspace.to_string(), + buffer: buffer.to_string(), } } - pub fn one(user: &str, buf: uuid::Uuid) -> Self { + pub fn one(user: &str, buf: &str) -> Self { Self { user: user.to_string(), invitee: None, - workspace: uuid::Uuid::new_v4(), - buffer: buf, + workspace: new_ws_id(), + buffer: buf.to_string(), } } - pub fn two(user: &str, invite: &str, buf: uuid::Uuid) -> Self { + pub fn two(user: &str, invite: &str, buf: &str) -> Self { Self { user: user.to_string(), invitee: Some(invite.to_string()), - workspace: uuid::Uuid::new_v4(), - buffer: buf, + workspace: new_ws_id(), + buffer: buf.to_string(), } } } @@ -247,17 +256,17 @@ impl ) .setup() .await?; - let ws_info = client.create_workspace(self.workspace.to_string()).await?; + client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(self.workspace, invitee_client.current_user().name.clone()) + .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(ws_info.id).await?; - workspace.create_buffer(&self.buffer.to_string(), false).await?; - let buffer = workspace.attach_buffer(self.buffer, &self.buffer.to_string()).await?; + let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; + workspace.create_buffer(self.buffer.to_string(), false).await?; + let buffer = workspace.attach_buffer(self.buffer.clone()).await?; - let invitee_workspace = invitee_client.attach_workspace(ws_info.id).await?; - let invitee_buffer = invitee_workspace.attach_buffer(self.buffer, &self.buffer.to_string()).await?; + let invitee_workspace = invitee_client.attach_workspace(ws_id(self.user.clone(),self.workspace.clone())).await?; + let invitee_buffer = invitee_workspace.attach_buffer(self.buffer.clone()).await?; Ok(( client, @@ -283,7 +292,7 @@ impl if let Some((client, ws, _, _, _, _)) = resource { // buffer deletion is implied in workspace deletion client.leave_workspace(ws.id()); - if let Err(e) = client.delete_workspace(self.workspace).await { + if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } } diff --git a/src/tests/server.rs b/src/tests/server.rs index 815d3286..2f5c95ef 100644 --- a/src/tests/server.rs +++ b/src/tests/server.rs @@ -5,15 +5,16 @@ use super::{ #[tokio::test] async fn test_buffer_create() { - WorkspaceFixture::one("alice", "test-buffer-create") + WorkspaceFixture::one("alice") .with(|(_, workspace_alice)| { let buffer_name = uuid::Uuid::new_v4().to_string(); let workspace_alice = workspace_alice.clone(); async move { - workspace_alice.create_buffer(&buffer_name).await?; - assert_or_err!(vec![buffer_name.clone()] == workspace_alice.fetch_buffers().await?); - workspace_alice.delete_buffer(&buffer_name).await?; + workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice.fetch_buffers().await?; + assert_or_err!(vec![buffer_name.clone()] == workspace_alice.search_buffers(None)); + workspace_alice.delete_buffer(buffer_name).await?; Ok(()) } @@ -23,13 +24,13 @@ async fn test_buffer_create() { #[tokio::test] async fn test_cant_create_buffer_twice() { - WorkspaceFixture::one("alice", "test-cant-create-buffer-twice") + WorkspaceFixture::one("alice") .with(|(_, ws)| { let ws = ws.clone(); async move { - ws.create_buffer("cacca").await?; + ws.create_buffer("cacca".to_string(), false).await?; assert!( - ws.create_buffer("cacca").await.is_err(), + ws.create_buffer("cacca".to_string(), false).await.is_err(), "alice could create again the same buffer" ); Ok(()) @@ -41,15 +42,15 @@ async fn test_cant_create_buffer_twice() { #[tokio::test] #[ignore] // TODO reference server has no concept of buffer ownership yet! async fn cannot_delete_others_buffers() { - WorkspaceFixture::two("alice", "bob", "test-cannot-delete-others-buffers") + WorkspaceFixture::two("alice", "bob") .with(|(_, workspace_alice, _, workspace_bob)| { let buffer_name = uuid::Uuid::new_v4().to_string(); let workspace_alice = workspace_alice.clone(); let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(&buffer_name).await?; - assert_or_err!(workspace_bob.delete_buffer(&buffer_name).await.is_err()); + workspace_alice.create_buffer(buffer_name.clone(), false).await?; + assert_or_err!(workspace_bob.delete_buffer(buffer_name).await.is_err()); Ok(()) } }) @@ -62,28 +63,29 @@ async fn test_workspace_interactions() { let client_alice = ClientFixture::of("alice").setup().await?; let client_bob = ClientFixture::of("bob").setup().await?; let workspace_name = format!("test-workspace-interactions-{}", uuid::Uuid::new_v4()); + let wsid = crate::api::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: workspace_name.clone() }; - client_alice.create_workspace(&workspace_name).await?; + client_alice.create_workspace(workspace_name.clone()).await?; let owned_workspaces = client_alice.fetch_owned_workspaces().await?; - assert_or_err!(owned_workspaces.contains(&workspace_name)); - client_alice.attach_workspace(&workspace_name).await?; - assert_or_err!(vec![workspace_name.clone()] == client_alice.active_workspaces()); + assert_or_err!(owned_workspaces.contains(&wsid)); + client_alice.attach_workspace(wsid.clone()).await?; + assert_or_err!(vec![wsid.clone()] == client_alice.active_workspaces()); client_alice - .invite_to_workspace(&workspace_name, &client_bob.current_user().name) + .invite_to_workspace(workspace_name.clone(), client_bob.current_user().name.clone()) .await?; - client_bob.attach_workspace(&workspace_name).await?; + client_bob.attach_workspace(wsid.clone()).await?; assert_or_err!( client_bob .fetch_joined_workspaces() .await? - .contains(&workspace_name) + .contains(&wsid) ); - assert_or_err!(client_bob.leave_workspace(&workspace_name)); - assert_or_err!(client_alice.leave_workspace(&workspace_name)); + assert_or_err!(client_bob.leave_workspace(&wsid)); + assert_or_err!(client_alice.leave_workspace(&wsid)); - client_alice.delete_workspace(&workspace_name).await?; + client_alice.delete_workspace(workspace_name.clone()).await?; Ok::<(), Box>(()) } diff --git a/src/workspace.rs b/src/workspace.rs index 81aa8347..32cb5dc7 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -5,7 +5,7 @@ use crate::{ api::{ - Event, User, + Event, UserInfo, controller::{AsyncReceiver, ControllerCallback}, }, buffer, cursor, @@ -15,8 +15,8 @@ use crate::{ }; use codemp_proto::{ - common::{Empty, Identifier}, - files::{BufferNode, BufferRequest}, + common::Empty, + files::{BufferNode, BufferPath}, workspace::{ WorkspaceEvent, workspace_event::{ Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace @@ -31,7 +31,6 @@ use tokio::sync::{ oneshot, watch, }; use tonic::Streaming; -use uuid::Uuid; #[cfg(feature = "js")] use napi_derive::napi; @@ -49,14 +48,14 @@ pub struct Workspace(pub(crate) Arc); #[derive(Debug)] pub(crate) struct WorkspaceInner { - id: Uuid, - current_user: Arc, + id: crate::api::WorkspaceIdentifier, + current_user: Arc, cursor: cursor::Controller, buffers: DashMap, services: Services, filetree: DashMap, - buffer_users: DashMap>, - users: Arc>, + buffer_users: DashMap>, + users: Arc>, events: tokio::sync::Mutex>, callback: watch::Sender>>, poll_tx: mpsc::UnboundedSender>, @@ -89,8 +88,8 @@ impl AsyncReceiver for Workspace { impl Workspace { #[tracing::instrument(skip(id, user, workspace_claim, user_claim), fields(ws = %id))] pub(crate) async fn connect( - id: Uuid, - user: Arc, + id: crate::api::WorkspaceIdentifier, + user: Arc, config: crate::api::Config, workspace_claim: tokio::sync::watch::Receiver, user_claim: tokio::sync::watch::Receiver, @@ -110,10 +109,10 @@ impl Workspace { .into_inner(); let users = Arc::new(DashMap::default()); - let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id); + let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id.clone()); let ws = Self(Arc::new(WorkspaceInner { - id, + id: id.clone(), current_user: user, cursor: controller, buffers: DashMap::default(), @@ -159,22 +158,19 @@ impl Workspace { } /// Create a new buffer in the current workspace. - pub async fn create_buffer(&self, path: &str, ephemeral: bool) -> RemoteResult<()> { + pub async fn create_buffer(&self, path: String, ephemeral: bool) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); workspace_client .create_buffer(tonic::Request::new(BufferNode { - path: path.to_string(), + path: path.clone().into(), ephemeral, })) .await?; // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( - path.to_string(), - crate::api::BufferNode { - path: path.to_string(), - ephemeral, - }, + path.clone(), + crate::api::BufferNode { path, ephemeral }, ); Ok(()) @@ -182,10 +178,10 @@ impl Workspace { /// Attach to a buffer and return a handle to it. #[tracing::instrument(skip(self))] - pub async fn attach_buffer(&self, id: uuid::Uuid, path: &str) -> ConnectionResult { + pub async fn attach_buffer(&self, path: String) -> ConnectionResult { let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let credentials = workspace_client.get_buffer_token(Identifier::from(id)).await?.into_inner(); + let credentials = workspace_client.get_buffer_token(BufferPath::from(&path)).await?.into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); @@ -193,11 +189,11 @@ impl Workspace { let stream = buffer_client.attach(req).await?.into_inner(); let controller = - buffer::Controller::spawn(self.0.current_user.id, path, tx, stream, self.0.id); + buffer::Controller::spawn(self.0.current_user.name.clone(), path.clone(), tx, stream, self.0.id.clone()); - self.0.buffers.insert(path.to_string(), controller.clone()); + self.0.buffers.insert(path.clone(), controller.clone()); - let path = path.to_string(); + let _path = path.clone(); let weak = Arc::downgrade(&controller.0); tokio::spawn(async move { let fut = async move { @@ -205,7 +201,7 @@ impl Workspace { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(20)).await; if weak.upgrade().is_none() { break }; - let new_credentials = workspace_client.get_buffer_token(Identifier::from(id)) + let new_credentials = workspace_client.get_buffer_token(BufferPath::from(&_path)) .await? .into_inner(); let mut request = tonic::Request::new(Empty {}); @@ -254,7 +250,7 @@ impl Workspace { for b in resp.buffers { self.0 .filetree - .insert(b.path.clone(), crate::api::BufferNode::from(b)); + .insert(b.path.clone().into(), crate::api::BufferNode::from(b)); } Ok(()) @@ -263,17 +259,15 @@ impl Workspace { /// Re-fetch the list of all users in the workspace. pub async fn fetch_users(&self) -> RemoteResult<()> { let mut workspace_client = self.services().ws(); - let users = workspace_client - .fetch_users(tonic::Request::new(Empty {})) + let resp = workspace_client + .fetch_users(Empty {}) .await? - .into_inner() - .users - .into_iter() - .map(User::from); + .into_inner(); self.0.users.clear(); - for u in users { - self.0.users.insert(u.id, u); + for user_name in resp.users { + // TODO need to fetch whole user profiles here maybe? + self.0.users.insert(user_name.clone(), UserInfo::default_for(user_name)); } Ok(()) @@ -281,40 +275,35 @@ impl Workspace { /// Fetch a list of the [User]s attached to a specific buffer. pub async fn fetch_buffer_users(&self, path: String) -> RemoteResult<()> { - let users = self.services().ws() - .fetch_buffer_users(tonic::Request::new(BufferRequest { - path: path.to_string(), - })) + let resp = self.services().ws() + .fetch_buffer_users(BufferPath::from(&path)) .await? - .into_inner() - .users - .into_iter() - .map(|x| Uuid::from(x.id)) - .collect(); + .into_inner(); - self.0.buffer_users.insert(path, users); + self.0.buffer_users.insert(path, resp.users); Ok(()) } /// Delete a buffer. - pub async fn delete_buffer(&self, id: uuid::Uuid, path: &str) -> RemoteResult<()> { - self.detach_buffer(path); // just in case + pub async fn delete_buffer(&self, path: String) -> RemoteResult<()> { + self.detach_buffer(&path); // just in case let mut workspace_client = self.0.services.ws(); workspace_client - .delete_buffer(Identifier::from(id)) + .delete_buffer(BufferPath::from(&path)) .await?; - self.0.filetree.remove(path); + // TODO may deadlock! how fun.... + self.0.filetree.remove(&path); Ok(()) } /// Get the workspace unique id. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 - pub fn id(&self) -> Uuid { - self.0.id + pub fn id(&self) -> &crate::api::WorkspaceIdentifier { + &self.0.id } /// Return a handle to the [`cursor::Controller`]. @@ -340,7 +329,7 @@ impl Workspace { } /// Get all users currently in this workspace - pub fn user_list(&self) -> Vec { + pub fn user_list(&self) -> Vec { self.0 .users .iter() @@ -349,7 +338,7 @@ impl Workspace { } /// Get all users currently attached to specified buffer - pub fn buffer_user_list(&self, path: &str) -> Vec { + pub fn buffer_user_list(&self, path: &str) -> Vec { let mut out = Vec::new(); if let Some(buf_ref) = self.0.buffer_users.get(path) { for uid in buf_ref.value() { @@ -389,7 +378,7 @@ impl WorkspaceWorker { #[tracing::instrument(skip(self, stream, weak))] pub(crate) async fn work( mut self, - ws: Uuid, + ws: crate::api::WorkspaceIdentifier, mut stream: Streaming, weak: Weak, ) { @@ -416,20 +405,20 @@ impl WorkspaceWorker { match ev { // user WorkspaceEventInner::WorkspaceJoin(UserJoinWorkspace { user }) => { - inner.users.insert(user.id.uuid(), user.into()); + inner.users.insert(user.clone(), UserInfo::default_for(user)); } WorkspaceEventInner::WorkspaceLeave(UserLeaveWorkspace { user }) => { - inner.users.remove(&user.id.uuid()); + inner.users.remove(&user); } WorkspaceEventInner::BufferJoin(UserJoinBuffer { user, buffer }) => { match inner.buffer_users.get_mut(&buffer) { - Some(mut buf_users_ref) => buf_users_ref.push(Uuid::from(user.id)), + Some(mut buf_users_ref) => buf_users_ref.push(user), None => tracing::warn!("received UserJoinBuffer event for an unknown buffer"), } }, WorkspaceEventInner::BufferLeave(UserLeaveBuffer { user, buffer }) => { match inner.buffer_users.get_mut(&buffer) { - Some(mut buf_users_ref) => buf_users_ref.retain(|x| *x != Uuid::from(user.id)), + Some(mut buf_users_ref) => buf_users_ref.retain(|x| *x != user), None => tracing::warn!("received UserLeaveBuffer event for an unknown buffer"), } }, From 84ca1097ba49d63692db39573732f251486a95c1 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 15:47:36 +0100 Subject: [PATCH 026/121] fix: re-exports in prelude --- src/api/mod.rs | 2 +- src/api/workspace.rs | 2 +- src/prelude.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 86076e2b..67c84678 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -29,7 +29,7 @@ pub use buffer::BufferNode; pub use change::{BufferUpdate, TextChange}; pub use config::Config; pub use controller::{AsyncReceiver, AsyncSender, Controller}; -pub use cursor::{Cursor, Selection}; +pub use cursor::{Cursor, Selection, CursorEvent}; pub use event::Event; pub use user::UserInfo; pub use workspace::WorkspaceIdentifier; diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 7fc3f724..d12e779e 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -36,6 +36,6 @@ impl From for codemp_proto::session::WorkspaceIdentifier { impl std::fmt::Display for WorkspaceIdentifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}:{}]", self.user, self.workspace) + write!(f, "#{}:{}", self.user, self.workspace) } } diff --git a/src/prelude.rs b/src/prelude.rs index ff1a47d4..385a456a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use crate::api::{ AsyncReceiver as CodempAsyncReceiver, AsyncSender as CodempAsyncSender, BufferUpdate as CodempBufferUpdate, Config as CodempConfig, Controller as CodempController, - Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection, + Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection, CursorEvent as CodempCursorEvent, TextChange as CodempTextChange, UserInfo as CodempUserInfo, WorkspaceIdentifier as CodempWorkspaceIdentifier, BufferNode as CodempBufferNode, }; From adef582bf2c8264ece558f055a20ebafaffde507 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 15:47:47 +0100 Subject: [PATCH 027/121] feat(lua): update glue --- src/ffi/lua/client.rs | 18 +++++++++--------- src/ffi/lua/cursor.rs | 4 ++-- src/ffi/lua/ext/a_sync.rs | 2 +- src/ffi/lua/ext/callback.rs | 8 +++++--- src/ffi/lua/ext/mod.rs | 1 + src/ffi/lua/workspace.rs | 26 +++++++++++++------------- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index 5921d869..644de014 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -3,7 +3,7 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempConfig CodempUser } +super::ext::impl_lua_serde! { CodempConfig CodempUserInfo } impl LuaUserData for CodempClient { fn add_methods>(methods: &mut M) { @@ -25,8 +25,8 @@ impl LuaUserData for CodempClient { methods.add_method( "attach_workspace", - |_, this, (ws,): (String,)| { - let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; + |_, this, (user, workspace): (String,String)| { + let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; a_sync! { this => this.attach_workspace(ws_id).await? } }, ); @@ -55,14 +55,14 @@ impl LuaUserData for CodempClient { |_, this, ()| a_sync! { this => this.fetch_joined_workspaces().await? }, ); - methods.add_method("leave_workspace", |_, this, (ws,): (String,)| { - let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; - Ok(this.leave_workspace(ws_id)) + methods.add_method("leave_workspace", |_, this, (user, workspace): (String,String)| { + let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; + Ok(this.leave_workspace(&ws_id)) }); - methods.add_method("get_workspace", |_, this, (ws,): (String,)| { - let ws_id = super::ext::lua_parse_uuid(&ws, 1, "ws")?; - Ok(this.get_workspace(ws_id)) + methods.add_method("get_workspace", |_, this, (user, workspace): (String,String)| { + let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; + Ok(this.get_workspace(&ws_id)) }); } } diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index 17f18fb4..43fa4bb1 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -3,7 +3,7 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempCursor CodempSelection } +super::ext::impl_lua_serde! { CodempSelection CodempCursor CodempCursorEvent } impl LuaUserData for CodempCursorController { fn add_methods>(methods: &mut M) { @@ -11,7 +11,7 @@ impl LuaUserData for CodempCursorController { Ok(format!("{:?}", this)) }); - methods.add_method("send", |_, this, (cursor,): (CodempSelection,)| { + methods.add_method("send", |_, this, (cursor,): (CodempCursor,)| { Ok(this.send(cursor)?) }); methods.add_method( diff --git a/src/ffi/lua/ext/a_sync.rs b/src/ffi/lua/ext/a_sync.rs index fe68402a..90fa58a1 100644 --- a/src/ffi/lua/ext/a_sync.rs +++ b/src/ffi/lua/ext/a_sync.rs @@ -39,7 +39,7 @@ pub(crate) struct Promise( impl LuaUserData for Promise { fn add_fields>(fields: &mut F) { fields.add_field_method_get("ready", |_, this| { - Ok(this.0.as_ref().map_or(true, |x| x.is_finished())) + Ok(this.0.as_ref().is_none_or(|x| x.is_finished())) }); } diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index 6426886d..b9384d6a 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -118,17 +118,19 @@ macro_rules! callback_args { callback_args! { Str: String, VecStr: Vec, - VecUser: Vec, + VecUserInfo: Vec, Client: CodempClient, CursorController: CodempCursorController, BufferController: CodempBufferController, Workspace: CodempWorkspace, - WorkspaceInfo: CodempWorkspaceInfo, - VecWorkspaceInfo: Vec, + WorkspaceIdentifier: CodempWorkspaceIdentifier, + VecWorkspaceIdentifier: Vec, Event: CodempEvent, MaybeEvent: Option, Cursor: CodempCursor, MaybeCursor: Option, + CursorEvent: CodempCursorEvent, + MaybeCursorEvent: Option, Selection: CodempSelection, VecSelection: Vec, MaybeSelection: Option, diff --git a/src/ffi/lua/ext/mod.rs b/src/ffi/lua/ext/mod.rs index 8062db64..b23fc779 100644 --- a/src/ffi/lua/ext/mod.rs +++ b/src/ffi/lua/ext/mod.rs @@ -5,6 +5,7 @@ pub mod log; pub(crate) use a_sync::tokio; pub(crate) use callback::callback; +#[allow(unused)] // for now pub(crate) fn lua_parse_uuid(uuid: &str, pos: usize, name: &str) -> mlua::Result { use std::str::FromStr; match uuid::Uuid::from_str(uuid) { diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index c5ac5e97..e5944b30 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -3,7 +3,7 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempEvent CodempWorkspaceInfo } +super::ext::impl_lua_serde! { CodempEvent CodempWorkspaceIdentifier } impl LuaUserData for CodempWorkspace { fn add_methods>(methods: &mut M) { @@ -12,12 +12,12 @@ impl LuaUserData for CodempWorkspace { }); methods.add_method( "create_buffer", - |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(&name, ephemeral).await? }, + |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(name, ephemeral).await? }, ); methods.add_method( "attach_buffer", - |_, this, (name,): (String,)| a_sync! { this => this.attach_buffer(&name).await? }, + |_, this, (name,): (String,)| a_sync! { this => this.attach_buffer(name).await? }, ); methods.add_method("detach_buffer", |_, this, (name,): (String,)| { @@ -26,7 +26,7 @@ impl LuaUserData for CodempWorkspace { methods.add_method( "delete_buffer", - |_, this, (name,): (String,)| a_sync! { this => this.delete_buffer(&name).await? }, + |_, this, (name,): (String,)| a_sync! { this => this.delete_buffer(name).await? }, ); methods.add_method("get_buffer", |_, this, (name,): (String,)| { @@ -34,23 +34,23 @@ impl LuaUserData for CodempWorkspace { }); methods.add_method( - "list_buffers", - |_, this, (filter,): (String,)| a_sync! { this => this.list_buffers(filter).await? }, + "fetch_buffers", + |_, this, ()| a_sync! { this => this.fetch_buffers().await? }, ); methods.add_method( - "list_users", - |_, this, ()| a_sync! { this => this.list_users().await? }, + "fetch_users", + |_, this, ()| a_sync! { this => this.fetch_users().await? }, + ); + methods.add_method( + "fetch_buffer_users", + |_, this, (buffer,): (String,)| a_sync! { this => this.fetch_buffer_users(buffer).await? }, ); methods.add_method("search_buffers", |_, this, (filter,): (Option,)| { Ok(this.search_buffers(filter.as_deref())) }); - methods.add_method("list_buffer_users", |_, this, (path,): (String,)| { - a_sync! { - this => this.list_buffer_users(&path).await? - } - }); + methods.add_method("list_buffer_users", |_, this, (path,): (String,)| Ok(this.buffer_user_list(&path))); methods.add_method("id", |_, this, ()| Ok(this.id().to_string())); methods.add_method("cursor", |_, this, ()| Ok(this.cursor())); From f33c49fc12f11c53a6d57c8c8ab94f44218a2993 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 16:10:57 +0100 Subject: [PATCH 028/121] feat: new procedures from proto v0.8.1 --- Cargo.toml | 1 + src/client.rs | 43 +++++++++++++++++++++++++++++++++++++++- src/cursor/controller.rs | 7 ++++--- src/cursor/worker.rs | 18 ++++++++++++++--- src/network.rs | 2 +- src/workspace.rs | 18 ++++++++++++++++- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 782e4c52..a9487367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ exclude = ["dist/*"] [lints] rust.unsafe_code = "warn" +rust.missing_docs = "warn" clippy.unwrap_used = "warn" clippy.collapsible_if = "allow" diff --git a/src/client.rs b/src/client.rs index e13dc00f..96f11e55 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, common::{Empty, Token}, session::{ - InviteRequest, OwnedWorkspaceIdentifier, session_client::SessionClient, + InviteRequest, OwnedWorkspaceIdentifier, UserId, WorkspaceIdentifier, session_client::SessionClient }, }; @@ -113,6 +113,36 @@ impl Client { Ok(()) } + /// Quit a joined workspace. Cannot quit owned workspaces: must delete them + pub async fn quit_workspace(&self, user: String, workspace: String) -> RemoteResult<()> { + self.0 + .session + .clone() + .quit_workspace(WorkspaceIdentifier { user, workspace }) + .await?; + Ok(()) + } + + /// Accept an invitation to a workspace, making it accessible + pub async fn accept_invite(&self, user: String, workspace: String) -> RemoteResult<()> { + self.0 + .session + .clone() + .accept_invite(WorkspaceIdentifier { user, workspace }) + .await?; + Ok(()) + } + + /// Reject an invitation to a workspace + pub async fn reject_invite(&self, user: String, workspace: String) -> RemoteResult<()> { + self.0 + .session + .clone() + .reject_invite(WorkspaceIdentifier { user, workspace }) + .await?; + Ok(()) + } + /// Invite user with given username to the given workspace, if possible. pub async fn invite_to_workspace(&self, workspace_name: String, user_name: String) -> RemoteResult<()> { self.0 @@ -156,6 +186,17 @@ impl Client { .collect()) } + pub async fn get_user_info(&self, user: String) -> RemoteResult { + Ok( + self.0 + .session + .clone() + .get_user_info(UserId { user }) + .await? + .into_inner() + ) + } + /// Join and return a [`Workspace`]. #[tracing::instrument(skip(self, workspace), fields(ws = %workspace))] pub async fn attach_workspace(&self, workspace: crate::api::WorkspaceIdentifier) -> ConnectionResult { diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 15990f17..74ccf976 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -7,11 +7,11 @@ use tokio::sync::{mpsc, oneshot, watch}; use crate::{ api::{ - controller::{AsyncReceiver, AsyncSender, ControllerCallback}, cursor::CursorEvent, Controller, Cursor + Controller, Cursor, controller::{AsyncReceiver, AsyncSender, ControllerCallback}, cursor::CursorEvent }, - errors::ControllerResult, + errors::ControllerResult, network::AuthedService, }; -use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol}; +use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol, cursor_client::CursorClient}; /// A [Controller] for asynchronously sending and receiving [Cursor] event. /// @@ -34,6 +34,7 @@ pub(crate) struct CursorControllerInner { pub(crate) poll: mpsc::UnboundedSender>, pub(crate) callback: watch::Sender>>, pub(crate) workspace_id: crate::api::WorkspaceIdentifier, + pub(crate) service: CursorClient, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index c630e0e3..b2b321fe 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -4,10 +4,9 @@ use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; use crate::{ - api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, - ext::IgnorableError, + api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, errors::RemoteResult, ext::IgnorableError, network::AuthedService }; -use codemp_proto::cursor::{CursorEvent, CursorUpdate}; +use codemp_proto::{common::Empty, cursor::{CursorEvent, CursorUpdate, cursor_client::CursorClient}}; use super::controller::{CursorController, CursorControllerInner}; @@ -60,6 +59,7 @@ impl CursorController { tx: mpsc::Sender, rx: Streaming, workspace_id: crate::api::WorkspaceIdentifier, + cursor_service: CursorClient, // TODO ughh passing these around ) -> Self { // TODO we should tweak the channel buffer size to better propagate backpressure let (op_tx, op_rx) = mpsc::unbounded_channel(); @@ -72,6 +72,7 @@ impl CursorController { callback: cb_tx, poll: poll_tx, workspace_id: workspace_id.clone(), + service: cursor_service, }); let weak = Arc::downgrade(&controller); @@ -93,6 +94,17 @@ impl CursorController { CursorController(controller) } + pub async fn list(&self) -> RemoteResult> { + Ok(self.0 + .service + .clone() + .list(Empty {}) + .await? + .into_inner() + .cursors + ) + } + #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id))] async fn work( mut worker: CursorWorker, diff --git a/src/network.rs b/src/network.rs index c4994abb..4988d0d7 100644 --- a/src/network.rs +++ b/src/network.rs @@ -9,7 +9,7 @@ use tonic::{ use crate::errors::ConnectionResult; -type AuthedService = InterceptedService; +pub(crate) type AuthedService = InterceptedService; #[derive(Debug, Clone)] pub struct SessionInterceptor(pub tokio::sync::watch::Receiver); diff --git a/src/workspace.rs b/src/workspace.rs index 32cb5dc7..ddd70f41 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -109,7 +109,7 @@ impl Workspace { .into_inner(); let users = Arc::new(DashMap::default()); - let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id.clone()); + let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id.clone(), services.cur().clone()); let ws = Self(Arc::new(WorkspaceInner { id: id.clone(), @@ -176,6 +176,22 @@ impl Workspace { Ok(()) } + pub async fn pin_buffer(&self, path: String) -> RemoteResult<()> { + self.0.services.ws() + .clone() + .pin_buffer(BufferPath::from(path)) + .await?; + Ok(()) + } + + pub async fn un_pin_buffer(&self, path: String) -> RemoteResult<()> { + self.0.services.ws() + .clone() + .un_pin_buffer(BufferPath::from(path)) + .await?; + Ok(()) + } + /// Attach to a buffer and return a handle to it. #[tracing::instrument(skip(self))] pub async fn attach_buffer(&self, path: String) -> ConnectionResult { From 34ab4832ede59c4877155a62be4e2cb0efd49603 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 20:14:47 +0100 Subject: [PATCH 029/121] fix(lua): add new methods to glue --- src/api/cursor.rs | 22 ++++++++++++++++++++++ src/ffi/lua/buffer.rs | 3 +++ src/ffi/lua/client.rs | 33 +++++++++++++++++++++++++++------ src/ffi/lua/cursor.rs | 6 ++++++ src/ffi/lua/ext/callback.rs | 2 ++ src/ffi/lua/workspace.rs | 14 ++++++++++++++ 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/api/cursor.rs b/src/api/cursor.rs index a9eed4e6..f2fda9d6 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -47,3 +47,25 @@ pub struct Selection { /// Cursor position final column in buffer. pub end_col: i32, } + +// TODO this re-wrapping of our API is not elegant at all +impl From for CursorEvent { + fn from(value: codemp_proto::cursor::CursorEvent) -> Self { + Self { + user: value.user, + cursor: Cursor { + buffer: value.position.buffer, + sel: value.position.cursors + .into_iter() + .map(|c| Selection { + start_row: c.start.row, + end_row: c.end.row, + start_col: c.start.col, + end_col: c.end.col, + }) + .collect(), + }, + } + } +} + diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index dff38dc9..7b79bd2a 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -11,6 +11,9 @@ impl LuaUserData for CodempBufferController { Ok(format!("{:?}", this)) }); + methods.add_method("workspace_id", |_, this, ()| Ok(this.workspace_id().clone())); + methods.add_method("path", |_, this, ()| Ok(this.path().to_string())); + methods.add_method("send", |_, this, (change,): (CodempTextChange,)| { Ok(this.send(change)?) }); diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index 644de014..d1116306 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -11,12 +11,8 @@ impl LuaUserData for CodempClient { Ok(format!("{:?}", this)) }); - methods.add_method("current_user", |_, this, ()| { - Ok(this.current_user().clone()) - }); - methods.add_method("active_workspaces", |_, this, ()| { - Ok(this.active_workspaces().into_iter().map(|x| x.to_string()).collect::>()) - }); + methods.add_method("current_user", |_, this, ()| Ok(this.current_user().clone())); + methods.add_method("active_workspaces", |_, this, ()| Ok(this.active_workspaces())); methods.add_method( "refresh", @@ -41,6 +37,27 @@ impl LuaUserData for CodempClient { |_, this, (ws,): (String,)| a_sync! { this => this.delete_workspace(ws).await? }, ); + methods.add_method( + "quit_workspace", + |_, this, (user, workspace):(String,String)| a_sync! { + this => this.quit_workspace(user, workspace).await? + }, + ); + + methods.add_method( + "accept_invite", + |_, this, (user, workspace):(String,String)| a_sync! { + this => this.accept_invite(user, workspace).await? + }, + ); + + methods.add_method( + "reject_invite", + |_, this, (user, workspace):(String,String)| a_sync! { + this => this.reject_invite(user, workspace).await? + }, + ); + methods.add_method("invite_to_workspace", |_, this, (ws,user):(String,String)| a_sync! { this => this.invite_to_workspace(ws, user).await? } ); @@ -64,5 +81,9 @@ impl LuaUserData for CodempClient { let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; Ok(this.get_workspace(&ws_id)) }); + + methods.add_method("get_user_info", |_, this, (user,):(String,)| a_sync! { + this => crate::api::UserInfo::from(this.get_user_info(user).await?) + }); } } diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index 43fa4bb1..67036696 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -11,6 +11,12 @@ impl LuaUserData for CodempCursorController { Ok(format!("{:?}", this)) }); + methods.add_method("workspace_id", |_, this, ()| Ok(this.workspace_id().clone())); + + methods.add_method("list", |_, this, ()| a_sync! { + this => this.list().await?.into_iter().map(CodempCursorEvent::from).collect::>() + }); + methods.add_method("send", |_, this, (cursor,): (CodempCursor,)| { Ok(this.send(cursor)?) }); diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index b9384d6a..44490264 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -118,6 +118,7 @@ macro_rules! callback_args { callback_args! { Str: String, VecStr: Vec, + UserInfo: CodempUserInfo, VecUserInfo: Vec, Client: CodempClient, CursorController: CodempCursorController, @@ -130,6 +131,7 @@ callback_args! { Cursor: CodempCursor, MaybeCursor: Option, CursorEvent: CodempCursorEvent, + VecCursorEvent: Vec, MaybeCursorEvent: Option, Selection: CodempSelection, VecSelection: Vec, diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index e5944b30..fca5cf15 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -15,6 +15,20 @@ impl LuaUserData for CodempWorkspace { |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(name, ephemeral).await? }, ); + methods.add_method( + "pin_buffer", + |_, this, (path,):(String,)| a_sync! { + this => this.pin_buffer(path).await? + }, + ); + + methods.add_method( + "un_pin_buffer", + |_, this, (path,):(String,)| a_sync! { + this => this.un_pin_buffer(path).await? + }, + ); + methods.add_method( "attach_buffer", |_, this, (name,): (String,)| a_sync! { this => this.attach_buffer(name).await? }, From 2389ece7c6e1515326e1cff6fd627225654779ad Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 20:33:57 +0100 Subject: [PATCH 030/121] docs(lua): type hints update --- dist/lua/annotations.lua | 144 ++++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 05726df6..b3d51bde 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -158,49 +158,38 @@ function MaybeBufferUpdatePromise:cancel() end ---invoke callback asynchronously as soon as promise is ready function MaybeBufferUpdatePromise:and_then(cb) end ----@class (exact) UserListPromise : Promise -local UserListPromise = {} +---@class (exact) UserInfoListPromise : Promise +local UserInfoListPromise = {} --- block until promise is ready and return value ---- @return User[] -function UserListPromise:await() end +--- @return UserInfo[] +function UserInfoListPromise:await() end --- cancel promise execution -function UserListPromise:cancel() end ----@param cb fun(x: User[]) callback to invoke +function UserInfoListPromise:cancel() end +---@param cb fun(x: UserInfo[]) callback to invoke ---invoke callback asynchronously as soon as promise is ready -function UserListPromise:and_then(cb) end +function UserInfoListPromise:and_then(cb) end ----@class (exact) WorkspaceInfoPromise : Promise -local WorkspaceInfoPromise = {} +---@class (exact) UserInfoPromise : Promise +local UserInfoPromise = {} --- block until promise is ready and return value ---- @return WorkspaceInfo -function WorkspaceInfoPromise:await() end +--- @return UserInfo +function UserInfoPromise:await() end --- cancel promise execution -function WorkspaceInfoPromise:cancel() end ----@param cb fun(x: WorkspaceInfo) callback to invoke +function UserInfoPromise:cancel() end +---@param cb fun(x: UserInfo) callback to invoke ---invoke callback asynchronously as soon as promise is ready -function WorkspaceInfoPromise:and_then(cb) end +function UserInfoPromise:and_then(cb) end ----@class (exact) WorkspaceInfoListPromise : Promise -local WorkspaceInfoListPromise = {} +---@class (exact) WorkspaceIdentifierListPromise : Promise +local WorkspaceIdentifierListPromise = {} --- block until promise is ready and return value ---- @return WorkspaceInfo[] -function WorkspaceInfoListPromise:await() end +--- @return WorkspaceIdentifier[] +function WorkspaceIdentifierListPromise:await() end --- cancel promise execution -function WorkspaceInfoListPromise:cancel() end ----@param cb fun(x: WorkspaceInfo[]) callback to invoke +function WorkspaceIdentifierListPromise:cancel() end +---@param cb fun(x: WorkspaceIdentifier[]) callback to invoke ---invoke callback asynchronously as soon as promise is ready -function WorkspaceInfoListPromise:and_then(cb) end - ----@class (exact) BufferNodeListPromise : Promise -local BufferNodeListPromise = {} ---- block until promise is ready and return value ---- @return BufferNode[] -function BufferNodeListPromise:await() end ---- cancel promise execution -function BufferNodeListPromise:cancel() end ----@param cb fun(x: BufferNode[]) callback to invoke ----invoke callback asynchronously as soon as promise is ready -function BufferNodeListPromise:and_then(cb) end +function WorkspaceIdentifierListPromise:and_then(cb) end -- [[ END ASYNC STUFF ]] @@ -209,7 +198,7 @@ function BufferNodeListPromise:and_then(cb) end ---the effective local client, handling connecting to codemp server local Client = {} ----@return User +---@return UserInfo ---current logged in user for this client function Client:current_user() end @@ -231,7 +220,7 @@ function Client:refresh() end function Client:attach_workspace(ws) end ---@param ws string workspace id to create ----@return WorkspaceInfoPromise +---@return NilPromise ---@async ---@nodiscard ---create a new workspace with given id @@ -248,6 +237,30 @@ function Client:leave_workspace(ws) end ---delete workspace with given id function Client:delete_workspace(ws) end +---@param user string user owning the workspace to quit +---@param workspace string workspace to quit +---@return NilPromise +---@async +---@nodiscard +---quit a joined workspace, by user + workspace name +function Client:quit_workspace(user, workspace) end + +---@param user string user inviting us +---@param workspace string workspace being invited to +---@return NilPromise +---@async +---@nodiscard +---accept an invite to a new workspace +function Client:accept_invite(user, workspace) end + +---@param user string user inviting us +---@param workspace string workspace being invited to +---@return NilPromise +---@async +---@nodiscard +---reject an invite to a new workspace +function Client:reject_invite(user, workspace) end + ---@param ws string workspace id to delete ---@param user string user name to invite to given workspace ---@return NilPromise @@ -256,13 +269,13 @@ function Client:delete_workspace(ws) end ---grant user acccess to workspace function Client:invite_to_workspace(ws, user) end ----@return WorkspaceInfoListPromise +---@return WorkspaceIdentifierListPromise ---@async ---@nodiscard ---fetch and list owned workspaces function Client:fetch_owned_workspaces() end ----@return WorkspaceInfoListPromise +---@return WorkspaceIdentifierListPromise ---@async ---@nodiscard ---fetch and list joined workspaces @@ -273,19 +286,27 @@ function Client:fetch_joined_workspaces() end ---get an active workspace by name function Client:get_workspace(ws) end +---@param user string username to lookup +---@return UserInfoPromise +---@async +---@nodiscard +---get full user info for given username from server +function Client:get_user_info(user) end + ----@class User +---@class UserInfo ---represents a service user and contains all its relevant info ----@field id string user uuid ----@field name string user display name +---@field name string user unique, immutable name +---@field display_name string|nil user display name, mutable and not guaranteed to be unique +---@field description string|nil user description, maybe containing contact info +---@field avatar any|nil user avatar image, as bytes +---@class WorkspaceIdentifier +---uniquely identifies a workspace, by its owner and workspace name +---@field user string username of workspace owner +---@field workspace string workspace name ----@class WorkspaceInfo ----represents informations about a workspace, without having an handle to it ----@field id string ----@field name string ----@field owner User @@ -320,6 +341,20 @@ function Workspace:create_buffer(path, ephemeral) end ---delete buffer from workspace function Workspace:delete_buffer(path) end +---@param path string relative path ("name") of buffer to pin +---@return NilPromise +---@async +---@nodiscard +---pin a buffer, meaning it will persist even if no users are attached +function Workspace:pin_buffer(path) end + +---@param path string relative path ("name") of buffer to un-pin +---@return NilPromise +---@async +---@nodiscard +---un-pin a buffer, meaning it will get deleted once all users leave +function Workspace:un_pin_buffer(path) end + ---@param path string relative path ("name") of buffer to get ---@return BufferController? ---get an active buffer controller by name @@ -342,16 +377,20 @@ function Workspace:detach_buffer(path) end ---return the list of available buffers in this workspace, as relative paths from workspace root function Workspace:search_buffers(filter) end ----@return User[] +---@return UserInfo[] ---return all names of users currently in this workspace function Workspace:user_list() end ----@param filter string filter buffers we want to fetch relative to this path ----@return BufferNodeListPromise +---@param path string path of buffer queried for attached users +---@return UserInfo[] +---return all names of users currently attached to given buffer (by path) +function Workspace:buffer_user_list(path) end + +---@return NilPromise ---@async ---@nodiscard ---force refresh buffer list from workspace -function Workspace:list_buffers(filter) end +function Workspace:fetch_buffers() end ---@return NilPromise ---@async @@ -360,7 +399,7 @@ function Workspace:list_buffers(filter) end function Workspace:fetch_users(path) end ---@param path string the buffer to look in ----@return UserListPromise +---@return NilPromise ---@async ---@nodiscard ---fetch the list of users in the given buffer @@ -399,12 +438,6 @@ function Workspace:callback(cb) end ----@class BufferNode ----@field id string ----@field name string ----@field owner User - - ---@class (exact) BufferController ---handle to a remote buffer, for async send/recv operations @@ -567,3 +600,4 @@ function Codemp.setup_driver(block) end ---@return boolean success if logger was setup correctly, false otherwise ---setup a global logger for codemp, note that can only be done once function Codemp.setup_tracing(printer, debug) end + From 8f75b4c7166920088d432729f0892ea4514a2100 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 22:34:16 +0100 Subject: [PATCH 031/121] feat: crude session event receiver --- src/client.rs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 96f11e55..54dd50cc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,9 +10,9 @@ use tonic::{ }; use crate::{ - api::UserInfo, + api::{AsyncReceiver, UserInfo}, errors::{ConnectionResult, RemoteResult}, - ext::InternallyMutable, + ext::{IgnorableError, InternallyMutable}, network, workspace::Workspace, }; @@ -45,6 +45,9 @@ struct ClientInner { auth: AuthClient, session: SessionClient>, claims: InternallyMutable, + poll_tx: tokio::sync::mpsc::UnboundedSender>, + callback: tokio::sync::watch::Sender>>, + events: tokio::sync::Mutex>, } impl Client { @@ -66,17 +69,40 @@ impl Client { let claims = InternallyMutable::new(resp.token); // TODO move this one into network.rs - let session = + let mut session = SessionClient::with_interceptor(channel, network::SessionInterceptor(claims.channel())); - Ok(Client(Arc::new(ClientInner { + let (ev_tx, ev_rx) = tokio::sync::mpsc::unbounded_channel(); + let (poll_tx, poll_rx) = tokio::sync::mpsc::unbounded_channel(); + let (cb_tx, cb_rx) = tokio::sync::watch::channel(None); + + let stream = session.attach(Empty {}).await?.into_inner(); + + let worker = ClientWorker { + callback: cb_rx, + pollers: Vec::new(), + poll_rx, + events: ev_tx, + }; + + let inner = Arc::new(ClientInner { user: Arc::new(resp.user.into()), workspaces: DashMap::default(), + poll_tx, + events: tokio::sync::Mutex::new(ev_rx), claims, auth, session, config, - }))) + callback: cb_tx, + }); + + let weak = Arc::downgrade(&inner); + let _t = tokio::spawn(async move { + worker.work(stream, weak).await; + }); + + Ok(Client(inner)) } /// Refresh session token. @@ -271,3 +297,96 @@ impl Client { &self.0.user } } + +impl AsyncReceiver for Client { + async fn try_recv(&self) -> crate::errors::ControllerResult> { + match self.0.events.lock().await.try_recv() { + Ok(x) => Ok(Some(x)), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => Ok(None), + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => Err(crate::errors::ControllerError::Stopped), + } + } + + async fn poll(&self) -> crate::errors::ControllerResult<()> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.0.poll_tx.send(tx)?; + Ok(rx.await?) + } + + fn clear_callback(&self) { + self.0.callback.send_replace(None); + } + + fn callback(&self, cb: impl Into>) { + self.0.callback.send_replace(Some(cb.into())); + } +} + +struct ClientWorker { + callback: tokio::sync::watch::Receiver>>, + pollers: Vec>, + poll_rx: tokio::sync::mpsc::UnboundedReceiver>, + events: tokio::sync::mpsc::UnboundedSender, +} + +impl ClientWorker { + #[tracing::instrument(skip(self, stream, weak))] + pub(crate) async fn work( + mut self, + mut stream: tonic::Streaming , + weak: std::sync::Weak , + ) { + tracing::debug!("client worker starting"); + loop { + tokio::select! { + res = self.poll_rx.recv() => match res { + None => break tracing::debug!("pollers channel closed: client has been dropped"), + Some(x) => self.pollers.push(x), + }, + + res = stream.message() => match res { + Err(e) => break tracing::error!("client stream closed: {e}"), + Ok(None) => break tracing::info!("closing client"), + Ok(Some(codemp_proto::session::SessionEvent { event: None })) => { + tracing::warn!("client received empty event") + } + Ok(Some(codemp_proto::session::SessionEvent { event: Some(ev) })) => { + let Some(_inner) = weak.upgrade() else { + break tracing::debug!("client worker clean exit"); + }; + tracing::debug!("received client event: {ev:?}"); + match ev.clone() { + codemp_proto::session::session_event::Event::Invite(invitation_event) => { + tracing::info!("got invited to workspace: {invitation_event:?}"); + }, + codemp_proto::session::session_event::Event::Leave(quit_event) => { + tracing::info!("user left workspace: {quit_event:?}"); + }, + codemp_proto::session::session_event::Event::Join(accept_event) => { + tracing::info!("user accepted invite: {accept_event:?}"); + }, + codemp_proto::session::session_event::Event::Reject(reject_event) => { + tracing::info!("user rejected invite: {reject_event:?}"); + }, + } + + if self.events.send(ev).is_err() { + tracing::warn!("no active controller to receive workspace event"); + } + self.pollers.drain(..).for_each(|x| { + x.send(()).unwrap_or_warn("poller dropped before completion"); + }); + if let Some(cb) = self.callback.borrow().as_ref() { + if let Some(ws) = weak.upgrade() { + cb.call(Client(ws)); + } else { + break tracing::debug!("workspace worker clean (late) exit"); + } + } + } + }, + } + } + tracing::debug!("workspace worker stopping"); + } +} From cb8217b558569e5162d69f5bf15abe3935020f4f Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 23:14:48 +0100 Subject: [PATCH 032/121] chore: methods take both String and &str, dont use WorkspaceIdentifier --- src/client.rs | 86 ++++++++++++++++++++++++++----------------- src/ffi/lua/client.rs | 38 ++++++++++++++++--- src/lib.rs | 1 + src/tests/fixtures.rs | 21 ++++------- src/workspace.rs | 45 +++++++++++----------- 5 files changed, 117 insertions(+), 74 deletions(-) diff --git a/src/client.rs b/src/client.rs index 54dd50cc..7464fd05 100644 --- a/src/client.rs +++ b/src/client.rs @@ -41,7 +41,7 @@ pub struct Client(Arc); struct ClientInner { user: Arc, config: crate::api::Config, - workspaces: DashMap, + workspaces: DashMap>, auth: AuthClient, session: SessionClient>, claims: InternallyMutable, @@ -119,64 +119,64 @@ impl Client { } /// Attempt to create a new workspace with given name. - pub async fn create_workspace(&self, name: String) -> RemoteResult<()> { + pub async fn create_workspace(&self, name: impl ToString) -> RemoteResult<()> { self.0 .session .clone() - .create_workspace(OwnedWorkspaceIdentifier { workspace: name }) + .create_workspace(OwnedWorkspaceIdentifier { workspace: name.to_string() }) .await? .into_inner(); Ok(()) } /// Delete an existing workspace if possible. - pub async fn delete_workspace(&self, name: String) -> RemoteResult<()> { + pub async fn delete_workspace(&self, name: impl ToString) -> RemoteResult<()> { self.0 .session .clone() - .delete_workspace(OwnedWorkspaceIdentifier { workspace: name }) + .delete_workspace(OwnedWorkspaceIdentifier { workspace: name.to_string() }) .await?; Ok(()) } /// Quit a joined workspace. Cannot quit owned workspaces: must delete them - pub async fn quit_workspace(&self, user: String, workspace: String) -> RemoteResult<()> { + pub async fn quit_workspace(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { self.0 .session .clone() - .quit_workspace(WorkspaceIdentifier { user, workspace }) + .quit_workspace(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) .await?; Ok(()) } /// Accept an invitation to a workspace, making it accessible - pub async fn accept_invite(&self, user: String, workspace: String) -> RemoteResult<()> { + pub async fn accept_invite(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { self.0 .session .clone() - .accept_invite(WorkspaceIdentifier { user, workspace }) + .accept_invite(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) .await?; Ok(()) } /// Reject an invitation to a workspace - pub async fn reject_invite(&self, user: String, workspace: String) -> RemoteResult<()> { + pub async fn reject_invite(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { self.0 .session .clone() - .reject_invite(WorkspaceIdentifier { user, workspace }) + .reject_invite(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) .await?; Ok(()) } /// Invite user with given username to the given workspace, if possible. - pub async fn invite_to_workspace(&self, workspace_name: String, user_name: String) -> RemoteResult<()> { + pub async fn invite_to_workspace(&self, workspace_name: impl ToString, user_name: impl ToString) -> RemoteResult<()> { self.0 .session .clone() .invite_to_workspace(InviteRequest { - workspace: workspace_name, - user: user_name, + workspace: workspace_name.to_string(), + user: user_name.to_string(), }) .await?; Ok(()) @@ -212,42 +212,56 @@ impl Client { .collect()) } - pub async fn get_user_info(&self, user: String) -> RemoteResult { + pub async fn get_user_info(&self, user: impl ToString) -> RemoteResult { Ok( self.0 .session .clone() - .get_user_info(UserId { user }) + .get_user_info(UserId { user: user.to_string() }) .await? .into_inner() ) } /// Join and return a [`Workspace`]. - #[tracing::instrument(skip(self, workspace), fields(ws = %workspace))] - pub async fn attach_workspace(&self, workspace: crate::api::WorkspaceIdentifier) -> ConnectionResult { + #[tracing::instrument(skip(self, user, workspace), fields(owner = user.to_string(), ws = workspace.to_string()))] + pub async fn attach_workspace(&self, user: impl ToString, workspace: impl ToString) -> ConnectionResult { + let workspace_id = crate::api::WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }; + let user = user.to_string(); + let workspace = workspace.to_string(); let mut session_client = self.0.session.clone(); let token = session_client - .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(workspace.clone())) + .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(workspace_id.clone())) .await? .into_inner(); let workspace_claims = InternallyMutable::new(token); let ws = Workspace::connect( - workspace.clone(), + workspace_id.clone(), self.0.user.clone(), self.0.config.clone(), workspace_claims.channel(), self.0.claims.channel(), ) .await?; - self.0.workspaces.insert(workspace.clone(), ws.clone()); + + match self.0.workspaces.get_mut(&user) { + Some(mutref) => { + mutref.insert(workspace.clone(), ws.clone()); + }, + None => { + let map = DashMap::default(); + map.insert(workspace.clone(), ws.clone()); + self.0.workspaces.insert(user.clone(), map); + }, + }; + let mut workspace_client = ws.services().ws(); let weak = Arc::downgrade(&ws.0); tokio::spawn(async move { - let _workspace = workspace.clone(); + let _workspace = workspace_id.clone(); let fut = async move { loop { // TODO either configurable token refresh time or calculate depending on token lifetime @@ -271,25 +285,31 @@ impl Client { } /// Leave the [`Workspace`] with the given name. - pub fn leave_workspace(&self, id: &crate::api::WorkspaceIdentifier) -> bool { - match self.0.workspaces.remove(id) { - None => true, - Some(x) => x.1.consume(), + pub fn leave_workspace(&self, user: impl AsRef, workspace: impl AsRef) -> bool { + if let Some(wss) = self.0.workspaces.get_mut(user.as_ref()) { + if wss.remove(workspace.as_ref()).is_some() { + return true; + } } + + false } /// Gets a [`Workspace`] handle by name. - pub fn get_workspace(&self, id: &crate::api::WorkspaceIdentifier) -> Option { - self.0.workspaces.get(id).map(|x| x.clone()) + pub fn get_workspace(&self, user: impl AsRef, workspace: impl AsRef) -> Option { + self.0.workspaces.get(user.as_ref())?.get(workspace.as_ref()).map(|x| x.clone()) } /// Get the names of all active [`Workspace`]s. + // TODO get rid of WorkspaceIdentifier pub fn active_workspaces(&self) -> Vec { - self.0 - .workspaces - .iter() - .map(|x| x.value().id().clone()) - .collect() + let mut out = Vec::new(); + for wss in self.0.workspaces.iter() { + for ws in wss.value().iter() { + out.push(ws.value().id().clone()); + } + } + out } /// Get the currently logged in user. diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index d1116306..d4e016ce 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -22,8 +22,7 @@ impl LuaUserData for CodempClient { methods.add_method( "attach_workspace", |_, this, (user, workspace): (String,String)| { - let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; - a_sync! { this => this.attach_workspace(ws_id).await? } + a_sync! { this => this.attach_workspace(user, workspace).await? } }, ); @@ -73,17 +72,44 @@ impl LuaUserData for CodempClient { ); methods.add_method("leave_workspace", |_, this, (user, workspace): (String,String)| { - let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; - Ok(this.leave_workspace(&ws_id)) + Ok(this.leave_workspace(user, workspace)) }); methods.add_method("get_workspace", |_, this, (user, workspace): (String,String)| { - let ws_id = crate::api::WorkspaceIdentifier { user, workspace }; - Ok(this.get_workspace(&ws_id)) + Ok(this.get_workspace(user, workspace)) }); methods.add_method("get_user_info", |_, this, (user,):(String,)| a_sync! { this => crate::api::UserInfo::from(this.get_user_info(user).await?) }); + + // TODO need to derive ser/de on Event, but this is in protobuf... + // methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? }); + + // methods.add_method( + // "try_recv", + // |_, this, ()| a_sync! { this => this.try_recv().await? }, + // ); + + methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? }); + + methods.add_method("callback", |lua, this, (cb,): (LuaFunction,)| { + let key = this.lua_callback_id(); + lua.set_named_registry_value(&key, cb)?; + Ok(this.callback(move |controller: CodempClient| { + super::ext::callback().invoke(key.clone(), controller, false) + })) + }); + + methods.add_method("clear_callback", |lua, this, ()| { + this.clear_callback(); + lua.unset_named_registry_value(&this.lua_callback_id()) + }); + } +} + +impl CodempClient { + fn lua_callback_id(&self) -> String { + format!("codemp-client({})-callback-registry", self.current_user().name) } } diff --git a/src/lib.rs b/src/lib.rs index 7177d8fc..4b1232c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,6 +125,7 @@ pub mod ext; /// language-specific ffi "glue" pub mod ffi; +/// end-to-end tests, useful to assert server compliance #[cfg(any(feature = "test-e2e", test))] pub mod tests; diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 65095187..5a78a4f1 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -4,11 +4,6 @@ fn new_ws_id() -> String { format!("ws-test-{}", uuid::Uuid::new_v4()) } -#[deprecated = "need to rethink this API"] -fn ws_id(user: String, workspace: String) -> crate::api::WorkspaceIdentifier { - crate::api::WorkspaceIdentifier { user, workspace } -} - #[allow(async_fn_in_trait)] pub trait ScopedFixture { async fn setup(&mut self) -> Result>; @@ -119,13 +114,13 @@ impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { let client = ClientFixture::of(&self.user).setup().await?; client.create_workspace(self.workspace.to_string()).await?; - let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; + let workspace = client.attach_workspace(&self.user, &self.workspace).await?; Ok((client, workspace)) } async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { if let Some((client, workspace)) = resource { - client.leave_workspace(workspace.id()); + client.leave_workspace(&client.current_user().name, &workspace.id().workspace); if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } @@ -165,8 +160,8 @@ impl client .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; - let invitee_workspace = invitee_client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; + let workspace = client.attach_workspace(&self.user, &self.workspace).await?; + let invitee_workspace = invitee_client.attach_workspace(&self.user, &self.workspace).await?; Ok((client, workspace, invitee_client, invitee_workspace)) } @@ -180,7 +175,7 @@ impl )>, ) { if let Some((client, ws, _, _)) = resource { - client.leave_workspace(ws.id()); + client.leave_workspace(&client.current_user().name, &ws.id().workspace); if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } @@ -261,11 +256,11 @@ impl .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) .await?; - let workspace = client.attach_workspace(ws_id(self.user.clone(), self.workspace.clone())).await?; + let workspace = client.attach_workspace(&self.user, &self.workspace).await?; workspace.create_buffer(self.buffer.to_string(), false).await?; let buffer = workspace.attach_buffer(self.buffer.clone()).await?; - let invitee_workspace = invitee_client.attach_workspace(ws_id(self.user.clone(),self.workspace.clone())).await?; + let invitee_workspace = invitee_client.attach_workspace(&self.user, &self.workspace).await?; let invitee_buffer = invitee_workspace.attach_buffer(self.buffer.clone()).await?; Ok(( @@ -291,7 +286,7 @@ impl ) { if let Some((client, ws, _, _, _, _)) = resource { // buffer deletion is implied in workspace deletion - client.leave_workspace(ws.id()); + client.leave_workspace(&client.current_user().name, &ws.id().workspace); if let Err(e) = client.delete_workspace(self.workspace.clone()).await { eprintln!("could not delete workspace: {e}"); } diff --git a/src/workspace.rs b/src/workspace.rs index ddd70f41..5c1ca1af 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -158,43 +158,44 @@ impl Workspace { } /// Create a new buffer in the current workspace. - pub async fn create_buffer(&self, path: String, ephemeral: bool) -> RemoteResult<()> { + pub async fn create_buffer(&self, path: impl ToString, ephemeral: bool) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); workspace_client .create_buffer(tonic::Request::new(BufferNode { - path: path.clone().into(), + path: path.to_string().into(), ephemeral, })) .await?; // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( - path.clone(), - crate::api::BufferNode { path, ephemeral }, + path.to_string(), + crate::api::BufferNode { path: path.to_string(), ephemeral }, ); Ok(()) } - pub async fn pin_buffer(&self, path: String) -> RemoteResult<()> { + pub async fn pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { self.0.services.ws() .clone() - .pin_buffer(BufferPath::from(path)) + .pin_buffer(BufferPath::from(path.as_ref())) .await?; Ok(()) } - pub async fn un_pin_buffer(&self, path: String) -> RemoteResult<()> { + pub async fn un_pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { self.0.services.ws() .clone() - .un_pin_buffer(BufferPath::from(path)) + .un_pin_buffer(BufferPath::from(path.as_ref())) .await?; Ok(()) } /// Attach to a buffer and return a handle to it. - #[tracing::instrument(skip(self))] - pub async fn attach_buffer(&self, path: String) -> ConnectionResult { + #[tracing::instrument(skip(self, path), fields(path = path.to_string()))] + pub async fn attach_buffer(&self, path: impl ToString) -> ConnectionResult { + let path = path.to_string(); let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); let credentials = workspace_client.get_buffer_token(BufferPath::from(&path)).await?.into_inner(); @@ -244,8 +245,8 @@ impl Workspace { /// a dangling reference somewhere. It may just be waiting for garbage collection, but as long /// as it exists, it will prevent the controller from being completely dropped. #[allow(clippy::redundant_pattern_matching)] // all cases are clearer this way - pub fn detach_buffer(&self, path: &str) -> bool { - match self.0.buffers.remove(path) { + pub fn detach_buffer(&self, path: impl AsRef) -> bool { + match self.0.buffers.remove(path.as_ref()) { None => true, // noop: we werent attached in the first place Some((_name, controller)) => match Arc::into_inner(controller.0) { None => false, // dangling ref! we can't drop this @@ -290,7 +291,8 @@ impl Workspace { } /// Fetch a list of the [User]s attached to a specific buffer. - pub async fn fetch_buffer_users(&self, path: String) -> RemoteResult<()> { + pub async fn fetch_buffer_users(&self, path: impl ToString) -> RemoteResult<()> { + let path = path.to_string(); let resp = self.services().ws() .fetch_buffer_users(BufferPath::from(&path)) .await? @@ -302,16 +304,15 @@ impl Workspace { } /// Delete a buffer. - pub async fn delete_buffer(&self, path: String) -> RemoteResult<()> { - self.detach_buffer(&path); // just in case + pub async fn delete_buffer(&self, path: impl AsRef) -> RemoteResult<()> { + self.detach_buffer(path.as_ref()); // just in case let mut workspace_client = self.0.services.ws(); workspace_client - .delete_buffer(BufferPath::from(&path)) + .delete_buffer(BufferPath::from(path.as_ref())) .await?; - // TODO may deadlock! how fun.... - self.0.filetree.remove(&path); + self.0.filetree.remove(path.as_ref()); Ok(()) } @@ -330,8 +331,8 @@ impl Workspace { /// Return a handle to the [buffer::Controller] with the given path, if present. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 - pub fn get_buffer(&self, path: &str) -> Option { - self.0.buffers.get(path).map(|x| x.clone()) + pub fn get_buffer(&self, path: impl AsRef) -> Option { + self.0.buffers.get(path.as_ref()).map(|x| x.clone()) } /// Get a list of all the currently attached buffers. @@ -354,9 +355,9 @@ impl Workspace { } /// Get all users currently attached to specified buffer - pub fn buffer_user_list(&self, path: &str) -> Vec { + pub fn buffer_user_list(&self, path: impl AsRef) -> Vec { let mut out = Vec::new(); - if let Some(buf_ref) = self.0.buffer_users.get(path) { + if let Some(buf_ref) = self.0.buffer_users.get(path.as_ref()) { for uid in buf_ref.value() { if let Some(user_ref) = self.0.users.get(uid) { out.push(user_ref.value().clone()); From 53607cf5d83d38e46764ae234e44d728ddb5dffe Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 23:34:33 +0100 Subject: [PATCH 033/121] test: fix doc tests --- src/ffi/mod.rs | 6 +++--- src/lib.rs | 8 ++++---- src/tests/mod.rs | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index c594ee7b..116f0c6e 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -14,10 +14,10 @@ //! //! // create and join a workspace //! client.create_workspace("some-workspace").await?; -//! let workspace = client.attach_workspace("some-workspace").await?; +//! let workspace = client.attach_workspace("my-username", "some-workspace").await?; //! //! // create a new buffer in this workspace and attach to it -//! workspace.create_buffer("/my/file.txt").await?; +//! workspace.create_buffer("/my/file.txt", false).await?; //! let buffer = workspace.attach_buffer("/my/file.txt").await?; //! //! // write `hello!` at the beginning of this buffer @@ -29,7 +29,7 @@ //! // wait for cursor movements //! loop { //! let event = workspace.cursor().recv().await?; -//! println!("user {} moved on buffer {}", event.user, event.sel.buffer); +//! println!("user {} moved on buffer {}", event.user, event.cursor.buffer); //! } //! # Ok::<(),Box>(()) //! # }; diff --git a/src/lib.rs b/src/lib.rs index 4b1232c6..891b4fe8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ //! # async { //! # let client = codemp::Client::connect(codemp::api::Config::new("", "")).await.unwrap(); //! client.create_workspace("my-workspace").await.expect("failed to create workspace!"); -//! let workspace = client.attach_workspace("my-workspace").await.expect("failed to attach!"); +//! let workspace = client.attach_workspace("my-user", "my-workspace").await.expect("failed to attach!"); //! # }; //! ``` //! @@ -49,11 +49,11 @@ //! # async { //! # let client = codemp::Client::connect(codemp::api::Config::new("", "")).await.unwrap(); //! # client.create_workspace("").await.unwrap(); -//! # let workspace = client.attach_workspace("").await.unwrap(); +//! # let workspace = client.attach_workspace("", "").await.unwrap(); //! use codemp::api::controller::{AsyncSender, AsyncReceiver}; // needed to access trait methods //! let cursor = workspace.cursor(); //! let event = cursor.recv().await.expect("disconnected while waiting for event!"); -//! println!("user {} moved on buffer {}", event.user, event.sel.buffer); +//! println!("user {} moved on buffer {}", event.user, event.cursor.buffer); //! # }; //! ``` //! @@ -65,7 +65,7 @@ //! # async { //! # let client = codemp::Client::connect(codemp::api::Config::new("", "")).await.unwrap(); //! # client.create_workspace("").await.unwrap(); -//! # let workspace = client.attach_workspace("").await.unwrap(); +//! # let workspace = client.attach_workspace("", "").await.unwrap(); //! # use codemp::api::controller::{AsyncSender, AsyncReceiver}; //! let buffer = workspace.attach_buffer("/some/file.txt").await.expect("failed to attach"); //! buffer.content(); // force-sync diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 63496521..d6a17c64 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] // TODO need a better solution + #[cfg(all(test, feature = "test-e2e"))] mod client; From 6fdb9140836e5ac5d270018ccb56a53dcbec4fde Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 23:44:44 +0100 Subject: [PATCH 034/121] test: fix e2e tests (maybe?) --- src/tests/client.rs | 30 ++++++++++++------------------ src/tests/server.rs | 8 ++++---- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/tests/client.rs b/src/tests/client.rs index e63d1f13..9b9cba4a 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -38,12 +38,11 @@ async fn test_attach_and_leave_workspace() { let workspace_name = uuid::Uuid::new_v4().to_string(); client.create_workspace(workspace_name.clone()).await?; - let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name.clone() }; // leaving a workspace you are not attached to, returns true - let leave_workspace_before = client.leave_workspace(&wsid); + let leave_workspace_before = client.leave_workspace(&client.current_user().name, &workspace_name); - let attach_workspace_that_exists = match client.attach_workspace(wsid.clone()).await { + let attach_workspace_that_exists = match client.attach_workspace(&client.current_user().name, &workspace_name).await { Ok(_) => true, Err(e) => { eprintln!("error attaching to workspace: {e}"); @@ -53,7 +52,7 @@ async fn test_attach_and_leave_workspace() { // leaving a workspace you are attached to, returns true // when there is only one reference to it. - let leave_workspace_after = client.leave_workspace(&wsid); + let leave_workspace_after = client.leave_workspace(&client.current_user().name, &workspace_name); let _ = client.delete_workspace(workspace_name).await; @@ -103,11 +102,8 @@ async fn test_invite_user_to_workspace() { async fn test_workspace_lookup() { super::fixture! { WorkspaceFixture::one("alice") => |client, workspace| { - assert_or_err!(client.get_workspace(workspace.id()).is_some()); - assert_or_err!(client.get_workspace(&crate::api::WorkspaceIdentifier { - user: uuid::Uuid::new_v4().to_string(), - workspace: uuid::Uuid::new_v4().to_string(), - }).is_none()); + assert_or_err!(client.get_workspace(&workspace.id().user, &workspace.id().workspace).is_some()); + assert_or_err!(client.get_workspace("asd", "dsa").is_none()); Ok(()) } } @@ -117,7 +113,7 @@ async fn test_workspace_lookup() { async fn test_leave_workspace_with_dangling_ref() { super::fixture! { WorkspaceFixture::one("alice") => |client, workspace| { - assert_or_err!(client.leave_workspace(workspace.id()) == false); + assert_or_err!(client.leave_workspace(&workspace.id().user, &workspace.id().workspace) == false); Ok(()) } } @@ -127,8 +123,8 @@ async fn test_leave_workspace_with_dangling_ref() { async fn test_lookup_after_leave() { super::fixture! { WorkspaceFixture::one("alice") => |client, workspace| { - client.leave_workspace(workspace.id()); - assert_or_err!(client.get_workspace(workspace.id()).is_none()); + client.leave_workspace(&workspace.id().user, &workspace.id().workspace); + assert_or_err!(client.get_workspace(&workspace.id().user, &workspace.id().workspace).is_none()); Ok(()) } } @@ -140,14 +136,13 @@ async fn test_attach_after_leave() { ClientFixture::of("alice") => |client| { let ws_name = uuid::Uuid::new_v4().to_string(); client.create_workspace(ws_name.clone()).await?; - let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: ws_name.clone() }; - let could_attach = client.attach_workspace(wsid.clone()).await.is_ok(); - let clean_leave = client.leave_workspace(&wsid); + let could_attach = client.attach_workspace(&client.current_user().name, &ws_name).await.is_ok(); + let clean_leave = client.leave_workspace(&client.current_user().name, &ws_name); // TODO this is very server specific! disconnect may be instant or caught with next // keepalive, let's arbitrarily say that after 20 seconds we should have been disconnected tokio::time::sleep(std::time::Duration::from_secs(20)).await; - let could_attach_again = client.attach_workspace(wsid.clone()).await; + let could_attach_again = client.attach_workspace(&client.current_user().name, &ws_name).await; let could_delete = client.delete_workspace(ws_name).await; assert_or_err!(could_attach); @@ -185,10 +180,9 @@ async fn test_attaching_to_non_existing_is_error() { super::fixture! { ClientFixture::of("alice") => |client| { let workspace_name = uuid::Uuid::new_v4().to_string(); - let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name }; // we don't create any workspace. - assert_or_err!(client.attach_workspace(wsid).await.is_err()); + assert_or_err!(client.attach_workspace(&client.current_user().name, workspace_name).await.is_err()); Ok(()) } } diff --git a/src/tests/server.rs b/src/tests/server.rs index 2f5c95ef..88ee5623 100644 --- a/src/tests/server.rs +++ b/src/tests/server.rs @@ -68,13 +68,13 @@ async fn test_workspace_interactions() { client_alice.create_workspace(workspace_name.clone()).await?; let owned_workspaces = client_alice.fetch_owned_workspaces().await?; assert_or_err!(owned_workspaces.contains(&wsid)); - client_alice.attach_workspace(wsid.clone()).await?; + client_alice.attach_workspace(&client_alice.current_user().name, &workspace_name).await?; assert_or_err!(vec![wsid.clone()] == client_alice.active_workspaces()); client_alice .invite_to_workspace(workspace_name.clone(), client_bob.current_user().name.clone()) .await?; - client_bob.attach_workspace(wsid.clone()).await?; + client_bob.attach_workspace(&client_alice.current_user().name, &workspace_name).await?; assert_or_err!( client_bob .fetch_joined_workspaces() @@ -82,8 +82,8 @@ async fn test_workspace_interactions() { .contains(&wsid) ); - assert_or_err!(client_bob.leave_workspace(&wsid)); - assert_or_err!(client_alice.leave_workspace(&wsid)); + assert_or_err!(client_bob.leave_workspace(&client_alice.current_user().name, &workspace_name)); + assert_or_err!(client_alice.leave_workspace(&client_alice.current_user().name, &workspace_name)); client_alice.delete_workspace(workspace_name.clone()).await?; From 89979c55ab76e87de3de1817a54be3229acc959c Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 6 Mar 2026 23:46:41 +0100 Subject: [PATCH 035/121] ci: run functional tests last --- .github/workflows/test.yml | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d3d55e8..25174fc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,24 +49,8 @@ jobs: toolchain: ${{ matrix.toolchain }} - run: cargo test --verbose - test-functional: - needs: [test-unit] - runs-on: ubuntu-latest - steps: - - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test --verbose --features=test-e2e - env: - CODEMP_TEST_USERNAME_ALICE: ${{ secrets.CODEMP_TEST_USERNAME_ALICE }} - CODEMP_TEST_PASSWORD_ALICE: ${{ secrets.CODEMP_TEST_PASSWORD_ALICE }} - CODEMP_TEST_USERNAME_BOB: ${{ secrets.CODEMP_TEST_USERNAME_BOB }} - CODEMP_TEST_PASSWORD_BOB: ${{ secrets.CODEMP_TEST_PASSWORD_BOB }} - test-build: - needs: [test-functional] + needs: [test-unit] runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -87,3 +71,20 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build --release --verbose --features=${{ matrix.features }} + + test-functional: + needs: [test-build] + runs-on: ubuntu-latest + steps: + - uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --verbose --features=test-e2e + env: + CODEMP_TEST_USERNAME_ALICE: ${{ secrets.CODEMP_TEST_USERNAME_ALICE }} + CODEMP_TEST_PASSWORD_ALICE: ${{ secrets.CODEMP_TEST_PASSWORD_ALICE }} + CODEMP_TEST_USERNAME_BOB: ${{ secrets.CODEMP_TEST_USERNAME_BOB }} + CODEMP_TEST_PASSWORD_BOB: ${{ secrets.CODEMP_TEST_PASSWORD_BOB }} + From 9c6572e51006fbcce3b888e9cdc7974a68051822 Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 5 Mar 2026 22:13:02 +0100 Subject: [PATCH 036/121] chore: pyo3 implement 0.25 to 0.26 changes --- src/ffi/python/client.rs | 18 +++++++++--------- src/ffi/python/controllers.rs | 24 +++++++++++------------ src/ffi/python/mod.rs | 36 +++++++++++++++++------------------ src/ffi/python/workspace.rs | 24 +++++++++++------------ 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index fa28bbb8..5394e849 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -1,5 +1,5 @@ use super::Client; -use super::a_sync_allow_threads; +use super::a_sync_detach; use crate::api::User; use crate::workspace::Workspace; use pyo3::prelude::*; @@ -20,13 +20,13 @@ impl Client { fn pyattach_workspace(&self, py: Python<'_>, workspace: Uuid) -> PyResult { tracing::info!("attempting to join the workspace {}", workspace); let this = self.clone(); - a_sync_allow_threads!(py, this.attach_workspace(workspace).await) + a_sync_detach!(py, this.attach_workspace(workspace).await) // let this = self.clone(); // Ok(super::Promise(Some(tokio().spawn(async move { // Ok(this // .join_workspace(workspace) // .await - // .map(|f| Python::with_gil(|py| f.into_py(py)))?) + // .map(|f| Python::attach(|py| f.into_py(py)))?) // })))) } @@ -34,14 +34,14 @@ impl Client { fn pycreate_workspace(&self, py: Python<'_>, workspace: String) -> PyResult { tracing::info!("creating workspace {}", workspace); let this = self.clone(); - a_sync_allow_threads!(py, this.create_workspace(workspace).await) + a_sync_detach!(py, this.create_workspace(workspace).await) } #[pyo3(name = "delete_workspace")] fn pydelete_workspace(&self, py: Python<'_>, workspace: String) -> PyResult { tracing::info!("deleting workspace {}", workspace); let this = self.clone(); - a_sync_allow_threads!(py, this.delete_workspace(workspace).await) + a_sync_detach!(py, this.delete_workspace(workspace).await) } #[pyo3(name = "invite_to_workspace")] @@ -53,21 +53,21 @@ impl Client { ) -> PyResult { tracing::info!("inviting {user} to workspace {workspace}"); let this = self.clone(); - a_sync_allow_threads!(py, this.invite_to_workspace(workspace, user).await) + a_sync_detach!(py, this.invite_to_workspace(workspace, user).await) } #[pyo3(name = "fetch_owned_workspaces")] fn pyfetch_owned_workspaces(&self, py: Python<'_>) -> PyResult { tracing::info!("fetching owned workspaces"); let this = self.clone(); - a_sync_allow_threads!(py, this.fetch_owned_workspaces().await) + a_sync_detach!(py, this.fetch_owned_workspaces().await) } #[pyo3(name = "fetch_joined_workspaces")] fn pyfetch_joined_workspaces(&self, py: Python<'_>) -> PyResult { tracing::info!("fetching joined workspaces"); let this = self.clone(); - a_sync_allow_threads!(py, this.fetch_joined_workspaces().await) + a_sync_detach!(py, this.fetch_joined_workspaces().await) } #[pyo3(name = "leave_workspace")] @@ -95,6 +95,6 @@ impl Client { fn pyrefresh(&self, py: Python<'_>) -> PyResult { tracing::info!("attempting to refresh token"); let this = self.clone(); - a_sync_allow_threads!(py, this.refresh().await) + a_sync_detach!(py, this.refresh().await) } } diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index 848ccf2b..28bd5d38 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -7,7 +7,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use super::Promise; -use super::a_sync_allow_threads; +use super::a_sync_detach; // need to do manually since Controller is a trait implementation #[pymethods] @@ -21,29 +21,29 @@ impl CursorController { #[pyo3(name = "try_recv")] fn pytry_recv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.try_recv().await) + a_sync_detach!(py, this.try_recv().await) } #[pyo3(name = "recv")] fn pyrecv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.recv().await) + a_sync_detach!(py, this.recv().await) } #[pyo3(name = "poll")] fn pypoll(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.poll().await) + a_sync_detach!(py, this.poll().await) } #[pyo3(name = "callback")] - fn pycallback(&self, py: Python, cb: PyObject) -> PyResult<()> { + fn pycallback(&self, py: Python, cb: Py) -> PyResult<()> { if !cb.bind_borrowed(py).is_callable() { return Err(PyValueError::new_err("The object passed must be callable.")); } self.callback(move |ctl| { - Python::with_gil(|py| { + Python::attach(|py| { // TODO what to do with this error? let _ = cb.call1(py, (ctl,)); }) @@ -68,7 +68,7 @@ impl BufferController { #[pyo3(name = "content")] fn pycontent(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.content().await) + a_sync_detach!(py, this.content().await) } #[pyo3(name = "ack")] @@ -86,29 +86,29 @@ impl BufferController { #[pyo3(name = "try_recv")] fn pytry_recv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.try_recv().await) + a_sync_detach!(py, this.try_recv().await) } #[pyo3(name = "recv")] fn pyrecv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.recv().await) + a_sync_detach!(py, this.recv().await) } #[pyo3(name = "poll")] fn pypoll(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.poll().await) + a_sync_detach!(py, this.poll().await) } #[pyo3(name = "callback")] - fn pycallback(&self, py: Python, cb: PyObject) -> PyResult<()> { + fn pycallback(&self, py: Python, cb: Py) -> PyResult<()> { if !cb.bind_borrowed(py).is_callable() { return Err(PyValueError::new_err("The object passed must be callable.")); } self.callback(move |ctl| { - Python::with_gil(|py| { + Python::attach(|py| { // TODO what to do with this error? let _ = cb.call1(py, (ctl,)); }) diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 86563808..1af635d4 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -30,17 +30,17 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { } // #[pyfunction] -// fn register_event_loop(event_loop: PyObject) { -// static EVENT_LOOP: OnceLock = OnceLock::new(); +// fn register_event_loop(event_loop: Py) { +// static EVENT_LOOP: OnceLock> = OnceLock::new(); // EVENT_LOOP. // } // #[pyfunction] // fn setup_async( -// event_loop: PyObject, -// call_soon_thread_safe: PyObject, // asyncio.EventLoop.call_soon_threadsafe -// call_coroutine_thread_safe: PyObject, // asyncio.call_coroutine_threadsafe -// create_future: PyObject, // asyncio.EventLoop.create_future +// event_loop: Py, +// call_soon_thread_safe: Py, // asyncio.EventLoop.call_soon_threadsafe +// call_coroutine_thread_safe: Py, // asyncio.call_coroutine_threadsafe +// create_future: Py, // asyncio.EventLoop.create_future // ) { // let _ = EVENT_LOOP.get_or_init(|| event_loop); // let _ = CALL_SOON.get_or_init(|| call_soon_thread_safe); @@ -49,15 +49,15 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { // } #[pyclass] -pub struct Promise(Option>>); +pub struct Promise(Option>>>); #[pymethods] impl Promise { // Can't use this in callbacks since tokio will complain about running // a runtime inside another runtime. #[pyo3(name = "wait")] - fn _await(&mut self, py: Python<'_>) -> PyResult { - py.allow_threads(move || match self.0.take() { + fn _await(&mut self, py: Python<'_>) -> PyResult> { + py.detach(move || match self.0.take() { None => Err(PyRuntimeError::new_err( "promise can't be awaited multiple times!", )), @@ -71,7 +71,7 @@ impl Promise { } fn done(&self, py: Python<'_>) -> PyResult { - py.allow_threads(|| { + py.detach(|| { if let Some(handle) = &self.0 { Ok(handle.is_finished()) } else { @@ -86,26 +86,26 @@ macro_rules! a_sync { Ok($crate::ffi::python::Promise(Some( $crate::ffi::python::tokio().spawn(async move { let res = $x?; - Python::with_gil(|py| Ok(res.into_pyobject(py)?.into_any().unbind())) + Python::attach(|py| Ok(res.into_pyobject(py)?.into_any().unbind())) }), ))) }}; } //pub(crate) use a_sync; -macro_rules! a_sync_allow_threads { +macro_rules! a_sync_detach { ($py:ident, $x:expr) => {{ - $py.allow_threads(move || { + $py.detach(move || { Ok($crate::ffi::python::Promise(Some( $crate::ffi::python::tokio().spawn(async move { let res = $x?; - Python::with_gil(|gil| Ok(res.into_pyobject(gil)?.into_any().unbind())) + Python::attach(|gil| Ok(res.into_pyobject(gil)?.into_any().unbind())) }), ))) }) }}; } -pub(crate) use a_sync_allow_threads; +pub(crate) use a_sync_detach; #[derive(Debug, Clone)] struct LoggerProducer(mpsc::UnboundedSender); @@ -307,14 +307,14 @@ fn connect(py: Python, config: Py) -> PyResult { Ok(Promise(Some(crate::ffi::python::tokio().spawn( async move { let client = Client::connect(conf).await?; - Python::with_gil(|py| Ok(client.into_pyobject(py)?.into_any().unbind())) + Python::attach(|py| Ok(client.into_pyobject(py)?.into_any().unbind())) }, )))) // a_sync!(Client::connect(conf).await) } #[pyfunction] -fn set_logger(py: Python, logging_cb: PyObject, debug: bool) -> bool { +fn set_logger(py: Python, logging_cb: Py, debug: bool) -> bool { if !logging_cb.bind_borrowed(py).is_callable() { return false; } @@ -347,7 +347,7 @@ fn set_logger(py: Python, logging_cb: PyObject, debug: bool) -> bool { if log_subscribed { tokio().spawn(async move { while let Some(msg) = rx.recv().await { - let _ = Python::with_gil(|py| logging_cb.call1(py, (msg,))); + let _ = Python::attach(|py| logging_cb.call1(py, (msg,))); } }); } diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index bbd040bb..05568f05 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -7,7 +7,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use super::Promise; -use super::a_sync_allow_threads; +use super::a_sync_detach; #[pymethods] impl Workspace { @@ -15,13 +15,13 @@ impl Workspace { #[pyo3(name = "create_buffer")] fn pycreate_buffer(&self, py: Python, path: String, ephemeral: bool) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.create_buffer(path.as_str(), ephemeral).await) + a_sync_detach!(py, this.create_buffer(path.as_str(), ephemeral).await) } #[pyo3(name = "attach_buffer")] fn pyattach_buffer(&self, py: Python, path: String) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.attach_buffer(path.as_str()).await) + a_sync_detach!(py, this.attach_buffer(path.as_str()).await) } #[pyo3(name = "detach_buffer")] @@ -32,26 +32,26 @@ impl Workspace { #[pyo3(name = "fetch_buffers")] fn pylist_buffers(&self, py: Python, filter: String) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.list_buffers(filter.as_str()).await) + a_sync_detach!(py, this.list_buffers(filter.as_str()).await) } #[pyo3(name = "fetch_users")] fn pylist_users(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.list_users().await) + a_sync_detach!(py, this.list_users().await) } #[pyo3(name = "fetch_buffer_users")] fn pylist_buffer_users(&self, py: Python, path: String) -> PyResult { // crate::Result> let this = self.clone(); - a_sync_allow_threads!(py, this.list_buffer_users(path.as_str()).await) + a_sync_detach!(py, this.list_buffer_users(path.as_str()).await) } #[pyo3(name = "delete_buffer")] fn pydelete_buffer(&self, py: Python, path: String) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.delete_buffer(path.as_str()).await) + a_sync_detach!(py, this.delete_buffer(path.as_str()).await) } #[pyo3(name = "id")] @@ -88,19 +88,19 @@ impl Workspace { #[pyo3(name = "recv")] fn pyrecv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.recv().await) + a_sync_detach!(py, this.recv().await) } #[pyo3(name = "try_recv")] fn pytry_recv(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.try_recv().await) + a_sync_detach!(py, this.try_recv().await) } #[pyo3(name = "poll")] fn pypoll(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_allow_threads!(py, this.poll().await) + a_sync_detach!(py, this.poll().await) } #[pyo3(name = "clear_callback")] @@ -109,13 +109,13 @@ impl Workspace { } #[pyo3(name = "callback")] - fn pycallback(&self, py: Python, cb: PyObject) -> PyResult<()> { + fn pycallback(&self, py: Python, cb: Py) -> PyResult<()> { if !cb.bind_borrowed(py).is_callable() { return Err(PyValueError::new_err("The object passed must be callable.")); } self.callback(move |ws| { - Python::with_gil(|py| { + Python::attach(|py| { // TODO what to do with this error? let _ = cb.call1(py, (ws,)); }) From 386c4f74ae63f252f7f42e3c909bf5916f619d0f Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 5 Mar 2026 22:14:54 +0100 Subject: [PATCH 037/121] chore: pyo3 implement 0.26 to 0.28 changes --- src/api/buffer.rs | 2 +- src/api/change.rs | 4 ++-- src/api/config.rs | 2 +- src/api/cursor.rs | 7 +++--- src/api/event.rs | 10 ++++++++- src/api/user.rs | 2 +- src/api/workspace.rs | 2 +- src/buffer/controller.rs | 2 +- src/client.rs | 25 +++++++++++---------- src/cursor/controller.rs | 9 ++++---- src/ffi/python/mod.rs | 4 ++-- src/ffi/python/workspace.rs | 1 - src/workspace.rs | 45 ++++++++++++++++++++++++------------- 13 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/api/buffer.rs b/src/api/buffer.rs index 3a04fa79..9ce320e0 100644 --- a/src/api/buffer.rs +++ b/src/api/buffer.rs @@ -3,7 +3,7 @@ /// Represents a service buffer #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct BufferNode { /// Buffer path, sort of like a UNIX path. diff --git a/src/api/change.rs b/src/api/change.rs index ad1675d8..29488bc1 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -9,7 +9,7 @@ /// be provided every time. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyo3::pyclass(get_all))] +#[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct BufferUpdate { /// Optional content hash after applying this change. @@ -52,7 +52,7 @@ pub struct BufferUpdate { /// ``` #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyo3::pyclass(get_all))] +#[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. diff --git a/src/api/config.rs b/src/api/config.rs index 09e15707..da632010 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -10,7 +10,7 @@ /// http{tls?'s':''}://{host}:{port} #[derive(Clone, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all))] +#[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Config { /// User identifier used to register, possibly your email. diff --git a/src/api/cursor.rs b/src/api/cursor.rs index f2fda9d6..13866ffc 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -7,7 +7,7 @@ use pyo3::prelude::*; /// An event that occurred about a user's cursor. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all))] +#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] // #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] pub struct CursorEvent { @@ -17,11 +17,10 @@ pub struct CursorEvent { pub cursor: Cursor, } - /// An event that occurred about a user's cursor. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all))] +#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] // #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] pub struct Cursor { @@ -34,7 +33,7 @@ pub struct Cursor { /// A cursor selection span. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all))] +#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] // #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] pub struct Selection { diff --git a/src/api/event.rs b/src/api/event.rs index 487c14db..4d1c178b 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -6,7 +6,7 @@ use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner; /// Event in a [crate::Workspace]. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", serde(tag = "type"))] pub enum Event { @@ -33,6 +33,14 @@ impl From for Event { WorkspaceEventInner::Rename(e) => Self::FileTreeUpdated { path: e.after }, WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user, buffer: e.buffer }, WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user, buffer: e.buffer }, + WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { + name: e.user.name, + buffer: e.buffer, + }, + WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { + name: e.user.name, + buffer: e.buffer, + }, } } } diff --git a/src/api/user.rs b/src/api/user.rs index 1f67ecb5..89351a55 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -4,7 +4,7 @@ /// Represents a service user #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UserInfo { /// User name, unique and immutable diff --git a/src/api/workspace.rs b/src/api/workspace.rs index d12e779e..be111087 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -7,7 +7,7 @@ /// Represents a service workspace #[derive(Debug, Clone, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct WorkspaceIdentifier { /// Workspace name, cannot change and is unique per owner. diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index 2d7a1d79..a74bfe94 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -17,7 +17,7 @@ use crate::ext::IgnorableError; /// 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 = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "js", napi_derive::napi)] pub struct BufferController(pub(crate) Arc); diff --git a/src/client.rs b/src/client.rs index 7464fd05..70ffae1c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -34,7 +34,7 @@ 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 = "py", pyclass)] +#[cfg_attr(feature = "py", pyclass(from_py_object))] pub struct Client(Arc); #[derive(Debug)] @@ -267,11 +267,15 @@ impl Client { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(240)).await; if weak.upgrade().is_none() { break }; - let new_credentials = session_client.get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(_workspace.clone())) + let new_credentials = session_client.get_workspace_token( + tonic::Request::new(WorkspaceRequest { id: Identifier::from(workspace) }) + ) .await? .into_inner(); workspace_claims.set(new_credentials); - workspace_client.keep_alive(tonic::Request::new(Empty {})).await?; + workspace_client + .keep_alive(tonic::Request::new(Empty {})) + .await?; } Ok::<(), tonic::Status>(()) }; @@ -301,15 +305,12 @@ impl Client { } /// Get the names of all active [`Workspace`]s. - // TODO get rid of WorkspaceIdentifier - pub fn active_workspaces(&self) -> Vec { - let mut out = Vec::new(); - for wss in self.0.workspaces.iter() { - for ws in wss.value().iter() { - out.push(ws.value().id().clone()); - } - } - out + pub fn active_workspaces(&self) -> Vec { + self.0 + .workspaces + .iter() + .map(|x| *x.key()) + .collect() } /// Get the currently logged in user. diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 74ccf976..3ee2ea6e 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -17,7 +17,7 @@ use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol, cursor_client:: /// /// An unique [CursorController] exists for each active [crate::Workspace]. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "js", napi_derive::napi)] pub struct CursorController(pub(crate) Arc); @@ -54,7 +54,8 @@ impl AsyncSender for CursorController { Ok(self.0.op.send(CursorUpdate { buffer: cursor.buffer, - cursors: cursor.sel + cursors: cursor + .sel .into_iter() .map(|x| CursorPosition { start: RowCol { @@ -64,9 +65,9 @@ impl AsyncSender for CursorController { end: RowCol { row: x.end_row, col: x.end_col, - } + }, }) - .collect() + .collect(), })?) } } diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 1af635d4..34fcc151 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -48,7 +48,7 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { // let _ = CREATE_FUTURE.get_or_init(|| create_future); // } -#[pyclass] +#[pyclass(from_py_object)] pub struct Promise(Option>>>); #[pymethods] @@ -121,7 +121,7 @@ impl std::io::Write for LoggerProducer { } } -#[pyclass] +#[pyclass(from_py_object) pub struct Driver(Option>); #[pymethods] impl Driver { diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index 05568f05..a4e63593 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -63,7 +63,6 @@ impl Workspace { fn pycursor(&self) -> CursorController { self.cursor() } - #[pyo3(name = "get_buffer")] fn pyget_buffer(&self, path: String) -> Option { self.get_buffer(path.as_str()) diff --git a/src/workspace.rs b/src/workspace.rs index 5c1ca1af..e98fea1d 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -18,9 +18,11 @@ use codemp_proto::{ common::Empty, files::{BufferNode, BufferPath}, workspace::{ - WorkspaceEvent, workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace - } + WorkspaceEvent, + workspace_event::{ + Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, + UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace, + }, }, }; @@ -42,7 +44,7 @@ use napi_derive::napi; /// Using a workspace handle, it's possible to receive events (user join/leave, filetree updates) /// and create/delete/attach to new buffers. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "js", napi)] pub struct Workspace(pub(crate) Arc); @@ -94,8 +96,7 @@ impl Workspace { workspace_claim: tokio::sync::watch::Receiver, user_claim: tokio::sync::watch::Receiver, ) -> ConnectionResult { - let services = - Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; + let services = Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; let ws_stream = services.ws().attach(Empty {}).await?.into_inner(); let (tx, rx) = mpsc::channel(128); @@ -198,11 +199,16 @@ impl Workspace { let path = path.to_string(); let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let credentials = workspace_client.get_buffer_token(BufferPath::from(&path)).await?.into_inner(); + let request = tonic::Request::new(BufferRequest { path: path.clone() }); + let credentials = workspace_client + .get_buffer_token(request) + .await? + .into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); - req.metadata_mut().insert("buffer", crate::ext::token_to_metadata(credentials)?); + req.metadata_mut() + .insert("buffer", crate::ext::token_to_metadata(credentials)?); let stream = buffer_client.attach(req).await?.into_inner(); let controller = @@ -217,12 +223,17 @@ impl Workspace { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(20)).await; - if weak.upgrade().is_none() { break }; - let new_credentials = workspace_client.get_buffer_token(BufferPath::from(&_path)) + if weak.upgrade().is_none() { + break; + }; + let new_credentials = workspace_client + .get_buffer_token(tonic::Request::new(BufferRequest { path: path.clone() })) .await? .into_inner(); let mut request = tonic::Request::new(Empty {}); - request.metadata_mut().insert("buffer", crate::ext::token_to_metadata(new_credentials)?); + request + .metadata_mut() + .insert("buffer", crate::ext::token_to_metadata(new_credentials)?); buffer_client.keep_alive(request).await?; } Ok::<(), tonic::Status>(()) @@ -291,10 +302,13 @@ impl Workspace { } /// Fetch a list of the [User]s attached to a specific buffer. - pub async fn fetch_buffer_users(&self, path: impl ToString) -> RemoteResult<()> { - let path = path.to_string(); - let resp = self.services().ws() - .fetch_buffer_users(BufferPath::from(&path)) + pub async fn fetch_buffer_users(&self, path: String) -> RemoteResult<()> { + let users = self + .services() + .ws() + .fetch_buffer_users(tonic::Request::new(BufferRequest { + path: path.to_string(), + })) .await? .into_inner(); @@ -383,7 +397,6 @@ impl Workspace { } } - struct WorkspaceWorker { callback: watch::Receiver>>, pollers: Vec>, From ed88d105c5a2f4f5b269521cf350d7545c989156 Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 5 Mar 2026 22:38:39 +0100 Subject: [PATCH 038/121] (chore): minort touchups --- src/ffi/python/controllers.rs | 14 +++++++------- src/ffi/python/mod.rs | 9 +++------ src/ffi/python/workspace.rs | 8 ++++---- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index 28bd5d38..270eae98 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -1,6 +1,6 @@ +use crate::api::Cursor; use crate::api::TextChange; use crate::api::controller::{AsyncReceiver, AsyncSender}; -use crate::api::{Cursor, Selection}; use crate::buffer::Controller as BufferController; use crate::cursor::Controller as CursorController; use pyo3::exceptions::PyValueError; @@ -13,7 +13,7 @@ use super::a_sync_detach; #[pymethods] impl CursorController { #[pyo3(name = "send")] - fn pysend(&self, _py: Python, pos: Selection) -> PyResult<()> { + fn pysend(&self, _py: Python, pos: Cursor) -> PyResult<()> { self.send(pos)?; Ok(()) } @@ -142,11 +142,11 @@ impl Cursor { #[getter(buffer)] fn pybuffer(&self) -> String { - self.sel.iter().map(|s| s.buffer.clone()).collect() + self.buffer.clone() } - #[getter(user)] - fn pyuser(&self) -> Option { - Some(self.user.clone()) - } + // #[getter(user)] + // fn pyuser(&self) -> Option { + // Some(self.user.clone()) + // } } diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 34fcc151..77b02c31 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -48,7 +48,7 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { // let _ = CREATE_FUTURE.get_or_init(|| create_future); // } -#[pyclass(from_py_object)] +#[pyclass] pub struct Promise(Option>>>); #[pymethods] @@ -121,7 +121,7 @@ impl std::io::Write for LoggerProducer { } } -#[pyclass(from_py_object) +#[pyclass] pub struct Driver(Option>); #[pymethods] impl Driver { @@ -231,13 +231,12 @@ impl Cursor { #[pymethods] impl Selection { #[new] - #[pyo3(signature = (*, start_row, start_col, end_row, end_col, buffer, **kwds))] + #[pyo3(signature = (*, start_row, start_col, end_row, end_col, **kwds))] pub fn py_new( start_row: i32, start_col: i32, end_row: i32, end_col: i32, - buffer: String, kwds: Option<&Bound<'_, PyDict>>, ) -> PyResult { if let Some(_kwds) = kwds { @@ -246,7 +245,6 @@ impl Selection { start_col, end_row, end_col, - buffer, }) } else { Ok(Self { @@ -254,7 +252,6 @@ impl Selection { start_col, end_row, end_col, - buffer, }) } } diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index a4e63593..82bf39d4 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -21,7 +21,7 @@ impl Workspace { #[pyo3(name = "attach_buffer")] fn pyattach_buffer(&self, py: Python, path: String) -> PyResult { let this = self.clone(); - a_sync_detach!(py, this.attach_buffer(path.as_str()).await) + a_sync_detach!(py, this.attach_buffer(path).await) } #[pyo3(name = "detach_buffer")] @@ -32,20 +32,20 @@ impl Workspace { #[pyo3(name = "fetch_buffers")] fn pylist_buffers(&self, py: Python, filter: String) -> PyResult { let this = self.clone(); - a_sync_detach!(py, this.list_buffers(filter.as_str()).await) + a_sync_detach!(py, this.fetch_buffers(filter).await) } #[pyo3(name = "fetch_users")] fn pylist_users(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_detach!(py, this.list_users().await) + a_sync_detach!(py, this.fetch_users().await) } #[pyo3(name = "fetch_buffer_users")] fn pylist_buffer_users(&self, py: Python, path: String) -> PyResult { // crate::Result> let this = self.clone(); - a_sync_detach!(py, this.list_buffer_users(path.as_str()).await) + a_sync_detach!(py, this.fetch_buffer_users(path).await) } #[pyo3(name = "delete_buffer")] From 76cb299f76053015d775420585e6a2345eb41250 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 7 Mar 2026 22:23:03 +0100 Subject: [PATCH 039/121] docs(lua): more annotations updates --- dist/lua/annotations.lua | 61 ++++++++++++++++++++++------------------ src/api/cursor.rs | 2 +- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index b3d51bde..e6818c08 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -111,28 +111,28 @@ function BufferControllerPromise:cancel() end function BufferControllerPromise:and_then(cb) end ----@class (exact) CursorPromise : Promise -local CursorPromise = {} +---@class (exact) CursorEventPromise : Promise +local CursorEventPromise = {} --- block until promise is ready and return value ---- @return Cursor -function CursorPromise:await() end +--- @return CursorEvent +function CursorEventPromise:await() end --- cancel promise execution -function CursorPromise:cancel() end ----@param cb fun(x: Cursor) callback to invoke +function CursorEventPromise:cancel() end +---@param cb fun(x: CursorEvent) callback to invoke ---invoke callback asynchronously as soon as promise is ready -function CursorPromise:and_then(cb) end +function CursorEventPromise:and_then(cb) end ----@class (exact) MaybeCursorPromise : Promise -local MaybeCursorPromise = {} +---@class (exact) MaybeCursorEventPromise : Promise +local MaybeCursorEventPromise = {} --- block until promise is ready and return value ---- @return Cursor | nil -function MaybeCursorPromise:await() end +--- @return CursorEvent | nil +function MaybeCursorEventPromise:await() end --- cancel promise execution -function MaybeCursorPromise:cancel() end ----@param cb fun(x: Cursor | nil) callback to invoke +function MaybeCursorEventPromise:cancel() end +---@param cb fun(x: CursorEvent | nil) callback to invoke ---invoke callback asynchronously as soon as promise is ready -function MaybeCursorPromise:and_then(cb) end +function MaybeCursorEventPromise:and_then(cb) end ---@class (exact) BufferUpdatePromise : Promise @@ -212,12 +212,13 @@ function Client:active_workspaces() end ---refresh current user token if possible function Client:refresh() end +---@param user string workspace owning user ---@param ws string workspace id to connect to ---@return WorkspacePromise ---@async ---@nodiscard ---join requested workspace if possible and subscribe to event bus -function Client:attach_workspace(ws) end +function Client:attach_workspace(user, ws) end ---@param ws string workspace id to create ---@return NilPromise @@ -226,9 +227,10 @@ function Client:attach_workspace(ws) end ---create a new workspace with given id function Client:create_workspace(ws) end +---@param user string workspace owning user ---@param ws string workspace id to leave ---leave workspace with given id, detaching and disconnecting -function Client:leave_workspace(ws) end +function Client:leave_workspace(user, ws) end ---@param ws string workspace id to delete ---@return NilPromise @@ -505,30 +507,35 @@ function BufferController:ack(version) end ---handle to a workspace's cursor channel, allowing send/recv operations local CursorController = {} +---a cursor selection span ---@class Selection ----@field buffer string relative path ("name") of buffer on which this cursor is ----@field start_row integer ----@field start_col integer ----@field end_row integer ----@field end_col integer ----a cursor selected region, as row-col indices +---@field start_row integer cursor position starting row in buffer +---@field start_col integer cursor position starting column in buffer +---@field end_row integer cursor position final row in buffer +---@field end_col integer cursor position final column in buffer +---a cursor instantaneous state ---@class Cursor ----@field user string id of user owning this cursor ----@field sel Selection selected region for this user +---@field buffer string path of buffer this cursor is on +---@field sel Selection[] the updated cursor selection(s) ----@param cursor Selection cursor position to broadcast +---an event that occurred about a user's cursor +---@class CursorEvent +---@field user string user who sent this cursor +---@field cursor Cursor cursor position data + +---@param cursor Cursor cursor position to broadcast ---update cursor position by sending a cursor event to server function CursorController:send(cursor) end ----@return MaybeCursorPromise +---@return MaybeCursorEventPromise ---@async ---@nodiscard ---try to receive cursor events, returning nil if none is available function CursorController:try_recv() end ----@return CursorPromise +---@return CursorEventPromise ---@async ---@nodiscard ---block until next cursor event and return it diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 13866ffc..69bc7900 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -17,7 +17,7 @@ pub struct CursorEvent { pub cursor: Cursor, } -/// An event that occurred about a user's cursor. +/// A cursor instantaneous state #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] From c64181d6981599ff8662b4180a329683b099c825 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 7 Mar 2026 22:29:06 +0100 Subject: [PATCH 040/121] fix: oops merge issues? --- src/client.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/client.rs b/src/client.rs index 70ffae1c..97cbb3a9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -267,15 +267,11 @@ impl Client { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(240)).await; if weak.upgrade().is_none() { break }; - let new_credentials = session_client.get_workspace_token( - tonic::Request::new(WorkspaceRequest { id: Identifier::from(workspace) }) - ) + let new_credentials = session_client.get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(_workspace.clone())) .await? .into_inner(); workspace_claims.set(new_credentials); - workspace_client - .keep_alive(tonic::Request::new(Empty {})) - .await?; + workspace_client.keep_alive(tonic::Request::new(Empty {})).await?; } Ok::<(), tonic::Status>(()) }; @@ -305,12 +301,15 @@ impl Client { } /// Get the names of all active [`Workspace`]s. - pub fn active_workspaces(&self) -> Vec { - self.0 - .workspaces - .iter() - .map(|x| *x.key()) - .collect() + // TODO get rid of WorkspaceIdentifier + pub fn active_workspaces(&self) -> Vec { + let mut out = Vec::new(); + for wss in self.0.workspaces.iter() { + for ws in wss.value().iter() { + out.push(ws.value().id().clone()); + } + } + out } /// Get the currently logged in user. From 4924108ed92e56a8e94752af51494b0d33996139 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 7 Mar 2026 22:32:01 +0100 Subject: [PATCH 041/121] fix: more merge issues? --- src/api/event.rs | 2 +- src/workspace.rs | 43 +++++++++++++++---------------------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/api/event.rs b/src/api/event.rs index 4d1c178b..b4697a62 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -34,7 +34,7 @@ impl From for Event { WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user, buffer: e.buffer }, WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user, buffer: e.buffer }, WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { - name: e.user.name, + name: e.user, buffer: e.buffer, }, WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { diff --git a/src/workspace.rs b/src/workspace.rs index e98fea1d..05d04e6e 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -18,11 +18,9 @@ use codemp_proto::{ common::Empty, files::{BufferNode, BufferPath}, workspace::{ - WorkspaceEvent, - workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, - UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace, - }, + WorkspaceEvent, workspace_event::{ + Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace + } }, }; @@ -96,7 +94,8 @@ impl Workspace { workspace_claim: tokio::sync::watch::Receiver, user_claim: tokio::sync::watch::Receiver, ) -> ConnectionResult { - let services = Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; + let services = + Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; let ws_stream = services.ws().attach(Empty {}).await?.into_inner(); let (tx, rx) = mpsc::channel(128); @@ -199,16 +198,11 @@ impl Workspace { let path = path.to_string(); let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let request = tonic::Request::new(BufferRequest { path: path.clone() }); - let credentials = workspace_client - .get_buffer_token(request) - .await? - .into_inner(); + let credentials = workspace_client.get_buffer_token(BufferPath::from(&path)).await?.into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); - req.metadata_mut() - .insert("buffer", crate::ext::token_to_metadata(credentials)?); + req.metadata_mut().insert("buffer", crate::ext::token_to_metadata(credentials)?); let stream = buffer_client.attach(req).await?.into_inner(); let controller = @@ -223,17 +217,12 @@ impl Workspace { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(20)).await; - if weak.upgrade().is_none() { - break; - }; - let new_credentials = workspace_client - .get_buffer_token(tonic::Request::new(BufferRequest { path: path.clone() })) + if weak.upgrade().is_none() { break }; + let new_credentials = workspace_client.get_buffer_token(BufferPath::from(&_path)) .await? .into_inner(); let mut request = tonic::Request::new(Empty {}); - request - .metadata_mut() - .insert("buffer", crate::ext::token_to_metadata(new_credentials)?); + request.metadata_mut().insert("buffer", crate::ext::token_to_metadata(new_credentials)?); buffer_client.keep_alive(request).await?; } Ok::<(), tonic::Status>(()) @@ -302,13 +291,10 @@ impl Workspace { } /// Fetch a list of the [User]s attached to a specific buffer. - pub async fn fetch_buffer_users(&self, path: String) -> RemoteResult<()> { - let users = self - .services() - .ws() - .fetch_buffer_users(tonic::Request::new(BufferRequest { - path: path.to_string(), - })) + pub async fn fetch_buffer_users(&self, path: impl ToString) -> RemoteResult<()> { + let path = path.to_string(); + let resp = self.services().ws() + .fetch_buffer_users(BufferPath::from(&path)) .await? .into_inner(); @@ -397,6 +383,7 @@ impl Workspace { } } + struct WorkspaceWorker { callback: watch::Receiver>>, pollers: Vec>, From 78201c6e6909036d6551c0da5d07dd4e9a2cd4ba Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 7 Mar 2026 22:33:36 +0100 Subject: [PATCH 042/121] chore: cargo fmt --- src/api/cursor.rs | 5 +- src/api/event.rs | 10 ++- src/api/mod.rs | 2 +- src/client.rs | 146 +++++++++++++++++++++++++++++---------- src/cursor/controller.rs | 7 +- src/cursor/worker.rs | 18 +++-- src/ext.rs | 9 +-- src/ffi/js/buffer.rs | 9 +-- src/ffi/js/cursor.rs | 7 +- src/ffi/lua/buffer.rs | 4 +- src/ffi/lua/client.rs | 53 +++++++++----- src/ffi/lua/cursor.rs | 4 +- src/ffi/lua/ext/mod.rs | 1 - src/ffi/lua/workspace.rs | 22 +++--- src/prelude.rs | 8 +-- src/tests/client.rs | 22 ++++-- src/tests/fixtures.rs | 22 ++++-- src/tests/server.rs | 47 +++++++++---- src/workspace.rs | 79 +++++++++++++-------- 19 files changed, 326 insertions(+), 149 deletions(-) diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 69bc7900..405dde8c 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -54,7 +54,9 @@ impl From for CursorEvent { user: value.user, cursor: Cursor { buffer: value.position.buffer, - sel: value.position.cursors + sel: value + .position + .cursors .into_iter() .map(|c| Selection { start_row: c.start.row, @@ -67,4 +69,3 @@ impl From for CursorEvent { } } } - diff --git a/src/api/event.rs b/src/api/event.rs index b4697a62..33ca7e63 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -31,8 +31,14 @@ impl From for Event { WorkspaceEventInner::Create(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Delete(e) => Self::FileTreeUpdated { path: e.path }, WorkspaceEventInner::Rename(e) => Self::FileTreeUpdated { path: e.after }, - WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user, buffer: e.buffer }, - WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { name: e.user, buffer: e.buffer }, + WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { + name: e.user, + buffer: e.buffer, + }, + WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { + name: e.user, + buffer: e.buffer, + }, WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { name: e.user, buffer: e.buffer, diff --git a/src/api/mod.rs b/src/api/mod.rs index 67c84678..b899d878 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -29,7 +29,7 @@ pub use buffer::BufferNode; pub use change::{BufferUpdate, TextChange}; pub use config::Config; pub use controller::{AsyncReceiver, AsyncSender, Controller}; -pub use cursor::{Cursor, Selection, CursorEvent}; +pub use cursor::{Cursor, CursorEvent, Selection}; pub use event::Event; pub use user::UserInfo; pub use workspace::WorkspaceIdentifier; diff --git a/src/client.rs b/src/client.rs index 97cbb3a9..862ec8ac 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,8 @@ use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, common::{Empty, Token}, session::{ - InviteRequest, OwnedWorkspaceIdentifier, UserId, WorkspaceIdentifier, session_client::SessionClient + InviteRequest, OwnedWorkspaceIdentifier, UserId, WorkspaceIdentifier, + session_client::SessionClient, }, }; @@ -46,8 +47,11 @@ struct ClientInner { session: SessionClient>, claims: InternallyMutable, poll_tx: tokio::sync::mpsc::UnboundedSender>, - callback: tokio::sync::watch::Sender>>, - events: tokio::sync::Mutex>, + callback: + tokio::sync::watch::Sender>>, + events: tokio::sync::Mutex< + tokio::sync::mpsc::UnboundedReceiver, + >, } impl Client { @@ -123,7 +127,9 @@ impl Client { self.0 .session .clone() - .create_workspace(OwnedWorkspaceIdentifier { workspace: name.to_string() }) + .create_workspace(OwnedWorkspaceIdentifier { + workspace: name.to_string(), + }) .await? .into_inner(); Ok(()) @@ -134,43 +140,70 @@ impl Client { self.0 .session .clone() - .delete_workspace(OwnedWorkspaceIdentifier { workspace: name.to_string() }) + .delete_workspace(OwnedWorkspaceIdentifier { + workspace: name.to_string(), + }) .await?; Ok(()) } /// Quit a joined workspace. Cannot quit owned workspaces: must delete them - pub async fn quit_workspace(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { + pub async fn quit_workspace( + &self, + user: impl ToString, + workspace: impl ToString, + ) -> RemoteResult<()> { self.0 .session .clone() - .quit_workspace(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) + .quit_workspace(WorkspaceIdentifier { + user: user.to_string(), + workspace: workspace.to_string(), + }) .await?; Ok(()) } /// Accept an invitation to a workspace, making it accessible - pub async fn accept_invite(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { + pub async fn accept_invite( + &self, + user: impl ToString, + workspace: impl ToString, + ) -> RemoteResult<()> { self.0 .session .clone() - .accept_invite(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) + .accept_invite(WorkspaceIdentifier { + user: user.to_string(), + workspace: workspace.to_string(), + }) .await?; Ok(()) } /// Reject an invitation to a workspace - pub async fn reject_invite(&self, user: impl ToString, workspace: impl ToString) -> RemoteResult<()> { + pub async fn reject_invite( + &self, + user: impl ToString, + workspace: impl ToString, + ) -> RemoteResult<()> { self.0 .session .clone() - .reject_invite(WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }) + .reject_invite(WorkspaceIdentifier { + user: user.to_string(), + workspace: workspace.to_string(), + }) .await?; Ok(()) } /// Invite user with given username to the given workspace, if possible. - pub async fn invite_to_workspace(&self, workspace_name: impl ToString, user_name: impl ToString) -> RemoteResult<()> { + pub async fn invite_to_workspace( + &self, + workspace_name: impl ToString, + user_name: impl ToString, + ) -> RemoteResult<()> { self.0 .session .clone() @@ -183,7 +216,9 @@ impl Client { } /// Fetch the names of all workspaces owned by the current user. - pub async fn fetch_owned_workspaces(&self) -> RemoteResult> { + pub async fn fetch_owned_workspaces( + &self, + ) -> RemoteResult> { Ok(self .0 .session @@ -198,7 +233,9 @@ impl Client { } /// Fetch the names of all workspaces the current user has joined. - pub async fn fetch_joined_workspaces(&self) -> RemoteResult> { + pub async fn fetch_joined_workspaces( + &self, + ) -> RemoteResult> { Ok(self .0 .session @@ -212,26 +249,39 @@ impl Client { .collect()) } - pub async fn get_user_info(&self, user: impl ToString) -> RemoteResult { - Ok( - self.0 - .session - .clone() - .get_user_info(UserId { user: user.to_string() }) - .await? - .into_inner() - ) + pub async fn get_user_info( + &self, + user: impl ToString, + ) -> RemoteResult { + Ok(self + .0 + .session + .clone() + .get_user_info(UserId { + user: user.to_string(), + }) + .await? + .into_inner()) } /// Join and return a [`Workspace`]. #[tracing::instrument(skip(self, user, workspace), fields(owner = user.to_string(), ws = workspace.to_string()))] - pub async fn attach_workspace(&self, user: impl ToString, workspace: impl ToString) -> ConnectionResult { - let workspace_id = crate::api::WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string() }; + pub async fn attach_workspace( + &self, + user: impl ToString, + workspace: impl ToString, + ) -> ConnectionResult { + let workspace_id = crate::api::WorkspaceIdentifier { + user: user.to_string(), + workspace: workspace.to_string(), + }; let user = user.to_string(); let workspace = workspace.to_string(); let mut session_client = self.0.session.clone(); let token = session_client - .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(workspace_id.clone())) + .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from( + workspace_id.clone(), + )) .await? .into_inner(); @@ -249,12 +299,12 @@ impl Client { match self.0.workspaces.get_mut(&user) { Some(mutref) => { mutref.insert(workspace.clone(), ws.clone()); - }, + } None => { let map = DashMap::default(); map.insert(workspace.clone(), ws.clone()); self.0.workspaces.insert(user.clone(), map); - }, + } }; let mut workspace_client = ws.services().ws(); @@ -266,12 +316,19 @@ impl Client { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(240)).await; - if weak.upgrade().is_none() { break }; - let new_credentials = session_client.get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from(_workspace.clone())) + if weak.upgrade().is_none() { + break; + }; + let new_credentials = session_client + .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from( + _workspace.clone(), + )) .await? .into_inner(); workspace_claims.set(new_credentials); - workspace_client.keep_alive(tonic::Request::new(Empty {})).await?; + workspace_client + .keep_alive(tonic::Request::new(Empty {})) + .await?; } Ok::<(), tonic::Status>(()) }; @@ -296,8 +353,16 @@ impl Client { } /// Gets a [`Workspace`] handle by name. - pub fn get_workspace(&self, user: impl AsRef, workspace: impl AsRef) -> Option { - self.0.workspaces.get(user.as_ref())?.get(workspace.as_ref()).map(|x| x.clone()) + pub fn get_workspace( + &self, + user: impl AsRef, + workspace: impl AsRef, + ) -> Option { + self.0 + .workspaces + .get(user.as_ref())? + .get(workspace.as_ref()) + .map(|x| x.clone()) } /// Get the names of all active [`Workspace`]s. @@ -319,11 +384,15 @@ impl Client { } impl AsyncReceiver for Client { - async fn try_recv(&self) -> crate::errors::ControllerResult> { + async fn try_recv( + &self, + ) -> crate::errors::ControllerResult> { match self.0.events.lock().await.try_recv() { Ok(x) => Ok(Some(x)), Err(tokio::sync::mpsc::error::TryRecvError::Empty) => Ok(None), - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => Err(crate::errors::ControllerError::Stopped), + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + Err(crate::errors::ControllerError::Stopped) + } } } @@ -343,7 +412,8 @@ impl AsyncReceiver for Client { } struct ClientWorker { - callback: tokio::sync::watch::Receiver>>, + callback: + tokio::sync::watch::Receiver>>, pollers: Vec>, poll_rx: tokio::sync::mpsc::UnboundedReceiver>, events: tokio::sync::mpsc::UnboundedSender, @@ -353,8 +423,8 @@ impl ClientWorker { #[tracing::instrument(skip(self, stream, weak))] pub(crate) async fn work( mut self, - mut stream: tonic::Streaming , - weak: std::sync::Weak , + mut stream: tonic::Streaming, + weak: std::sync::Weak, ) { tracing::debug!("client worker starting"); loop { diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 3ee2ea6e..08cedb70 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -7,9 +7,12 @@ use tokio::sync::{mpsc, oneshot, watch}; use crate::{ api::{ - Controller, Cursor, controller::{AsyncReceiver, AsyncSender, ControllerCallback}, cursor::CursorEvent + Controller, Cursor, + controller::{AsyncReceiver, AsyncSender, ControllerCallback}, + cursor::CursorEvent, }, - errors::ControllerResult, network::AuthedService, + errors::ControllerResult, + network::AuthedService, }; use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol, cursor_client::CursorClient}; diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index b2b321fe..b14f16d7 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -4,9 +4,15 @@ use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; use crate::{ - api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, errors::RemoteResult, ext::IgnorableError, network::AuthedService + api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, + errors::RemoteResult, + ext::IgnorableError, + network::AuthedService, +}; +use codemp_proto::{ + common::Empty, + cursor::{CursorEvent, CursorUpdate, cursor_client::CursorClient}, }; -use codemp_proto::{common::Empty, cursor::{CursorEvent, CursorUpdate, cursor_client::CursorClient}}; use super::controller::{CursorController, CursorControllerInner}; @@ -42,7 +48,7 @@ impl CursorWorker { end_col: x.end.col, }) .collect(), - } + }, }) } else { tracing::warn!("received cursor for unknown user {}", event.user); @@ -95,14 +101,14 @@ impl CursorController { } pub async fn list(&self) -> RemoteResult> { - Ok(self.0 + Ok(self + .0 .service .clone() .list(Empty {}) .await? .into_inner() - .cursors - ) + .cursors) } #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id))] diff --git a/src/ext.rs b/src/ext.rs index ab7e57a9..177de61c 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -116,8 +116,9 @@ where } } -pub(crate) fn token_to_metadata(tok: codemp_proto::common::Token) -> tonic::Result> { - tonic::metadata::MetadataValue::try_from(tok.token).map_err(|e| { - tonic::Status::internal(format!("failed representing token to string: {e}")) - }) +pub(crate) fn token_to_metadata( + tok: codemp_proto::common::Token, +) -> tonic::Result> { + tonic::metadata::MetadataValue::try_from(tok.token) + .map_err(|e| tonic::Status::internal(format!("failed representing token to string: {e}"))) } diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index ae3742aa..38c92de4 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,9 +1,7 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::{BufferUpdate, TextChange}; use crate::buffer::controller::BufferController; -use napi::threadsafe_function::{ - ThreadsafeFunction, ThreadsafeFunctionCallMode, -}; +use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] @@ -14,7 +12,10 @@ impl BufferController { js_name = "callback", ts_args_type = "fun: (event: BufferController) => void" )] - pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + pub fn js_callback( + &self, + fun: ThreadsafeFunction, + ) -> napi::Result<()> { self.callback(move |controller: BufferController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index fc14ad0b..97837ae4 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,6 +1,6 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::cursor::controller::CursorController; -use napi::threadsafe_function::{ ThreadsafeFunction, ThreadsafeFunctionCallMode}; +use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] @@ -11,7 +11,10 @@ impl CursorController { js_name = "callback", ts_args_type = "fun: (event: CursorController) => void" )] - pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + pub fn js_callback( + &self, + fun: ThreadsafeFunction, + ) -> napi::Result<()> { self.callback(move |controller: CursorController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index 7b79bd2a..4b306513 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -11,7 +11,9 @@ impl LuaUserData for CodempBufferController { Ok(format!("{:?}", this)) }); - methods.add_method("workspace_id", |_, this, ()| Ok(this.workspace_id().clone())); + methods.add_method("workspace_id", |_, this, ()| { + Ok(this.workspace_id().clone()) + }); methods.add_method("path", |_, this, ()| Ok(this.path().to_string())); methods.add_method("send", |_, this, (change,): (CodempTextChange,)| { diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index d4e016ce..1ca71f74 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -11,8 +11,12 @@ impl LuaUserData for CodempClient { Ok(format!("{:?}", this)) }); - methods.add_method("current_user", |_, this, ()| Ok(this.current_user().clone())); - methods.add_method("active_workspaces", |_, this, ()| Ok(this.active_workspaces())); + methods.add_method("current_user", |_, this, ()| { + Ok(this.current_user().clone()) + }); + methods.add_method("active_workspaces", |_, this, ()| { + Ok(this.active_workspaces()) + }); methods.add_method( "refresh", @@ -21,7 +25,7 @@ impl LuaUserData for CodempClient { methods.add_method( "attach_workspace", - |_, this, (user, workspace): (String,String)| { + |_, this, (user, workspace): (String, String)| { a_sync! { this => this.attach_workspace(user, workspace).await? } }, ); @@ -38,22 +42,28 @@ impl LuaUserData for CodempClient { methods.add_method( "quit_workspace", - |_, this, (user, workspace):(String,String)| a_sync! { - this => this.quit_workspace(user, workspace).await? + |_, this, (user, workspace): (String, String)| { + a_sync! { + this => this.quit_workspace(user, workspace).await? + } }, ); methods.add_method( "accept_invite", - |_, this, (user, workspace):(String,String)| a_sync! { - this => this.accept_invite(user, workspace).await? + |_, this, (user, workspace): (String, String)| { + a_sync! { + this => this.accept_invite(user, workspace).await? + } }, ); methods.add_method( "reject_invite", - |_, this, (user, workspace):(String,String)| a_sync! { - this => this.reject_invite(user, workspace).await? + |_, this, (user, workspace): (String, String)| { + a_sync! { + this => this.reject_invite(user, workspace).await? + } }, ); @@ -71,16 +81,20 @@ impl LuaUserData for CodempClient { |_, this, ()| a_sync! { this => this.fetch_joined_workspaces().await? }, ); - methods.add_method("leave_workspace", |_, this, (user, workspace): (String,String)| { - Ok(this.leave_workspace(user, workspace)) - }); + methods.add_method( + "leave_workspace", + |_, this, (user, workspace): (String, String)| Ok(this.leave_workspace(user, workspace)), + ); - methods.add_method("get_workspace", |_, this, (user, workspace): (String,String)| { - Ok(this.get_workspace(user, workspace)) - }); + methods.add_method( + "get_workspace", + |_, this, (user, workspace): (String, String)| Ok(this.get_workspace(user, workspace)), + ); - methods.add_method("get_user_info", |_, this, (user,):(String,)| a_sync! { - this => crate::api::UserInfo::from(this.get_user_info(user).await?) + methods.add_method("get_user_info", |_, this, (user,): (String,)| { + a_sync! { + this => crate::api::UserInfo::from(this.get_user_info(user).await?) + } }); // TODO need to derive ser/de on Event, but this is in protobuf... @@ -110,6 +124,9 @@ impl LuaUserData for CodempClient { impl CodempClient { fn lua_callback_id(&self) -> String { - format!("codemp-client({})-callback-registry", self.current_user().name) + format!( + "codemp-client({})-callback-registry", + self.current_user().name + ) } } diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index 67036696..6a8e6a60 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -11,7 +11,9 @@ impl LuaUserData for CodempCursorController { Ok(format!("{:?}", this)) }); - methods.add_method("workspace_id", |_, this, ()| Ok(this.workspace_id().clone())); + methods.add_method("workspace_id", |_, this, ()| { + Ok(this.workspace_id().clone()) + }); methods.add_method("list", |_, this, ()| a_sync! { this => this.list().await?.into_iter().map(CodempCursorEvent::from).collect::>() diff --git a/src/ffi/lua/ext/mod.rs b/src/ffi/lua/ext/mod.rs index b23fc779..8818de40 100644 --- a/src/ffi/lua/ext/mod.rs +++ b/src/ffi/lua/ext/mod.rs @@ -42,4 +42,3 @@ macro_rules! impl_lua_serde { } pub(crate) use impl_lua_serde; - diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index fca5cf15..4770996e 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -15,19 +15,17 @@ impl LuaUserData for CodempWorkspace { |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(name, ephemeral).await? }, ); - methods.add_method( - "pin_buffer", - |_, this, (path,):(String,)| a_sync! { + methods.add_method("pin_buffer", |_, this, (path,): (String,)| { + a_sync! { this => this.pin_buffer(path).await? - }, - ); + } + }); - methods.add_method( - "un_pin_buffer", - |_, this, (path,):(String,)| a_sync! { + methods.add_method("un_pin_buffer", |_, this, (path,): (String,)| { + a_sync! { this => this.un_pin_buffer(path).await? - }, - ); + } + }); methods.add_method( "attach_buffer", @@ -64,7 +62,9 @@ impl LuaUserData for CodempWorkspace { Ok(this.search_buffers(filter.as_deref())) }); - methods.add_method("list_buffer_users", |_, this, (path,): (String,)| Ok(this.buffer_user_list(&path))); + methods.add_method("list_buffer_users", |_, this, (path,): (String,)| { + Ok(this.buffer_user_list(&path)) + }); methods.add_method("id", |_, this, ()| Ok(this.id().to_string())); methods.add_method("cursor", |_, this, ()| Ok(this.cursor())); diff --git a/src/prelude.rs b/src/prelude.rs index 385a456a..05e9e3ca 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,10 +3,10 @@ pub use crate::api::{ AsyncReceiver as CodempAsyncReceiver, AsyncSender as CodempAsyncSender, - BufferUpdate as CodempBufferUpdate, Config as CodempConfig, Controller as CodempController, - Cursor as CodempCursor, Event as CodempEvent, Selection as CodempSelection, CursorEvent as CodempCursorEvent, - TextChange as CodempTextChange, UserInfo as CodempUserInfo, - WorkspaceIdentifier as CodempWorkspaceIdentifier, BufferNode as CodempBufferNode, + BufferNode as CodempBufferNode, BufferUpdate as CodempBufferUpdate, Config as CodempConfig, + Controller as CodempController, Cursor as CodempCursor, CursorEvent as CodempCursorEvent, + Event as CodempEvent, Selection as CodempSelection, TextChange as CodempTextChange, + UserInfo as CodempUserInfo, WorkspaceIdentifier as CodempWorkspaceIdentifier, }; pub use crate::{ diff --git a/src/tests/client.rs b/src/tests/client.rs index 9b9cba4a..73db628c 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -77,7 +77,10 @@ async fn test_invite_user_to_workspace() { .expect("failed setting up bob's client"); let ws_name = uuid::Uuid::new_v4().to_string(); - let wsid = crate::api::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: ws_name.clone() }; + let wsid = crate::api::WorkspaceIdentifier { + user: client_alice.current_user().name.clone(), + workspace: ws_name.clone(), + }; // after this we can't just fail anymore: we need to cleanup, so store errs client_alice @@ -229,7 +232,10 @@ async fn cannot_delete_others_workspaces() { let client_bob = client_bob.clone(); async move { assert_or_err!( - client_bob.delete_workspace(ws_alice.id().workspace.clone()).await.is_err(), + client_bob + .delete_workspace(ws_alice.id().workspace.clone()) + .await + .is_err(), "bob was allowed to delete a workspace he didn't own!" ); Ok(()) @@ -246,7 +252,9 @@ async fn test_buffer_search() { let workspace_alice = workspace_alice.clone(); async move { - workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice + .create_buffer(buffer_name.clone(), false) + .await?; assert_or_err!( !workspace_alice .search_buffers(Some(&buffer_name[0..4])) @@ -269,7 +277,9 @@ async fn test_send_operation() { let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice + .create_buffer(buffer_name.clone(), false) + .await?; let alice = workspace_alice.attach_buffer(buffer_name.clone()).await?; let bob = workspace_bob.attach_buffer(buffer_name.clone()).await?; @@ -299,7 +309,9 @@ async fn test_content_converges() { let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice + .create_buffer(buffer_name.clone(), false) + .await?; let alice = workspace_alice.attach_buffer(buffer_name.clone()).await?; let bob = workspace_bob.attach_buffer(buffer_name.clone()).await?; diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 5a78a4f1..65f64763 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -158,10 +158,15 @@ impl .await?; client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) + .invite_to_workspace( + self.workspace.clone(), + invitee_client.current_user().name.clone(), + ) .await?; let workspace = client.attach_workspace(&self.user, &self.workspace).await?; - let invitee_workspace = invitee_client.attach_workspace(&self.user, &self.workspace).await?; + let invitee_workspace = invitee_client + .attach_workspace(&self.user, &self.workspace) + .await?; Ok((client, workspace, invitee_client, invitee_workspace)) } @@ -253,14 +258,21 @@ impl .await?; client.create_workspace(self.workspace.to_string()).await?; client - .invite_to_workspace(self.workspace.clone(), invitee_client.current_user().name.clone()) + .invite_to_workspace( + self.workspace.clone(), + invitee_client.current_user().name.clone(), + ) .await?; let workspace = client.attach_workspace(&self.user, &self.workspace).await?; - workspace.create_buffer(self.buffer.to_string(), false).await?; + workspace + .create_buffer(self.buffer.to_string(), false) + .await?; let buffer = workspace.attach_buffer(self.buffer.clone()).await?; - let invitee_workspace = invitee_client.attach_workspace(&self.user, &self.workspace).await?; + let invitee_workspace = invitee_client + .attach_workspace(&self.user, &self.workspace) + .await?; let invitee_buffer = invitee_workspace.attach_buffer(self.buffer.clone()).await?; Ok(( diff --git a/src/tests/server.rs b/src/tests/server.rs index 88ee5623..1dafdf04 100644 --- a/src/tests/server.rs +++ b/src/tests/server.rs @@ -11,7 +11,9 @@ async fn test_buffer_create() { let workspace_alice = workspace_alice.clone(); async move { - workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice + .create_buffer(buffer_name.clone(), false) + .await?; workspace_alice.fetch_buffers().await?; assert_or_err!(vec![buffer_name.clone()] == workspace_alice.search_buffers(None)); workspace_alice.delete_buffer(buffer_name).await?; @@ -49,7 +51,9 @@ async fn cannot_delete_others_buffers() { let workspace_bob = workspace_bob.clone(); async move { - workspace_alice.create_buffer(buffer_name.clone(), false).await?; + workspace_alice + .create_buffer(buffer_name.clone(), false) + .await?; assert_or_err!(workspace_bob.delete_buffer(buffer_name).await.is_err()); Ok(()) } @@ -63,29 +67,42 @@ async fn test_workspace_interactions() { let client_alice = ClientFixture::of("alice").setup().await?; let client_bob = ClientFixture::of("bob").setup().await?; let workspace_name = format!("test-workspace-interactions-{}", uuid::Uuid::new_v4()); - let wsid = crate::api::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: workspace_name.clone() }; + let wsid = crate::api::WorkspaceIdentifier { + user: client_alice.current_user().name.clone(), + workspace: workspace_name.clone(), + }; - client_alice.create_workspace(workspace_name.clone()).await?; + client_alice + .create_workspace(workspace_name.clone()) + .await?; let owned_workspaces = client_alice.fetch_owned_workspaces().await?; assert_or_err!(owned_workspaces.contains(&wsid)); - client_alice.attach_workspace(&client_alice.current_user().name, &workspace_name).await?; + client_alice + .attach_workspace(&client_alice.current_user().name, &workspace_name) + .await?; assert_or_err!(vec![wsid.clone()] == client_alice.active_workspaces()); client_alice - .invite_to_workspace(workspace_name.clone(), client_bob.current_user().name.clone()) + .invite_to_workspace( + workspace_name.clone(), + client_bob.current_user().name.clone(), + ) + .await?; + client_bob + .attach_workspace(&client_alice.current_user().name, &workspace_name) .await?; - client_bob.attach_workspace(&client_alice.current_user().name, &workspace_name).await?; + assert_or_err!(client_bob.fetch_joined_workspaces().await?.contains(&wsid)); + assert_or_err!( - client_bob - .fetch_joined_workspaces() - .await? - .contains(&wsid) + client_bob.leave_workspace(&client_alice.current_user().name, &workspace_name) + ); + assert_or_err!( + client_alice.leave_workspace(&client_alice.current_user().name, &workspace_name) ); - assert_or_err!(client_bob.leave_workspace(&client_alice.current_user().name, &workspace_name)); - assert_or_err!(client_alice.leave_workspace(&client_alice.current_user().name, &workspace_name)); - - client_alice.delete_workspace(workspace_name.clone()).await?; + client_alice + .delete_workspace(workspace_name.clone()) + .await?; Ok::<(), Box>(()) } diff --git a/src/workspace.rs b/src/workspace.rs index 05d04e6e..8a8facf6 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -18,9 +18,11 @@ use codemp_proto::{ common::Empty, files::{BufferNode, BufferPath}, workspace::{ - WorkspaceEvent, workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace - } + WorkspaceEvent, + workspace_event::{ + Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, + UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace, + }, }, }; @@ -94,8 +96,7 @@ impl Workspace { workspace_claim: tokio::sync::watch::Receiver, user_claim: tokio::sync::watch::Receiver, ) -> ConnectionResult { - let services = - Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; + let services = Services::try_new(&config.endpoint(), user_claim, workspace_claim).await?; let ws_stream = services.ws().attach(Empty {}).await?.into_inner(); let (tx, rx) = mpsc::channel(128); @@ -109,7 +110,13 @@ impl Workspace { .into_inner(); let users = Arc::new(DashMap::default()); - let controller = cursor::Controller::spawn(users.clone(), tx, cur_stream, id.clone(), services.cur().clone()); + let controller = cursor::Controller::spawn( + users.clone(), + tx, + cur_stream, + id.clone(), + services.cur().clone(), + ); let ws = Self(Arc::new(WorkspaceInner { id: id.clone(), @@ -170,14 +177,19 @@ impl Workspace { // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( path.to_string(), - crate::api::BufferNode { path: path.to_string(), ephemeral }, + crate::api::BufferNode { + path: path.to_string(), + ephemeral, + }, ); Ok(()) } pub async fn pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { - self.0.services.ws() + self.0 + .services + .ws() .clone() .pin_buffer(BufferPath::from(path.as_ref())) .await?; @@ -185,7 +197,9 @@ impl Workspace { } pub async fn un_pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { - self.0.services.ws() + self.0 + .services + .ws() .clone() .un_pin_buffer(BufferPath::from(path.as_ref())) .await?; @@ -198,15 +212,24 @@ impl Workspace { let path = path.to_string(); let mut workspace_client = self.0.services.ws(); let mut buffer_client = self.0.services.buf(); - let credentials = workspace_client.get_buffer_token(BufferPath::from(&path)).await?.into_inner(); + let credentials = workspace_client + .get_buffer_token(BufferPath::from(&path)) + .await? + .into_inner(); let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); - req.metadata_mut().insert("buffer", crate::ext::token_to_metadata(credentials)?); + req.metadata_mut() + .insert("buffer", crate::ext::token_to_metadata(credentials)?); let stream = buffer_client.attach(req).await?.into_inner(); - let controller = - buffer::Controller::spawn(self.0.current_user.name.clone(), path.clone(), tx, stream, self.0.id.clone()); + let controller = buffer::Controller::spawn( + self.0.current_user.name.clone(), + path.clone(), + tx, + stream, + self.0.id.clone(), + ); self.0.buffers.insert(path.clone(), controller.clone()); @@ -217,12 +240,17 @@ impl Workspace { loop { // TODO either configurable token refresh time or calculate depending on token lifetime tokio::time::sleep(std::time::Duration::from_secs(20)).await; - if weak.upgrade().is_none() { break }; - let new_credentials = workspace_client.get_buffer_token(BufferPath::from(&_path)) + if weak.upgrade().is_none() { + break; + }; + let new_credentials = workspace_client + .get_buffer_token(BufferPath::from(&_path)) .await? .into_inner(); let mut request = tonic::Request::new(Empty {}); - request.metadata_mut().insert("buffer", crate::ext::token_to_metadata(new_credentials)?); + request + .metadata_mut() + .insert("buffer", crate::ext::token_to_metadata(new_credentials)?); buffer_client.keep_alive(request).await?; } Ok::<(), tonic::Status>(()) @@ -258,10 +286,7 @@ impl Workspace { /// Re-fetch the list of available buffers in the workspace. pub async fn fetch_buffers(&self) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); - let resp = workspace_client - .fetch_buffers(Empty {}) - .await? - .into_inner(); + let resp = workspace_client.fetch_buffers(Empty {}).await?.into_inner(); self.0.filetree.clear(); for b in resp.buffers { @@ -276,15 +301,14 @@ impl Workspace { /// Re-fetch the list of all users in the workspace. pub async fn fetch_users(&self) -> RemoteResult<()> { let mut workspace_client = self.services().ws(); - let resp = workspace_client - .fetch_users(Empty {}) - .await? - .into_inner(); + let resp = workspace_client.fetch_users(Empty {}).await?.into_inner(); self.0.users.clear(); for user_name in resp.users { // TODO need to fetch whole user profiles here maybe? - self.0.users.insert(user_name.clone(), UserInfo::default_for(user_name)); + self.0 + .users + .insert(user_name.clone(), UserInfo::default_for(user_name)); } Ok(()) @@ -293,7 +317,9 @@ impl Workspace { /// Fetch a list of the [User]s attached to a specific buffer. pub async fn fetch_buffer_users(&self, path: impl ToString) -> RemoteResult<()> { let path = path.to_string(); - let resp = self.services().ws() + let resp = self + .services() + .ws() .fetch_buffer_users(BufferPath::from(&path)) .await? .into_inner(); @@ -383,7 +409,6 @@ impl Workspace { } } - struct WorkspaceWorker { callback: watch::Receiver>>, pollers: Vec>, From 892270df745a5718c6d56dd2c3fe010a51fa2d86 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 7 Mar 2026 22:36:55 +0100 Subject: [PATCH 043/121] fix: ughhh merges!! --- src/api/event.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/api/event.rs b/src/api/event.rs index 33ca7e63..0c64a0d6 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -39,14 +39,6 @@ impl From for Event { name: e.user, buffer: e.buffer, }, - WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { - name: e.user, - buffer: e.buffer, - }, - WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { - name: e.user.name, - buffer: e.buffer, - }, } } } From 2d96ce592898182acb97f89f57e267522125dffa Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 8 Mar 2026 09:16:07 +0100 Subject: [PATCH 044/121] fix: fix Partial order implementation to depend on the Ord trait implementation fix: get_user_info correctly returns a crate::Api::UserInfo object instead of the protobuf counterpart. --- src/api/user.rs | 2 +- src/client.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/api/user.rs b/src/api/user.rs index 89351a55..0f7f66d6 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -60,7 +60,7 @@ impl Eq for UserInfo {} impl PartialOrd for UserInfo { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.name.cmp(&other.name)) + Some(self.cmp(other)) } } diff --git a/src/client.rs b/src/client.rs index 862ec8ac..1a8850d3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -249,10 +249,8 @@ impl Client { .collect()) } - pub async fn get_user_info( - &self, - user: impl ToString, - ) -> RemoteResult { + /// Get the meta information for a user + pub async fn get_user_info(&self, user: impl ToString) -> RemoteResult { Ok(self .0 .session @@ -261,7 +259,8 @@ impl Client { user: user.to_string(), }) .await? - .into_inner()) + .into_inner() + .into()) } /// Join and return a [`Workspace`]. From 7369d01e2aca4a73706960857bde51838b6ecf68 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 8 Mar 2026 09:16:07 +0100 Subject: [PATCH 045/121] py/chore: update the ffi to reflect latest changes --- src/ffi/python/client.rs | 100 ++++++++++++++++++++++++++---------- src/ffi/python/mod.rs | 64 +++++++++++++---------- src/ffi/python/workspace.rs | 28 +++++++--- src/tests/mod.rs | 3 ++ 4 files changed, 134 insertions(+), 61 deletions(-) diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index 5394e849..82210d26 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -1,9 +1,9 @@ use super::Client; use super::a_sync_detach; -use crate::api::User; +use crate::api::UserInfo; +use crate::api::WorkspaceIdentifier; use crate::workspace::Workspace; use pyo3::prelude::*; -use uuid::Uuid; #[pymethods] impl Client { @@ -16,18 +16,11 @@ impl Client { // super::tokio().block_on(Client::connect(host, username, password)) // } - #[pyo3(name = "attach_workspace")] - fn pyattach_workspace(&self, py: Python<'_>, workspace: Uuid) -> PyResult { - tracing::info!("attempting to join the workspace {}", workspace); + #[pyo3(name = "refresh")] + fn pyrefresh(&self, py: Python<'_>) -> PyResult { + tracing::info!("attempting to refresh token"); let this = self.clone(); - a_sync_detach!(py, this.attach_workspace(workspace).await) - // let this = self.clone(); - // Ok(super::Promise(Some(tokio().spawn(async move { - // Ok(this - // .join_workspace(workspace) - // .await - // .map(|f| Python::attach(|py| f.into_py(py)))?) - // })))) + a_sync_detach!(py, this.refresh().await) } #[pyo3(name = "create_workspace")] @@ -44,6 +37,18 @@ impl Client { a_sync_detach!(py, this.delete_workspace(workspace).await) } + #[pyo3(name = "quit_workspace")] + fn pyquit_workspace( + &self, + py: Python<'_>, + user: String, + workspace: String, + ) -> PyResult { + tracing::info!("quitting workspace {}", workspace); + let this = self.clone(); + a_sync_detach!(py, this.quit_workspace(user, workspace).await) + } + #[pyo3(name = "invite_to_workspace")] fn pyinvite_to_workspace( &self, @@ -56,6 +61,30 @@ impl Client { a_sync_detach!(py, this.invite_to_workspace(workspace, user).await) } + #[pyo3(name = "accept_invite")] + fn pyaccept_invite( + &self, + py: Python<'_>, + user: String, + workspace: String, + ) -> PyResult { + tracing::info!("Invite to workspace {user}::{workspace} accepted."); + let this = self.clone(); + a_sync_detach!(py, this.accept_invite(user, workspace).await) + } + + #[pyo3(name = "reject_invite")] + fn pyreject_invite( + &self, + py: Python<'_>, + user: String, + workspace: String, + ) -> PyResult { + tracing::info!("Invite to workspace {user}::{workspace} rejected."); + let this = self.clone(); + a_sync_detach!(py, this.reject_invite(user, workspace).await) + } + #[pyo3(name = "fetch_owned_workspaces")] fn pyfetch_owned_workspaces(&self, py: Python<'_>) -> PyResult { tracing::info!("fetching owned workspaces"); @@ -70,31 +99,50 @@ impl Client { a_sync_detach!(py, this.fetch_joined_workspaces().await) } - #[pyo3(name = "leave_workspace")] - fn pyleave_workspace(&self, id: Uuid) -> bool { - self.leave_workspace(id) - } - // join a workspace #[pyo3(name = "get_workspace")] - fn pyget_workspace(&self, id: Uuid) -> Option { - self.get_workspace(id) + fn pyget_workspace(&self, user: String, workspace: String) -> Option { + self.get_workspace(user, workspace) } #[pyo3(name = "active_workspaces")] - fn pyactive_workspaces(&self) -> Vec { + fn pyactive_workspaces(&self) -> Vec { self.active_workspaces() } + #[pyo3(name = "get_user_info")] + fn pyget_user_info(&self, py: Python<'_>, user: String) -> PyResult { + tracing::info!("fetching joined workspaces"); + let this = self.clone(); + a_sync_detach!(py, this.get_user_info(user).await) + } + #[pyo3(name = "current_user")] - fn pycurrent_user(&self) -> User { + fn pycurrent_user(&self) -> UserInfo { self.current_user().clone() } - #[pyo3(name = "refresh")] - fn pyrefresh(&self, py: Python<'_>) -> PyResult { - tracing::info!("attempting to refresh token"); + #[pyo3(name = "attach_workspace")] + fn pyattach_workspace( + &self, + py: Python<'_>, + user: String, + workspace: String, + ) -> PyResult { + tracing::info!("attempting to join the workspace {}", workspace); let this = self.clone(); - a_sync_detach!(py, this.refresh().await) + a_sync_detach!(py, this.attach_workspace(user, workspace).await) + // let this = self.clone(); + // Ok(super::Promise(Some(tokio().spawn(async move { + // Ok(this + // .join_workspace(workspace) + // .await + // .map(|f| Python::attach(|py| f.into_py(py)))?) + // })))) + } + + #[pyo3(name = "leave_workspace")] + fn pyleave_workspace(&self, user: String, workspace: String) -> bool { + self.leave_workspace(user, workspace) } } diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 77b02c31..5017cd4d 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -4,7 +4,7 @@ pub mod workspace; use crate::{ Client, Workspace, - api::{BufferUpdate, Config, Cursor, Event, Selection, TextChange, User}, + api::{BufferUpdate, Config, Cursor, Event, Selection, TextChange, UserInfo}, buffer::Controller as BufferController, cursor::Controller as CursorController, }; @@ -18,14 +18,14 @@ use pyo3::{ use std::sync::OnceLock; use tokio::sync::{mpsc, oneshot}; -// global reference to a current_thread tokio runtime +/// global reference to a current_thread tokio runtime pub fn tokio() -> &'static tokio::runtime::Runtime { static RT: OnceLock = OnceLock::new(); RT.get_or_init(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .unwrap() + .expect("Failed to start the tokio runtime!") }) } @@ -48,6 +48,8 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { // let _ = CREATE_FUTURE.get_or_init(|| create_future); // } +/// Implements a simple future like object between python and rust to allow for async operations +/// between the two runtimes. #[pyclass] pub struct Promise(Option>>>); @@ -81,18 +83,20 @@ impl Promise { } } -macro_rules! a_sync { - ($x:expr) => {{ - Ok($crate::ffi::python::Promise(Some( - $crate::ffi::python::tokio().spawn(async move { - let res = $x?; - Python::attach(|py| Ok(res.into_pyobject(py)?.into_any().unbind())) - }), - ))) - }}; -} +// macro_rules! a_sync { +// ($x:expr) => {{ +// Ok($crate::ffi::python::Promise(Some( +// $crate::ffi::python::tokio().spawn(async move { +// let res = $x?; +// Python::attach(|py| Ok(res.into_pyobject(py)?.into_any().unbind())) +// }), +// ))) +// }}; +// } //pub(crate) use a_sync; +/// creates a future that will run detached from the gil, up until when it will need to resolve itself. +/// at which point it will wait for the gil to become available, attach itself and populate the promise macro_rules! a_sync_detach { ($py:ident, $x:expr) => {{ $py.detach(move || { @@ -121,6 +125,7 @@ impl std::io::Write for LoggerProducer { } } +/// This is a helper struct that allows for managing (i.e. stop the rust tokio runtime) from python directly. #[pyclass] pub struct Driver(Option>); #[pymethods] @@ -158,30 +163,35 @@ fn init() -> PyResult { } #[pymethods] -impl User { +impl UserInfo { #[getter] - fn get_id(&self) -> pyo3::PyResult { - Ok(self.id.to_string()) + fn get_name(&self) -> pyo3::PyResult { + Ok(self.name.clone()) + } + + #[getter] + fn get_display_name(&self) -> pyo3::PyResult { + Ok(self.display_name.clone().unwrap_or(self.name.clone())) } #[setter] - fn set_id(&mut self, value: String) -> pyo3::PyResult<()> { - self.id = value - .parse() - .map_err(|x: ::Err| { - pyo3::exceptions::PyRuntimeError::new_err(x.to_string()) - })?; + fn set_display_name(&mut self, value: String) -> pyo3::PyResult<()> { + self.display_name.replace(value); + Ok(()) } #[getter] - fn get_name(&self) -> pyo3::PyResult { - Ok(self.name.clone()) + fn get_description(&self) -> pyo3::PyResult { + Ok(self + .description + .clone() + .unwrap_or("No description.".to_string())) } #[setter] - fn set_name(&mut self, value: String) -> pyo3::PyResult<()> { - self.name = value; + fn set_description(&mut self, value: String) -> pyo3::PyResult<()> { + self.description.replace(value); Ok(()) } @@ -385,7 +395,7 @@ fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index 82bf39d4..16f13689 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -1,4 +1,4 @@ -use crate::api::User; +use crate::api::UserInfo; use crate::api::controller::AsyncReceiver; use crate::buffer::Controller as BufferController; use crate::cursor::Controller as CursorController; @@ -18,6 +18,18 @@ impl Workspace { a_sync_detach!(py, this.create_buffer(path.as_str(), ephemeral).await) } + #[pyo3(name = "pin_buffer")] + fn pypin_buffer(&self, py: Python, path: String) -> PyResult { + let this = self.clone(); + a_sync_detach!(py, this.pin_buffer(path.as_str()).await) + } + + #[pyo3(name = "un_pin_buffer")] + fn pyun_pin_buffer(&self, py: Python, path: String) -> PyResult { + let this = self.clone(); + a_sync_detach!(py, this.un_pin_buffer(path.as_str()).await) + } + #[pyo3(name = "attach_buffer")] fn pyattach_buffer(&self, py: Python, path: String) -> PyResult { let this = self.clone(); @@ -30,19 +42,19 @@ impl Workspace { } #[pyo3(name = "fetch_buffers")] - fn pylist_buffers(&self, py: Python, filter: String) -> PyResult { + fn pyfetch_buffers(&self, py: Python) -> PyResult { let this = self.clone(); - a_sync_detach!(py, this.fetch_buffers(filter).await) + a_sync_detach!(py, this.fetch_buffers().await) } #[pyo3(name = "fetch_users")] - fn pylist_users(&self, py: Python) -> PyResult { + fn pyfetch_users(&self, py: Python) -> PyResult { let this = self.clone(); a_sync_detach!(py, this.fetch_users().await) } #[pyo3(name = "fetch_buffer_users")] - fn pylist_buffer_users(&self, py: Python, path: String) -> PyResult { + fn pyfetch_buffer_users(&self, py: Python, path: String) -> PyResult { // crate::Result> let this = self.clone(); a_sync_detach!(py, this.fetch_buffer_users(path).await) @@ -55,8 +67,8 @@ impl Workspace { } #[pyo3(name = "id")] - fn pyid(&self) -> uuid::Uuid { - self.id() + fn pyid(&self) -> crate::api::WorkspaceIdentifier { + self.id().clone() } #[pyo3(name = "cursor")] @@ -80,7 +92,7 @@ impl Workspace { } #[pyo3(name = "user_list")] - fn pyuser_list(&self) -> Vec { + fn pyuser_list(&self) -> Vec { self.user_list() } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d6a17c64..1f1a53ee 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -6,6 +6,9 @@ mod client; #[cfg(all(test, feature = "test-e2e"))] mod server; +#[cfg(test)] +mod ffi_coverage; + pub mod fixtures; use crate::errors::{ConnectionError, RemoteError}; From 0dd87aac851e19e0b89e330346c913d839d6c0c4 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 8 Mar 2026 16:43:17 +0100 Subject: [PATCH 046/121] py/chore: fix type hints --- dist/py/src/codemp/codemp.pyi | 401 +++++++++++++++++++++------------- 1 file changed, 245 insertions(+), 156 deletions(-) diff --git a/dist/py/src/codemp/codemp.pyi b/dist/py/src/codemp/codemp.pyi index eeda90e4..69cee0f1 100644 --- a/dist/py/src/codemp/codemp.pyi +++ b/dist/py/src/codemp/codemp.pyi @@ -1,196 +1,285 @@ -from typing import Optional, Callable -from uuid import uuid4, UUID +from typing import Callable, Generic, Optional, TypeVar + +T = TypeVar("T") + def version() -> str: ... +def init() -> Driver: ... +def set_logger(logger_cb: Callable[[str], None], debug: bool) -> bool: ... +def connect(config: Config) -> Promise[Client]: ... + class Driver: - """ - this is akin to a big red button with a white "STOP" on top of it. - it is used to stop the runtime. - """ - def stop(self) -> None: ... - -class User: - """ - A remote user, with uuid and username - """ - id: str - name: str + """ + This is akin to a big red button with a white "STOP" on top of it. + It is used to stop the runtime. + """ -class Config: - """ - Configuration data structure for codemp clients - """ - username: str - password: str - host: Optional[str] - port: Optional[int] - tls: Optional[bool] + def stop(self) -> None: ... - def __new__(cls, *, username: str, password: str, **kwargs) -> Config: ... -def init() -> Driver: ... -def set_logger(logger_cb: Callable[[str], None], debug: bool) -> bool: ... -def connect(config: Config) -> Promise[Client]: ... +class Promise(Generic[T]): + """ + Future-like object for async operations started in Rust. + Call `wait()` to block until completion. + """ + + def wait(self) -> T: ... + def done(self) -> bool: ... + + +class WorkspaceIdentifier: + """ + Workspace identifier made of owner username and workspace name. + """ + + user: str + workspace: str -class Promise[T]: - """ - This is a class akin to a future, which wraps a join handle from a spawned - task on the rust side. you may call .pyawait() on this promise to block - until we have a result, or return immediately if we already have one. - This only goes one way rust -> python. - It can either be used directly or you can wrap it inside a future python side. - """ - def wait(self) -> T: ... - def is_done(self) -> bool: ... +class UserInfo: + """ + A remote user profile. + """ + + @property + def name(self) -> str: ... + + @property + def display_name(self) -> str: ... + + @display_name.setter + def display_name(self, value: str) -> None: ... + + @property + def description(self) -> str: ... + + @description.setter + def description(self, value: str) -> None: ... + + +class Config: + """ + Configuration data structure for codemp clients. + """ + + username: str + password: str + host: Optional[str] + port: Optional[int] + tls: Optional[bool] + + def __new__( + cls, + *, + username: str, + password: str, + host: Optional[str] = ..., + port: Optional[int] = ..., + tls: Optional[bool] = ..., + ) -> Config: ... + class Client: - """ - Handle to the actual client that manages the session. It manages the connection - to a server and joining/creating new workspaces - """ - def attach_workspace(self, workspace: UUID) -> Promise[Workspace]: ... - def create_workspace(self, workspace: str) -> Promise[None]: ... - def delete_workspace(self, workspace: str) -> Promise[None]: ... - def invite_to_workspace(self, workspace: str, username: str) -> Promise[None]: ... - def fetch_owned_workspaces(self) -> Promise[list[str]]: ... - def fetch_joined_workspaces(self) -> Promise[list[str]]: ... - def leave_workspace(self, workspace: UUID) -> bool: ... - def get_workspace(self, id: UUID) -> Workspace: ... - def active_workspaces(self) -> list[UUID]: ... - def current_user(self) -> User: ... - def refresh(self) -> Promise[None]: ... + """ + Handle to the session client. Manages workspaces and account operations. + """ + + def refresh(self) -> Promise[None]: ... + def create_workspace(self, workspace: str) -> Promise[None]: ... + def delete_workspace(self, workspace: str) -> Promise[None]: ... + def quit_workspace(self, user: str, workspace: str) -> Promise[None]: ... + def invite_to_workspace(self, workspace: str, user: str) -> Promise[None]: ... + def accept_invite(self, user: str, workspace: str) -> Promise[None]: ... + def reject_invite(self, user: str, workspace: str) -> Promise[None]: ... + def fetch_owned_workspaces(self) -> Promise[list[WorkspaceIdentifier]]: ... + def fetch_joined_workspaces(self) -> Promise[list[WorkspaceIdentifier]]: ... + def get_workspace(self, user: str, workspace: str) -> Optional[Workspace]: ... + def active_workspaces(self) -> list[WorkspaceIdentifier]: ... + def get_user_info(self, user: str) -> Promise[UserInfo]: ... + def current_user(self) -> UserInfo: ... + def attach_workspace(self, user: str, workspace: str) -> Promise[Workspace]: ... + def leave_workspace(self, user: str, workspace: str) -> bool: ... + class FileTreeUpdated: - """ - Fired when the file tree changes. - Contains the modified buffer path (deleted, created or renamed) - """ - path: str + """ + Fired when the file tree changes. + Contains the modified buffer path (deleted, created or renamed). + """ + + path: str + class UserJoin: - """ - Fired when a user joins the workspace - """ - name: str + """ + Fired when a user joins the workspace. + """ + + name: str + class UserLeave: - """ - Fired when a user leaves the workspace - """ - name: str + """ + Fired when a user leaves the workspace. + """ + + name: str + + +class UserJoinBuffer: + """ + Fired when a user joins a specific buffer. + """ + + name: str + buffer: str + + +class UserLeaveBuffer: + """ + Fired when a user leaves a specific buffer. + """ + + name: str + buffer: str + class Event: - """ - Workspace events to notify users of changes happening in the workspace. - """ - FileTreeUpdated: FileTreeUpdated - UserJoin: UserJoin - UserLeave: UserLeave + """ + Workspace events to notify users of changes happening in the workspace. + """ + + FileTreeUpdated: FileTreeUpdated + UserJoin: UserJoin + UserLeave: UserLeave + UserJoinBuffer: UserJoinBuffer + UserLeaveBuffer: UserLeaveBuffer + class Workspace: - """ - Handle to a workspace inside codemp. It manages buffers. - A cursor is tied to the single workspace. - """ - def create_buffer(self, path: str, ephemeral: str) -> Promise[None]: ... - def attach_buffer(self, path: str) -> Promise[BufferController]: ... - def detach_buffer(self, path: str) -> bool: ... - def list_buffers(self, filter: str) -> Promise[list[str]]: ... - def list_users(self) -> Promise[list[User]]: ... - def list_buffer_users(self, path: str) -> Promise[list[User]]: ... - def delete_buffer(self, path: str) -> Promise[None]: ... - def id(self) -> str: ... - def cursor(self) -> CursorController: ... - def get_buffer(self, path: str) -> Optional[BufferController]: ... - def user_list(self) -> list[User]: ... - def active_buffers(self) -> list[str]: ... - def search_buffers(self, filter: Optional[str]) -> list[str]: ... - def recv(self) -> Promise[Event]: ... - def try_recv(self) -> Promise[Optional[Event]]: ... - def poll(self) -> Promise[None]: ... - def clear_callback(self) -> None: ... - def callback(self, cb: Callable[[Workspace], None]) -> None: ... + """ + Handle to a workspace. It manages buffers and workspace events. + """ + + def create_buffer(self, path: str, ephemeral: bool) -> Promise[None]: ... + def pin_buffer(self, path: str) -> Promise[None]: ... + def un_pin_buffer(self, path: str) -> Promise[None]: ... + def attach_buffer(self, path: str) -> Promise[BufferController]: ... + def detach_buffer(self, path: str) -> bool: ... + def fetch_buffers(self) -> Promise[None]: ... + def fetch_users(self) -> Promise[None]: ... + def fetch_buffer_users(self, path: str) -> Promise[None]: ... + def delete_buffer(self, path: str) -> Promise[None]: ... + def id(self) -> WorkspaceIdentifier: ... + def cursor(self) -> CursorController: ... + def get_buffer(self, path: str) -> Optional[BufferController]: ... + def active_buffers(self) -> list[str]: ... + def search_buffers(self, filter: Optional[str] = None) -> list[str]: ... + def user_list(self) -> list[UserInfo]: ... + def recv(self) -> Promise[Event]: ... + def try_recv(self) -> Promise[Optional[Event]]: ... + def poll(self) -> Promise[None]: ... + def clear_callback(self) -> None: ... + def callback(self, cb: Callable[[Workspace], None]) -> None: ... + class TextChange: - """ - Editor agnostic representation of a text change, it translate between internal - codemp text operations and editor operations - """ - start_idx: int - end_idx: int - content: str + """ + Editor-agnostic representation of a text change. + """ + + start_idx: int + end_idx: int + content: str - def __new__(cls, *, start: int, end: int, content: str, **kwargs): ... + def __new__( + cls, + *, + start: int, + end: int, + content: str, + ) -> TextChange: ... + + def is_delete(self) -> bool: ... + def is_insert(self) -> bool: ... + def is_empty(self) -> bool: ... + def apply(self, txt: str) -> str: ... - def is_delete(self) -> bool: ... - def is_insert(self) -> bool: ... - def is_empty(self) -> bool: ... - def apply(self, txt: str) -> str: ... class BufferUpdate: - """ - A single editor delta event, wrapping a TextChange and the new version - """ - change: TextChange - hash: Optional[int] - version: list[int] + """ + A single buffer delta event with version and optional post-change hash. + """ + + hash: Optional[int] + version: list[int] + change: TextChange class BufferController: - """ - Handle to the controller for a specific buffer, which manages the back and forth - of operations to and from other peers. - """ - def path(self) -> str: ... - def content(self) -> Promise[str]: ... - def ack(self, v: list[int]) -> None: ... - def send(self, op: TextChange) -> None: ... - def try_recv(self) -> Promise[Optional[BufferUpdate]]: ... - def recv(self) -> Promise[BufferUpdate]: ... - def poll(self) -> Promise[None]: ... - def callback(self, - cb: Callable[[BufferController], None]) -> None: ... - def clear_callback(self) -> None: ... + """ + Handle to a specific buffer controller. + """ + def path(self) -> str: ... + def content(self) -> Promise[str]: ... + def ack(self, v: list[int]) -> None: ... + def send(self, op: TextChange) -> None: ... + def try_recv(self) -> Promise[Optional[BufferUpdate]]: ... + def recv(self) -> Promise[BufferUpdate]: ... + def poll(self) -> Promise[None]: ... + def callback(self, cb: Callable[[BufferController], None]) -> None: ... + def clear_callback(self) -> None: ... class Selection: - """ - An Editor agnostic cursor position representation - """ - start_row: int - start_col: int - end_row: int - end_col: int - buffer: str - - def __new__(cls, * - start_row: int, - start_col: int, - end_row: int, - end_col: int, - buffer: str, **kwargs): ... + """ + Editor-agnostic cursor selection representation. + """ + + start_row: int + start_col: int + end_row: int + end_col: int + + def __new__( + cls, + *, + start_row: int, + start_col: int, + end_row: int, + end_col: int, + ) -> Selection: ... + class Cursor: - """ - A remote cursor event - """ - user: str - sel: Selection + """ + Cursor payload with flattened getters exposed by FFI. + """ + + start: list[tuple[int, int]] + end: list[tuple[int, int]] + buffer: str + + +class CursorEvent: + """ + Cursor event with sending user and cursor payload. + """ + + user: str + cursor: Cursor class CursorController: - """ - Handle to the controller for a workspace, which manages the back and forth of - cursor movements to and from other peers - """ - def send(self, pos: Selection) -> None: ... - def try_recv(self) -> Promise[Optional[Cursor]]: ... - def recv(self) -> Promise[Cursor]: ... - def poll(self) -> Promise[None]: ... - def callback(self, - cb: Callable[[CursorController], None]) -> None: ... - def clear_callback(self) -> None: ... + """ + Handle to the workspace cursor controller. + """ + def send(self, pos: Cursor) -> None: ... + def try_recv(self) -> Promise[Optional[CursorEvent]]: ... + def recv(self) -> Promise[CursorEvent]: ... + def poll(self) -> Promise[None]: ... + def callback(self, cb: Callable[[CursorController], None]) -> None: ... + def clear_callback(self) -> None: ... From 7f0b77accd51e6f9b8a23221a08ec5d94fdbdcf2 Mon Sep 17 00:00:00 2001 From: cschen Date: Mon, 9 Mar 2026 19:38:28 +0100 Subject: [PATCH 047/121] fix: leftover from bad rebase --- src/tests/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1f1a53ee..d6a17c64 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -6,9 +6,6 @@ mod client; #[cfg(all(test, feature = "test-e2e"))] mod server; -#[cfg(test)] -mod ffi_coverage; - pub mod fixtures; use crate::errors::{ConnectionError, RemoteError}; From be16f53391509ad9c1da0b2ce8e33e4634233f8e Mon Sep 17 00:00:00 2001 From: frelodev Date: Tue, 10 Mar 2026 00:29:51 +0100 Subject: [PATCH 048/121] fix(js): ffi glue --- src/ffi/js/client.rs | 45 +++++++++++++++++++++++++++++------------ src/ffi/js/cursor.rs | 15 ++++++++------ src/ffi/js/workspace.rs | 40 +++++++++++++++++++----------------- 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index b6034f69..62ab157a 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -5,23 +5,33 @@ use napi_derive::napi; pub struct JsUser { pub uuid: String, pub name: String, + pub display_name: Option, + pub description: Option, + pub avatar: Option>, } -impl TryFrom for crate::api::User { + +impl TryFrom for crate::api::UserInfo { type Error = ::Err; fn try_from(value: JsUser) -> Result { Ok(Self { - id: value.uuid.parse()?, name: value.name, + display_name: value.display_name, + description: value.description, + avatar: value.avatar, }) } } -impl From for JsUser { - fn from(value: crate::api::User) -> Self { + +impl From for JsUser { + fn from(value: crate::api::UserInfo) -> Self { Self { - uuid: value.id.to_string(), + uuid: String::new(), name: value.name, + display_name: value.display_name, + description: value.description, + avatar: value.avatar, } } } @@ -49,13 +59,19 @@ impl Client { #[napi(js_name = "fetchOwnedWorkspaces")] /// fetch owned workspaces pub async fn js_fetch_owned_workspaces(&self) -> napi::Result> { - Ok(self.fetch_owned_workspaces().await?) + Ok(self.fetch_owned_workspaces().await? + .into_iter() + .map(|w| w.to_string()) + .collect()) } #[napi(js_name = "fetchJoinedWorkspaces")] /// fetch joined workspaces pub async fn js_fetch_joined_workspaces(&self) -> napi::Result> { - Ok(self.fetch_joined_workspaces().await?) + Ok(self.fetch_joined_workspaces().await? + .into_iter() + .map(|w| w.to_string()) + .collect()) } #[napi(js_name = "inviteToWorkspace")] @@ -70,20 +86,20 @@ impl Client { #[napi(js_name = "attachWorkspace")] /// join workspace with given id (will start its cursor controller) - pub async fn js_attach_workspace(&self, workspace: String) -> napi::Result { - Ok(self.attach_workspace(workspace).await?) + pub async fn js_attach_workspace(&self, user: String, workspace: String) -> napi::Result { + Ok(self.attach_workspace(&user, &workspace).await?.into()) } #[napi(js_name = "leaveWorkspace")] /// leave workspace and disconnect, returns true if workspace was active - pub async fn js_leave_workspace(&self, workspace: String) -> bool { - self.leave_workspace(&workspace) + pub async fn js_leave_workspace(&self, user: String, workspace: String) -> bool { + self.leave_workspace(&user, workspace) } #[napi(js_name = "getWorkspace")] /// get workspace with given id, if it exists - pub fn js_get_workspace(&self, workspace: String) -> Option { - self.get_workspace(&workspace) + pub fn js_get_workspace(&self, user: String, workspace: String) -> Option { + self.get_workspace(&user, &workspace) } #[napi(js_name = "currentUser")] @@ -96,6 +112,9 @@ impl Client { /// get list of all active workspaces pub fn js_active_workspaces(&self) -> Vec { self.active_workspaces() + .into_iter() + .map(|w| w.to_string()) + .collect() } #[napi(js_name = "refresh")] diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 97837ae4..0c3b45cc 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -32,19 +32,22 @@ impl CursorController { /// Send a new cursor event to remote #[napi(js_name = "send")] - pub fn js_send(&self, sel: crate::api::Selection) -> napi::Result<()> { - Ok(self.send(sel)?) - } + pub fn js_send(&self, buffer: String, sel: crate::api::Selection) -> napi::Result<()> { + Ok(self.send(crate::api::Cursor { + buffer, + sel: vec![sel], + })?) +} /// Get next cursor event if available without blocking #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { - Ok(self.try_recv().await?.map(crate::api::Cursor::from)) + pub async fn js_try_recv(&self) -> napi::Result> { + Ok(self.try_recv().await?.map(crate::api::CursorEvent::from)) } /// Block until next #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index a46e70a1..d2b8cb21 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -11,6 +11,7 @@ use super::client::JsUser; pub struct JsEvent { pub r#type: String, pub value: String, + pub buffer: Option, } impl From for JsEvent { @@ -19,14 +20,27 @@ impl From for JsEvent { crate::api::Event::FileTreeUpdated { path: value } => Self { r#type: "filetree".into(), value, + buffer: None, }, crate::api::Event::UserJoin { name: value } => Self { r#type: "join".into(), value, + buffer: None, }, crate::api::Event::UserLeave { name: value } => Self { r#type: "leave".into(), value, + buffer: None, + }, + crate::api::Event::UserJoinBuffer { name: value, buffer} => Self { + r#type: "joinBuffer".into(), + value, + buffer: Some(buffer), + }, + crate::api::Event::UserLeaveBuffer { name: value, buffer} => Self { + r#type: "leaveBuffer".into(), + value, + buffer: Some(buffer), }, } } @@ -37,7 +51,7 @@ impl Workspace { /// Get the unique workspace id #[napi(js_name = "id")] pub fn js_id(&self) -> String { - self.id() + self.id().to_string() } /// List all available buffers in this workspace @@ -72,8 +86,8 @@ impl Workspace { /// Create a new buffer in the current workspace #[napi(js_name = "createBuffer")] - pub async fn js_create_buffer(&self, path: String) -> napi::Result<()> { - Ok(self.create_buffer(&path).await?) + pub async fn js_create_buffer(&self, path: String, ephemeral: bool) -> napi::Result<()> { + Ok(self.create_buffer(&path, ephemeral).await?) } /// Attach to a workspace buffer, starting a BufferController @@ -131,18 +145,13 @@ impl Workspace { /// Re-fetch remote buffer list #[napi(js_name = "fetchBuffers")] - pub async fn js_fetch_buffers(&self) -> napi::Result> { + pub async fn js_fetch_buffers(&self) -> napi::Result<()> { Ok(self.fetch_buffers().await?) } /// Re-fetch the list of all users in the workspace. #[napi(js_name = "fetchUsers")] - pub async fn js_fetch_users(&self) -> napi::Result> { - Ok(self - .fetch_users() - .await? - .into_iter() - .map(JsUser::from) - .collect()) + pub async fn js_fetch_users(&self) -> napi::Result<()> { + Ok(self.fetch_users().await?) } /// List users attached to a specific buffer @@ -150,12 +159,7 @@ impl Workspace { pub async fn js_fetch_buffer_users( &self, path: String, - ) -> napi::Result> { - Ok(self - .fetch_buffer_users(&path) - .await? - .into_iter() - .map(super::client::JsUser::from) - .collect()) + ) -> napi::Result<()> { + Ok(self.fetch_buffer_users(&path).await?) } } From c49ca052754b9a19340e4fcb5bbf68308a1862a9 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 10 Mar 2026 12:25:22 +0100 Subject: [PATCH 049/121] ci: for now, run coverage even if build fails we probably want to restore the order that was here before, since if an ffi doesn't build, its coverage means nothing --- .github/workflows/test.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a1c22730..0be0e4fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,24 +72,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo build --verbose --features=${{ matrix.features }} - test-functional: - needs: [test-build] - runs-on: ubuntu-latest - steps: - - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test --verbose --features=test-e2e tests::e2e - env: - CODEMP_TEST_USERNAME_ALICE: ${{ secrets.CODEMP_TEST_USERNAME_ALICE }} - CODEMP_TEST_PASSWORD_ALICE: ${{ secrets.CODEMP_TEST_PASSWORD_ALICE }} - CODEMP_TEST_USERNAME_BOB: ${{ secrets.CODEMP_TEST_USERNAME_BOB }} - CODEMP_TEST_PASSWORD_BOB: ${{ secrets.CODEMP_TEST_PASSWORD_BOB }} - test-coverage: - needs: [test-build] + needs: [test-unit] runs-on: ubuntu-latest strategy: fail-fast: false @@ -107,3 +91,19 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo test --no-default-features --features=ci,${{ matrix.features }},test-coverage,py-abi3,lua-jit tests::coverage + test-functional: + needs: [test-build] + runs-on: ubuntu-latest + steps: + - uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --verbose --features=test-e2e tests::e2e + env: + CODEMP_TEST_USERNAME_ALICE: ${{ secrets.CODEMP_TEST_USERNAME_ALICE }} + CODEMP_TEST_PASSWORD_ALICE: ${{ secrets.CODEMP_TEST_PASSWORD_ALICE }} + CODEMP_TEST_USERNAME_BOB: ${{ secrets.CODEMP_TEST_USERNAME_BOB }} + CODEMP_TEST_PASSWORD_BOB: ${{ secrets.CODEMP_TEST_PASSWORD_BOB }} + From 2b682f93a8c40bea61f0112ffa3e8b8a7ce1cf44 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 10 Mar 2026 16:36:03 +0100 Subject: [PATCH 050/121] fix(java): bump jni-toolbox, some initial work --- Cargo.lock | 149 +++++------------------------------------ Cargo.toml | 3 +- src/ffi/java/buffer.rs | 4 +- src/ffi/java/client.rs | 22 +++--- src/ffi/java/mod.rs | 12 ++-- 5 files changed, 39 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a807f714..f413c75e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,12 +136,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -169,7 +163,7 @@ dependencies = [ "codemp-proto", "dashmap", "diamond-types", - "jni 0.22.3", + "jni", "jni-toolbox", "mlua", "napi", @@ -179,7 +173,7 @@ dependencies = [ "pyo3-build-config", "serde", "syn 2.0.117", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tonic", @@ -749,22 +743,6 @@ dependencies = [ "glob", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.0", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.3" @@ -775,11 +753,11 @@ dependencies = [ "combine", "java-locator", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "libloading 0.8.9", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror", "walkdir", "windows-link", ] @@ -797,12 +775,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "jni-sys" version = "0.4.1" @@ -825,19 +797,18 @@ dependencies = [ [[package]] name = "jni-toolbox" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce03cf89bc32b81de142a323a71e9903ee88127a0e04bbd7f215ab74ab6b10a" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=f153192a71c8c031a68bdf30aeef8acdddf15788#f153192a71c8c031a68bdf30aeef8acdddf15788" dependencies = [ - "jni 0.21.1", + "jni", "jni-toolbox-macro", + "thiserror", "uuid", ] [[package]] name = "jni-toolbox-macro" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609491ce00edcf12946945a514d033bf6e8bfbab02c6a25a46ed8cd4749707da" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=f153192a71c8c031a68bdf30aeef8acdddf15788#f153192a71c8c031a68bdf30aeef8acdddf15788" dependencies = [ "proc-macro2", "quote", @@ -1820,33 +1791,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "thiserror-impl", ] [[package]] @@ -2370,22 +2321,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2397,67 +2339,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2470,48 +2379,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index dfb8d8d2..781f3df5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,8 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } -jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } +#jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "f153192a71c8c031a68bdf30aeef8acdddf15788", optional = true, features = ["uuid"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index e30a6718..71d4fba2 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -1,4 +1,4 @@ -use jni::{JNIEnv, objects::JObject}; +use jni::{Env, objects::JObject}; use jni_toolbox::jni; use crate::{ @@ -46,7 +46,7 @@ fn send( /// Register a callback for buffer changes. #[jni(package = "mp.code", class = "BufferController")] fn callback<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, controller: &mut crate::buffer::Controller, cb: JObject<'local>, ) { diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index ecd392f6..1ed4b086 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,6 +1,6 @@ use crate::{ Workspace, - api::Config, + api::{Config, WorkspaceIdentifier}, client::Client, errors::{ConnectionError, RemoteError}, }; @@ -14,14 +14,14 @@ fn connect(config: Config) -> Result { /// Gets the current [crate::api::User]. #[jni(package = "mp.code", class = "Client")] -fn current_user(client: &mut Client) -> crate::api::User { +fn current_user(client: &mut Client) -> crate::api::UserInfo { client.current_user().clone() } /// Join a [Workspace] and return a pointer to it. #[jni(package = "mp.code", class = "Client")] -fn attach_workspace(client: &mut Client, workspace: String) -> Result { - super::tokio().block_on(client.attach_workspace(workspace)) +fn attach_workspace(client: &mut Client, user: String, workspace: String) -> Result { + super::tokio().block_on(client.attach_workspace(user, workspace)) } /// Create a workspace on server, if allowed to. @@ -48,32 +48,32 @@ fn invite_to_workspace( /// List owned workspaces. #[jni(package = "mp.code", class = "Client")] -fn fetch_owned_workspaces(client: &mut Client) -> Result, RemoteError> { +fn fetch_owned_workspaces(client: &mut Client) -> Result, RemoteError> { super::tokio().block_on(client.fetch_owned_workspaces()) } /// List joined workspaces. #[jni(package = "mp.code", class = "Client")] -fn fetch_joined_workspaces(client: &mut Client) -> Result, RemoteError> { +fn fetch_joined_workspaces(client: &mut Client) -> Result, RemoteError> { super::tokio().block_on(client.fetch_joined_workspaces()) } /// List available workspaces. #[jni(package = "mp.code", class = "Client")] -fn active_workspaces(client: &mut Client) -> Vec { +fn active_workspaces(client: &mut Client) -> Vec { client.active_workspaces() } /// Leave a [Workspace] and return whether or not the client was in such workspace. #[jni(package = "mp.code", class = "Client")] -fn leave_workspace(client: &mut Client, workspace: String) -> bool { - client.leave_workspace(&workspace) +fn leave_workspace(client: &mut Client, user: String, workspace: String) -> bool { + client.leave_workspace(user, workspace) } /// Get a [Workspace] by name and returns a pointer to it. #[jni(package = "mp.code", class = "Client")] -fn get_workspace(client: &mut Client, workspace: String) -> Option { - client.get_workspace(&workspace) +fn get_workspace(client: &mut Client, user: String, workspace: String) -> Option { + client.get_workspace(user, workspace) } /// Refresh the client's session token. diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index bd88f7ab..43aab2d7 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -82,17 +82,19 @@ macro_rules! null_check { pub(crate) use null_check; -impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError { - fn jclass(&self) -> String { - match self { +impl From for jni_toolbox::Error { + fn from(value: crate::errors::ConnectionError) -> Self { + let clazz = match self { crate::errors::ConnectionError::Transport(_) => { "mp/code/exceptions/ConnectionTransportException" } crate::errors::ConnectionError::Remote(_) => { "mp/code/exceptions/ConnectionRemoteException" } - } - .to_string() + }; + let message = Some(format!("{value} -- {value:?}")); + + Self { message, clazz } } } From 9b1681c89e969ae1fd5c6b3ddaeacef24f7d47b9 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 10 Mar 2026 17:38:27 +0100 Subject: [PATCH 051/121] fix(java): more glue code work --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/ffi/java/buffer.rs | 53 ++++++++++++--------------- src/ffi/java/cursor.rs | 64 +++++++++++++++----------------- src/ffi/java/mod.rs | 57 +++++++++++++---------------- src/ffi/java/workspace.rs | 77 ++++++++++++++++++--------------------- 6 files changed, 116 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f413c75e..ce4f3e5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "jni-toolbox" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=f153192a71c8c031a68bdf30aeef8acdddf15788#f153192a71c8c031a68bdf30aeef8acdddf15788" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=bbc84322d9c7d93b2af62f595187f5c04754f1cf#bbc84322d9c7d93b2af62f595187f5c04754f1cf" dependencies = [ "jni", "jni-toolbox-macro", @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "jni-toolbox-macro" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=f153192a71c8c031a68bdf30aeef8acdddf15788#f153192a71c8c031a68bdf30aeef8acdddf15788" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=bbc84322d9c7d93b2af62f595187f5c04754f1cf#bbc84322d9c7d93b2af62f595187f5c04754f1cf" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 781f3df5..d45d02e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "f153192a71c8c031a68bdf30aeef8acdddf15788", optional = true, features = ["uuid"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "bbc84322d9c7d93b2af62f595187f5c04754f1cf", optional = true, features = ["uuid"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 71d4fba2..41cdd076 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -6,8 +6,6 @@ use crate::{ errors::ControllerError, }; -use super::null_check; - /// Get the name of the buffer. #[jni(package = "mp.code", class = "BufferController")] fn get_name(controller: &mut crate::buffer::Controller) -> String { @@ -49,39 +47,34 @@ fn callback<'local>( env: &mut Env<'local>, controller: &mut crate::buffer::Controller, cb: JObject<'local>, -) { - null_check!(env, cb, {}); - 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; - }; +) -> Result<(), jni::errors::Error> { + if cb.is_null() { + return Err(jni::errors::Error::NullPtr("null pointer to buffer callback")); + } + + let cb_ref = env.new_global_ref(cb)?; controller.callback(move |controller: crate::buffer::Controller| { - let jvm = super::jvm(); - let mut env = jvm - .attach_current_thread_permanently() - .expect("failed attaching to main JVM thread"); - if let Err(e) = env.with_local_frame(5, |env| { - use jni_toolbox::IntoJavaObject; - let jcontroller = controller.into_java_object(env)?; - if let Err(e) = env.call_method( - &cb_ref, - "accept", - "(Ljava/lang/Object;)V", - &[jni::objects::JValueGen::Object(&jcontroller)], - ) { - tracing::error!("error invoking callback: {e:?}"); - }; - Ok::<(), jni::errors::Error>(()) + if let Err(e) = super::jvm().attach_current_thread(|mut env| { + env.with_local_frame(5, |env| { + use jni_toolbox::IntoJavaObject; + let jcontroller = controller.into_java_object(env)?; + env.call_method( + &cb_ref, + jni::jni_str!("accept"), + jni::jni_sig!((buf: java.lang.Object) -> ()), + &[jni::objects::JValue::Object(&jcontroller)], + )?; + Ok(()) + })?; + + Ok(()) }) { - tracing::error!("error invoking callback: {e}"); - let _ = env.exception_describe(); + tracing::error!("error invoking buffer callback: {e}"); } }); + + Ok(()) } /// Clear the callback for buffer changes. diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 60dae442..87d5bca5 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,69 +1,63 @@ use crate::{ - api::{AsyncReceiver, AsyncSender, Cursor, Selection}, + api::{AsyncReceiver, AsyncSender, Cursor, CursorEvent}, errors::ControllerError, }; -use jni::{JNIEnv, objects::JObject}; +use jni::{Env, objects::JObject}; use jni_toolbox::jni; use super::null_check; /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] -fn try_recv(controller: &mut crate::cursor::Controller) -> Result, ControllerError> { +fn try_recv(controller: &mut crate::cursor::Controller) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } /// Block until it receives a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn recv(controller: &mut crate::cursor::Controller) -> Result { +fn recv(controller: &mut crate::cursor::Controller) -> Result { super::tokio().block_on(controller.recv()) } /// Receive from Java, converts and sends a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn send(controller: &mut crate::cursor::Controller, sel: Selection) -> Result<(), ControllerError> { +fn send(controller: &mut crate::cursor::Controller, sel: Cursor) -> Result<(), ControllerError> { controller.send(sel) } /// Register a callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] fn callback<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, controller: &mut crate::cursor::Controller, cb: JObject<'local>, -) { - null_check!(env, cb, {}); - 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; - }; +) -> Result<(), jni::errors::Error> { + null_check!(cb); + let cb_ref = env.new_global_ref(cb)?; controller.callback(move |controller: crate::cursor::Controller| { - let jvm = super::jvm(); - let mut env = jvm - .attach_current_thread_permanently() - .expect("failed attaching to main JVM thread"); - if let Err(e) = env.with_local_frame(5, |env| { - use jni_toolbox::IntoJavaObject; - let jcontroller = controller.into_java_object(env)?; - if let Err(e) = env.call_method( - &cb_ref, - "accept", - "(Ljava/lang/Object;)V", - &[jni::objects::JValueGen::Object(&jcontroller)], - ) { - tracing::error!("error invoking callback: {e:?}"); - }; - Ok::<(), jni::errors::Error>(()) - }) { - tracing::error!("error invoking callback: {e}"); - let _ = env.exception_describe(); + let res = super::jvm().attach_current_thread(|mut env| { + env.with_local_frame(5, |env| { + use jni_toolbox::IntoJavaObject; + let jcontroller = controller.into_java_object(env)?; + env.call_method( + &cb_ref, + jni::jni_str!("accept"), + jni::jni_sig!((arg1: java.lang.Object) -> ()), + &[jni::objects::JValue::Object(&jcontroller)], + )?; + Ok(()) + })?; + + Ok(()) + }); + + if let Err(e) = res { + tracing::error!("error invoking cursor callback: {e}"); } }); + + Ok(()) } /// Clear the callback for cursor changes. diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 43aab2d7..c96d387f 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -69,43 +69,38 @@ pub(crate) fn setup_logger(debug: bool, path: Option) { /// if it is null. Finally, it returns with the given default value. macro_rules! null_check { // TODO replace - ($env: ident, $var: ident, $return: expr) => { + ($var: ident) => { if $var.is_null() { - let mut message = stringify!($var).to_string(); - message.push_str(" cannot be null!"); - $env.throw_new("java/lang/NullPointerException", message) - .expect("Failed to throw exception!"); - return $return; + return Err(jni::errors::Error::NullPtr(stringify!($var))); } }; } pub(crate) use null_check; -impl From for jni_toolbox::Error { - fn from(value: crate::errors::ConnectionError) -> Self { - let clazz = match self { +use jni_toolbox::IntoException; + +impl IntoException for crate::errors::ConnectionError { + fn jclass(&self) -> &'static str { + match self { crate::errors::ConnectionError::Transport(_) => { "mp/code/exceptions/ConnectionTransportException" } crate::errors::ConnectionError::Remote(_) => { "mp/code/exceptions/ConnectionRemoteException" } - }; - let message = Some(format!("{value} -- {value:?}")); - - Self { message, clazz } + } } } -impl jni_toolbox::JniToolboxError for crate::errors::RemoteError { - fn jclass(&self) -> String { - "mp/code/exceptions/ConnectionRemoteException".to_string() +impl IntoException for crate::errors::RemoteError { + fn jclass(&self) -> &'static str { + "mp/code/exceptions/ConnectionRemoteException" } } -impl jni_toolbox::JniToolboxError for crate::errors::ControllerError { - fn jclass(&self) -> String { +impl IntoException for crate::errors::ControllerError { + fn jclass(&self) -> &'static str { match self { crate::errors::ControllerError::Stopped => { "mp/code/exceptions/ControllerStoppedException" @@ -114,7 +109,6 @@ impl jni_toolbox::JniToolboxError for crate::errors::ControllerError { "mp/code/exceptions/ControllerUnfulfilledException" } } - .to_string() } } @@ -125,13 +119,13 @@ macro_rules! into_java_ptr_class { const CLASS: &'static str = $jclass; fn into_java_object( self, - env: &mut jni::JNIEnv<'j>, + env: &mut jni::Env<'j>, ) -> Result, jni::errors::Error> { - let class = env.find_class(Self::CLASS)?; + let class = env.find_class(jni::strings::JNIString::new(Self::CLASS))?; env.new_object( class, - "(J)V", - &[jni::objects::JValueGen::Long( + jni::jni_sig!((ptr: i64) -> ()), + &[jni::objects::JValue::Long( Box::into_raw(Box::new(self)) as jni::sys::jlong )], ) @@ -145,21 +139,20 @@ into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User { +impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::UserInfo { const CLASS: &'static str = "mp/code/data/User"; fn into_java_object( self, - env: &mut jni::JNIEnv<'j>, + env: &mut jni::Env<'j>, ) -> Result, jni::errors::Error> { - let id_field = self.id.into_java_object(env)?; let name_field = env.new_string(self.name)?; - let class = env.find_class(Self::CLASS)?; + let class = env.find_class(jni::strings::JNIString::new(Self::CLASS))?; env.new_object( &class, - "(Ljava/util/UUID;Ljava/lang/String;)V", + jni::jni_sig!((id: java.util.UUID, name: java.lang.String) -> ()), &[ - jni::objects::JValueGen::Object(&id_field), - jni::objects::JValueGen::Object(&name_field), + // jni::objects::JValue::Object(&id_field), + jni::objects::JValue::Object(&name_field), ], ) } @@ -169,7 +162,7 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { const CLASS: &'static str = "mp/code/Workspace$Event"; fn into_java_object( self, - env: &mut jni::JNIEnv<'j>, + env: &mut jni::Env<'j>, ) -> Result, jni::errors::Error> { let (ordinal, arg) = match self { crate::api::Event::UserJoin { name: arg } => (0, env.new_string(arg)?), @@ -301,7 +294,7 @@ macro_rules! from_java_ptr { impl<'j> jni_toolbox::FromJava<'j> for &mut $type { type From = jni::sys::jobject; fn from_java( - _env: &mut jni::JNIEnv<'j>, + _env: &mut jni::Env<'j>, value: Self::From, ) -> Result { Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) }) diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 42174cb2..775f7b9f 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,16 +1,15 @@ use crate::{ Workspace, - api::{User, controller::AsyncReceiver}, + api::{UserInfo, WorkspaceIdentifier, controller::AsyncReceiver}, errors::{ConnectionError, ControllerError, RemoteError}, - ffi::java::null_check, }; -use jni::{JNIEnv, objects::JObject}; +use jni::{Env, objects::JObject}; use jni_toolbox::jni; /// Get the workspace id. #[jni(package = "mp.code", class = "Workspace")] -fn id(workspace: &mut Workspace) -> String { - workspace.id() +fn id(workspace: &mut Workspace) -> WorkspaceIdentifier { + workspace.id().clone() } /// Get a cursor controller by name and returns a pointer to it. @@ -39,14 +38,14 @@ fn active_buffers(workspace: &mut Workspace) -> Vec { /// Gets a list of the active buffers. #[jni(package = "mp.code", class = "Workspace")] -fn user_list(workspace: &mut Workspace) -> Vec { +fn user_list(workspace: &mut Workspace) -> Vec { workspace.user_list() } /// Create a new buffer. #[jni(package = "mp.code", class = "Workspace")] -fn create_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { - super::tokio().block_on(workspace.create_buffer(&path)) +fn create_buffer(workspace: &mut Workspace, path: String, ephemeral: bool) -> Result<(), RemoteError> { + super::tokio().block_on(workspace.create_buffer(path, ephemeral)) } /// Attach to a buffer and return a pointer to its [`crate::buffer::Controller`]. @@ -66,13 +65,13 @@ fn detach_buffer(workspace: &mut Workspace, path: String) -> bool { /// Update the local buffer list. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_buffers(workspace: &mut Workspace) -> Result, RemoteError> { +fn fetch_buffers(workspace: &mut Workspace) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_buffers()) } /// Update the local user list. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_users(workspace: &mut Workspace) -> Result, RemoteError> { +fn fetch_users(workspace: &mut Workspace) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_users()) } @@ -81,7 +80,7 @@ fn fetch_users(workspace: &mut Workspace) -> Result, RemoteError> { fn fetch_buffer_users( workspace: &mut Workspace, path: String, -) -> Result, RemoteError> { +) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_buffer_users(&path)) } @@ -118,42 +117,38 @@ fn clear_callback(workspace: &mut Workspace) { /// Register a callback for workspace events. #[jni(package = "mp.code", class = "Workspace")] fn callback<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, controller: &mut crate::Workspace, cb: JObject<'local>, -) { - null_check!(env, cb, {}); - 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; - }; +) -> Result<(), jni::errors::Error> { + if cb.is_null() { + return Err(jni::errors::Error::NullPtr("null pointer to workspace callback")); + } + + let cb_ref = env.new_global_ref(cb)?; controller.callback(move |workspace: crate::Workspace| { - let jvm = super::jvm(); - let mut env = jvm - .attach_current_thread_permanently() - .expect("failed attaching to main JVM thread"); - if let Err(e) = env.with_local_frame(5, |env| { - use jni_toolbox::IntoJavaObject; - let jworkspace = workspace.into_java_object(env)?; - if let Err(e) = env.call_method( - &cb_ref, - "accept", - "(Ljava/lang/Object;)V", - &[jni::objects::JValueGen::Object(&jworkspace)], - ) { - tracing::error!("error invoking callback: {e:?}"); - }; - Ok::<(), jni::errors::Error>(()) - }) { - tracing::error!("error invoking callback: {e}"); - let _ = env.exception_describe(); + let out = super::jvm().attach_current_thread(|mut env| { + env.with_local_frame(5, |env| { + use jni_toolbox::IntoJavaObject; + let jworkspace = workspace.into_java_object(env)?; + env.call_method( + &cb_ref, + jni::jni_str!("accept"), + jni::jni_sig!((ws: java.lang.Object) -> ()), + &[jni::objects::JValue::Object(&jworkspace)], + )?; + Ok(()) + })?; + Ok(()) + }); + + if let Err(e) = out { + tracing::error!("error invoking workspace callback: {e}"); } }); + + Ok(()) } /// Called by the Java GC to drop a [Workspace]. From 6a22d7c46bdf5cbeee7bcadf7c5e9ab213db6cd8 Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 10 Mar 2026 20:25:14 +0100 Subject: [PATCH 052/121] fix(py): complete ffi API coverage. tests are green --- src/ffi/python/controllers.rs | 11 +++++++++++ src/ffi/python/workspace.rs | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index 270eae98..84e38b7b 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -1,5 +1,6 @@ use crate::api::Cursor; use crate::api::TextChange; +use crate::api::WorkspaceIdentifier; use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::buffer::Controller as BufferController; use crate::cursor::Controller as CursorController; @@ -12,6 +13,11 @@ use super::a_sync_detach; // need to do manually since Controller is a trait implementation #[pymethods] impl CursorController { + #[pyo3(name = "workspace_id")] + fn pyworkspace_id(&self) -> WorkspaceIdentifier { + self.workspace_id().clone() + } + #[pyo3(name = "send")] fn pysend(&self, _py: Python, pos: Cursor) -> PyResult<()> { self.send(pos)?; @@ -65,6 +71,11 @@ impl BufferController { self.path().to_string() } + #[pyo3(name = "workspace_id")] + fn pyworkspace_id(&self) -> WorkspaceIdentifier { + self.workspace_id().clone() + } + #[pyo3(name = "content")] fn pycontent(&self, py: Python) -> PyResult { let this = self.clone(); diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index 16f13689..94d8e6c5 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -96,6 +96,11 @@ impl Workspace { self.user_list() } + #[pyo3(name = "buffer_user_list")] + fn pybuffer_user_list(&self, path: String) -> Vec { + self.buffer_user_list(path) + } + #[pyo3(name = "recv")] fn pyrecv(&self, py: Python) -> PyResult { let this = self.clone(); From 62354a7ed2ee8d726a4cca4690e0f0631806cc94 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 10 Mar 2026 23:48:22 +0100 Subject: [PATCH 053/121] fix(java): use new jni-toolbox, fix method signatures --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/api/buffer.rs | 1 + src/api/change.rs | 2 + src/api/config.rs | 1 + src/api/cursor.rs | 3 + src/api/user.rs | 1 + src/api/workspace.rs | 1 + src/ffi/java/buffer.rs | 8 +- src/ffi/java/mod.rs | 372 +------------------------------------- src/ffi/java/workspace.rs | 4 +- 11 files changed, 22 insertions(+), 377 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce4f3e5c..cb943d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "jni-toolbox" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=bbc84322d9c7d93b2af62f595187f5c04754f1cf#bbc84322d9c7d93b2af62f595187f5c04754f1cf" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=f6b9962b41f34b0004f6865247a4c603b5a3a37b#f6b9962b41f34b0004f6865247a4c603b5a3a37b" dependencies = [ "jni", "jni-toolbox-macro", @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "jni-toolbox-macro" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=bbc84322d9c7d93b2af62f595187f5c04754f1cf#bbc84322d9c7d93b2af62f595187f5c04754f1cf" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=f6b9962b41f34b0004f6865247a4c603b5a3a37b#f6b9962b41f34b0004f6865247a4c603b5a3a37b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d45d02e3..aa51fd41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "bbc84322d9c7d93b2af62f595187f5c04754f1cf", optional = true, features = ["uuid"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "f6b9962b41f34b0004f6865247a4c603b5a3a37b", optional = true, features = ["uuid"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } diff --git a/src/api/buffer.rs b/src/api/buffer.rs index 9ce320e0..23f5e924 100644 --- a/src/api/buffer.rs +++ b/src/api/buffer.rs @@ -5,6 +5,7 @@ #[derive(Debug, Clone)] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/BufferNode"))] pub struct BufferNode { /// Buffer path, sort of like a UNIX path. pub path: String, diff --git a/src/api/change.rs b/src/api/change.rs index 29488bc1..1c52c307 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -11,6 +11,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/BufferUpdate"))] pub struct BufferUpdate { /// Optional content hash after applying this change. #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] @@ -54,6 +55,7 @@ pub struct BufferUpdate { #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/TextChange"))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. pub start_idx: u32, diff --git a/src/api/config.rs b/src/api/config.rs index da632010..6742fb0c 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -12,6 +12,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Config"))] pub struct Config { /// User identifier used to register, possibly your email. pub username: String, diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 405dde8c..1f199e65 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -6,6 +6,7 @@ use pyo3::prelude::*; /// An event that occurred about a user's cursor. #[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/CursorEvent"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -19,6 +20,7 @@ pub struct CursorEvent { /// A cursor instantaneous state #[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Cursor"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -32,6 +34,7 @@ pub struct Cursor { /// A cursor selection span. #[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Selection"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/api/user.rs b/src/api/user.rs index 0f7f66d6..8b95a513 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -4,6 +4,7 @@ /// Represents a service user #[derive(Debug, Clone)] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/UserInfo"))] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UserInfo { diff --git a/src/api/workspace.rs b/src/api/workspace.rs index be111087..424c7910 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -7,6 +7,7 @@ /// Represents a service workspace #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/WorkspaceIdentifier"))] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct WorkspaceIdentifier { diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 41cdd076..24cde16e 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -55,7 +55,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; controller.callback(move |controller: crate::buffer::Controller| { - if let Err(e) = super::jvm().attach_current_thread(|mut env| { + let result: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jcontroller = controller.into_java_object(env)?; @@ -65,11 +65,13 @@ fn callback<'local>( jni::jni_sig!((buf: java.lang.Object) -> ()), &[jni::objects::JValue::Object(&jcontroller)], )?; - Ok(()) + Ok::<(), jni::errors::Error>(()) })?; Ok(()) - }) { + }); + + if let Err(e) = result { tracing::error!("error invoking buffer callback: {e}"); } }); diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index c96d387f..d6691eb1 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -65,22 +65,7 @@ pub(crate) fn setup_logger(debug: bool, path: Option) { } } -/// Performs a null check on the given variable and throws a NullPointerException on the Java side -/// if it is null. Finally, it returns with the given default value. -macro_rules! null_check { - // TODO replace - ($var: ident) => { - if $var.is_null() { - return Err(jni::errors::Error::NullPtr(stringify!($var))); - } - }; -} - -pub(crate) use null_check; - -use jni_toolbox::IntoException; - -impl IntoException for crate::errors::ConnectionError { +impl jni_toolbox::IntoException for crate::errors::ConnectionError { fn jclass(&self) -> &'static str { match self { crate::errors::ConnectionError::Transport(_) => { @@ -93,13 +78,13 @@ impl IntoException for crate::errors::ConnectionError { } } -impl IntoException for crate::errors::RemoteError { +impl jni_toolbox::IntoException for crate::errors::RemoteError { fn jclass(&self) -> &'static str { "mp/code/exceptions/ConnectionRemoteException" } } -impl IntoException for crate::errors::ControllerError { +impl jni_toolbox::IntoException for crate::errors::ControllerError { fn jclass(&self) -> &'static str { match self { crate::errors::ControllerError::Stopped => { @@ -112,354 +97,3 @@ impl IntoException for crate::errors::ControllerError { } } -/// Generates a [JObjectify] implementation for a class that is just a holder for a pointer. -macro_rules! into_java_ptr_class { - ($type: ty, $jclass: literal) => { - impl<'j> jni_toolbox::IntoJavaObject<'j> for $type { - const CLASS: &'static str = $jclass; - fn into_java_object( - self, - env: &mut jni::Env<'j>, - ) -> Result, jni::errors::Error> { - let class = env.find_class(jni::strings::JNIString::new(Self::CLASS))?; - env.new_object( - class, - jni::jni_sig!((ptr: i64) -> ()), - &[jni::objects::JValue::Long( - Box::into_raw(Box::new(self)) as jni::sys::jlong - )], - ) - } - } - }; -} - -into_java_ptr_class!(crate::Client, "mp/code/Client"); -into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); -into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); -into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::UserInfo { - const CLASS: &'static str = "mp/code/data/User"; - fn into_java_object( - self, - env: &mut jni::Env<'j>, - ) -> Result, jni::errors::Error> { - let name_field = env.new_string(self.name)?; - let class = env.find_class(jni::strings::JNIString::new(Self::CLASS))?; - env.new_object( - &class, - jni::jni_sig!((id: java.util.UUID, name: java.lang.String) -> ()), - &[ - // jni::objects::JValue::Object(&id_field), - jni::objects::JValue::Object(&name_field), - ], - ) - } -} - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { - const CLASS: &'static str = "mp/code/Workspace$Event"; - fn into_java_object( - self, - env: &mut jni::Env<'j>, - ) -> Result, jni::errors::Error> { - let (ordinal, arg) = match self { - crate::api::Event::UserJoin { name: arg } => (0, env.new_string(arg)?), - crate::api::Event::UserLeave { name: arg } => (1, env.new_string(arg)?), - crate::api::Event::FileTreeUpdated { path: arg } => (2, env.new_string(arg)?), - }; - - let type_class = env.find_class("mp/code/Workspace$Event$Type")?; - let variants: jni::objects::JObjectArray = env - .call_method(type_class, "getEnumConstants", "()[Ljava/lang/Object;", &[])? - .l()? - .into(); - let event_type = env.get_object_array_element(variants, ordinal)?; - - let event_class = env.find_class(Self::CLASS)?; - env.new_object( - event_class, - "(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V", - &[ - jni::objects::JValueGen::Object(&event_type), - jni::objects::JValueGen::Object(&arg), - ], - ) - } -} - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::BufferUpdate { - const CLASS: &'static str = "mp/code/data/BufferUpdate"; - fn into_java_object( - self, - env: &mut jni::JNIEnv<'j>, - ) -> Result, jni::errors::Error> { - let class = env.find_class(Self::CLASS)?; - - let hash_class = env.find_class("java/util/OptionalLong")?; - let hash = if let Some(h) = self.hash { - env.call_static_method( - hash_class, - "of", - "(J)Ljava/util/OptionalLong;", - &[jni::objects::JValueGen::Long(h)], - ) - } else { - env.call_static_method(hash_class, "empty", "()Ljava/util/OptionalLong;", &[]) - }? - .l()?; - - let version = self.version.into_java_object(env)?; - let change = self.change.into_java_object(env)?; - - env.new_object( - class, - "(Ljava/util/OptionalLong;[JLmp/code/data/TextChange;)V", - &[ - jni::objects::JValueGen::Object(&hash), - jni::objects::JValueGen::Object(&version), - jni::objects::JValueGen::Object(&change), - ], - ) - } -} - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange { - const CLASS: &'static str = "mp/code/data/TextChange"; - fn into_java_object( - self, - env: &mut jni::JNIEnv<'j>, - ) -> Result, jni::errors::Error> { - let content = env.new_string(self.content)?; - let class = env.find_class(Self::CLASS)?; - env.new_object( - class, - "(JJLjava/lang/String;)V", - &[ - jni::objects::JValueGen::Long(self.start_idx.into()), - jni::objects::JValueGen::Long(self.end_idx.into()), - jni::objects::JValueGen::Object(&content), - ], - ) - } -} - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor { - const CLASS: &'static str = "mp/code/data/Cursor"; - fn into_java_object( - self, - env: &mut jni::JNIEnv<'j>, - ) -> Result, jni::errors::Error> { - let class = env.find_class(Self::CLASS)?; - let user = env.new_string(&self.user)?; - let sel = self.sel.into_java_object(env)?; - - env.new_object( - class, - "(Ljava/lang/String;Lmp/code/data/Selection;)V", - &[ - jni::objects::JValueGen::Object(&user), - jni::objects::JValueGen::Object(&sel), - ], - ) - } -} - -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Selection { - const CLASS: &'static str = "mp/code/data/Selection"; - fn into_java_object( - self, - env: &mut jni::JNIEnv<'j>, - ) -> Result, jni::errors::Error> { - let class = env.find_class(Self::CLASS)?; - let buffer = env.new_string(&self.buffer)?; - - env.new_object( - class, - "(IIIILjava/lang/String;)V", - &[ - jni::objects::JValueGen::Int(self.start_row), - jni::objects::JValueGen::Int(self.start_col), - jni::objects::JValueGen::Int(self.end_row), - jni::objects::JValueGen::Int(self.end_col), - jni::objects::JValueGen::Object(&buffer), - ], - ) - } -} - -macro_rules! from_java_ptr { - ($type: ty) => { - impl<'j> jni_toolbox::FromJava<'j> for &mut $type { - type From = jni::sys::jobject; - fn from_java( - _env: &mut jni::Env<'j>, - value: Self::From, - ) -> Result { - Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) }) - } - } - }; -} - -from_java_ptr!(crate::Client); -from_java_ptr!(crate::Workspace); -from_java_ptr!(crate::cursor::Controller); -from_java_ptr!(crate::buffer::Controller); - -impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { - type From = jni::objects::JObject<'j>; - fn from_java( - env: &mut jni::JNIEnv<'j>, - config: Self::From, - ) -> Result { - let username = { - let jfield: jni::objects::JString<'j> = env - .get_field(&config, "username", "Ljava/lang/String;")? - .l()? - .into(); - if jfield.is_null() { - return Err(jni::errors::Error::NullPtr("Username can never be null!")); - } - unsafe { env.get_string_unchecked(&jfield) }?.into() - }; - - let password = { - let jfield: jni::objects::JString<'j> = env - .get_field(&config, "password", "Ljava/lang/String;")? - .l()? - .into(); - if jfield.is_null() { - return Err(jni::errors::Error::NullPtr("Password can never be null!")); - } - unsafe { env.get_string_unchecked(&jfield) }?.into() - }; - - let host = { - let jfield = env - .get_field(&config, "host", "Ljava/util/Optional;")? - .l()?; - if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { - let field = env - .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])? - .l()? - .into(); - Some(unsafe { env.get_string_unchecked(&field) }?.into()) - } else { - None - } - }; - - let port = { - let jfield = env - .get_field(&config, "port", "Ljava/util/OptionalInt;")? - .l()?; - if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { - let ivalue = env.call_method(&jfield, "getAsInt", "()I", &[])?.i()?; - Some(ivalue.clamp(0, 65535) as u16) - } else { - None - } - }; - - let tls = { - let jfield = env - .get_field(&config, "host", "Ljava/util/Optional;")? - .l()?; - if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { - let field = env - .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])? - .l()?; - let bool_true = env - .get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")? - .l()?; - Some( - env.call_method( - field, - "equals", - "(Ljava/lang/Object;)Z", - &[jni::objects::JValueGen::Object(&bool_true)], - )? - .z()?, - ) // what a joke - } else { - None - } - }; - - Ok(Self { - username, - password, - host, - port, - tls, - }) - } -} - -impl<'j> jni_toolbox::FromJava<'j> for crate::api::Selection { - type From = jni::objects::JObject<'j>; - fn from_java( - env: &mut jni::JNIEnv<'j>, - cursor: Self::From, - ) -> Result { - let start_row = env.get_field(&cursor, "startRow", "I")?.i()?; - let start_col = env.get_field(&cursor, "startCol", "I")?.i()?; - let end_row = env.get_field(&cursor, "endRow", "I")?.i()?; - let end_col = env.get_field(&cursor, "endCol", "I")?.i()?; - - let buffer = { - let jfield: jni::objects::JString<'j> = env - .get_field(&cursor, "buffer", "Ljava/lang/String;")? - .l()? - .into(); - if jfield.is_null() { - return Err(jni::errors::Error::NullPtr("Buffer can never be null!")); - } - unsafe { env.get_string_unchecked(&jfield) }?.into() - }; - - Ok(Self { - start_row, - start_col, - end_row, - end_col, - buffer, - }) - } -} - -impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange { - type From = jni::objects::JObject<'j>; - fn from_java( - env: &mut jni::JNIEnv<'j>, - change: Self::From, - ) -> Result { - let start = env - .get_field(&change, "startIdx", "J")? - .j()? - .clamp(0, u32::MAX.into()) as u32; - let end = env - .get_field(&change, "endIdx", "J")? - .j()? - .clamp(0, u32::MAX.into()) as u32; - - let content = { - let jfield: jni::objects::JString<'j> = env - .get_field(&change, "content", "Ljava/lang/String;")? - .l()? - .into(); - if jfield.is_null() { - return Err(jni::errors::Error::NullPtr("Content can never be null!")); - } - unsafe { env.get_string_unchecked(&jfield) }?.into() - }; - - Ok(Self { - start_idx: start, - end_idx: end, - content, - }) - } -} diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 775f7b9f..135f90fb 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -128,7 +128,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; controller.callback(move |workspace: crate::Workspace| { - let out = super::jvm().attach_current_thread(|mut env| { + let out: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jworkspace = workspace.into_java_object(env)?; @@ -138,7 +138,7 @@ fn callback<'local>( jni::jni_sig!((ws: java.lang.Object) -> ()), &[jni::objects::JValue::Object(&jworkspace)], )?; - Ok(()) + Ok::<(), jni::errors::Error>(()) })?; Ok(()) }); From 9e3f7bfc99340f3724ffe30cb832f92c370304ff Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 10 Mar 2026 23:53:08 +0100 Subject: [PATCH 054/121] fix(java): bump jni-toolbox for uuid fix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index aa51fd41..5297b961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "f6b9962b41f34b0004f6865247a4c603b5a3a37b", optional = true, features = ["uuid"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "5e063560a6affd99ffd99f6f1ffa8249864f1fee", optional = true, features = ["uuid"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } From 3786fd26b23e4d714ae93f58f6f1f67b844a8c60 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 11 Mar 2026 00:05:41 +0100 Subject: [PATCH 055/121] fix(java): restore ptr impls --- src/ffi/java/mod.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index d6691eb1..76b8caa7 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -97,3 +97,56 @@ impl jni_toolbox::IntoException for crate::errors::ControllerError { } } + +macro_rules! from_java_ptr { + ($type: ty) => { + impl<'j> jni_toolbox::FromJava<'j> for &mut $type { + type From = jni::sys::jobject; + fn from_java( + _env: &mut jni::Env<'j>, + value: Self::From, + ) -> Result { + Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) }) + } + + fn from_jvalue( + env: &mut jni::Env<'j>, + value: jni::JValueOwned, + ) -> Result { + Self::from_java(env, value.l()?.into_raw()) + } + } + }; +} + +from_java_ptr!(crate::Client); +from_java_ptr!(crate::Workspace); +from_java_ptr!(crate::cursor::Controller); +from_java_ptr!(crate::buffer::Controller); + +/// Generates a [JObjectify] implementation for a class that is just a holder for a pointer. +macro_rules! into_java_ptr_class { + ($type: ty, $jclass: literal) => { + impl<'j> jni_toolbox::IntoJavaObject<'j> for $type { + const CLASS: &'static str = $jclass; + fn into_java_object( + self, + env: &mut jni::Env<'j>, + ) -> Result, jni::errors::Error> { + let class = env.find_class(jni::strings::JNIString::new(Self::CLASS))?; + env.new_object( + class, + jni::jni_sig!((ptr: i64) -> ()), + &[jni::objects::JValue::Long( + Box::into_raw(Box::new(self)) as jni::sys::jlong + )], + ) + } + } + }; +} + +into_java_ptr_class!(crate::Client, "mp/code/Client"); +into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); +into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); +into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); From 1e79e87246fc26ac80859c078a6980a6503a2bb6 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 11 Mar 2026 00:05:52 +0100 Subject: [PATCH 056/121] fix(java): cursor callback cleanup --- src/ffi/java/cursor.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 87d5bca5..884bb2b0 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -5,8 +5,6 @@ use crate::{ use jni::{Env, objects::JObject}; use jni_toolbox::jni; -use super::null_check; - /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] fn try_recv(controller: &mut crate::cursor::Controller) -> Result, ControllerError> { @@ -32,11 +30,14 @@ fn callback<'local>( controller: &mut crate::cursor::Controller, cb: JObject<'local>, ) -> Result<(), jni::errors::Error> { - null_check!(cb); + if cb.is_null() { + return Err(jni::errors::Error::NullPtr("cursor callback is null")); + } + let cb_ref = env.new_global_ref(cb)?; controller.callback(move |controller: crate::cursor::Controller| { - let res = super::jvm().attach_current_thread(|mut env| { + let res: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jcontroller = controller.into_java_object(env)?; @@ -46,7 +47,7 @@ fn callback<'local>( jni::jni_sig!((arg1: java.lang.Object) -> ()), &[jni::objects::JValue::Object(&jcontroller)], )?; - Ok(()) + Ok::<(), jni::errors::Error>(()) })?; Ok(()) From 5201aa650d229ead6b78471c3270142d402466cc Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 11 Mar 2026 17:55:21 +0100 Subject: [PATCH 057/121] tests: added annotations coverage test --- Cargo.lock | 1 + Cargo.toml | 5 ++-- src/tests/coverage/annotations.rs | 31 ++++++++++++++++++++++ src/tests/{coverage.rs => coverage/ffi.rs} | 30 ++++++++++----------- src/tests/coverage/mod.rs | 2 ++ 5 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/tests/coverage/annotations.rs rename src/tests/{coverage.rs => coverage/ffi.rs} (91%) create mode 100644 src/tests/coverage/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cb943d0a..ef341b1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,7 @@ dependencies = [ "napi-derive", "pyo3", "pyo3-build-config", + "regex", "serde", "syn 2.0.117", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 5297b961..7f6b32e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,8 @@ pyo3 = { version = "0.28", features = ["multiple-pymethods", "uuid"], optional = # extra async-trait = { version = "0.1", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } -syn = { version = "2.0.117", features = ["full", "visit"] } +syn = { version = "2.0.117", features = ["full", "visit"], optional = true } +regex = { version = "1.12", optional = true } [build-dependencies] # glue (js) @@ -77,7 +78,7 @@ async-trait = ["dep:async-trait"] serialize = ["dep:serde", "uuid/serde"] # special tests which require more setup test-e2e = [] -test-coverage = [] +test-coverage = ["dep:syn", "dep:regex"] # ffi java = ["dep:jni", "dep:tracing-subscriber", "dep:jni-toolbox"] js = ["dep:napi-build", "dep:tracing-subscriber", "dep:napi", "dep:napi-derive"] diff --git a/src/tests/coverage/annotations.rs b/src/tests/coverage/annotations.rs new file mode 100644 index 00000000..f3cdb1bb --- /dev/null +++ b/src/tests/coverage/annotations.rs @@ -0,0 +1,31 @@ +#[test] +#[cfg(all(test, feature = "lua"))] +fn lua_annotations_should_cover_ffi_api_surface() { + let annotations = include_str!("../../../dist/lua/annotations.lua"); + + let source = concat!( + include_str!("../../ffi/lua/client.rs"), + include_str!("../../ffi/lua/workspace.rs"), + include_str!("../../ffi/lua/buffer.rs"), + include_str!("../../ffi/lua/cursor.rs"), + ); + + let re = regex::Regex::new("add_method\\(\\s+\"(\\w+)\",").expect("failed building regex"); + + let mut missing = Vec::new(); + for (_, [fn_name]) in re.captures_iter(source).map(|c| c.extract()) { + if !annotations.contains(fn_name) { + #[cfg(feature = "ci")] + { + println!("::warning title=Coverage::Missing Lua annotations for method: '{fn_name}'"); + } + missing.push(fn_name.to_string()); + } + } + + assert!( + missing.is_empty(), + "missing lua annotations for methods: '{}'", + missing.join("', '"), + ); +} diff --git a/src/tests/coverage.rs b/src/tests/coverage/ffi.rs similarity index 91% rename from src/tests/coverage.rs rename to src/tests/coverage/ffi.rs index f4731f08..b1b9147c 100644 --- a/src/tests/coverage.rs +++ b/src/tests/coverage/ffi.rs @@ -181,9 +181,9 @@ fn python_ffi_should_cover_rust_api_surface() { let required = discover_core_surface(files, targets); let python_src = concat!( - include_str!("../ffi/python/client.rs"), - include_str!("../ffi/python/workspace.rs"), - include_str!("../ffi/python/controllers.rs"), + include_str!("../../ffi/python/client.rs"), + include_str!("../../ffi/python/workspace.rs"), + include_str!("../../ffi/python/controllers.rs"), ); let python_ignore = ["Client.connect"]; @@ -224,10 +224,10 @@ fn javascript_ffi_should_cover_rust_api_surface() { let required = discover_core_surface(files, targets); let js_src = concat!( - include_str!("../ffi/js/client.rs"), - include_str!("../ffi/js/workspace.rs"), - include_str!("../ffi/js/buffer.rs"), - include_str!("../ffi/js/cursor.rs"), + include_str!("../../ffi/js/client.rs"), + include_str!("../../ffi/js/workspace.rs"), + include_str!("../../ffi/js/buffer.rs"), + include_str!("../../ffi/js/cursor.rs"), ); let js_ignore = []; @@ -268,10 +268,10 @@ fn lua_ffi_should_cover_rust_api_surface() { let required = discover_core_surface(files, targets); let lua_src = concat!( - include_str!("../ffi/lua/client.rs"), - include_str!("../ffi/lua/workspace.rs"), - include_str!("../ffi/lua/buffer.rs"), - include_str!("../ffi/lua/cursor.rs"), + include_str!("../../ffi/lua/client.rs"), + include_str!("../../ffi/lua/workspace.rs"), + include_str!("../../ffi/lua/buffer.rs"), + include_str!("../../ffi/lua/cursor.rs"), ); let lua_ignore = ["Client.connect"]; @@ -312,10 +312,10 @@ fn java_ffi_should_cover_rust_api_surface() { let required = discover_core_surface(files, targets); let java_src = concat!( - include_str!("../ffi/java/client.rs"), - include_str!("../ffi/java/workspace.rs"), - include_str!("../ffi/java/buffer.rs"), - include_str!("../ffi/java/cursor.rs"), + include_str!("../../ffi/java/client.rs"), + include_str!("../../ffi/java/workspace.rs"), + include_str!("../../ffi/java/buffer.rs"), + include_str!("../../ffi/java/cursor.rs"), ); let java_ignore = []; diff --git a/src/tests/coverage/mod.rs b/src/tests/coverage/mod.rs new file mode 100644 index 00000000..092e4db6 --- /dev/null +++ b/src/tests/coverage/mod.rs @@ -0,0 +1,2 @@ +pub mod ffi; +pub mod annotations; From 2ef9eb74eb637f6f391e68330fb32ec654c099c5 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 12 Mar 2026 12:41:31 +0100 Subject: [PATCH 058/121] chore(java): update to new jni-toolbox --- Cargo.lock | 4 +- Cargo.toml | 4 +- dist/java/src/mp/code/Workspace.java | 40 +++++++++++------ src/ffi/java/buffer.rs | 4 +- src/ffi/java/client.rs | 1 + src/ffi/java/cursor.rs | 4 +- src/ffi/java/mod.rs | 67 +++++++++++++++++++++------- src/ffi/java/workspace.rs | 4 +- 8 files changed, 92 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef341b1c..2a8d9202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "jni-toolbox" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=f6b9962b41f34b0004f6865247a4c603b5a3a37b#f6b9962b41f34b0004f6865247a4c603b5a3a37b" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae#11897b11153dccd82044e980947c090ef12d08ae" dependencies = [ "jni", "jni-toolbox-macro", @@ -809,7 +809,7 @@ dependencies = [ [[package]] name = "jni-toolbox-macro" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=f6b9962b41f34b0004f6865247a4c603b5a3a37b#f6b9962b41f34b0004f6865247a4c603b5a3a37b" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae#11897b11153dccd82044e980947c090ef12d08ae" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7f6b32e9..3dcf3a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "5e063560a6affd99ffd99f6f1ffa8249864f1fee", optional = true, features = ["uuid"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "11897b11153dccd82044e980947c090ef12d08ae", optional = true, features = ["uuid", "unsigned"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } @@ -72,7 +72,7 @@ napi-build = { version = "2.2", optional = true } pyo3-build-config = { version = "0.28", optional = true } [features] -default = ["lua-jit", "py-abi3", "py-extmod"] +default = ["lua-jit", "py-abi3", "py-extmod", "java"] # extra async-trait = ["dep:async-trait"] serialize = ["dep:serde", "uuid/serde"] diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 8e3af729..bd417a72 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -238,11 +238,13 @@ public void poll() throws ControllerException { public static final class Event { /** The type of the event. */ public final @Getter Type type; - private final String argument; + private final String user; + private final String buffer; - Event(Type type, String argument) { + Event(Type type, String user, String buffer) { this.type = type; - this.argument = argument; + this.user = user; + this.buffer = buffer; } /** @@ -250,8 +252,8 @@ public static final class Event { * @return the user who joined, if any did */ public Optional getUserJoined() { - if(this.type == Type.USER_JOIN) { - return Optional.of(this.argument); + if(this.type == Type.USER_JOIN || this.type == Type.USER_JOIN_BUFFER) { + return Optional.of(this.user); } else return Optional.empty(); } @@ -260,8 +262,8 @@ public Optional getUserJoined() { * @return the user who left, if any did */ public Optional getUserLeft() { - if(this.type == Type.USER_LEAVE) { - return Optional.of(this.argument); + if(this.type == Type.USER_LEAVE || this.type == Type.USER_LEAVE_BUFFER) { + return Optional.of(this.user); } else return Optional.empty(); } @@ -269,9 +271,9 @@ public Optional getUserLeft() { * Gets the path of buffer that changed, if any did. * @return the path of buffer that changed, if any did */ - public Optional getChangedBuffer() { - if(this.type == Type.FILE_TREE_UPDATED) { - return Optional.of(this.argument); + public Optional getAffectedBuffer() { + if(this.type == Type.FILE_TREE_UPDATED || this.type == Type.USER_JOIN_BUFFER || this.type == Type.USER_LEAVE_BUFFER) { + return Optional.of(this.buffer); } else return Optional.empty(); } @@ -285,15 +287,27 @@ public enum Type { */ USER_JOIN, /** - * Somebody left a workspace + * Somebody left a workspace. * @see #getUserLeft() to get the name */ USER_LEAVE, /** * The filetree was updated. - * @see #getChangedBuffer() to see the buffer that changed + * @see #getAffectedBuffer() to see the buffer that changed */ - FILE_TREE_UPDATED + FILE_TREE_UPDATED, + /** + * Somebody joined a buffer. + * @see #getUserJoined() to get the name of the user that joined + * @see #getAffectedBuffer() to see the buffer that they joined + */ + USER_JOIN_BUFFER, + /** + * Somebody left a buffer. + * @see #getUserLeft() to get the name of the user that left + * @see #getAffectedBuffer() to see the buffer that they left + */ + USER_LEAVE_BUFFER } } } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 24cde16e..fcc6456d 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -53,9 +53,10 @@ fn callback<'local>( } let cb_ref = env.new_global_ref(cb)?; + let jvm = env.get_java_vm()?; controller.callback(move |controller: crate::buffer::Controller| { - let result: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { + let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jcontroller = controller.into_java_object(env)?; @@ -98,6 +99,7 @@ fn ack(controller: &mut crate::buffer::Controller, version: Vec) { } /// Called by the Java GC to drop a [crate::buffer::Controller]. +#[allow(unsafe_code)] #[jni(package = "mp.code", class = "BufferController")] fn free(input: jni::sys::jlong) { let _ = unsafe { Box::from_raw(input as *mut crate::buffer::Controller) }; diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 1ed4b086..b627ff14 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -83,6 +83,7 @@ fn refresh(client: &mut Client) -> Result<(), RemoteError> { } /// Called by the Java GC to drop a [Client]. +#[allow(unsafe_code)] #[jni(package = "mp.code", class = "Client")] fn free(input: jni::sys::jlong) { let _ = unsafe { Box::from_raw(input as *mut Client) }; diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 884bb2b0..544c61d9 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -35,9 +35,10 @@ fn callback<'local>( } let cb_ref = env.new_global_ref(cb)?; + let jvm = env.get_java_vm()?; controller.callback(move |controller: crate::cursor::Controller| { - let res: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { + let res: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jcontroller = controller.into_java_object(env)?; @@ -74,6 +75,7 @@ fn poll(controller: &mut crate::cursor::Controller) -> Result<(), ControllerErro } /// Called by the Java GC to drop a [crate::cursor::Controller]. +#[allow(unsafe_code)] #[jni(package = "mp.code", class = "CursorController")] fn free(input: jni::sys::jlong) { let _ = unsafe { Box::from_raw(input as *mut crate::cursor::Controller) }; diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 76b8caa7..bf615f08 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -1,7 +1,16 @@ +/// FFI methods relating to buffers. pub mod buffer; + +/// FFI methods relating to clients. pub mod client; + +/// FFI methods relating to cursors. pub mod cursor; + +/// FFI methods to access extra functions. pub mod ext; + +/// FFI methods relating to the workspace. pub mod workspace; /// Gets or creates the relevant [tokio::runtime::Runtime]. @@ -16,22 +25,6 @@ fn tokio() -> &'static tokio::runtime::Runtime { }) } -/// A static reference to [jni::JavaVM] that is set on JNI load. -static mut JVM: Option> = None; - -/// Safe accessor for the [jni::JavaVM] static. -pub(crate) fn jvm() -> std::sync::Arc { - unsafe { JVM.clone() }.unwrap() -} - -/// Called upon initialisation of the JVM. -#[allow(non_snake_case)] -#[unsafe(no_mangle)] -pub extern "system" fn JNI_OnLoad(vm: jni::JavaVM, _: *mut std::ffi::c_void) -> jni::sys::jint { - unsafe { JVM = Some(std::sync::Arc::new(vm)) }; - jni::sys::JNI_VERSION_1_1 -} - /// Set up logging. Useful for debugging. pub(crate) fn setup_logger(debug: bool, path: Option) { let format = tracing_subscriber::fmt::format() @@ -102,6 +95,7 @@ macro_rules! from_java_ptr { ($type: ty) => { impl<'j> jni_toolbox::FromJava<'j> for &mut $type { type From = jni::sys::jobject; + #[allow(unsafe_code)] fn from_java( _env: &mut jni::Env<'j>, value: Self::From, @@ -150,3 +144,44 @@ into_java_ptr_class!(crate::Client, "mp/code/Client"); into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); + +// #[allow(unsafe_code)] +impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { + const CLASS: &'static str = "mp/code/Workspace$Event"; + fn into_java_object( + self, + env: &mut jni::Env<'j>, + ) -> Result, jni::errors::Error> { + let (ordinal, user, buffer) = match self { + crate::api::Event::UserJoin { name } => (0, Some(name), None), + crate::api::Event::UserLeave { name } => (1, Some(name), None), + crate::api::Event::FileTreeUpdated { path } => (2, None, Some(path)), + crate::api::Event::UserJoinBuffer { name, buffer } => (3, Some(name), Some(buffer)), + crate::api::Event::UserLeaveBuffer { name, buffer } => (4, Some(name), Some(buffer)), + }; + + let type_class = env.find_class(jni::jni_str!("mp/code/Workspace$Event$Type"))?; + let variants = env + .call_method(type_class, jni::jni_str!("getEnumConstants"), jni::jni_sig!("()[Ljava/lang/Object;"), &[])? + .l()?; + let variants_array = jni::objects::JObjectArray::::cast_local(env, variants)?; + let event_type = variants_array.get_element(env, ordinal)?; + + let class_name = jni::strings::JNIString::new(Self::CLASS); + let event_class = env.find_class(class_name)?; + + let j_event_type = event_type.into_java_object(env)?; + let j_user = user.into_java_object(env)?; + let j_buffer = buffer.into_java_object(env)?; + + env.new_object( + event_class, + jni::jni_sig!("(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V"), + &[ + jni::JValue::Object(&j_event_type), + jni::JValue::Object(&j_user), + jni::JValue::Object(&j_buffer) + ] + ) + } +} diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 135f90fb..6eaba302 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -126,9 +126,10 @@ fn callback<'local>( } let cb_ref = env.new_global_ref(cb)?; + let jvm = env.get_java_vm()?; controller.callback(move |workspace: crate::Workspace| { - let out: Result<(), jni::errors::Error> = super::jvm().attach_current_thread(|env| { + let out: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; let jworkspace = workspace.into_java_object(env)?; @@ -152,6 +153,7 @@ fn callback<'local>( } /// Called by the Java GC to drop a [Workspace]. +#[allow(unsafe_code)] #[jni(package = "mp.code", class = "Workspace")] fn free(input: jni::sys::jlong) { let _ = unsafe { Box::from_raw(input as *mut crate::Workspace) }; From 98f39ecd4caf4817b5672a25c14ed97a12438a87 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Fri, 13 Mar 2026 03:35:39 +0100 Subject: [PATCH 059/121] feat(java): attempt at coverage --- dist/java/src/mp/code/BufferController.java | 33 +++-- dist/java/src/mp/code/Client.java | 130 ++++++++++++------ dist/java/src/mp/code/CursorController.java | 19 ++- dist/java/src/mp/code/Workspace.java | 81 +++++++---- dist/java/src/mp/code/data/BufferNode.java | 23 ++++ dist/java/src/mp/code/data/BufferUpdate.java | 6 +- dist/java/src/mp/code/data/Config.java | 42 +----- dist/java/src/mp/code/data/Cursor.java | 8 +- dist/java/src/mp/code/data/CursorEvent.java | 23 ++++ dist/java/src/mp/code/data/Selection.java | 5 - dist/java/src/mp/code/data/User.java | 25 ---- dist/java/src/mp/code/data/UserInfo.java | 33 +++++ .../src/mp/code/data/WorkspaceIdentifier.java | 23 ++++ src/api/buffer.rs | 2 +- src/api/change.rs | 4 +- src/api/config.rs | 2 +- src/api/cursor.rs | 6 +- src/api/user.rs | 2 +- src/api/workspace.rs | 2 +- src/ffi/java/buffer.rs | 14 +- src/ffi/java/client.rs | 32 ++++- src/ffi/java/cursor.rs | 8 +- src/ffi/java/workspace.rs | 28 ++-- 23 files changed, 368 insertions(+), 183 deletions(-) create mode 100644 dist/java/src/mp/code/data/BufferNode.java create mode 100644 dist/java/src/mp/code/data/CursorEvent.java delete mode 100644 dist/java/src/mp/code/data/User.java create mode 100644 dist/java/src/mp/code/data/UserInfo.java create mode 100644 dist/java/src/mp/code/data/WorkspaceIdentifier.java diff --git a/dist/java/src/mp/code/BufferController.java b/dist/java/src/mp/code/BufferController.java index fe1becd0..09192ffa 100644 --- a/dist/java/src/mp/code/BufferController.java +++ b/dist/java/src/mp/code/BufferController.java @@ -2,9 +2,9 @@ import mp.code.data.BufferUpdate; import mp.code.data.TextChange; +import mp.code.data.WorkspaceIdentifier; import mp.code.exceptions.ControllerException; -import java.util.Optional; import java.util.function.Consumer; /** @@ -21,17 +21,27 @@ public final class BufferController { Extensions.CLEANER.register(this, () -> free(ptr)); } - private static native String get_name(long self); + private static native String path(long self); /** - * Gets the name (path) of the buffer. + * Gets the path (used as an identifier) of the buffer. * @return the path of the buffer */ - public String getName() { - return get_name(this.ptr); + public String path() { + return path(this.ptr); } - private static native String get_content(long self) throws ControllerException; + private static native WorkspaceIdentifier workspace_id(long self); + + /** + * Gets the identifier for the one that contains this buffer. + * @return a {@link WorkspaceIdentifier} for the owner + */ + public WorkspaceIdentifier workspaceId() { + return workspace_id(this.ptr); + } + + private static native String content(long self) throws ControllerException; /** * Gets the contents of the buffer as a flat string. @@ -39,20 +49,19 @@ public String getName() { * @return the contents fo the buffer as a flat string * @throws ControllerException if the controller was stopped */ - public String getContent() throws ControllerException { - return get_content(this.ptr); + public String content() throws ControllerException { + return content(this.ptr); } private static native BufferUpdate try_recv(long self) throws ControllerException; /** - * Tries to get a {@link BufferUpdate} from the queue if any were present, and returns - * an empty optional otherwise. + * Tries to get a {@link BufferUpdate} from the queue if any were present, null otherwise. * @return the first text change in queue, if any are present * @throws ControllerException if the controller was stopped */ - public Optional tryRecv() throws ControllerException { - return Optional.ofNullable(try_recv(this.ptr)); + public BufferUpdate tryRecv() throws ControllerException { + return try_recv(this.ptr); } private static native BufferUpdate recv(long self) throws ControllerException; diff --git a/dist/java/src/mp/code/Client.java b/dist/java/src/mp/code/Client.java index 113aa652..f5fb3554 100644 --- a/dist/java/src/mp/code/Client.java +++ b/dist/java/src/mp/code/Client.java @@ -2,12 +2,11 @@ import lombok.Getter; import mp.code.data.Config; -import mp.code.data.User; +import mp.code.data.UserInfo; +import mp.code.data.WorkspaceIdentifier; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; -import java.util.Optional; - /** * The main entrypoint of the library. * This is the only object you are expected to hold yourself; unlike all the others, @@ -34,116 +33,167 @@ public final class Client { */ public static native Client connect(Config config) throws ConnectionException; - private static native User current_user(long self); + private static native UserInfo current_user(long self); /** * Gets information about the current user. - * @return a {@link User} object representing the user + * @return a {@link UserInfo} object representing the user */ - public User currentUser() { + public UserInfo currentUser() { return current_user(this.ptr); } - private static native Workspace attach_workspace(long self, String workspaceId) throws ConnectionException; + private static native Workspace attach_workspace(long self, String user, String workspace) throws ConnectionException; /** * Joins a {@link Workspace} and returns it. - * @param workspaceId the id of the workspace to connect to + * @param user the owner of the workspace + * @param workspace the identifier of the workspace * @return the relevant {@link Workspace} * @throws ConnectionException if an error occurs in communicating with the server */ - public Workspace attachWorkspace(String workspaceId) throws ConnectionException { - return attach_workspace(this.ptr, workspaceId); + public Workspace attachWorkspace(String user, String workspace) throws ConnectionException { + return attach_workspace(this.ptr, user, workspace); + } + + private static native void accept_invite(long self, String user, String workspace) throws ConnectionRemoteException; + + /** + * Accept an invitation to a workspace. + * @param user the owner of the workspace + * @param workspace the identifier of the workspace + * @throws ConnectionRemoteException if an error occurs in communicating with the server + */ + public void acceptInvite(String user, String workspace) throws ConnectionRemoteException { + accept_invite(this.ptr, user, workspace); + } + + private static native void reject_invite(long self, String user, String workspace) throws ConnectionRemoteException; + + /** + * Rejects an invitation to a workspace + * @param user the owner of the workspace + * @param workspace the identifier of the workspace + * @throws ConnectionRemoteException if an error occurs in communicating with the server + */ + public void rejectInvite(String user, String workspace) throws ConnectionRemoteException { + reject_invite(this.ptr, user, workspace); } - private static native void create_workspace(long self, String workspaceId) throws ConnectionRemoteException; + private static native void quit_workspace(long self, String user, String workspace) throws ConnectionRemoteException; /** - * Creates a workspace. You need to call {@link #attachWorkspace(String)} to actually join + * Quits a workspace. + * @param user the owner of the workspace + * @param workspace the identifier of the workspace + * @throws ConnectionRemoteException if an error occurs in communicating with the server + */ + public void quitWorkspace(String user, String workspace) throws ConnectionRemoteException { + quit_workspace(this.ptr, user, workspace); + } + + private static native void create_workspace(long self, String workspace) throws ConnectionRemoteException; + + /** + * Creates a workspace. You need to call {@link #attachWorkspace(String, String)} to actually join * and interact with it. - * @param workspaceId the id of the new workspace + * @param workspace the id of the new workspace * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public void createWorkspace(String workspaceId) throws ConnectionRemoteException { - create_workspace(this.ptr, workspaceId); + public void createWorkspace(String workspace) throws ConnectionRemoteException { + create_workspace(this.ptr, workspace); } - private static native void delete_workspace(long self, String workspaceId) throws ConnectionRemoteException; + private static native void delete_workspace(long self, String workspace) throws ConnectionRemoteException; /** * Deletes a workspace. - * @param workspaceId the id of the workspace to delete + * @param workspace the id of the workspace to delete * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public void deleteWorkspace(String workspaceId) throws ConnectionRemoteException { - delete_workspace(this.ptr, workspaceId); + public void deleteWorkspace(String workspace) throws ConnectionRemoteException { + delete_workspace(this.ptr, workspace); } private static native void invite_to_workspace(long self, String workspaceId, String user) throws ConnectionRemoteException; /** * Invites a user to a workspace. - * @param workspaceId the id of the new workspace + * @param workspace the id of the new workspace * @param user the name of the user to invite * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public void inviteToWorkspace(String workspaceId, String user) throws ConnectionRemoteException { - invite_to_workspace(this.ptr, workspaceId, user); + public void inviteToWorkspace(String workspace, String user) throws ConnectionRemoteException { + invite_to_workspace(this.ptr, workspace, user); } - private static native String[] fetch_owned_workspaces(long self) throws ConnectionRemoteException; + private static native WorkspaceIdentifier[] fetch_owned_workspaces(long self) throws ConnectionRemoteException; /** * Lists workspaces owned by the current user. - * @return an array of workspace IDs + * @return an array of {@link WorkspaceIdentifier}s * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public String[] fetchOwnedWorkspaces() throws ConnectionRemoteException { + public WorkspaceIdentifier[] fetchOwnedWorkspaces() throws ConnectionRemoteException { return fetch_owned_workspaces(this.ptr); } - private static native String[] fetch_joined_workspaces(long self) throws ConnectionRemoteException; + private static native WorkspaceIdentifier[] fetch_joined_workspaces(long self) throws ConnectionRemoteException; /** * Lists workspaces the current user has joined. - * @return an array of workspace IDs + * @return an array of {@link WorkspaceIdentifier}s * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public String[] fetchJoinedWorkspaces() throws ConnectionRemoteException { + public WorkspaceIdentifier[] fetchJoinedWorkspaces() throws ConnectionRemoteException { return fetch_joined_workspaces(this.ptr); } - private static native String[] active_workspaces(long self); + private static native WorkspaceIdentifier[] active_workspaces(long self); /** * Lists the currently active workspaces (the ones the user has currently joined). - * @return an array of workspace IDs + * @return an array of {@link WorkspaceIdentifier}s */ - public String[] activeWorkspaces() { + public WorkspaceIdentifier[] activeWorkspaces() { return active_workspaces(this.ptr); } - private static native boolean leave_workspace(long self, String workspaceId); + private static native boolean leave_workspace(long self, String user, String workspace); /** * Leaves a workspace. - * @param workspaceId the id of the workspaces to leave + * @param user the owner of the workspace + * @param workspace the identifier of the workspace * @return true if it succeeded or wasn't in the workspace; false if there are still * leftover references around */ - public boolean leaveWorkspace(String workspaceId) { - return leave_workspace(this.ptr, workspaceId); + public boolean leaveWorkspace(String user, String workspace) { + return leave_workspace(this.ptr, user, workspace); } - private static native Workspace get_workspace(long self, String workspace); + private static native Workspace get_workspace(long self, String user, String workspace); /** * Gets an active workspace. - * @param workspaceId the id of the workspaces to get - * @return a {@link Workspace} with that name, if it was present and active + * @param user the owner of the workspace + * @param workspace the identifier of the workspace + * @return a {@link Workspace} with that name, if it was present and active, null otherwise + */ + public Workspace getWorkspace(String user, String workspace) { + return get_workspace(this.ptr, user, workspace); + } + + private static native UserInfo get_user_info(long self, String user) throws ConnectionRemoteException; + + /** + * Fetches information about a user by name. + * @param user the name of the user + * @return the {@link UserInfo} for the user + * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public Optional getWorkspace(String workspaceId) { - return Optional.ofNullable(get_workspace(this.ptr, workspaceId)); + public UserInfo getUserInfo(String user) throws ConnectionRemoteException { + return get_user_info(this.ptr, user); } private static native void refresh(long self) throws ConnectionRemoteException; diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index 2a0f5ee8..e612f732 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -2,9 +2,9 @@ import mp.code.data.Cursor; import mp.code.data.Selection; +import mp.code.data.WorkspaceIdentifier; import mp.code.exceptions.ControllerException; -import java.util.Optional; import java.util.function.Consumer; /** @@ -20,16 +20,25 @@ public final class CursorController { Extensions.CLEANER.register(this, () -> free(ptr)); } + private static native WorkspaceIdentifier workspace_id(long self); + + /** + * Gets the identifier for the one that contains this cursor. + * @return a {@link WorkspaceIdentifier} for the owner + */ + public WorkspaceIdentifier workspaceId() { + return workspace_id(this.ptr); + } + private static native Cursor try_recv(long self) throws ControllerException; /** - * Tries to get a {@link Cursor} update from the queue if any were present, and returns - * an empty optional otherwise. + * Tries to get a {@link Cursor} update from the queue if any were present, null otherwise. * @return the first cursor event in queue, if any are present * @throws ControllerException if the controller was stopped */ - public Optional tryRecv() throws ControllerException { - return Optional.ofNullable(try_recv(this.ptr)); + public Cursor tryRecv() throws ControllerException { + return try_recv(this.ptr); } private static native Cursor recv(long self) throws ControllerException; diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index bd417a72..8e321694 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -4,7 +4,7 @@ import java.util.function.Consumer; import lombok.Getter; -import mp.code.data.User; +import mp.code.data.UserInfo; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; @@ -15,7 +15,7 @@ * Generally, it is safer to avoid storing this directly. Instead, * users should let the native library manage as much as possible for * them. They should store the workspace ID and retrieve the object - * whenever needed with {@link Client#getWorkspace(String)}. + * whenever needed with {@link Client#getWorkspace(String, String)}. */ public final class Workspace { private final long ptr; @@ -61,12 +61,11 @@ public Optional getBuffer(String path) { /** * Searches for buffers matching the filter in this workspace. - * @param filter the filter to apply + * @param filter the filter to apply (may be null) * @return an array containing file tree as flat paths */ - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - public String[] searchBuffers(Optional filter) { - return search_buffers(this.ptr, filter.orElse(null)); + public String[] searchBuffers(String filter) { + return search_buffers(this.ptr, filter); } private static native String[] active_buffers(long self); @@ -80,25 +79,48 @@ public String[] activeBuffers() { return active_buffers(this.ptr); } - private static native User[] user_list(long self); + private static native UserInfo[] user_list(long self); /** * Returns the users currently in the workspace. * @return an array containing the users in the workspace */ - public User[] userList() { + public UserInfo[] userList() { return user_list(this.ptr); } - private static native void create_buffer(long self, String path) throws ConnectionRemoteException; + private static native void create_buffer(long self, String path, boolean ephemeral) throws ConnectionRemoteException; /** * Creates a buffer with the given path. * @param path the new buffer's path + * @param ephemeral whether the buffer should be ephemeral * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public void createBuffer(String path) throws ConnectionRemoteException { - create_buffer(this.ptr, path); + public void createBuffer(String path, boolean ephemeral) throws ConnectionRemoteException { + create_buffer(this.ptr, path, ephemeral); + } + + private static native void pin_buffer(long self, String path) throws ConnectionRemoteException; + + /** + * Pins an ephemeral buffer, making it non-ephemeral. + * @param path the buffer's path + * @throws ConnectionRemoteException if an error occurs in communicating with the server + */ + public void pinBuffer(String path) throws ConnectionRemoteException { + pin_buffer(this.ptr, path); + } + + private static native void un_pin_buffer(long self, String path) throws ConnectionRemoteException; + + /** + * Unpins a buffer, making it ephemeral. + * @param path the buffer's path + * @throws ConnectionRemoteException if an error occurs in communicating with the server + */ + public void unpinBuffer(String path) throws ConnectionRemoteException { + un_pin_buffer(this.ptr, path); } private static native BufferController attach_buffer(long self, String path) throws ConnectionException; @@ -135,28 +157,38 @@ public String[] fetchBuffers() throws ConnectionRemoteException { return fetch_buffers(this.ptr); } - private static native User[] fetch_users(long self) throws ConnectionRemoteException; + private static native void fetch_users(long self) throws ConnectionRemoteException; /** - * Updates and fetches the local list of users. - * @return the updated list + * Updates the local list of users. * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public User[] fetchUsers() throws ConnectionRemoteException { - return fetch_users(this.ptr); + public void fetchUsers() throws ConnectionRemoteException { + fetch_users(this.ptr); } - private static native User[] fetch_buffer_users(long self, String path) throws ConnectionRemoteException; + private static native void fetch_buffer_users(long self, String path) throws ConnectionRemoteException; /** - * Fetches the users attached to a certain buffer. + * Updates the local list of users attached to a certain buffer. * The user must be attached to the buffer to perform this operation. * @param path the path of the buffer to search - * @return an array of {@link User}s * @throws ConnectionRemoteException if an error occurs in communicating with the server, or the user wasn't attached */ - public User[] fetchBufferUsers(String path) throws ConnectionRemoteException { - return fetch_buffer_users(this.ptr, path); + public void fetchBufferUsers(String path) throws ConnectionRemoteException { + fetch_buffer_users(this.ptr, path); + } + + private static native UserInfo[] buffer_user_list(long self, String path); + + /** + * Gets the local list of users attached to a certain buffer. + * The user must be attached to the buffer to perform this operation. + * You can force-update the list with {@link #fetchBufferUsers(String)}. + * @param path the path of the buffer to search + */ + public UserInfo[] bufferUserList(String path) { + return buffer_user_list(this.ptr, path); } private static native void delete_buffer(long self, String path) throws ConnectionRemoteException; @@ -173,13 +205,12 @@ public void deleteBuffer(String path) throws ConnectionRemoteException { private static native Event try_recv(long self) throws ControllerException; /** - * Tries to get a {@link Event} from the queue if any were present, and returns - * an empty optional otherwise. + * Tries to get a {@link Event} from the queue if any were present, null otherwise * @return the first workspace event in queue, if any are present * @throws ControllerException if the controller was stopped */ - public Optional tryRecv() throws ControllerException { - return Optional.ofNullable(try_recv(this.ptr)); + public Event tryRecv() throws ControllerException { + return try_recv(this.ptr); } private static native Event recv(long self) throws ControllerException; diff --git a/dist/java/src/mp/code/data/BufferNode.java b/dist/java/src/mp/code/data/BufferNode.java new file mode 100644 index 00000000..a8c9eff5 --- /dev/null +++ b/dist/java/src/mp/code/data/BufferNode.java @@ -0,0 +1,23 @@ +package mp.code.data; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * The unique identifier of a buffer. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class BufferNode { + /** + * The path the buffer is treated to be in, UNIX-type. + */ + public final String path; + + /** + * Whether this buffer gets auto-deleted once all users leave. + */ + public final boolean ephemeral; +} diff --git a/dist/java/src/mp/code/data/BufferUpdate.java b/dist/java/src/mp/code/data/BufferUpdate.java index 99794112..7cff9518 100644 --- a/dist/java/src/mp/code/data/BufferUpdate.java +++ b/dist/java/src/mp/code/data/BufferUpdate.java @@ -5,22 +5,20 @@ import lombok.ToString; import mp.code.Extensions; -import java.util.OptionalLong; - /** * A data class holding information about a buffer update. */ @ToString @EqualsAndHashCode @RequiredArgsConstructor -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class BufferUpdate { /** * The hash of the content after applying it (calculated with {@link Extensions#hash(String)}). * It is generally meaningless to send, but when received it is an invitation to check the hash * and forcefully re-sync if necessary. + * Most of the time, this will be null. */ - public final OptionalLong hash; // xxh3 hash + public final Long hash; // xxh3 hash /** * The CRDT version after the associated change has been applied. diff --git a/dist/java/src/mp/code/data/Config.java b/dist/java/src/mp/code/data/Config.java index 7864e973..73286bc9 100644 --- a/dist/java/src/mp/code/data/Config.java +++ b/dist/java/src/mp/code/data/Config.java @@ -5,27 +5,23 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; -import java.util.Optional; -import java.util.OptionalInt; - /** * A data class representing the connection configuration. */ @ToString @EqualsAndHashCode @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class Config { /** The username to connect with. */ public final String username; /** The password to connect with. */ public final String password; - /** The host to connect to, if custom. */ - public final Optional host; - /** The port to connect to, if custom. */ - public final OptionalInt port; - /** Whether to use TLS, if custom. */ - public final Optional tls; + /** The host to connect to, if custom. Null otherwise. */ + public final String host; + /** The port to connect to, if custom. Null otherwise. */ + public final Integer port; + /** Whether to use TLS, if custom. Null otherwise. */ + public final Boolean tls; /** * Provides the given username and password on the default server. @@ -33,30 +29,6 @@ public class Config { * @param password the password */ public Config(String username, String password) { - this( - username, - password, - Optional.empty(), - OptionalInt.empty(), - Optional.empty() - ); - } - - /** - * Provides the given username and password as well as a custom server. - * @param username the username - * @param password the password - * @param host the host server - * @param port the port CodeMP is running on, must be between 0 and 65535 (will be clamped) - * @param tls whether to use TLS - */ - public Config(String username, String password, String host, int port, boolean tls) { - this( - username, - password, - Optional.of(host), - OptionalInt.of(port), - Optional.of(tls) - ); + this(username, password, null, null, null); } } diff --git a/dist/java/src/mp/code/data/Cursor.java b/dist/java/src/mp/code/data/Cursor.java index d52ad589..4c5ae71d 100644 --- a/dist/java/src/mp/code/data/Cursor.java +++ b/dist/java/src/mp/code/data/Cursor.java @@ -12,12 +12,12 @@ @RequiredArgsConstructor public class Cursor { /** - * The user who controls the cursor. + * The buffer the cursor is on. */ - public final String user; + public final String buffer; /** - * The associated selection update. + * The associated selection updates. */ - public final Selection selection; + public final Selection[] selection; } diff --git a/dist/java/src/mp/code/data/CursorEvent.java b/dist/java/src/mp/code/data/CursorEvent.java new file mode 100644 index 00000000..aafd30b2 --- /dev/null +++ b/dist/java/src/mp/code/data/CursorEvent.java @@ -0,0 +1,23 @@ +package mp.code.data; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * A data class representing an event about a user's cursor. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class CursorEvent { + /** + * The user who sent the cursor. + */ + public final String user; + + /** + * The cursor position data. + */ + public final Cursor cursor; +} diff --git a/dist/java/src/mp/code/data/Selection.java b/dist/java/src/mp/code/data/Selection.java index cc31cd48..f80ced8c 100644 --- a/dist/java/src/mp/code/data/Selection.java +++ b/dist/java/src/mp/code/data/Selection.java @@ -34,9 +34,4 @@ public class Selection { * If negative, it is clamped to 0. */ public final int endCol; - - /** - * The buffer the cursor is located on. - */ - public final String buffer; } diff --git a/dist/java/src/mp/code/data/User.java b/dist/java/src/mp/code/data/User.java deleted file mode 100644 index b5ef86bd..00000000 --- a/dist/java/src/mp/code/data/User.java +++ /dev/null @@ -1,25 +0,0 @@ -package mp.code.data; - -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - -import java.util.UUID; - -/** - * A data class holding information about a user. - */ -@ToString -@EqualsAndHashCode -@RequiredArgsConstructor -public class User { - /** - * The {@link UUID} of the user. - */ - public final UUID id; - - /** - * The human-readable name of the user. - */ - public final String name; -} diff --git a/dist/java/src/mp/code/data/UserInfo.java b/dist/java/src/mp/code/data/UserInfo.java new file mode 100644 index 00000000..963c1726 --- /dev/null +++ b/dist/java/src/mp/code/data/UserInfo.java @@ -0,0 +1,33 @@ +package mp.code.data; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * A data class holding information about a user. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class UserInfo { + /** + * The unique name of the user. + */ + public final String name; + + /** + * The visible name of the user, may be null. + */ + public final String displayName; + + /** + * User description ("bio"). + */ + public final String description; + + /** + * A small image some editors can display, may be null. + */ + public final byte[] avatar; +} diff --git a/dist/java/src/mp/code/data/WorkspaceIdentifier.java b/dist/java/src/mp/code/data/WorkspaceIdentifier.java new file mode 100644 index 00000000..38a2f4d7 --- /dev/null +++ b/dist/java/src/mp/code/data/WorkspaceIdentifier.java @@ -0,0 +1,23 @@ +package mp.code.data; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * The unique identifier of a workspace. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class WorkspaceIdentifier { + /** + * The workspace name, cannot change and is guaranteed to be unique per owner. + */ + public final String workspace; + + /** + * The workspace's owner. + */ + public final String user; +} diff --git a/src/api/buffer.rs b/src/api/buffer.rs index 23f5e924..403d2da4 100644 --- a/src/api/buffer.rs +++ b/src/api/buffer.rs @@ -5,7 +5,7 @@ #[derive(Debug, Clone)] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/BufferNode"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/BufferNode"))] pub struct BufferNode { /// Buffer path, sort of like a UNIX path. pub path: String, diff --git a/src/api/change.rs b/src/api/change.rs index 1c52c307..721b7373 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -11,7 +11,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/BufferUpdate"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/BufferUpdate"))] pub struct BufferUpdate { /// Optional content hash after applying this change. #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] @@ -55,7 +55,7 @@ pub struct BufferUpdate { #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/TextChange"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/TextChange"))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. pub start_idx: u32, diff --git a/src/api/config.rs b/src/api/config.rs index 6742fb0c..cf46e925 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -12,7 +12,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Config"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Config"))] pub struct Config { /// User identifier used to register, possibly your email. pub username: String, diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 1f199e65..e180c5ab 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -6,7 +6,7 @@ use pyo3::prelude::*; /// An event that occurred about a user's cursor. #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/CursorEvent"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/CursorEvent"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -20,7 +20,7 @@ pub struct CursorEvent { /// A cursor instantaneous state #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Cursor"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Cursor"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -34,7 +34,7 @@ pub struct Cursor { /// A cursor selection span. #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/Selection"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Selection"))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/api/user.rs b/src/api/user.rs index 8b95a513..bcea34ee 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -4,7 +4,7 @@ /// Represents a service user #[derive(Debug, Clone)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/UserInfo"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/UserInfo"))] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct UserInfo { diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 424c7910..5ec657ce 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -7,7 +7,7 @@ /// Represents a service workspace #[derive(Debug, Clone, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/WorkspaceIdentifier"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/WorkspaceIdentifier"))] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct WorkspaceIdentifier { diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index fcc6456d..73a4699b 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -2,19 +2,25 @@ use jni::{Env, objects::JObject}; use jni_toolbox::jni; use crate::{ - api::{AsyncReceiver, AsyncSender, BufferUpdate, TextChange}, + api::{AsyncReceiver, AsyncSender, BufferUpdate, TextChange, WorkspaceIdentifier}, errors::ControllerError, }; /// Get the name of the buffer. #[jni(package = "mp.code", class = "BufferController")] -fn get_name(controller: &mut crate::buffer::Controller) -> String { - controller.path().to_string() //TODO: &str is built into the newer version +fn path(controller: &mut crate::buffer::Controller) -> String { + controller.path().to_string() +} + +/// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. +#[jni(package = "mp.code", class = "BufferController")] +fn workspace_id(controller: &mut crate::buffer::Controller) -> WorkspaceIdentifier { + controller.workspace_id().clone() } /// Get the contents of the buffers. #[jni(package = "mp.code", class = "BufferController")] -fn get_content(controller: &mut crate::buffer::Controller) -> Result { +fn content(controller: &mut crate::buffer::Controller) -> Result { super::tokio().block_on(controller.content()) } diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index b627ff14..ae343f46 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,6 +1,6 @@ use crate::{ Workspace, - api::{Config, WorkspaceIdentifier}, + api::{Config, UserInfo, WorkspaceIdentifier}, client::Client, errors::{ConnectionError, RemoteError}, }; @@ -24,6 +24,24 @@ fn attach_workspace(client: &mut Client, user: String, workspace: String) -> Res super::tokio().block_on(client.attach_workspace(user, workspace)) } +/// Accepts an invitation to a workspace. +#[jni(package = "mp.code", class = "Client")] +fn accept_invite(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { + super::tokio().block_on(client.accept_invite(user, workspace)) +} + +/// Rejects an invitation to a workspace. +#[jni(package = "mp.code", class = "Client")] +fn reject_invite(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { + super::tokio().block_on(client.reject_invite(user, workspace)) +} + +/// Quit a joined [Workspace]. +#[jni(package = "mp.code", class = "Client")] +fn quit_workspace(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { + super::tokio().block_on(client.quit_workspace(user, workspace)) +} + /// Create a workspace on server, if allowed to. #[jni(package = "mp.code", class = "Client")] fn create_workspace(client: &mut Client, workspace: String) -> Result<(), RemoteError> { @@ -38,11 +56,7 @@ fn delete_workspace(client: &mut Client, workspace: String) -> Result<(), Remote /// Invite another user to an owned workspace. #[jni(package = "mp.code", class = "Client")] -fn invite_to_workspace( - client: &mut Client, - workspace: String, - user: String, -) -> Result<(), RemoteError> { +fn invite_to_workspace(client: &mut Client, workspace: String, user: String) -> Result<(), RemoteError> { super::tokio().block_on(client.invite_to_workspace(workspace, user)) } @@ -76,6 +90,12 @@ fn get_workspace(client: &mut Client, user: String, workspace: String) -> Option client.get_workspace(user, workspace) } +/// Fetches information about a user. +#[jni(package = "mp.code", class = "Client")] +fn get_user_info(client: &mut Client, user: String) -> Result { + super::tokio().block_on(client.get_user_info(user)) +} + /// Refresh the client's session token. #[jni(package = "mp.code", class = "Client")] fn refresh(client: &mut Client) -> Result<(), RemoteError> { diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 544c61d9..2b4da9b8 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,10 +1,16 @@ use crate::{ - api::{AsyncReceiver, AsyncSender, Cursor, CursorEvent}, + api::{AsyncReceiver, AsyncSender, Cursor, CursorEvent, WorkspaceIdentifier}, errors::ControllerError, }; use jni::{Env, objects::JObject}; use jni_toolbox::jni; +/// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. +#[jni(package = "mp.code", class = "CursorController")] +fn workspace_id(controller: &mut crate::cursor::Controller) -> WorkspaceIdentifier { + controller.workspace_id().clone() +} + /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] fn try_recv(controller: &mut crate::cursor::Controller) -> Result, ControllerError> { diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 6eaba302..e515e807 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -48,12 +48,21 @@ fn create_buffer(workspace: &mut Workspace, path: String, ephemeral: bool) -> Re super::tokio().block_on(workspace.create_buffer(path, ephemeral)) } +/// Pins an ephemeral buffer. +#[jni(package = "mp.code", class = "Workspace")] +fn pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { + super::tokio().block_on(workspace.pin_buffer(path)) +} + +/// Un-pins an ephemeral buffer. +#[jni(package = "mp.code", class = "Workspace")] +fn un_pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { + super::tokio().block_on(workspace.un_pin_buffer(path)) +} + /// Attach to a buffer and return a pointer to its [`crate::buffer::Controller`]. #[jni(package = "mp.code", class = "Workspace")] -fn attach_buffer( - workspace: &mut Workspace, - path: String, -) -> Result { +fn attach_buffer(workspace: &mut Workspace, path: String) -> Result { super::tokio().block_on(workspace.attach_buffer(&path)) } @@ -77,13 +86,16 @@ fn fetch_users(workspace: &mut Workspace) -> Result<(), RemoteError> { /// Fetch users attached to a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_buffer_users( - workspace: &mut Workspace, - path: String, -) -> Result<(), RemoteError> { +fn fetch_buffer_users(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_buffer_users(&path)) } +/// Fetch users attached to a buffer. +#[jni(package = "mp.code", class = "Workspace")] +fn buffer_user_list(workspace: &mut Workspace, path: String) -> Vec { + workspace.buffer_user_list(&path) +} + /// Delete a buffer. #[jni(package = "mp.code", class = "Workspace")] fn delete_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { From 1f5689bd7645922e08e19f3e70211c0a1f6e9e7e Mon Sep 17 00:00:00 2001 From: zaaarf Date: Fri, 13 Mar 2026 03:58:25 +0100 Subject: [PATCH 060/121] docs: added missing docs for various functions and fields --- src/api/event.rs | 32 +++++++++++++++++++++++++------- src/api/user.rs | 2 ++ src/buffer/controller.rs | 2 +- src/cursor/controller.rs | 1 + src/cursor/worker.rs | 1 + src/ext.rs | 5 +++++ src/workspace.rs | 2 ++ 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/api/event.rs b/src/api/event.rs index 0c64a0d6..46409cd4 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -10,17 +10,35 @@ use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", serde(tag = "type"))] pub enum Event { - /// Fired when the file tree changes. - /// Contains the modified buffer path (deleted, created or renamed). - FileTreeUpdated { path: String }, + /// Fired when the file tree changes (buffer created, deleted or renamed). + FileTreeUpdated { + /// The modifier buffer's path. + path: String + }, /// Fired when an user joins the current workspace. - UserJoin { name: String }, + UserJoin { + /// The name of the joining user. + name: String + }, /// Fired when an user leaves the current workspace. - UserLeave { name: String }, + UserLeave { + /// The name of the leaving user. + name: String + }, /// Fired when an user joins a buffer. - UserJoinBuffer { name: String, buffer: String }, + UserJoinBuffer { + /// The name of the joining user. + name: String, + /// The name of the buffer the user is joining. + buffer: String + }, /// Fired when an user leaves a buffer. - UserLeaveBuffer { name: String, buffer: String }, + UserLeaveBuffer { + /// The name of the leaving user. + name: String, + /// The name of the buffer the user is leaving. + buffer: String + }, } impl From for Event { diff --git a/src/api/user.rs b/src/api/user.rs index bcea34ee..121098fa 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -19,6 +19,8 @@ pub struct UserInfo { } impl UserInfo { + /// Creates a default [UserInfo] for the given username + /// with all the optional fields set to [None]. pub fn default_for(username: String) -> Self { Self { name: username, diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index a74bfe94..8d160a33 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -22,7 +22,7 @@ use crate::ext::IgnorableError; pub struct BufferController(pub(crate) Arc); impl BufferController { - /// Get id of workspace containing this controller + /// Get id of workspace containing this controller. pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { &self.0.workspace_id } diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 08cedb70..a78501d0 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -25,6 +25,7 @@ use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol, cursor_client:: pub struct CursorController(pub(crate) Arc); impl CursorController { + /// Get id of workspace containing this controller. pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { &self.0.workspace_id } diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index b14f16d7..2acce74a 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -100,6 +100,7 @@ impl CursorController { CursorController(controller) } + /// Retrieves all current cursor positions. pub async fn list(&self) -> RemoteResult> { Ok(self .0 diff --git a/src/ext.rs b/src/ext.rs index 177de61c..de162975 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -75,6 +75,7 @@ impl Default for InternallyMutable { } impl InternallyMutable { + /// Creates a new internally mutable type with the given value. pub fn new(init: T) -> Self { let (tx, rx) = tokio::sync::watch::channel(init); Self { @@ -83,16 +84,19 @@ impl InternallyMutable { } } + /// Updates the internal value. pub fn set(&self, state: T) -> T { self.setter.send_replace(state) } + /// Gets the [tokio::sync::watch::Receiver] that can get the internal value. pub fn channel(&self) -> tokio::sync::watch::Receiver { self.getter.clone() } } impl InternallyMutable { + /// Gets and clones the internal value. pub fn get(&self) -> T { self.getter.borrow().clone() } @@ -100,6 +104,7 @@ impl InternallyMutable { /// An error that can be ignored with just a warning. pub trait IgnorableError { + /// Unwraps the error and prints a warning with the contents. fn unwrap_or_warn(self, msg: &str); } diff --git a/src/workspace.rs b/src/workspace.rs index 8a8facf6..9a6ba021 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -186,6 +186,7 @@ impl Workspace { Ok(()) } + /// Pin an ephemeral buffer, making it permanent. pub async fn pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { self.0 .services @@ -196,6 +197,7 @@ impl Workspace { Ok(()) } + /// Unpins a permanen buffer, making it ephemeral. pub async fn un_pin_buffer(&self, path: impl AsRef) -> RemoteResult<()> { self.0 .services From 527b279086ee43cf10ff6cea4ac0c322507baa6a Mon Sep 17 00:00:00 2001 From: zaaarf Date: Fri, 13 Mar 2026 04:00:25 +0100 Subject: [PATCH 061/121] docs: add basic crate-level docs for build.rs --- build.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.rs b/build.rs index 0ee4a1e0..dab48246 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,6 @@ +//! Buildscript, required by some glue modules for initialisation. +//! Will do nothing if no glue modules are enabled. + #[cfg(feature = "js")] extern crate napi_build; From bc73902434575f1446d63d505e2a980a4d626548 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Fri, 13 Mar 2026 04:05:41 +0100 Subject: [PATCH 062/121] ci: fix coverage check for statics --- src/tests/coverage/ffi.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/coverage/ffi.rs b/src/tests/coverage/ffi.rs index b1b9147c..fa81375e 100644 --- a/src/tests/coverage/ffi.rs +++ b/src/tests/coverage/ffi.rs @@ -125,7 +125,8 @@ fn discover_core_surface(files: &[&str], targets: &[&str]) -> BTreeMap) -> Vec { required .iter() - .filter(|method| !ffi_src.contains(&format!(".{}(", method))) + // TODO: way to tighten it down, maybe by only checking :: for statics and vice versa + .filter(|method| !ffi_src.contains(&format!(".{}(", method)) && !ffi_src.contains(&format!("::{}(", method))) .map(|method| (*method).to_string()) .collect() } From db701e958d702296c9d9420e20eef8a7dc8deea8 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 13 Mar 2026 10:00:01 +0100 Subject: [PATCH 063/121] tests: better lua coverage, add java coverage --- src/tests/coverage/annotations.rs | 68 ++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/tests/coverage/annotations.rs b/src/tests/coverage/annotations.rs index f3cdb1bb..d5481f67 100644 --- a/src/tests/coverage/annotations.rs +++ b/src/tests/coverage/annotations.rs @@ -3,29 +3,77 @@ fn lua_annotations_should_cover_ffi_api_surface() { let annotations = include_str!("../../../dist/lua/annotations.lua"); + let source_maps = [ + ("Client", include_str!("../../ffi/lua/client.rs"), false), + ("Workspace", include_str!("../../ffi/lua/workspace.rs"), false), + ("BufferController", include_str!("../../ffi/lua/buffer.rs"), false), + ("CursorController", include_str!("../../ffi/lua/cursor.rs"), false), + ("Codemp", include_str!("../../ffi/lua/mod.rs"), true), + ]; + + let re = regex::Regex::new("add_method\\(\\s+\"(\\w+)\",|exports\\.set\\(\\s+\"(\\w+)\",").expect("failed building regex"); + + let mut missing = Vec::new(); + for (clazz, source, is_static) in source_maps { + for (_, [fn_name]) in re.captures_iter(source).map(|c| c.extract()) { + let sep = if is_static { '.' } else { ':' }; + let search = format!("{clazz}{sep}{fn_name}"); + if !annotations.contains(&search) { + #[cfg(feature = "ci")] + { + println!("::warning title=Coverage::Missing Lua annotations for method: '{search}'"); + } + missing.push(search); + } + } + } + + assert!( + missing.is_empty(), + "missing lua annotations for methods: '{}'", + missing.join("', '"), + ); +} + +#[test] +#[cfg(all(test, feature = "java"))] +fn java_annotations_should_cover_ffi_api_surface() { + + let mut annotations_map = std::collections::HashMap::new(); + for (clazz, content) in [ + ("Client", include_str!("../../../dist/java/src/mp/code/Client.java")), + ("Workspace", include_str!("../../../dist/java/src/mp/code/Workspace.java")), + ("Extensions", include_str!("../../../dist/java/src/mp/code/Extensions.java")), + ("BufferController", include_str!("../../../dist/java/src/mp/code/BufferController.java")), + ("CursorController", include_str!("../../../dist/java/src/mp/code/CursorController.java")), + ] { + annotations_map.insert(clazz, content); + } + let source = concat!( - include_str!("../../ffi/lua/client.rs"), - include_str!("../../ffi/lua/workspace.rs"), - include_str!("../../ffi/lua/buffer.rs"), - include_str!("../../ffi/lua/cursor.rs"), + include_str!("../../ffi/java/client.rs"), + include_str!("../../ffi/java/workspace.rs"), + include_str!("../../ffi/java/buffer.rs"), + include_str!("../../ffi/java/cursor.rs"), + include_str!("../../ffi/java/ext.rs"), ); - let re = regex::Regex::new("add_method\\(\\s+\"(\\w+)\",").expect("failed building regex"); + let re = regex::Regex::new("#\\[jni\\(.*class = \"(\\w+)\"\\)\\]\\nfn (\\w+)\\(").expect("failed building regex"); let mut missing = Vec::new(); - for (_, [fn_name]) in re.captures_iter(source).map(|c| c.extract()) { - if !annotations.contains(fn_name) { + for (_, [clazz, fn_name]) in re.captures_iter(source).map(|c| c.extract()) { + if !annotations_map.get(clazz).unwrap_or(&"").contains(fn_name) { #[cfg(feature = "ci")] { - println!("::warning title=Coverage::Missing Lua annotations for method: '{fn_name}'"); + println!("::warning title=Coverage::Missing Java annotations for method: '{clazz}.{fn_name}'"); } - missing.push(fn_name.to_string()); + missing.push(format!("{clazz}.{fn_name}")); } } assert!( missing.is_empty(), - "missing lua annotations for methods: '{}'", + "missing java annotations for methods: '{}'", missing.join("', '"), ); } From 14b0aeb84dbd4fff952513c9f1a511e0758db25e Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 13 Mar 2026 11:29:11 +0100 Subject: [PATCH 064/121] fix: java feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3dcf3a95..23e43c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ napi-build = { version = "2.2", optional = true } pyo3-build-config = { version = "0.28", optional = true } [features] -default = ["lua-jit", "py-abi3", "py-extmod", "java"] +default = ["lua-jit", "py-abi3", "py-extmod"] # extra async-trait = ["dep:async-trait"] serialize = ["dep:serde", "uuid/serde"] From 84f99886608778a4f054f973cbc369cf0077d31c Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 13 Mar 2026 12:11:04 +0100 Subject: [PATCH 065/121] tests: oops fix regex --- src/tests/coverage/annotations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/coverage/annotations.rs b/src/tests/coverage/annotations.rs index d5481f67..f1f745f8 100644 --- a/src/tests/coverage/annotations.rs +++ b/src/tests/coverage/annotations.rs @@ -11,7 +11,7 @@ fn lua_annotations_should_cover_ffi_api_surface() { ("Codemp", include_str!("../../ffi/lua/mod.rs"), true), ]; - let re = regex::Regex::new("add_method\\(\\s+\"(\\w+)\",|exports\\.set\\(\\s+\"(\\w+)\",").expect("failed building regex"); + let re = regex::Regex::new("add_method\\(\\s*\"(\\w+)\",|exports\\.set\\(\\s+\"(\\w+)\",").expect("failed building regex"); let mut missing = Vec::new(); for (clazz, source, is_static) in source_maps { @@ -58,7 +58,7 @@ fn java_annotations_should_cover_ffi_api_surface() { include_str!("../../ffi/java/ext.rs"), ); - let re = regex::Regex::new("#\\[jni\\(.*class = \"(\\w+)\"\\)\\]\\nfn (\\w+)\\(").expect("failed building regex"); + let re = regex::Regex::new("#\\[jni\\(.*class = \"(\\w+)\".*\\)\\]\\nfn (\\w+)\\(").expect("failed building regex"); let mut missing = Vec::new(); for (_, [clazz, fn_name]) in re.captures_iter(source).map(|c| c.extract()) { From ea871d50b4d2a6f463fd72bdf1b5aaf2fcf2f3f6 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 13 Mar 2026 12:11:12 +0100 Subject: [PATCH 066/121] fix(lua): print in debug level --- src/ffi/lua/ext/a_sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ffi/lua/ext/a_sync.rs b/src/ffi/lua/ext/a_sync.rs index 90fa58a1..a81ddeb5 100644 --- a/src/ffi/lua/ext/a_sync.rs +++ b/src/ffi/lua/ext/a_sync.rs @@ -83,7 +83,7 @@ impl LuaUserData for Promise { pub(crate) fn setup_driver(_: &Lua, (block,): (Option,)) -> LuaResult> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let future = async move { - tracing::info!(" :: driving runtime..."); + tracing::debug!(" :: driving runtime..."); tokio::select! { () = std::future::pending::<()>() => {}, _ = rx.recv() => {}, From 78c2c0d8ac7c5efc90fcb4834ba663383f154029 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 13 Mar 2026 12:20:49 +0100 Subject: [PATCH 067/121] fix(lua): missing annotations --- dist/lua/annotations.lua | 34 ++++++++++++++++++++++++++++++++-- src/ffi/lua/client.rs | 5 ++++- src/ffi/lua/workspace.rs | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index e6818c08..bb23924c 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -295,6 +295,19 @@ function Client:get_workspace(ws) end ---get full user info for given username from server function Client:get_user_info(user) end +---@return NilPromise +---@async +---@nodiscard +---block until next session event without returning it +function Client:poll() end + +---clears any previously registered session callback +function Client:clear_callback() end + +---@param cb fun(w: Client) callback to invoke on each workspace event received +---register a new callback to be called on session events (replaces any previously registered one) +function Client:callback(cb) end + ---@class UserInfo @@ -316,7 +329,7 @@ function Client:get_user_info(user) end ---a joined codemp workspace local Workspace = {} ----@return string +---@return WorkspaceIdentifier ---workspace id function Workspace:id() end @@ -461,6 +474,14 @@ local BufferUpdate = {} ---apply this text change to a string, returning the result function TextChange:apply(other) end +---@return WorkspaceIdentifier +---returns the workspace id this buffer belongs to +function BufferController:workspace_id() end + +---@return string +---returns the path this buffer belongs to +function BufferController:path() end + ---@param change TextChange text change to broadcast ---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(change) end @@ -524,11 +545,20 @@ local CursorController = {} ---@field user string user who sent this cursor ---@field cursor Cursor cursor position data +---@return WorkspaceIdentifier +---returns the workspace id this cursor controller belongs to +function CursorController:workspace_id() end + +---@return CursorEvent[] +---@async +---@nodiscard +---gets the current state of all user cursors +function CursorController:list() end + ---@param cursor Cursor cursor position to broadcast ---update cursor position by sending a cursor event to server function CursorController:send(cursor) end - ---@return MaybeCursorEventPromise ---@async ---@nodiscard diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index 1ca71f74..273bbc54 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -98,7 +98,10 @@ impl LuaUserData for CodempClient { }); // TODO need to derive ser/de on Event, but this is in protobuf... - // methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? }); + // methods.add_method( + // "recv", + // |_, this, ()| a_sync! { this => this.recv().await? } + // ); // methods.add_method( // "try_recv", diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index 4770996e..a6dd6be9 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -62,7 +62,7 @@ impl LuaUserData for CodempWorkspace { Ok(this.search_buffers(filter.as_deref())) }); - methods.add_method("list_buffer_users", |_, this, (path,): (String,)| { + methods.add_method("buffer_user_list", |_, this, (path,): (String,)| { Ok(this.buffer_user_list(&path)) }); From 77dc5f3022b10e35932a91760feccdd501063f39 Mon Sep 17 00:00:00 2001 From: frelodev Date: Sun, 15 Mar 2026 00:53:17 +0100 Subject: [PATCH 068/121] fix(js): ffi glue --- src/api/workspace.rs | 1 + src/ffi/js/buffer.rs | 7 +++++++ src/ffi/js/client.rs | 26 ++++++++++++++++++++++++++ src/ffi/js/cursor.rs | 7 +++++++ src/ffi/js/workspace.rs | 21 +++++++++++++++++++++ 5 files changed, 62 insertions(+) diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 5ec657ce..bb183d55 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -10,6 +10,7 @@ #[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/WorkspaceIdentifier"))] #[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "js", napi_derive::napi(object))] pub struct WorkspaceIdentifier { /// Workspace name, cannot change and is unique per owner. pub workspace: String, diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 38c92de4..61e385e3 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,5 +1,6 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::{BufferUpdate, TextChange}; +use crate::api::WorkspaceIdentifier; use crate::buffer::controller::BufferController; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; @@ -72,4 +73,10 @@ impl BufferController { pub async fn js_content(&self) -> napi::Result { Ok(self.content().await?) } + + /// Get id of workspace containing this controller. + #[napi(js_name = "workspaceId")] + pub fn js_workspace_id(&self) -> WorkspaceIdentifier { + self.workspace_id().clone().into() + } } diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 62ab157a..72533705 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -122,4 +122,30 @@ impl Client { pub async fn js_refresh(&self) -> napi::Result<()> { Ok(self.refresh().await?) } + + /// Accept an invitation to a workspace, making it accessible + #[napi(js_name = "acceptInvite")] + pub async fn js_accept_invite(&self, user: String, workspace: String) -> napi::Result<()> { + Ok(self.accept_invite(&user, &workspace).await?) + } + + /// Get the meta information for a user + #[napi(js_name = "getUserInfo")] + pub async fn js_get_user_info(&self, user: String) -> napi::Result { + Ok(self.get_user_info(&user).await?.into()) + } + + /// Quit a joined workspace. Cannot quit owned workspaces: must delete them + #[napi(js_name = "quitWorkspace")] + pub async fn js_quit_workspace(&self, user: String, workspace: String) -> napi::Result<()> { + Ok(self.quit_workspace(&user, &workspace).await?) + } + + /// Reject an invitation to a workspace + #[napi(js_name = "rejectInvite")] + pub async fn js_reject_invite(&self, user: String, workspace: String) -> napi::Result<()> { + Ok(self.reject_invite(&user, &workspace).await?) + } + + } diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 0c3b45cc..5d20bf49 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,4 +1,5 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; +use crate::api::WorkspaceIdentifier; use crate::cursor::controller::CursorController; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; @@ -50,4 +51,10 @@ impl CursorController { pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } + + /// Get id of workspace containing this controller. + #[napi(js_name = "workspaceId")] + pub fn js_workspace_id(&self) -> WorkspaceIdentifier { + self.workspace_id().clone().into() + } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index d2b8cb21..8c8b6fbd 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -162,4 +162,25 @@ impl Workspace { ) -> napi::Result<()> { Ok(self.fetch_buffer_users(&path).await?) } + + /// Get all users currently attached to specified buffer + #[napi(js_name = "bufferUserList")] + pub fn js_buffer_user_list(&self, path: String) -> Vec { + self.buffer_user_list(&path) + .into_iter() + .map(JsUser::from) + .collect() + } + + /// Pin an ephemeral buffer, making it permanent. + #[napi(js_name = "pinBuffer")] + pub async fn js_pin_buffer(&self, path: String) -> napi::Result<()> { + Ok(self.pin_buffer(&path).await?) + } + + /// Unpins a permanent buffer, making it ephemeral. + #[napi(js_name = "unpinBuffer")] + pub async fn js_un_pin_buffer(&self, path: String) -> napi::Result<()> { + Ok(self.un_pin_buffer(&path).await?) + } } From 135bd73d45cd3f59e44a8a6d605be69e4f9b4043 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 13:25:59 +0100 Subject: [PATCH 069/121] fix: user_join_buffer creates the buffer if it doesnt exist --- src/workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.rs b/src/workspace.rs index 9a6ba021..049d200f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -457,7 +457,7 @@ impl WorkspaceWorker { WorkspaceEventInner::BufferJoin(UserJoinBuffer { user, buffer }) => { match inner.buffer_users.get_mut(&buffer) { Some(mut buf_users_ref) => buf_users_ref.push(user), - None => tracing::warn!("received UserJoinBuffer event for an unknown buffer"), + None => { inner.buffer_users.insert(buffer, vec![user]); }, } }, WorkspaceEventInner::BufferLeave(UserLeaveBuffer { user, buffer }) => { From 76820b78326a974abc9f0ffaca52b044fa1c981e Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 13:26:40 +0100 Subject: [PATCH 070/121] fix(lua): workspace id, skip null fields, also correct wsid format --- src/api/config.rs | 3 +++ src/api/user.rs | 3 +++ src/api/workspace.rs | 2 +- src/ffi/lua/workspace.rs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api/config.rs b/src/api/config.rs index cf46e925..fa10d853 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -19,10 +19,13 @@ pub struct Config { /// User password chosen upon registration. pub password: String, // must not leak this! /// Address of server to connect to, default api.code.mp. + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub host: Option, /// Port to connect to, default 50053. + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub port: Option, /// Enable or disable tls, default true. + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub tls: Option, } diff --git a/src/api/user.rs b/src/api/user.rs index 121098fa..caef1c54 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -11,10 +11,13 @@ pub struct UserInfo { /// User name, unique and immutable pub name: String, /// User display name, can change and be duplicated + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub display_name: Option, /// User description ("bio"), may contain contacts + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub description: Option, /// User avatar: a small image some editors can display + #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] pub avatar: Option>, } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index bb183d55..97ade612 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -38,6 +38,6 @@ impl From for codemp_proto::session::WorkspaceIdentifier { impl std::fmt::Display for WorkspaceIdentifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "#{}:{}", self.user, self.workspace) + write!(f, "{}/{}", self.user, self.workspace) } } diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index a6dd6be9..662cdc07 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -66,7 +66,7 @@ impl LuaUserData for CodempWorkspace { Ok(this.buffer_user_list(&path)) }); - methods.add_method("id", |_, this, ()| Ok(this.id().to_string())); + methods.add_method("id", |_, this, ()| Ok(this.id().clone())); methods.add_method("cursor", |_, this, ()| Ok(this.cursor())); methods.add_method("active_buffers", |_, this, ()| Ok(this.active_buffers())); methods.add_method("user_list", |_, this, ()| Ok(this.user_list())); From 6512dfbd14fc8a8e0ca4fbe01c42430cd323ba6f Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 13:27:00 +0100 Subject: [PATCH 071/121] docs(lua): update annotations --- dist/lua/annotations.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index bb23924c..08302d2b 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -283,10 +283,11 @@ function Client:fetch_owned_workspaces() end ---fetch and list joined workspaces function Client:fetch_joined_workspaces() end +---@param user string user owning this workspace ---@param ws string workspace id to get ---@return Workspace? ---get an active workspace by name -function Client:get_workspace(ws) end +function Client:get_workspace(user, ws) end ---@param user string username to lookup ---@return UserInfoPromise @@ -313,9 +314,9 @@ function Client:callback(cb) end ---@class UserInfo ---represents a service user and contains all its relevant info ---@field name string user unique, immutable name ----@field display_name string|nil user display name, mutable and not guaranteed to be unique ----@field description string|nil user description, maybe containing contact info ----@field avatar any|nil user avatar image, as bytes +---@field display_name string? display name, mutable and not guaranteed to be unique +---@field description string? user description, maybe containing contact info +---@field avatar data? user avatar image, as bytes ---@class WorkspaceIdentifier ---uniquely identifies a workspace, by its owner and workspace name From 3f785fec36141cb470eb19a5a4ccc4210128fd9e Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 13:27:10 +0100 Subject: [PATCH 072/121] chore: bump codemp-proto --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a8d9202..9246a649 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=c38c582174b1c8845c5e008a07a3d4730070873c#c38c582174b1c8845c5e008a07a3d4730070873c" +source = "git+https://github.com/hexedtech/codemp-proto?rev=cba61da3ad6a852e368039e02281deb5a8d1b017#cba61da3ad6a852e368039e02281deb5a8d1b017" dependencies = [ "prost", "tonic", diff --git a/Cargo.toml b/Cargo.toml index 23e43c9e..6a0b9ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c38c582174b1c8845c5e008a07a3d4730070873c", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "cba61da3ad6a852e368039e02281deb5a8d1b017", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From ac86e37ef50950c946121e5b1d00eb824821584c Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 18:08:04 +0100 Subject: [PATCH 073/121] feat: BEGONE DTOs --- Cargo.lock | 46 ++++++++++++++++++++++++-- Cargo.toml | 10 +++--- src/api/buffer.rs | 23 ------------- src/api/cursor.rs | 74 ------------------------------------------ src/api/event.rs | 68 -------------------------------------- src/api/mod.rs | 20 ------------ src/api/user.rs | 77 -------------------------------------------- src/api/workspace.rs | 43 ------------------------- src/lib.rs | 3 ++ src/prelude.rs | 14 +++++--- 10 files changed, 61 insertions(+), 317 deletions(-) delete mode 100644 src/api/buffer.rs delete mode 100644 src/api/cursor.rs delete mode 100644 src/api/event.rs delete mode 100644 src/api/user.rs delete mode 100644 src/api/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 9246a649..4d1690e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ dependencies = [ "dashmap", "diamond-types", "jni", - "jni-toolbox", + "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae)", "mlua", "napi", "napi-build", @@ -187,9 +187,17 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=cba61da3ad6a852e368039e02281deb5a8d1b017#cba61da3ad6a852e368039e02281deb5a8d1b017" +source = "git+https://github.com/hexedtech/codemp-proto?rev=c0e2589cb0007412db1c005abbfd8fcf7b6b9151#c0e2589cb0007412db1c005abbfd8fcf7b6b9151" dependencies = [ + "jni", + "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", + "mlua", + "mlua-serde-derive", + "napi", + "napi-derive", "prost", + "pyo3", + "serde", "tonic", "tonic-prost", "tonic-prost-build", @@ -801,7 +809,18 @@ version = "0.2.2" source = "git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae#11897b11153dccd82044e980947c090ef12d08ae" dependencies = [ "jni", - "jni-toolbox-macro", + "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae)", + "thiserror", + "uuid", +] + +[[package]] +name = "jni-toolbox" +version = "0.2.2" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +dependencies = [ + "jni", + "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", "thiserror", "uuid", ] @@ -816,6 +835,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jni-toolbox-macro" +version = "0.2.2" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -953,6 +982,17 @@ dependencies = [ "serde-value", ] +[[package]] +name = "mlua-serde-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c68d667cdb8a4106f37e339746d046d670f846ce18cc69e9d3319a8e509241f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "mlua-sys" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 6a0b9ec4..ddd0fa7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "cba61da3ad6a852e368039e02281deb5a8d1b017", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c0e2589cb0007412db1c005abbfd8fcf7b6b9151", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api @@ -80,10 +80,10 @@ serialize = ["dep:serde", "uuid/serde"] test-e2e = [] test-coverage = ["dep:syn", "dep:regex"] # ffi -java = ["dep:jni", "dep:tracing-subscriber", "dep:jni-toolbox"] -js = ["dep:napi-build", "dep:tracing-subscriber", "dep:napi", "dep:napi-derive"] -py = ["dep:pyo3", "dep:tracing-subscriber", "dep:pyo3-build-config"] -lua = ["serialize", "dep:mlua", "dep:tracing-subscriber"] +java = ["dep:jni", "dep:tracing-subscriber", "dep:jni-toolbox", "codemp-proto/java"] +js = ["dep:napi-build", "dep:tracing-subscriber", "dep:napi", "dep:napi-derive", "codemp-proto/js"] +py = ["dep:pyo3", "dep:tracing-subscriber", "dep:pyo3-build-config", "codemp-proto/py"] +lua = ["serialize", "dep:mlua", "dep:tracing-subscriber", "codemp-proto/lua"] # ffi variants lua-jit = ["mlua?/luajit"] lua-54 = ["mlua?/lua54"] diff --git a/src/api/buffer.rs b/src/api/buffer.rs deleted file mode 100644 index 403d2da4..00000000 --- a/src/api/buffer.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! # Buffer -//! TODO TODO TODO - -/// Represents a service buffer -#[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/BufferNode"))] -pub struct BufferNode { - /// Buffer path, sort of like a UNIX path. - pub path: String, - /// Wether this buffer gets auto-deleted once all users left - pub ephemeral: bool, -} - -impl From for BufferNode { - fn from(value: codemp_proto::files::BufferNode) -> Self { - Self { - path: value.path.into(), - ephemeral: value.ephemeral, - } - } -} diff --git a/src/api/cursor.rs b/src/api/cursor.rs deleted file mode 100644 index e180c5ab..00000000 --- a/src/api/cursor.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! ### Cursor -//! Represents the position of a remote user's cursor. - -#[cfg(feature = "py")] -use pyo3::prelude::*; - -/// An event that occurred about a user's cursor. -#[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/CursorEvent"))] -#[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] -pub struct CursorEvent { - /// User who sent the cursor. - pub user: String, - /// Cursor position data - pub cursor: Cursor, -} - -/// A cursor instantaneous state -#[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Cursor"))] -#[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] -pub struct Cursor { - /// Path of buffer this cursor is on - pub buffer: String, - /// The updated cursor selection. - pub sel: Vec, -} - -/// A cursor selection span. -#[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Selection"))] -#[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyclass(get_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -// #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] -pub struct Selection { - /// Cursor position starting row in buffer. - pub start_row: i32, - /// Cursor position starting column in buffer. - pub start_col: i32, - /// Cursor position final row in buffer. - pub end_row: i32, - /// Cursor position final column in buffer. - pub end_col: i32, -} - -// TODO this re-wrapping of our API is not elegant at all -impl From for CursorEvent { - fn from(value: codemp_proto::cursor::CursorEvent) -> Self { - Self { - user: value.user, - cursor: Cursor { - buffer: value.position.buffer, - sel: value - .position - .cursors - .into_iter() - .map(|c| Selection { - start_row: c.start.row, - end_row: c.end.row, - start_col: c.start.col, - end_col: c.end.col, - }) - .collect(), - }, - } - } -} diff --git a/src/api/event.rs b/src/api/event.rs deleted file mode 100644 index 46409cd4..00000000 --- a/src/api/event.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! # Event -//! Real time notification of changes in a workspace, to either users or buffers. -#![allow(non_upper_case_globals, non_camel_case_types)] // pyo3 fix your shit - -use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner; - -/// Event in a [crate::Workspace]. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serialize", serde(tag = "type"))] -pub enum Event { - /// Fired when the file tree changes (buffer created, deleted or renamed). - FileTreeUpdated { - /// The modifier buffer's path. - path: String - }, - /// Fired when an user joins the current workspace. - UserJoin { - /// The name of the joining user. - name: String - }, - /// Fired when an user leaves the current workspace. - UserLeave { - /// The name of the leaving user. - name: String - }, - /// Fired when an user joins a buffer. - UserJoinBuffer { - /// The name of the joining user. - name: String, - /// The name of the buffer the user is joining. - buffer: String - }, - /// Fired when an user leaves a buffer. - UserLeaveBuffer { - /// The name of the leaving user. - name: String, - /// The name of the buffer the user is leaving. - buffer: String - }, -} - -impl From for Event { - fn from(event: WorkspaceEventInner) -> Self { - match event { - WorkspaceEventInner::WorkspaceJoin(e) => Self::UserJoin { name: e.user }, - WorkspaceEventInner::WorkspaceLeave(e) => Self::UserLeave { name: e.user }, - WorkspaceEventInner::Create(e) => Self::FileTreeUpdated { path: e.path }, - WorkspaceEventInner::Delete(e) => Self::FileTreeUpdated { path: e.path }, - WorkspaceEventInner::Rename(e) => Self::FileTreeUpdated { path: e.after }, - WorkspaceEventInner::BufferJoin(e) => Self::UserJoinBuffer { - name: e.user, - buffer: e.buffer, - }, - WorkspaceEventInner::BufferLeave(e) => Self::UserLeaveBuffer { - name: e.user, - buffer: e.buffer, - }, - } - } -} - -impl From<&WorkspaceEventInner> for Event { - fn from(event: &WorkspaceEventInner) -> Self { - Self::from(event.clone()) - } -} diff --git a/src/api/mod.rs b/src/api/mod.rs index b899d878..1b789f3a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -10,26 +10,6 @@ pub mod change; /// client configuration pub mod config; -/// representation for an user's cursor -pub mod cursor; - -/// representation of remote buffers -pub mod buffer; - -/// live events in workspaces -pub mod event; - -/// data structure for remote users -pub mod user; - -/// data structure for workspaces -pub mod workspace; - -pub use buffer::BufferNode; pub use change::{BufferUpdate, TextChange}; pub use config::Config; pub use controller::{AsyncReceiver, AsyncSender, Controller}; -pub use cursor::{Cursor, CursorEvent, Selection}; -pub use event::Event; -pub use user::UserInfo; -pub use workspace::WorkspaceIdentifier; diff --git a/src/api/user.rs b/src/api/user.rs deleted file mode 100644 index caef1c54..00000000 --- a/src/api/user.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! # User -//! An user is identified by an UUID, which should never change. -//! Each user has an username, which can change but should be unique. - -/// Represents a service user -#[derive(Debug, Clone)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/UserInfo"))] -#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct UserInfo { - /// User name, unique and immutable - pub name: String, - /// User display name, can change and be duplicated - #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] - pub display_name: Option, - /// User description ("bio"), may contain contacts - #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] - pub description: Option, - /// User avatar: a small image some editors can display - #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] - pub avatar: Option>, -} - -impl UserInfo { - /// Creates a default [UserInfo] for the given username - /// with all the optional fields set to [None]. - pub fn default_for(username: String) -> Self { - Self { - name: username, - display_name: None, - description: None, - avatar: None, - } - } -} - -impl From for UserInfo { - fn from(value: codemp_proto::common::UserInfo) -> Self { - Self { - name: value.name, - display_name: value.display_name, - description: value.description, - avatar: value.avatar, - } - } -} - -impl From for codemp_proto::common::UserInfo { - fn from(value: UserInfo) -> Self { - Self { - name: value.name, - display_name: value.display_name, - description: value.description, - avatar: value.avatar, - } - } -} - -impl PartialEq for UserInfo { - fn eq(&self, other: &Self) -> bool { - self.name.eq(&other.name) - } -} - -impl Eq for UserInfo {} - -impl PartialOrd for UserInfo { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for UserInfo { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.name.cmp(&other.name) - } -} diff --git a/src/api/workspace.rs b/src/api/workspace.rs deleted file mode 100644 index 97ade612..00000000 --- a/src/api/workspace.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! # Workspace -//! A workspace is a working environment containing many buffers, owned by one user. -//! Many users can be invited and join a workspace, accessing its buffer list and being able to -//! attach to its buffers (depending on permissions) to send changes. Workspaces are namespaced to -//! users, meaning two workspaces with the same name can exist, but one user can own only one -//! workspace with a given name. - -/// Represents a service workspace -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/WorkspaceIdentifier"))] -#[cfg_attr(feature = "py", pyo3::pyclass(from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "js", napi_derive::napi(object))] -pub struct WorkspaceIdentifier { - /// Workspace name, cannot change and is unique per owner. - pub workspace: String, - /// Workspace owning user - pub user: String, -} - -impl From for WorkspaceIdentifier { - fn from(value: codemp_proto::session::WorkspaceIdentifier) -> Self { - Self { - workspace: value.workspace, - user: value.user, - } - } -} - -impl From for codemp_proto::session::WorkspaceIdentifier { - fn from(value: WorkspaceIdentifier) -> Self { - Self { - workspace: value.workspace, - user: value.user, - } - } -} - -impl std::fmt::Display for WorkspaceIdentifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}/{}", self.user, self.workspace) - } -} diff --git a/src/lib.rs b/src/lib.rs index 891b4fe8..7124e009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,6 +132,9 @@ pub mod tests; /// internal network services and interceptors pub(crate) mod network; +/// re-export codemp_proto as codemp::proto +pub use codemp_proto as proto; + /// Get the current version of the client pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") diff --git a/src/prelude.rs b/src/prelude.rs index 05e9e3ca..bf4595cc 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,11 +2,17 @@ //! All-in-one renamed imports with `use codemp::prelude::*`. pub use crate::api::{ + TextChange as CodempTextChange, Config as CodempConfig, AsyncReceiver as CodempAsyncReceiver, AsyncSender as CodempAsyncSender, - BufferNode as CodempBufferNode, BufferUpdate as CodempBufferUpdate, Config as CodempConfig, - Controller as CodempController, Cursor as CodempCursor, CursorEvent as CodempCursorEvent, - Event as CodempEvent, Selection as CodempSelection, TextChange as CodempTextChange, - UserInfo as CodempUserInfo, WorkspaceIdentifier as CodempWorkspaceIdentifier, + Controller as CodempController, +}; + +pub use crate::proto::{ + files::BufferNode as CodempBufferNode, buffer::BufferEvent as CodempBufferEvent, + cursor::CursorEvent as CodempCursorEvent, cursor::CursorUpdate as CodempCursorUpdate, + cursor::CursorPosition as CodempCursorPosition, cursor::RowCol as CodempRowCol, + session::SessionEvent as CodempSessionEvent, workspace::WorkspaceEvent as CodempWorkspaceEvent, + session::WorkspaceIdentifier as CodempWorkspaceIdentifier, common::UserInfo as CodempUserInfo, }; pub use crate::{ From 7561db3724f6d2c52cad4c34badebc896cf88bb1 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 18:08:27 +0100 Subject: [PATCH 074/121] fix: logging --- src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1a8850d3..d269c3e0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -460,7 +460,7 @@ impl ClientWorker { } if self.events.send(ev).is_err() { - tracing::warn!("no active controller to receive workspace event"); + tracing::warn!("no active controller to receive client event"); } self.pollers.drain(..).for_each(|x| { x.send(()).unwrap_or_warn("poller dropped before completion"); @@ -469,7 +469,7 @@ impl ClientWorker { if let Some(ws) = weak.upgrade() { cb.call(Client(ws)); } else { - break tracing::debug!("workspace worker clean (late) exit"); + break tracing::debug!("client worker clean (late) exit"); } } } From e6df7cf572d489b77e4c22d256dcc79ccc7b7e04 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 18:16:00 +0100 Subject: [PATCH 075/121] chore(java): mp.code.data -> mp.code.proto --- dist/java/src/mp/code/BufferController.java | 6 +++--- dist/java/src/mp/code/Client.java | 6 +++--- dist/java/src/mp/code/CursorController.java | 6 +++--- dist/java/src/mp/code/Workspace.java | 2 +- dist/java/src/mp/code/{data => proto}/BufferNode.java | 2 +- dist/java/src/mp/code/{data => proto}/BufferUpdate.java | 2 +- dist/java/src/mp/code/{data => proto}/Config.java | 2 +- dist/java/src/mp/code/{data => proto}/Cursor.java | 2 +- dist/java/src/mp/code/{data => proto}/CursorEvent.java | 2 +- dist/java/src/mp/code/{data => proto}/Selection.java | 2 +- dist/java/src/mp/code/{data => proto}/TextChange.java | 2 +- dist/java/src/mp/code/{data => proto}/UserInfo.java | 2 +- .../src/mp/code/{data => proto}/WorkspaceIdentifier.java | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) rename dist/java/src/mp/code/{data => proto}/BufferNode.java (94%) rename dist/java/src/mp/code/{data => proto}/BufferUpdate.java (97%) rename dist/java/src/mp/code/{data => proto}/Config.java (97%) rename dist/java/src/mp/code/{data => proto}/Cursor.java (94%) rename dist/java/src/mp/code/{data => proto}/CursorEvent.java (94%) rename dist/java/src/mp/code/{data => proto}/Selection.java (96%) rename dist/java/src/mp/code/{data => proto}/TextChange.java (98%) rename dist/java/src/mp/code/{data => proto}/UserInfo.java (96%) rename dist/java/src/mp/code/{data => proto}/WorkspaceIdentifier.java (94%) diff --git a/dist/java/src/mp/code/BufferController.java b/dist/java/src/mp/code/BufferController.java index 09192ffa..dac3833e 100644 --- a/dist/java/src/mp/code/BufferController.java +++ b/dist/java/src/mp/code/BufferController.java @@ -1,8 +1,8 @@ package mp.code; -import mp.code.data.BufferUpdate; -import mp.code.data.TextChange; -import mp.code.data.WorkspaceIdentifier; +import mp.code.proto.BufferUpdate; +import mp.code.proto.TextChange; +import mp.code.proto.WorkspaceIdentifier; import mp.code.exceptions.ControllerException; import java.util.function.Consumer; diff --git a/dist/java/src/mp/code/Client.java b/dist/java/src/mp/code/Client.java index f5fb3554..cc415434 100644 --- a/dist/java/src/mp/code/Client.java +++ b/dist/java/src/mp/code/Client.java @@ -1,9 +1,9 @@ package mp.code; import lombok.Getter; -import mp.code.data.Config; -import mp.code.data.UserInfo; -import mp.code.data.WorkspaceIdentifier; +import mp.code.proto.Config; +import mp.code.proto.UserInfo; +import mp.code.proto.WorkspaceIdentifier; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index e612f732..3b927b8d 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -1,8 +1,8 @@ package mp.code; -import mp.code.data.Cursor; -import mp.code.data.Selection; -import mp.code.data.WorkspaceIdentifier; +import mp.code.proto.Cursor; +import mp.code.proto.Selection; +import mp.code.proto.WorkspaceIdentifier; import mp.code.exceptions.ControllerException; import java.util.function.Consumer; diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 8e321694..1960b576 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -4,7 +4,7 @@ import java.util.function.Consumer; import lombok.Getter; -import mp.code.data.UserInfo; +import mp.code.proto.UserInfo; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; diff --git a/dist/java/src/mp/code/data/BufferNode.java b/dist/java/src/mp/code/proto/BufferNode.java similarity index 94% rename from dist/java/src/mp/code/data/BufferNode.java rename to dist/java/src/mp/code/proto/BufferNode.java index a8c9eff5..ca6f987a 100644 --- a/dist/java/src/mp/code/data/BufferNode.java +++ b/dist/java/src/mp/code/proto/BufferNode.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/BufferUpdate.java b/dist/java/src/mp/code/proto/BufferUpdate.java similarity index 97% rename from dist/java/src/mp/code/data/BufferUpdate.java rename to dist/java/src/mp/code/proto/BufferUpdate.java index 7cff9518..729c4f7c 100644 --- a/dist/java/src/mp/code/data/BufferUpdate.java +++ b/dist/java/src/mp/code/proto/BufferUpdate.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/Config.java b/dist/java/src/mp/code/proto/Config.java similarity index 97% rename from dist/java/src/mp/code/data/Config.java rename to dist/java/src/mp/code/proto/Config.java index 73286bc9..596670c4 100644 --- a/dist/java/src/mp/code/data/Config.java +++ b/dist/java/src/mp/code/proto/Config.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.AccessLevel; import lombok.EqualsAndHashCode; diff --git a/dist/java/src/mp/code/data/Cursor.java b/dist/java/src/mp/code/proto/Cursor.java similarity index 94% rename from dist/java/src/mp/code/data/Cursor.java rename to dist/java/src/mp/code/proto/Cursor.java index 4c5ae71d..c5588033 100644 --- a/dist/java/src/mp/code/data/Cursor.java +++ b/dist/java/src/mp/code/proto/Cursor.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/CursorEvent.java b/dist/java/src/mp/code/proto/CursorEvent.java similarity index 94% rename from dist/java/src/mp/code/data/CursorEvent.java rename to dist/java/src/mp/code/proto/CursorEvent.java index aafd30b2..0023d73b 100644 --- a/dist/java/src/mp/code/data/CursorEvent.java +++ b/dist/java/src/mp/code/proto/CursorEvent.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/Selection.java b/dist/java/src/mp/code/proto/Selection.java similarity index 96% rename from dist/java/src/mp/code/data/Selection.java rename to dist/java/src/mp/code/proto/Selection.java index f80ced8c..2ddb5642 100644 --- a/dist/java/src/mp/code/data/Selection.java +++ b/dist/java/src/mp/code/proto/Selection.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/TextChange.java b/dist/java/src/mp/code/proto/TextChange.java similarity index 98% rename from dist/java/src/mp/code/data/TextChange.java rename to dist/java/src/mp/code/proto/TextChange.java index 6d1879e2..e3e4d676 100644 --- a/dist/java/src/mp/code/data/TextChange.java +++ b/dist/java/src/mp/code/proto/TextChange.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/UserInfo.java b/dist/java/src/mp/code/proto/UserInfo.java similarity index 96% rename from dist/java/src/mp/code/data/UserInfo.java rename to dist/java/src/mp/code/proto/UserInfo.java index 963c1726..b52ef577 100644 --- a/dist/java/src/mp/code/data/UserInfo.java +++ b/dist/java/src/mp/code/proto/UserInfo.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; diff --git a/dist/java/src/mp/code/data/WorkspaceIdentifier.java b/dist/java/src/mp/code/proto/WorkspaceIdentifier.java similarity index 94% rename from dist/java/src/mp/code/data/WorkspaceIdentifier.java rename to dist/java/src/mp/code/proto/WorkspaceIdentifier.java index 38a2f4d7..a3b4b280 100644 --- a/dist/java/src/mp/code/data/WorkspaceIdentifier.java +++ b/dist/java/src/mp/code/proto/WorkspaceIdentifier.java @@ -1,4 +1,4 @@ -package mp.code.data; +package mp.code.proto; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; From 811ed0c54949c3765c093fdbb8c47ddfc0495438 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 18:40:41 +0100 Subject: [PATCH 076/121] fix: new types from proto --- src/buffer/controller.rs | 4 +- src/buffer/worker.rs | 6 +- src/client.rs | 64 ++++++++--------- src/cursor/controller.rs | 46 ++++-------- src/cursor/worker.rs | 42 +++-------- src/workspace.rs | 146 +++++++++++++++++++++------------------ 6 files changed, 132 insertions(+), 176 deletions(-) diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index 8d160a33..f1654c97 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -23,7 +23,7 @@ pub struct BufferController(pub(crate) Arc); impl BufferController { /// Get id of workspace containing this controller. - pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { + pub fn workspace_id(&self) -> &crate::proto::session::WorkspaceIdentifier { &self.0.workspace_id } @@ -64,7 +64,7 @@ pub(crate) struct BufferControllerInner { pub(crate) delta_request: mpsc::Sender>>, pub(crate) callback: watch::Sender>>, pub(crate) ack_tx: mpsc::UnboundedSender, - pub(crate) workspace_id: crate::api::WorkspaceIdentifier, + pub(crate) workspace_id: crate::proto::session::WorkspaceIdentifier, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index a61f08d9..604ed9db 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -18,7 +18,7 @@ use super::controller::{BufferController, BufferControllerInner}; struct BufferWorker { agent_id: u32, path: String, - workspace_id: crate::api::WorkspaceIdentifier, + workspace_id: crate::proto::session::WorkspaceIdentifier, latest_version: watch::Sender, local_version: watch::Sender, ack_rx: mpsc::UnboundedReceiver, @@ -40,7 +40,7 @@ impl BufferController { path: String, tx: mpsc::Sender, rx: Streaming, - workspace_id: crate::api::WorkspaceIdentifier, + workspace_id: crate::proto::session::WorkspaceIdentifier, ) -> Self { let init = diamond_types::LocalVersion::default(); @@ -96,7 +96,7 @@ impl BufferController { BufferController(controller) } - #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id, path = worker.path))] + #[tracing::instrument(skip(worker, tx, rx), fields(owner = worker.workspace_id.user, ws = worker.workspace_id.workspace, path = worker.path))] async fn work( mut worker: BufferWorker, tx: mpsc::Sender, diff --git a/src/client.rs b/src/client.rs index d269c3e0..82062af3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,7 +10,7 @@ use tonic::{ }; use crate::{ - api::{AsyncReceiver, UserInfo}, + api::AsyncReceiver, errors::{ConnectionResult, RemoteResult}, ext::{IgnorableError, InternallyMutable}, network, @@ -18,10 +18,9 @@ use crate::{ }; use codemp_proto::{ auth::{LoginRequest, auth_client::AuthClient}, - common::{Empty, Token}, + common::{Empty, Token, UserInfo}, session::{ - InviteRequest, OwnedWorkspaceIdentifier, UserId, WorkspaceIdentifier, - session_client::SessionClient, + InviteRequest, OwnedWorkspaceIdentifier, SessionEvent, SessionEventKind, UserId, WorkspaceIdentifier, session_client::SessionClient }, }; @@ -40,7 +39,7 @@ pub struct Client(Arc); #[derive(Debug)] struct ClientInner { - user: Arc, + user: Arc, config: crate::api::Config, workspaces: DashMap>, auth: AuthClient, @@ -50,7 +49,7 @@ struct ClientInner { callback: tokio::sync::watch::Sender>>, events: tokio::sync::Mutex< - tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, >, } @@ -218,7 +217,7 @@ impl Client { /// Fetch the names of all workspaces owned by the current user. pub async fn fetch_owned_workspaces( &self, - ) -> RemoteResult> { + ) -> RemoteResult> { Ok(self .0 .session @@ -227,15 +226,13 @@ impl Client { .await? .into_inner() .workspaces - .into_iter() - .map(crate::api::WorkspaceIdentifier::from) - .collect()) + ) } /// Fetch the names of all workspaces the current user has joined. pub async fn fetch_joined_workspaces( &self, - ) -> RemoteResult> { + ) -> RemoteResult> { Ok(self .0 .session @@ -244,13 +241,11 @@ impl Client { .await? .into_inner() .workspaces - .into_iter() - .map(crate::api::WorkspaceIdentifier::from) - .collect()) + ) } /// Get the meta information for a user - pub async fn get_user_info(&self, user: impl ToString) -> RemoteResult { + pub async fn get_user_info(&self, user: impl ToString) -> RemoteResult { Ok(self .0 .session @@ -270,7 +265,7 @@ impl Client { user: impl ToString, workspace: impl ToString, ) -> ConnectionResult { - let workspace_id = crate::api::WorkspaceIdentifier { + let workspace_id = WorkspaceIdentifier { user: user.to_string(), workspace: workspace.to_string(), }; @@ -366,7 +361,7 @@ impl Client { /// Get the names of all active [`Workspace`]s. // TODO get rid of WorkspaceIdentifier - pub fn active_workspaces(&self) -> Vec { + pub fn active_workspaces(&self) -> Vec { let mut out = Vec::new(); for wss in self.0.workspaces.iter() { for ws in wss.value().iter() { @@ -382,10 +377,10 @@ impl Client { } } -impl AsyncReceiver for Client { +impl AsyncReceiver for Client { async fn try_recv( &self, - ) -> crate::errors::ControllerResult> { + ) -> crate::errors::ControllerResult> { match self.0.events.lock().await.try_recv() { Ok(x) => Ok(Some(x)), Err(tokio::sync::mpsc::error::TryRecvError::Empty) => Ok(None), @@ -415,14 +410,14 @@ struct ClientWorker { tokio::sync::watch::Receiver>>, pollers: Vec>, poll_rx: tokio::sync::mpsc::UnboundedReceiver>, - events: tokio::sync::mpsc::UnboundedSender, + events: tokio::sync::mpsc::UnboundedSender, } impl ClientWorker { #[tracing::instrument(skip(self, stream, weak))] pub(crate) async fn work( mut self, - mut stream: tonic::Streaming, + mut stream: tonic::Streaming, weak: std::sync::Weak, ) { tracing::debug!("client worker starting"); @@ -436,30 +431,27 @@ impl ClientWorker { res = stream.message() => match res { Err(e) => break tracing::error!("client stream closed: {e}"), Ok(None) => break tracing::info!("closing client"), - Ok(Some(codemp_proto::session::SessionEvent { event: None })) => { - tracing::warn!("client received empty event") - } - Ok(Some(codemp_proto::session::SessionEvent { event: Some(ev) })) => { + Ok(Some(event)) => { let Some(_inner) = weak.upgrade() else { break tracing::debug!("client worker clean exit"); }; - tracing::debug!("received client event: {ev:?}"); - match ev.clone() { - codemp_proto::session::session_event::Event::Invite(invitation_event) => { - tracing::info!("got invited to workspace: {invitation_event:?}"); + tracing::debug!("received client event: {event:?}"); + match event.kind() { + SessionEventKind::InvitationEvent => { + tracing::info!("got invited to workspace {}/{} by {}", event.workspace.user, event.workspace.workspace, event.user); }, - codemp_proto::session::session_event::Event::Leave(quit_event) => { - tracing::info!("user left workspace: {quit_event:?}"); + SessionEventKind::QuitEvent => { + tracing::info!("user {} left workspace {}/{}", event.user, event.workspace.user, event.workspace.workspace); }, - codemp_proto::session::session_event::Event::Join(accept_event) => { - tracing::info!("user accepted invite: {accept_event:?}"); + SessionEventKind::AcceptEvent => { + tracing::info!("user {} accepted invite to workspace {}/{}", event.user, event.workspace.user, event.workspace.workspace); }, - codemp_proto::session::session_event::Event::Reject(reject_event) => { - tracing::info!("user rejected invite: {reject_event:?}"); + SessionEventKind::RejectEvent => { + tracing::info!("user {} rejected invite to workspace {}/{}", event.user, event.workspace.user, event.workspace.workspace); }, } - if self.events.send(ev).is_err() { + if self.events.send(event).is_err() { tracing::warn!("no active controller to receive client event"); } self.pollers.drain(..).for_each(|x| { diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index a78501d0..ab6ae1e1 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -6,15 +6,11 @@ use std::sync::Arc; use tokio::sync::{mpsc, oneshot, watch}; use crate::{ - api::{ - Controller, Cursor, - controller::{AsyncReceiver, AsyncSender, ControllerCallback}, - cursor::CursorEvent, - }, + api::{Controller, controller::{AsyncReceiver, AsyncSender, ControllerCallback}}, errors::ControllerResult, network::AuthedService, }; -use codemp_proto::cursor::{CursorPosition, CursorUpdate, RowCol, cursor_client::CursorClient}; +use codemp_proto::cursor::{CursorEvent, CursorUpdate, cursor_client::CursorClient}; /// A [Controller] for asynchronously sending and receiving [Cursor] event. /// @@ -26,7 +22,7 @@ pub struct CursorController(pub(crate) Arc); impl CursorController { /// Get id of workspace containing this controller. - pub fn workspace_id(&self) -> &crate::api::WorkspaceIdentifier { + pub fn workspace_id(&self) -> &crate::proto::session::WorkspaceIdentifier { &self.0.workspace_id } } @@ -37,42 +33,26 @@ pub(crate) struct CursorControllerInner { pub(crate) stream: mpsc::Sender>>, pub(crate) poll: mpsc::UnboundedSender>, pub(crate) callback: watch::Sender>>, - pub(crate) workspace_id: crate::api::WorkspaceIdentifier, + pub(crate) workspace_id: crate::proto::session::WorkspaceIdentifier, pub(crate) service: CursorClient, } #[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl Controller for CursorController {} +impl Controller for CursorController {} #[cfg_attr(feature = "async-trait", async_trait::async_trait)] -impl AsyncSender for CursorController { - fn send(&self, mut cursor: Cursor) -> ControllerResult<()> { - for sel in cursor.sel.iter_mut() { - if sel.start_row > sel.end_row - || (sel.start_row == sel.end_row && sel.start_col > sel.end_col) +impl AsyncSender for CursorController { + fn send(&self, mut cursor: CursorUpdate) -> ControllerResult<()> { + for sel in cursor.cursors.iter_mut() { + if sel.start.row > sel.end.row + || (sel.start.row == sel.end.row && sel.start.col > sel.end.col) { - std::mem::swap(&mut sel.start_row, &mut sel.end_row); - std::mem::swap(&mut sel.start_col, &mut sel.end_col); + std::mem::swap(&mut sel.start.row, &mut sel.end.row); + std::mem::swap(&mut sel.start.col, &mut sel.end.col); } } - Ok(self.0.op.send(CursorUpdate { - buffer: cursor.buffer, - cursors: cursor - .sel - .into_iter() - .map(|x| CursorPosition { - start: RowCol { - row: x.start_row, - col: x.start_col, - }, - end: RowCol { - row: x.end_row, - col: x.end_col, - }, - }) - .collect(), - })?) + Ok(self.0.op.send(cursor)?) } } diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index 2acce74a..c03a6e0c 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -4,7 +4,7 @@ use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; use crate::{ - api::{Cursor, Selection, UserInfo, controller::ControllerCallback}, + api::controller::ControllerCallback, errors::RemoteResult, ext::IgnorableError, network::AuthedService, @@ -17,10 +17,10 @@ use codemp_proto::{ use super::controller::{CursorController, CursorControllerInner}; struct CursorWorker { - workspace_id: crate::api::WorkspaceIdentifier, + workspace_id: crate::proto::session::WorkspaceIdentifier, op: mpsc::UnboundedReceiver, - map: Arc>, - stream: mpsc::Receiver>>, + map: Arc>, + stream: mpsc::Receiver>>, poll: mpsc::UnboundedReceiver>, pollers: Vec>, store: std::collections::VecDeque, @@ -30,41 +30,17 @@ struct CursorWorker { impl CursorWorker { #[tracing::instrument(skip(self, tx))] - fn handle_recv(&mut self, tx: oneshot::Sender>) { - tx.send(self.store.pop_front().and_then(|event| { - if let Some(user_name) = self.map.get(&event.user).map(|u| u.name.clone()) { - Some(crate::api::cursor::CursorEvent { - user: user_name, - cursor: Cursor { - buffer: event.position.buffer, - sel: event - .position - .cursors - .into_iter() - .map(|x| Selection { - start_row: x.start.row, - start_col: x.start.col, - end_row: x.end.row, - end_col: x.end.col, - }) - .collect(), - }, - }) - } else { - tracing::warn!("received cursor for unknown user {}", event.user); - None - } - })) - .unwrap_or_warn("client gave up receiving!"); + fn handle_recv(&mut self, tx: oneshot::Sender>) { + tx.send(self.store.pop_front()).unwrap_or_warn("client gave up receiving!"); } } impl CursorController { pub(crate) fn spawn( - user_map: Arc>, + user_map: Arc>, tx: mpsc::Sender, rx: Streaming, - workspace_id: crate::api::WorkspaceIdentifier, + workspace_id: crate::proto::session::WorkspaceIdentifier, cursor_service: CursorClient, // TODO ughh passing these around ) -> Self { // TODO we should tweak the channel buffer size to better propagate backpressure @@ -112,7 +88,7 @@ impl CursorController { .cursors) } - #[tracing::instrument(skip(worker, tx, rx), fields(ws = %worker.workspace_id))] + #[tracing::instrument(skip(worker, tx, rx), fields(owner = worker.workspace_id.user, ws = worker.workspace_id.workspace))] async fn work( mut worker: CursorWorker, tx: mpsc::Sender, diff --git a/src/workspace.rs b/src/workspace.rs index 049d200f..92cc0b3f 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -5,7 +5,6 @@ use crate::{ api::{ - Event, UserInfo, controller::{AsyncReceiver, ControllerCallback}, }, buffer, cursor, @@ -17,13 +16,7 @@ use crate::{ use codemp_proto::{ common::Empty, files::{BufferNode, BufferPath}, - workspace::{ - WorkspaceEvent, - workspace_event::{ - Event as WorkspaceEventInner, FileCreate, FileDelete, FileRename, UserJoinBuffer, - UserJoinWorkspace, UserLeaveBuffer, UserLeaveWorkspace, - }, - }, + workspace::{WorkspaceEvent, WorkspaceEventKind}, }; use dashmap::DashMap; @@ -50,21 +43,21 @@ pub struct Workspace(pub(crate) Arc); #[derive(Debug)] pub(crate) struct WorkspaceInner { - id: crate::api::WorkspaceIdentifier, - current_user: Arc, + id: crate::proto::session::WorkspaceIdentifier, + current_user: Arc, cursor: cursor::Controller, buffers: DashMap, services: Services, - filetree: DashMap, + filetree: DashMap, buffer_users: DashMap>, - users: Arc>, - events: tokio::sync::Mutex>, + users: Arc>, + events: tokio::sync::Mutex>, callback: watch::Sender>>, poll_tx: mpsc::UnboundedSender>, } -impl AsyncReceiver for Workspace { - async fn try_recv(&self) -> ControllerResult> { +impl AsyncReceiver for Workspace { + async fn try_recv(&self) -> ControllerResult> { match self.0.events.lock().await.try_recv() { Ok(x) => Ok(Some(x)), Err(TryRecvError::Empty) => Ok(None), @@ -88,10 +81,10 @@ impl AsyncReceiver for Workspace { } impl Workspace { - #[tracing::instrument(skip(id, user, workspace_claim, user_claim), fields(ws = %id))] + #[tracing::instrument(skip(id, user, workspace_claim, user_claim), fields(owner = id.user, ws = id.workspace))] pub(crate) async fn connect( - id: crate::api::WorkspaceIdentifier, - user: Arc, + id: crate::proto::session::WorkspaceIdentifier, + user: Arc, config: crate::api::Config, workspace_claim: tokio::sync::watch::Receiver, user_claim: tokio::sync::watch::Receiver, @@ -167,18 +160,19 @@ impl Workspace { /// Create a new buffer in the current workspace. pub async fn create_buffer(&self, path: impl ToString, ephemeral: bool) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); + let path = path.to_string(); workspace_client .create_buffer(tonic::Request::new(BufferNode { - path: path.to_string().into(), + path: crate::proto::files::BufferPath::from(&path), ephemeral, })) .await?; // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( - path.to_string(), - crate::api::BufferNode { - path: path.to_string(), + path, + crate::proto::files::BufferNode { + path: crate::proto::files::BufferPath::from(&path), ephemeral, }, ); @@ -294,7 +288,7 @@ impl Workspace { for b in resp.buffers { self.0 .filetree - .insert(b.path.clone().into(), crate::api::BufferNode::from(b)); + .insert(b.path.clone().into(), b); } Ok(()) @@ -310,7 +304,7 @@ impl Workspace { // TODO need to fetch whole user profiles here maybe? self.0 .users - .insert(user_name.clone(), UserInfo::default_for(user_name)); + .insert(user_name.clone(), codemp_proto::common::UserInfo { name: user_name, ..Default::default() }); } Ok(()) @@ -347,7 +341,7 @@ impl Workspace { /// Get the workspace unique id. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 - pub fn id(&self) -> &crate::api::WorkspaceIdentifier { + pub fn id(&self) -> &crate::proto::session::WorkspaceIdentifier { &self.0.id } @@ -374,7 +368,7 @@ impl Workspace { } /// Get all users currently in this workspace - pub fn user_list(&self) -> Vec { + pub fn user_list(&self) -> Vec { self.0 .users .iter() @@ -383,7 +377,7 @@ impl Workspace { } /// Get all users currently attached to specified buffer - pub fn buffer_user_list(&self, path: impl AsRef) -> Vec { + pub fn buffer_user_list(&self, path: impl AsRef) -> Vec { let mut out = Vec::new(); if let Some(buf_ref) = self.0.buffer_users.get(path.as_ref()) { for uid in buf_ref.value() { @@ -415,14 +409,14 @@ struct WorkspaceWorker { callback: watch::Receiver>>, pollers: Vec>, poll_rx: mpsc::UnboundedReceiver>, - events: mpsc::UnboundedSender, + events: mpsc::UnboundedSender, } impl WorkspaceWorker { #[tracing::instrument(skip(self, stream, weak))] pub(crate) async fn work( mut self, - ws: crate::api::WorkspaceIdentifier, + ws: crate::proto::session::WorkspaceIdentifier, mut stream: Streaming, weak: Weak, ) { @@ -435,60 +429,74 @@ impl WorkspaceWorker { }, res = stream.message() => match res { - Err(e) => break tracing::error!("workspace '{ws}' stream closed: {e}"), - Ok(None) => break tracing::info!("leaving workspace {ws}"), - Ok(Some(WorkspaceEvent { event: None })) => { - tracing::warn!("workspace {ws} received empty event") - } - Ok(Some(WorkspaceEvent { event: Some(ev) })) => { + Err(e) => break tracing::error!("workspace '{ws:?}' stream closed: {e}"), + Ok(None) => break tracing::info!("leaving workspace {ws:?}"), + Ok(Some(event)) => { let Some(inner) = weak.upgrade() else { break tracing::debug!("workspace worker clean exit"); }; - tracing::debug!("received workspace event: {ev:?}"); - let update = crate::api::Event::from(&ev); - match ev { - // user - WorkspaceEventInner::WorkspaceJoin(UserJoinWorkspace { user }) => { - inner.users.insert(user.clone(), UserInfo::default_for(user)); + tracing::debug!("received workspace event: {event:?}"); + match event.kind() { + // TODO we should never get wrong optionals set but should we log if we do? + WorkspaceEventKind::UserJoinWorkspace => { + if let Some(user) = event.user { + inner.users.insert(user.clone(), codemp_proto::common::UserInfo { name: user, ..Default::default() }); + } } - WorkspaceEventInner::WorkspaceLeave(UserLeaveWorkspace { user }) => { - inner.users.remove(&user); + WorkspaceEventKind::UserLeaveWorkspace => { + if let Some(user) = event.user { + inner.users.remove(&user); + } } - WorkspaceEventInner::BufferJoin(UserJoinBuffer { user, buffer }) => { - match inner.buffer_users.get_mut(&buffer) { - Some(mut buf_users_ref) => buf_users_ref.push(user), - None => { inner.buffer_users.insert(buffer, vec![user]); }, + WorkspaceEventKind::UserJoinBuffer => { + if let (Some(user), Some(buffer)) = (event.user, event.path) { + match inner.buffer_users.get_mut(&buffer) { + Some(mut buf_users_ref) => buf_users_ref.push(user), + None => { inner.buffer_users.insert(buffer, vec![user]); }, + } } }, - WorkspaceEventInner::BufferLeave(UserLeaveBuffer { user, buffer }) => { - match inner.buffer_users.get_mut(&buffer) { - Some(mut buf_users_ref) => buf_users_ref.retain(|x| *x != user), - None => tracing::warn!("received UserLeaveBuffer event for an unknown buffer"), + WorkspaceEventKind::UserLeaveBuffer => { + if let (Some(user), Some(buffer)) = (event.user, event.path) { + match inner.buffer_users.get_mut(&buffer) { + Some(mut buf_users_ref) => buf_users_ref.retain(|x| *x != user), + None => tracing::warn!("received UserLeaveBuffer event for an unknown buffer"), + } } }, - // buffer - WorkspaceEventInner::Create(FileCreate { path, ephemeral }) => { - inner.buffer_users.insert(path.clone(), Vec::new()); - inner.filetree.insert(path.clone(), crate::api::BufferNode { path, ephemeral }); - } - WorkspaceEventInner::Rename(FileRename { before, after }) => { - if let Some((_path, controller)) = inner.buffers.remove(&before) { - inner.buffers.insert(after.clone(), controller); - } - if let Some((_path, node)) = inner.filetree.remove(&before) { - inner.filetree.insert(after.clone(), node); + + WorkspaceEventKind::FileCreate => { + if let (Some(path), Some(ephemeral)) = (event.path, event.ephemeral) { + inner.buffer_users.insert(path.clone(), Vec::new()); + inner.filetree.insert(path.clone(), crate::proto::files::BufferNode { + path: crate::proto::files::BufferPath::from(&path), + ephemeral + }); } - if let Some((_path, users)) = inner.buffer_users.remove(&before) { - inner.buffer_users.insert(after, users); + } + WorkspaceEventKind::FileRename => { + if let (Some(before), Some(after)) = (event.path, event.after) { + if let Some((_path, controller)) = inner.buffers.remove(&before) { + inner.buffers.insert(after.clone(), controller); + } + if let Some((_path, node)) = inner.filetree.remove(&before) { + inner.filetree.insert(after.clone(), node); + } + if let Some((_path, users)) = inner.buffer_users.remove(&before) { + inner.buffer_users.insert(after, users); + } } } - WorkspaceEventInner::Delete(FileDelete { path }) => { - inner.filetree.remove(&path); - inner.buffer_users.remove(&path); - let _ = inner.buffers.remove(&path); + WorkspaceEventKind::FileDelete => { + if let Some(path) = event.path { + inner.filetree.remove(&path); + inner.buffer_users.remove(&path); + let _ = inner.buffers.remove(&path); + } } } - if self.events.send(update).is_err() { + + if self.events.send(event).is_err() { tracing::warn!("no active controller to receive workspace event"); } self.pollers.drain(..).for_each(|x| { From 9d0e80658a8078c3a4af4668014d63dcf140a5b5 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 18:58:46 +0100 Subject: [PATCH 077/121] feat(java): use proto types in java glue (enums still missing) --- Cargo.lock | 29 +---- Cargo.toml | 4 +- dist/java/src/mp/code/Workspace.java | 103 ++---------------- .../src/mp/code/proto/WorkspaceEvent.java | 36 ++++++ .../src/mp/code/proto/WorkspaceEventKind.java | 35 ++++++ src/ffi/java/buffer.rs | 3 +- src/ffi/java/client.rs | 7 +- src/ffi/java/cursor.rs | 5 +- src/ffi/java/mod.rs | 2 +- src/ffi/java/workspace.rs | 7 +- 10 files changed, 103 insertions(+), 128 deletions(-) create mode 100644 dist/java/src/mp/code/proto/WorkspaceEvent.java create mode 100644 dist/java/src/mp/code/proto/WorkspaceEventKind.java diff --git a/Cargo.lock b/Cargo.lock index 4d1690e3..d69d5edf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ dependencies = [ "dashmap", "diamond-types", "jni", - "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae)", + "jni-toolbox", "mlua", "napi", "napi-build", @@ -187,10 +187,10 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=c0e2589cb0007412db1c005abbfd8fcf7b6b9151#c0e2589cb0007412db1c005abbfd8fcf7b6b9151" +source = "git+https://github.com/hexedtech/codemp-proto?rev=a36cc56bd6aa675baa2f1ca26e0e00074bc6a389#a36cc56bd6aa675baa2f1ca26e0e00074bc6a389" dependencies = [ "jni", - "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", + "jni-toolbox", "mlua", "mlua-serde-derive", "napi", @@ -803,38 +803,17 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-toolbox" -version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae#11897b11153dccd82044e980947c090ef12d08ae" -dependencies = [ - "jni", - "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae)", - "thiserror", - "uuid", -] - [[package]] name = "jni-toolbox" version = "0.2.2" source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" dependencies = [ "jni", - "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", + "jni-toolbox-macro", "thiserror", "uuid", ] -[[package]] -name = "jni-toolbox-macro" -version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=11897b11153dccd82044e980947c090ef12d08ae#11897b11153dccd82044e980947c090ef12d08ae" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "jni-toolbox-macro" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index ddd0fa7f..7b6eb6a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c0e2589cb0007412db1c005abbfd8fcf7b6b9151", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "a36cc56bd6aa675baa2f1ca26e0e00074bc6a389", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api @@ -47,7 +47,7 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "11897b11153dccd82044e980947c090ef12d08ae", optional = true, features = ["uuid", "unsigned"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "c40e925510fd563763db5dec4df300615453054b", optional = true, features = ["uuid", "unsigned"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 1960b576..9b84e9b9 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -3,11 +3,11 @@ import java.util.Optional; import java.util.function.Consumer; -import lombok.Getter; import mp.code.proto.UserInfo; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; +import mp.code.proto.WorkspaceEvent; /** * Represents a CodeMP workspace, which broadly speaking is a collection @@ -202,33 +202,33 @@ public void deleteBuffer(String path) throws ConnectionRemoteException { delete_buffer(this.ptr, path); } - private static native Event try_recv(long self) throws ControllerException; + private static native WorkspaceEvent try_recv(long self) throws ControllerException; /** - * Tries to get a {@link Event} from the queue if any were present, null otherwise - * @return the first workspace event in queue, if any are present + * Tries to get a {@link WorkspaceEvent} from the queue if any were present, null otherwise + * @return the first workspace WorkspaceEvent in queue, if any are present * @throws ControllerException if the controller was stopped */ - public Event tryRecv() throws ControllerException { + public WorkspaceEvent tryRecv() throws ControllerException { return try_recv(this.ptr); } - private static native Event recv(long self) throws ControllerException; + private static native WorkspaceEvent recv(long self) throws ControllerException; /** - * Blocks until a {@link Event} is available and returns it. - * @return the workspace event that occurred + * Blocks until a {@link WorkspaceEvent} is available and returns it. + * @return the workspace WorkspaceEvent that occurred * @throws ControllerException if the controller was stopped */ - public Event recv() throws ControllerException { + public WorkspaceEvent recv() throws ControllerException { return recv(this.ptr); } private static native void callback(long self, Consumer cb); /** - * Registers a callback to be invoked whenever a new {@link Event} is ready to be received. - * This will not work unless a Java thread has been dedicated to the event loop. + * Registers a callback to be invoked whenever a new {@link WorkspaceEvent} is ready to be received. + * This will not work unless a Java thread has been dedicated to the WorkspaceEvent loop. * @param cb a {@link Consumer} that receives the controller when the change occurs; * you should probably spawn a new thread in here, to avoid deadlocking * @see Extensions#drive(boolean) @@ -250,7 +250,7 @@ public void clearCallback() { private static native void poll(long self) throws ControllerException; /** - * Blocks until a {@link Event} is available. + * Blocks until a {@link WorkspaceEvent} is available. * @throws ControllerException if the controller was stopped */ public void poll() throws ControllerException { @@ -262,83 +262,4 @@ public void poll() throws ControllerException { static { NativeUtils.loadLibraryIfNeeded(); } - - /** - * Represents a workspace-wide event. - */ - public static final class Event { - /** The type of the event. */ - public final @Getter Type type; - private final String user; - private final String buffer; - - Event(Type type, String user, String buffer) { - this.type = type; - this.user = user; - this.buffer = buffer; - } - - /** - * Gets the user who joined, if any did. - * @return the user who joined, if any did - */ - public Optional getUserJoined() { - if(this.type == Type.USER_JOIN || this.type == Type.USER_JOIN_BUFFER) { - return Optional.of(this.user); - } else return Optional.empty(); - } - - /** - * Gets the user who left, if any did. - * @return the user who left, if any did - */ - public Optional getUserLeft() { - if(this.type == Type.USER_LEAVE || this.type == Type.USER_LEAVE_BUFFER) { - return Optional.of(this.user); - } else return Optional.empty(); - } - - /** - * Gets the path of buffer that changed, if any did. - * @return the path of buffer that changed, if any did - */ - public Optional getAffectedBuffer() { - if(this.type == Type.FILE_TREE_UPDATED || this.type == Type.USER_JOIN_BUFFER || this.type == Type.USER_LEAVE_BUFFER) { - return Optional.of(this.buffer); - } else return Optional.empty(); - } - - /** - * The type of workspace event. - */ - public enum Type { - /** - * Somebody joined a workspace. - * @see #getUserJoined() to get the name - */ - USER_JOIN, - /** - * Somebody left a workspace. - * @see #getUserLeft() to get the name - */ - USER_LEAVE, - /** - * The filetree was updated. - * @see #getAffectedBuffer() to see the buffer that changed - */ - FILE_TREE_UPDATED, - /** - * Somebody joined a buffer. - * @see #getUserJoined() to get the name of the user that joined - * @see #getAffectedBuffer() to see the buffer that they joined - */ - USER_JOIN_BUFFER, - /** - * Somebody left a buffer. - * @see #getUserLeft() to get the name of the user that left - * @see #getAffectedBuffer() to see the buffer that they left - */ - USER_LEAVE_BUFFER - } - } } diff --git a/dist/java/src/mp/code/proto/WorkspaceEvent.java b/dist/java/src/mp/code/proto/WorkspaceEvent.java new file mode 100644 index 00000000..ecc143ec --- /dev/null +++ b/dist/java/src/mp/code/proto/WorkspaceEvent.java @@ -0,0 +1,36 @@ +package mp.code.proto; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class WorkspaceEvent { + /** + * The underlying type of event, which will determine + * which fields are available. + */ + public final WorkspaceEventKind kind; + + /** + * The user that joined or left, or null. + */ + public final String user; + + /** + * The path of the relevant buffer, or null. + */ + public final String path; + + /** + * Whether the buffer is ephemeral, or null. + */ + public final Boolean ephemeral; + + /** + * The new path of the buffer after the rename, or null. + */ + public final String after; +} diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java new file mode 100644 index 00000000..50ea4d03 --- /dev/null +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -0,0 +1,35 @@ +package mp.code.proto; + +/** + * Represents the kind of workspace event. + */ +public enum WorkspaceEventKind { + /** + * Somebody joined a workspace. + */ + USER_JOIN_WORKSPACE, + /** + * Somebody left a workspace. + */ + USER_LEAVE_WORKSPACE, + /** + * A file was created. + */ + FILE_CREATE, + /** + * A file was renamed. + */ + FILE_RENAME, + /** + * A file was deleted. + */ + FILE_DELETE, + /** + * Somebody joined a buffer. + */ + USER_JOIN_BUFFER, + /** + * Somebody left a buffer. + */ + USER_LEAVE_BUFFER +} diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 73a4699b..39b59abd 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -2,8 +2,9 @@ use jni::{Env, objects::JObject}; use jni_toolbox::jni; use crate::{ - api::{AsyncReceiver, AsyncSender, BufferUpdate, TextChange, WorkspaceIdentifier}, + api::{AsyncReceiver, AsyncSender, BufferUpdate, TextChange}, errors::ControllerError, + proto::session::WorkspaceIdentifier }; /// Get the name of the buffer. diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index ae343f46..953df765 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,8 +1,9 @@ use crate::{ Workspace, - api::{Config, UserInfo, WorkspaceIdentifier}, + api::Config, client::Client, errors::{ConnectionError, RemoteError}, + proto::{common::UserInfo, session::WorkspaceIdentifier} }; use jni_toolbox::jni; @@ -12,9 +13,9 @@ fn connect(config: Config) -> Result { super::tokio().block_on(Client::connect(config)) } -/// Gets the current [crate::api::User]. +/// Gets the [UserInfo] for the current user. #[jni(package = "mp.code", class = "Client")] -fn current_user(client: &mut Client) -> crate::api::UserInfo { +fn current_user(client: &mut Client) -> UserInfo { client.current_user().clone() } diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 2b4da9b8..8fe1878e 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,6 +1,7 @@ use crate::{ - api::{AsyncReceiver, AsyncSender, Cursor, CursorEvent, WorkspaceIdentifier}, + api::{AsyncReceiver, AsyncSender}, errors::ControllerError, + proto::{cursor::{CursorEvent, CursorUpdate}, session::WorkspaceIdentifier} }; use jni::{Env, objects::JObject}; use jni_toolbox::jni; @@ -25,7 +26,7 @@ fn recv(controller: &mut crate::cursor::Controller) -> Result Result<(), ControllerError> { +fn send(controller: &mut crate::cursor::Controller, sel: CursorUpdate) -> Result<(), ControllerError> { controller.send(sel) } diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index bf615f08..80473ab4 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -146,7 +146,7 @@ into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); // #[allow(unsafe_code)] -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { +impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::proto::workspace::WorkspaceEventKind { // TODO const CLASS: &'static str = "mp/code/Workspace$Event"; fn into_java_object( self, diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index e515e807..2a3cefbe 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,7 +1,8 @@ use crate::{ Workspace, - api::{UserInfo, WorkspaceIdentifier, controller::AsyncReceiver}, + api::controller::AsyncReceiver, errors::{ConnectionError, ControllerError, RemoteError}, + proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent} }; use jni::{Env, objects::JObject}; use jni_toolbox::jni; @@ -104,13 +105,13 @@ fn delete_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr /// Block and receive a workspace event. #[jni(package = "mp.code", class = "Workspace")] -fn recv(workspace: &mut Workspace) -> Result { +fn recv(workspace: &mut Workspace) -> Result { super::tokio().block_on(workspace.recv()) } /// Receive a workspace event if present. #[jni(package = "mp.code", class = "Workspace")] -fn try_recv(workspace: &mut Workspace) -> Result, ControllerError> { +fn try_recv(workspace: &mut Workspace) -> Result, ControllerError> { super::tokio().block_on(workspace.try_recv()) } From f6f99746c383b78c64108fb089a6b4a4c8317f21 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:01:45 +0100 Subject: [PATCH 078/121] fix(js,py): update glues --- src/ffi/js/buffer.rs | 18 ++- src/ffi/js/client.rs | 68 ++------- src/ffi/js/cursor.rs | 27 ++-- src/ffi/js/workspace.rs | 81 +++-------- src/ffi/python/client.rs | 13 +- src/ffi/python/controllers.rs | 77 +++++----- src/ffi/python/mod.rs | 258 +++++++++++++++++----------------- src/ffi/python/workspace.rs | 18 +-- src/prelude.rs | 1 + 9 files changed, 224 insertions(+), 337 deletions(-) diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 61e385e3..d641daa6 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,12 +1,10 @@ use crate::api::controller::{AsyncReceiver, AsyncSender}; -use crate::api::{BufferUpdate, TextChange}; -use crate::api::WorkspaceIdentifier; -use crate::buffer::controller::BufferController; +use crate::prelude::*; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] -impl BufferController { +impl CodempBufferController { /// Register a callback to be invoked every time a new event is available to consume /// There can only be one callback registered at any given time. #[napi( @@ -15,9 +13,9 @@ impl BufferController { )] pub fn js_callback( &self, - fun: ThreadsafeFunction, + fun: ThreadsafeFunction, ) -> napi::Result<()> { - self.callback(move |controller: BufferController| { + self.callback(move |controller: CodempBufferController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this @@ -52,19 +50,19 @@ impl BufferController { /// Return next buffer event if present #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { + pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } /// Wait for next buffer event and return it #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } /// Send a buffer update to workspace #[napi(js_name = "send")] - pub fn js_send(&self, op: TextChange) -> napi::Result<()> { + pub fn js_send(&self, op: CodempTextChange) -> napi::Result<()> { Ok(self.send(op)?) } @@ -76,7 +74,7 @@ impl BufferController { /// Get id of workspace containing this controller. #[napi(js_name = "workspaceId")] - pub fn js_workspace_id(&self) -> WorkspaceIdentifier { + pub fn js_workspace_id(&self) -> CodempWorkspaceIdentifier { self.workspace_id().clone().into() } } diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 72533705..d7596049 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,41 +1,6 @@ -use crate::{Client, Workspace}; +use crate::prelude::*; use napi_derive::napi; -#[napi(object, js_name = "User")] -pub struct JsUser { - pub uuid: String, - pub name: String, - pub display_name: Option, - pub description: Option, - pub avatar: Option>, -} - - -impl TryFrom for crate::api::UserInfo { - type Error = ::Err; - fn try_from(value: JsUser) -> Result { - Ok(Self { - name: value.name, - display_name: value.display_name, - description: value.description, - avatar: value.avatar, - }) - } -} - - -impl From for JsUser { - fn from(value: crate::api::UserInfo) -> Self { - Self { - uuid: String::new(), - name: value.name, - display_name: value.display_name, - description: value.description, - avatar: value.avatar, - } - } -} - #[napi] /// connect to codemp servers and return a client session pub async fn connect(config: crate::api::Config) -> napi::Result { @@ -43,7 +8,7 @@ pub async fn connect(config: crate::api::Config) -> napi::Result } #[napi] -impl Client { +impl CodempClient { #[napi(js_name = "createWorkspace")] /// create workspace with given id, if able to pub async fn js_create_workspace(&self, workspace: String) -> napi::Result<()> { @@ -58,20 +23,14 @@ impl Client { #[napi(js_name = "fetchOwnedWorkspaces")] /// fetch owned workspaces - pub async fn js_fetch_owned_workspaces(&self) -> napi::Result> { - Ok(self.fetch_owned_workspaces().await? - .into_iter() - .map(|w| w.to_string()) - .collect()) + pub async fn js_fetch_owned_workspaces(&self) -> napi::Result> { + Ok(self.fetch_owned_workspaces().await?) } #[napi(js_name = "fetchJoinedWorkspaces")] /// fetch joined workspaces - pub async fn js_fetch_joined_workspaces(&self) -> napi::Result> { - Ok(self.fetch_joined_workspaces().await? - .into_iter() - .map(|w| w.to_string()) - .collect()) + pub async fn js_fetch_joined_workspaces(&self) -> napi::Result> { + Ok(self.fetch_joined_workspaces().await?) } #[napi(js_name = "inviteToWorkspace")] @@ -86,8 +45,8 @@ impl Client { #[napi(js_name = "attachWorkspace")] /// join workspace with given id (will start its cursor controller) - pub async fn js_attach_workspace(&self, user: String, workspace: String) -> napi::Result { - Ok(self.attach_workspace(&user, &workspace).await?.into()) + pub async fn js_attach_workspace(&self, user: String, workspace: String) -> napi::Result { + Ok(self.attach_workspace(&user, &workspace).await?) } #[napi(js_name = "leaveWorkspace")] @@ -98,23 +57,20 @@ impl Client { #[napi(js_name = "getWorkspace")] /// get workspace with given id, if it exists - pub fn js_get_workspace(&self, user: String, workspace: String) -> Option { + pub fn js_get_workspace(&self, user: String, workspace: String) -> Option { self.get_workspace(&user, &workspace) } #[napi(js_name = "currentUser")] /// return current sessions's user id - pub fn js_current_user(&self) -> JsUser { + pub fn js_current_user(&self) -> CodempUserInfo { self.current_user().clone().into() } #[napi(js_name = "activeWorkspaces")] /// get list of all active workspaces - pub fn js_active_workspaces(&self) -> Vec { + pub fn js_active_workspaces(&self) -> Vec { self.active_workspaces() - .into_iter() - .map(|w| w.to_string()) - .collect() } #[napi(js_name = "refresh")] @@ -131,7 +87,7 @@ impl Client { /// Get the meta information for a user #[napi(js_name = "getUserInfo")] - pub async fn js_get_user_info(&self, user: String) -> napi::Result { + pub async fn js_get_user_info(&self, user: String) -> napi::Result { Ok(self.get_user_info(&user).await?.into()) } diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 5d20bf49..521ba130 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,11 +1,9 @@ -use crate::api::controller::{AsyncReceiver, AsyncSender}; -use crate::api::WorkspaceIdentifier; -use crate::cursor::controller::CursorController; +use crate::prelude::*; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] -impl CursorController { +impl CodempCursorController { /// Register a callback to be called on receive. /// There can only be one callback registered at any given time. #[napi( @@ -14,9 +12,9 @@ impl CursorController { )] pub fn js_callback( &self, - fun: ThreadsafeFunction, + fun: ThreadsafeFunction, ) -> napi::Result<()> { - self.callback(move |controller: CursorController| { + self.callback(move |controller: CodempCursorController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this @@ -33,28 +31,25 @@ impl CursorController { /// Send a new cursor event to remote #[napi(js_name = "send")] - pub fn js_send(&self, buffer: String, sel: crate::api::Selection) -> napi::Result<()> { - Ok(self.send(crate::api::Cursor { - buffer, - sel: vec![sel], - })?) + pub fn js_send(&self, sel: CodempCursorUpdate) -> napi::Result<()> { + Ok(self.send(sel)?) } /// Get next cursor event if available without blocking #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { - Ok(self.try_recv().await?.map(crate::api::CursorEvent::from)) + pub async fn js_try_recv(&self) -> napi::Result> { + Ok(self.try_recv().await?) } /// Block until next #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } /// Get id of workspace containing this controller. #[napi(js_name = "workspaceId")] - pub fn js_workspace_id(&self) -> WorkspaceIdentifier { - self.workspace_id().clone().into() + pub fn js_workspace_id(&self) -> CodempWorkspaceIdentifier { + self.workspace_id().clone() } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index 8c8b6fbd..8c289da2 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,57 +1,13 @@ -use crate::Workspace; -use crate::api::controller::AsyncReceiver; -use crate::buffer::controller::BufferController; -use crate::cursor::controller::CursorController; +use crate::prelude::*; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use super::client::JsUser; - -#[napi(object, js_name = "Event")] -pub struct JsEvent { - pub r#type: String, - pub value: String, - pub buffer: Option, -} - -impl From for JsEvent { - fn from(value: crate::api::Event) -> Self { - match value { - crate::api::Event::FileTreeUpdated { path: value } => Self { - r#type: "filetree".into(), - value, - buffer: None, - }, - crate::api::Event::UserJoin { name: value } => Self { - r#type: "join".into(), - value, - buffer: None, - }, - crate::api::Event::UserLeave { name: value } => Self { - r#type: "leave".into(), - value, - buffer: None, - }, - crate::api::Event::UserJoinBuffer { name: value, buffer} => Self { - r#type: "joinBuffer".into(), - value, - buffer: Some(buffer), - }, - crate::api::Event::UserLeaveBuffer { name: value, buffer} => Self { - r#type: "leaveBuffer".into(), - value, - buffer: Some(buffer), - }, - } - } -} - #[napi] -impl Workspace { +impl CodempWorkspace { /// Get the unique workspace id #[napi(js_name = "id")] - pub fn js_id(&self) -> String { - self.id().to_string() + pub fn js_id(&self) -> CodempWorkspaceIdentifier { + self.id().clone() } /// List all available buffers in this workspace @@ -62,8 +18,8 @@ impl Workspace { /// List all user names currently in this workspace #[napi(js_name = "userList")] - pub fn js_user_list(&self) -> Vec { - self.user_list().into_iter().map(JsUser::from).collect() + pub fn js_user_list(&self) -> Vec { + self.user_list() } /// List all currently active buffers @@ -74,13 +30,13 @@ impl Workspace { /// Get workspace's Cursor Controller #[napi(js_name = "cursor")] - pub fn js_cursor(&self) -> CursorController { + pub fn js_cursor(&self) -> CodempCursorController { self.cursor() } /// Get a buffer controller by its name (path) #[napi(js_name = "getBuffer")] - pub fn js_get_buffer(&self, path: String) -> Option { + pub fn js_get_buffer(&self, path: String) -> Option { self.get_buffer(&path) } @@ -92,7 +48,7 @@ impl Workspace { /// Attach to a workspace buffer, starting a BufferController #[napi(js_name = "attachBuffer")] - pub async fn js_attach_buffer(&self, path: String) -> napi::Result { + pub async fn js_attach_buffer(&self, path: String) -> napi::Result { Ok(self.attach_buffer(&path).await?) } @@ -103,13 +59,13 @@ impl Workspace { } #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { - Ok(JsEvent::from(self.recv().await?)) + pub async fn js_recv(&self) -> napi::Result { + Ok(self.recv().await?) } #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { - Ok(self.try_recv().await?.map(JsEvent::from)) + pub async fn js_try_recv(&self) -> napi::Result> { + Ok(self.try_recv().await?) } #[napi(js_name = "poll")] @@ -125,9 +81,9 @@ impl Workspace { } #[napi(js_name = "callback", ts_args_type = "fun: (event: Workspace) => void")] - pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun; - self.callback(move |controller: Workspace| { + pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun; + self.callback(move |controller: CodempWorkspace| { tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); @@ -165,11 +121,8 @@ impl Workspace { /// Get all users currently attached to specified buffer #[napi(js_name = "bufferUserList")] - pub fn js_buffer_user_list(&self, path: String) -> Vec { + pub fn js_buffer_user_list(&self, path: String) -> Vec { self.buffer_user_list(&path) - .into_iter() - .map(JsUser::from) - .collect() } /// Pin an ephemeral buffer, making it permanent. diff --git a/src/ffi/python/client.rs b/src/ffi/python/client.rs index 82210d26..9ecfb74f 100644 --- a/src/ffi/python/client.rs +++ b/src/ffi/python/client.rs @@ -1,12 +1,9 @@ -use super::Client; use super::a_sync_detach; -use crate::api::UserInfo; -use crate::api::WorkspaceIdentifier; -use crate::workspace::Workspace; +use crate::prelude::*; use pyo3::prelude::*; #[pymethods] -impl Client { +impl CodempClient { // #[new] // fn __new__( // host: String, @@ -101,12 +98,12 @@ impl Client { // join a workspace #[pyo3(name = "get_workspace")] - fn pyget_workspace(&self, user: String, workspace: String) -> Option { + fn pyget_workspace(&self, user: String, workspace: String) -> Option { self.get_workspace(user, workspace) } #[pyo3(name = "active_workspaces")] - fn pyactive_workspaces(&self) -> Vec { + fn pyactive_workspaces(&self) -> Vec { self.active_workspaces() } @@ -118,7 +115,7 @@ impl Client { } #[pyo3(name = "current_user")] - fn pycurrent_user(&self) -> UserInfo { + fn pycurrent_user(&self) -> CodempUserInfo { self.current_user().clone() } diff --git a/src/ffi/python/controllers.rs b/src/ffi/python/controllers.rs index 84e38b7b..915a4d37 100644 --- a/src/ffi/python/controllers.rs +++ b/src/ffi/python/controllers.rs @@ -1,25 +1,20 @@ -use crate::api::Cursor; -use crate::api::TextChange; -use crate::api::WorkspaceIdentifier; -use crate::api::controller::{AsyncReceiver, AsyncSender}; -use crate::buffer::Controller as BufferController; -use crate::cursor::Controller as CursorController; -use pyo3::exceptions::PyValueError; +use crate::prelude::*; use pyo3::prelude::*; +use pyo3::exceptions::PyValueError; use super::Promise; use super::a_sync_detach; // need to do manually since Controller is a trait implementation #[pymethods] -impl CursorController { +impl CodempCursorController { #[pyo3(name = "workspace_id")] - fn pyworkspace_id(&self) -> WorkspaceIdentifier { + fn pyworkspace_id(&self) -> CodempWorkspaceIdentifier { self.workspace_id().clone() } #[pyo3(name = "send")] - fn pysend(&self, _py: Python, pos: Cursor) -> PyResult<()> { + fn pysend(&self, _py: Python, pos: CodempCursorUpdate) -> PyResult<()> { self.send(pos)?; Ok(()) } @@ -65,14 +60,14 @@ impl CursorController { // need to do manually since Controller is a trait implementation #[pymethods] -impl BufferController { +impl CodempBufferController { #[pyo3(name = "path")] fn pypath(&self) -> String { self.path().to_string() } #[pyo3(name = "workspace_id")] - fn pyworkspace_id(&self) -> WorkspaceIdentifier { + fn pyworkspace_id(&self) -> CodempWorkspaceIdentifier { self.workspace_id().clone() } @@ -88,7 +83,7 @@ impl BufferController { } #[pyo3(name = "send")] - fn pysend(&self, op: TextChange) -> PyResult<()> { + fn pysend(&self, op: CodempTextChange) -> PyResult<()> { let this = self.clone(); this.send(op)?; Ok(()) @@ -133,31 +128,31 @@ impl BufferController { } } -// We have to write this manually since -// cursor.user has type Option which cannot be translated -// automatically -#[pymethods] -impl Cursor { - #[getter(start)] - fn pystart(&self) -> Vec<(i32, i32)> { - self.sel - .iter() - .map(|s| (s.start_row, s.start_col)) - .collect() - } - - #[getter(end)] - fn pyend(&self) -> Vec<(i32, i32)> { - self.sel.iter().map(|s| (s.end_row, s.end_col)).collect() - } - - #[getter(buffer)] - fn pybuffer(&self) -> String { - self.buffer.clone() - } - - // #[getter(user)] - // fn pyuser(&self) -> Option { - // Some(self.user.clone()) - // } -} +// // We have to write this manually since +// // cursor.user has type Option which cannot be translated +// // automatically +// #[pymethods] +// impl CodempCursorUpdate { +// #[getter(start)] +// fn pystart(&self) -> Vec<(i32, i32)> { +// self.cursor +// .iter() +// .map(|s| (s.start_row, s.start_col)) +// .collect() +// } +// +// #[getter(end)] +// fn pyend(&self) -> Vec<(i32, i32)> { +// self.sel.iter().map(|s| (s.end_row, s.end_col)).collect() +// } +// +// #[getter(buffer)] +// fn pybuffer(&self) -> String { +// self.buffer.clone() +// } +// +// // #[getter(user)] +// // fn pyuser(&self) -> Option { +// // Some(self.user.clone()) +// // } +// } diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 5017cd4d..db315190 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -2,12 +2,7 @@ pub mod client; pub mod controllers; pub mod workspace; -use crate::{ - Client, Workspace, - api::{BufferUpdate, Config, Cursor, Event, Selection, TextChange, UserInfo}, - buffer::Controller as BufferController, - cursor::Controller as CursorController, -}; +use crate::prelude::*; use pyo3::{ exceptions::{PyConnectionError, PyRuntimeError, PySystemError}, @@ -162,46 +157,46 @@ fn init() -> PyResult { Ok(Driver(Some(rt_stop_tx))) } -#[pymethods] -impl UserInfo { - #[getter] - fn get_name(&self) -> pyo3::PyResult { - Ok(self.name.clone()) - } - - #[getter] - fn get_display_name(&self) -> pyo3::PyResult { - Ok(self.display_name.clone().unwrap_or(self.name.clone())) - } - - #[setter] - fn set_display_name(&mut self, value: String) -> pyo3::PyResult<()> { - self.display_name.replace(value); - - Ok(()) - } - - #[getter] - fn get_description(&self) -> pyo3::PyResult { - Ok(self - .description - .clone() - .unwrap_or("No description.".to_string())) - } - - #[setter] - fn set_description(&mut self, value: String) -> pyo3::PyResult<()> { - self.description.replace(value); - Ok(()) - } - - fn __str__(&self) -> String { - format!("{self:?}") - } -} +// #[pymethods] +// impl CodempUserInfo { +// #[getter] +// fn get_name(&self) -> pyo3::PyResult { +// Ok(self.name.clone()) +// } +// +// #[getter] +// fn get_display_name(&self) -> pyo3::PyResult { +// Ok(self.display_name.clone().unwrap_or(self.name.clone())) +// } +// +// #[setter] +// fn set_display_name(&mut self, value: String) -> pyo3::PyResult<()> { +// self.display_name.replace(value); +// +// Ok(()) +// } +// +// #[getter] +// fn get_description(&self) -> pyo3::PyResult { +// Ok(self +// .description +// .clone() +// .unwrap_or("No description.".to_string())) +// } +// +// #[setter] +// fn set_description(&mut self, value: String) -> pyo3::PyResult<()> { +// self.description.replace(value); +// Ok(()) +// } +// +// fn __str__(&self) -> String { +// format!("{self:?}") +// } +// } #[pymethods] -impl Config { +impl CodempConfig { #[new] #[pyo3(signature = (*, username, password, **kwds))] pub fn pynew( @@ -231,89 +226,89 @@ impl Config { } } -#[pymethods] -impl Cursor { - fn __str__(&self) -> String { - format!("{self:?}") - } -} - -#[pymethods] -impl Selection { - #[new] - #[pyo3(signature = (*, start_row, start_col, end_row, end_col, **kwds))] - pub fn py_new( - start_row: i32, - start_col: i32, - end_row: i32, - end_col: i32, - kwds: Option<&Bound<'_, PyDict>>, - ) -> PyResult { - if let Some(_kwds) = kwds { - Ok(Self { - start_row, - start_col, - end_row, - end_col, - }) - } else { - Ok(Self { - start_row, - start_col, - end_row, - end_col, - }) - } - } - - fn __str__(&self) -> String { - format!("{self:?}") - } -} +// #[pymethods] +// impl CodempCursor { +// fn __str__(&self) -> String { +// format!("{self:?}") +// } +// } -#[pymethods] -impl BufferUpdate { - fn __str__(&self) -> String { - format!("{self:?}") - } -} +// #[pymethods] +// impl CodempCursorPosition { +// #[new] +// #[pyo3(signature = (*, start_row, start_col, end_row, end_col, **kwds))] +// pub fn py_new( +// start_row: i32, +// start_col: i32, +// end_row: i32, +// end_col: i32, +// kwds: Option<&Bound<'_, PyDict>>, +// ) -> PyResult { +// if let Some(_kwds) = kwds { +// Ok(Self { +// start_row, +// start_col, +// end_row, +// end_col, +// }) +// } else { +// Ok(Self { +// start_row, +// start_col, +// end_row, +// end_col, +// }) +// } +// } +// +// fn __str__(&self) -> String { +// format!("{self:?}") +// } +// } -#[pymethods] -impl TextChange { - #[new] - #[pyo3(signature = (*, start, end, content, **kwds))] - pub fn py_new( - start: u32, - end: u32, - content: String, - kwds: Option<&Bound<'_, PyDict>>, - ) -> PyResult { - if let Some(_kwds) = kwds { - Ok(Self { - start_idx: start, - end_idx: end, - content, - }) - } else { - Ok(Self { - start_idx: start, - end_idx: end, - content, - }) - } - } +// #[pymethods] +// impl CodempBufferUpdate { +// fn __str__(&self) -> String { +// format!("{self:?}") +// } +// } - fn __str__(&self) -> String { - format!("{self:?}") - } -} +// #[pymethods] +// impl CodempTextChange { +// #[new] +// #[pyo3(signature = (*, start, end, content, **kwds))] +// pub fn py_new( +// start: u32, +// end: u32, +// content: String, +// kwds: Option<&Bound<'_, PyDict>>, +// ) -> PyResult { +// if let Some(_kwds) = kwds { +// Ok(Self { +// start_idx: start, +// end_idx: end, +// content, +// }) +// } else { +// Ok(Self { +// start_idx: start, +// end_idx: end, +// content, +// }) +// } +// } +// +// fn __str__(&self) -> String { +// format!("{self:?}") +// } +// } #[pyfunction] -fn connect(py: Python, config: Py) -> PyResult { - let conf: Config = config.extract(py)?; +fn connect(py: Python, config: Py) -> PyResult { + let conf: CodempConfig = config.extract(py)?; Ok(Promise(Some(crate::ffi::python::tokio().spawn( async move { - let client = Client::connect(conf).await?; + let client = CodempClient::connect(conf).await?; Python::attach(|py| Ok(client.into_pyobject(py)?.into_any().unbind())) }, )))) @@ -387,20 +382,21 @@ fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(set_logger, m)?)?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; - m.add_class::()?; + m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index 94d8e6c5..aec34e5b 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -1,8 +1,4 @@ -use crate::api::UserInfo; -use crate::api::controller::AsyncReceiver; -use crate::buffer::Controller as BufferController; -use crate::cursor::Controller as CursorController; -use crate::workspace::Workspace; +use crate::prelude::*; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -10,7 +6,7 @@ use super::Promise; use super::a_sync_detach; #[pymethods] -impl Workspace { +impl CodempWorkspace { // join a workspace #[pyo3(name = "create_buffer")] fn pycreate_buffer(&self, py: Python, path: String, ephemeral: bool) -> PyResult { @@ -67,16 +63,16 @@ impl Workspace { } #[pyo3(name = "id")] - fn pyid(&self) -> crate::api::WorkspaceIdentifier { + fn pyid(&self) -> CodempWorkspaceIdentifier { self.id().clone() } #[pyo3(name = "cursor")] - fn pycursor(&self) -> CursorController { + fn pycursor(&self) -> CodempCursorController { self.cursor() } #[pyo3(name = "get_buffer")] - fn pyget_buffer(&self, path: String) -> Option { + fn pyget_buffer(&self, path: String) -> Option { self.get_buffer(path.as_str()) } @@ -92,12 +88,12 @@ impl Workspace { } #[pyo3(name = "user_list")] - fn pyuser_list(&self) -> Vec { + fn pyuser_list(&self) -> Vec { self.user_list() } #[pyo3(name = "buffer_user_list")] - fn pybuffer_user_list(&self, path: String) -> Vec { + fn pybuffer_user_list(&self, path: String) -> Vec { self.buffer_user_list(path) } diff --git a/src/prelude.rs b/src/prelude.rs index bf4595cc..2c8c1822 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,6 +5,7 @@ pub use crate::api::{ TextChange as CodempTextChange, Config as CodempConfig, AsyncReceiver as CodempAsyncReceiver, AsyncSender as CodempAsyncSender, Controller as CodempController, + BufferUpdate as CodempBufferUpdate, }; pub use crate::proto::{ From b36f977f5246ca6e888526f6ec7b1fd5db9f5256 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:10:17 +0100 Subject: [PATCH 079/121] fix(java): packages on jclass --- src/api/change.rs | 4 ++-- src/api/config.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/change.rs b/src/api/change.rs index 721b7373..5e4ad37e 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -11,7 +11,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/BufferUpdate"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct BufferUpdate { /// Optional content hash after applying this change. #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))] @@ -55,7 +55,7 @@ pub struct BufferUpdate { #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/TextChange"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.data"))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. pub start_idx: u32, diff --git a/src/api/config.rs b/src/api/config.rs index fa10d853..f002d217 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -12,7 +12,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(class = "mp/code/data/Config"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.data"))] pub struct Config { /// User identifier used to register, possibly your email. pub username: String, From 88482a38e5ef5b96d94e64221095aa398cb8e515 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 19:12:50 +0100 Subject: [PATCH 080/121] chore(java): bring java glue up to speed --- dist/java/src/mp/code/proto/WorkspaceEvent.java | 6 +++--- .../src/mp/code/proto/WorkspaceEventKind.java | 16 ++++++++-------- src/api/change.rs | 2 +- src/api/config.rs | 2 +- src/ffi/java/mod.rs | 2 ++ 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/dist/java/src/mp/code/proto/WorkspaceEvent.java b/dist/java/src/mp/code/proto/WorkspaceEvent.java index ecc143ec..1a8a869d 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEvent.java +++ b/dist/java/src/mp/code/proto/WorkspaceEvent.java @@ -9,10 +9,10 @@ @RequiredArgsConstructor public class WorkspaceEvent { /** - * The underlying type of event, which will determine - * which fields are available. + * The underlying type of event, which will determine which fields are available. + * Always one of the constants from {@link WorkspaceEventKind}. */ - public final WorkspaceEventKind kind; + public final int kind; /** * The user that joined or left, or null. diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java index 50ea4d03..ebe175fa 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEventKind.java +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -3,33 +3,33 @@ /** * Represents the kind of workspace event. */ -public enum WorkspaceEventKind { +public class WorkspaceEventKind { /** * Somebody joined a workspace. */ - USER_JOIN_WORKSPACE, + public static final int USER_JOIN_WORKSPACE = 0; /** * Somebody left a workspace. */ - USER_LEAVE_WORKSPACE, + public static final int USER_LEAVE_WORKSPACE = 1; /** * A file was created. */ - FILE_CREATE, + public static final int FILE_CREATE = 2; /** * A file was renamed. */ - FILE_RENAME, + public static final int FILE_RENAME = 3; /** * A file was deleted. */ - FILE_DELETE, + public static final int FILE_DELETE = 4; /** * Somebody joined a buffer. */ - USER_JOIN_BUFFER, + public static final int USER_JOIN_BUFFER = 5; /** * Somebody left a buffer. */ - USER_LEAVE_BUFFER + public static final int USER_LEAVE_BUFFER = 6; } diff --git a/src/api/change.rs b/src/api/change.rs index 5e4ad37e..9c2a2f3b 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -55,7 +55,7 @@ pub struct BufferUpdate { #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.data"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. pub start_idx: u32, diff --git a/src/api/config.rs b/src/api/config.rs index f002d217..57393227 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -12,7 +12,7 @@ #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.data"))] +#[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct Config { /// User identifier used to register, possibly your email. pub username: String, diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 80473ab4..48d833d3 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -145,6 +145,7 @@ into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); +/* TODO: reminder to come up with a way to make fancy enums in java // #[allow(unsafe_code)] impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::proto::workspace::WorkspaceEventKind { // TODO const CLASS: &'static str = "mp/code/Workspace$Event"; @@ -185,3 +186,4 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::proto::workspace::WorkspaceE ) } } +*/ From 987c858d0f36f92b1be3af127e6f46a547b5f5a1 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:19:43 +0100 Subject: [PATCH 081/121] fix(lua): yay simpler glue now! no more derive_lua_serde! { } --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/api/change.rs | 6 ++++-- src/api/config.rs | 3 ++- src/ffi/lua/buffer.rs | 7 +++---- src/ffi/lua/client.rs | 22 ++++++++++------------ src/ffi/lua/cursor.rs | 9 ++++----- src/ffi/lua/ext/callback.rs | 16 +++++++++------- src/ffi/lua/ext/mod.rs | 20 -------------------- src/ffi/lua/workspace.rs | 4 +--- 10 files changed, 36 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d69d5edf..790114b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,7 @@ dependencies = [ "jni", "jni-toolbox", "mlua", + "mlua-serde-derive", "napi", "napi-build", "napi-derive", diff --git a/Cargo.toml b/Cargo.toml index 7b6eb6a4..1c91dec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "c40e925 # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } +mlua-serde-derive = { version = "0.1", optional = true } # glue (js) napi = { version = "3.1", features = ["full"], optional = true } @@ -83,7 +84,7 @@ test-coverage = ["dep:syn", "dep:regex"] java = ["dep:jni", "dep:tracing-subscriber", "dep:jni-toolbox", "codemp-proto/java"] js = ["dep:napi-build", "dep:tracing-subscriber", "dep:napi", "dep:napi-derive", "codemp-proto/js"] py = ["dep:pyo3", "dep:tracing-subscriber", "dep:pyo3-build-config", "codemp-proto/py"] -lua = ["serialize", "dep:mlua", "dep:tracing-subscriber", "codemp-proto/lua"] +lua = ["serialize", "dep:mlua", "dep:mlua-serde-derive", "dep:tracing-subscriber", "codemp-proto/lua"] # ffi variants lua-jit = ["mlua?/luajit"] lua-54 = ["mlua?/lua54"] diff --git a/src/api/change.rs b/src/api/change.rs index 9c2a2f3b..16410c7b 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -8,9 +8,10 @@ /// content **after** applying this change. Note that the `hash` field will not necessarily /// be provided every time. #[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "lua", derive(mlua_serde_derive::LuaSerde))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct BufferUpdate { /// Optional content hash after applying this change. @@ -52,9 +53,10 @@ pub struct BufferUpdate { /// assert_eq!(after, "hello mom!"); /// ``` #[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "lua", derive(mlua_serde_derive::LuaSerde))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. diff --git a/src/api/config.rs b/src/api/config.rs index 57393227..08b5b80d 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -9,9 +9,10 @@ /// resulting endpoint is composed like this: /// http{tls?'s':''}://{host}:{port} #[derive(Clone, Default)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "lua", derive(mlua_serde_derive::LuaSerde))] #[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all, from_py_object))] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "java", jni_toolbox::jclass(package = "mp.code.proto"))] pub struct Config { /// User identifier used to register, possibly your email. diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index 4b306513..c2e21459 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -3,8 +3,6 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempTextChange CodempBufferUpdate CodempBufferNode } - impl LuaUserData for CodempBufferController { fn add_methods>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { @@ -53,8 +51,9 @@ impl LuaUserData for CodempBufferController { impl CodempBufferController { fn lua_callback_id(&self) -> String { format!( - "codemp-buffercontroller({}:{})-callback-registry", - self.workspace_id(), + "codemp-buffercontroller({}/{}://{})-callback-registry", + self.workspace_id().user, + self.workspace_id().workspace, self.path() ) } diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index 273bbc54..1250f435 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -3,8 +3,6 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempConfig CodempUserInfo } - impl LuaUserData for CodempClient { fn add_methods>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { @@ -93,20 +91,20 @@ impl LuaUserData for CodempClient { methods.add_method("get_user_info", |_, this, (user,): (String,)| { a_sync! { - this => crate::api::UserInfo::from(this.get_user_info(user).await?) + this => this.get_user_info(user).await? } }); // TODO need to derive ser/de on Event, but this is in protobuf... - // methods.add_method( - // "recv", - // |_, this, ()| a_sync! { this => this.recv().await? } - // ); - - // methods.add_method( - // "try_recv", - // |_, this, ()| a_sync! { this => this.try_recv().await? }, - // ); + methods.add_method( + "recv", + |_, this, ()| a_sync! { this => this.recv().await? } + ); + + methods.add_method( + "try_recv", + |_, this, ()| a_sync! { this => this.try_recv().await? }, + ); methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? }); diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index 6a8e6a60..f5c26ff5 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -3,8 +3,6 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempSelection CodempCursor CodempCursorEvent } - impl LuaUserData for CodempCursorController { fn add_methods>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { @@ -19,7 +17,7 @@ impl LuaUserData for CodempCursorController { this => this.list().await?.into_iter().map(CodempCursorEvent::from).collect::>() }); - methods.add_method("send", |_, this, (cursor,): (CodempCursor,)| { + methods.add_method("send", |_, this, (cursor,): (CodempCursorUpdate,)| { Ok(this.send(cursor)?) }); methods.add_method( @@ -46,8 +44,9 @@ impl LuaUserData for CodempCursorController { impl CodempCursorController { fn lua_callback_id(&self) -> String { format!( - "codemp-cursorcontroller({})-callback-registry", - self.workspace_id() + "codemp-cursorcontroller({}/{})-callback-registry", + self.workspace_id().user, + self.workspace_id().workspace, ) } } diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index 44490264..0a708fff 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -126,20 +126,22 @@ callback_args! { Workspace: CodempWorkspace, WorkspaceIdentifier: CodempWorkspaceIdentifier, VecWorkspaceIdentifier: Vec, - Event: CodempEvent, - MaybeEvent: Option, - Cursor: CodempCursor, - MaybeCursor: Option, + WorkspaceEvent: CodempWorkspaceEvent, + MaybeWorkspaceEvent: Option, + CursorUpdate: CodempCursorUpdate, + MaybeCursorUpdate: Option, CursorEvent: CodempCursorEvent, VecCursorEvent: Vec, MaybeCursorEvent: Option, - Selection: CodempSelection, - VecSelection: Vec, - MaybeSelection: Option, + CursorPosition: CodempCursorPosition, + VecCursorPosition: Vec, + MaybeCursorPosition: Option, TextChange: CodempTextChange, MaybeTextChange: Option, BufferUpdate: CodempBufferUpdate, MaybeBufferUpdate: Option, BufferNode: CodempBufferNode, VecBufferNode: Vec, + SessionEvent: CodempSessionEvent, + MaybeSessionEvent: Option, } diff --git a/src/ffi/lua/ext/mod.rs b/src/ffi/lua/ext/mod.rs index 8818de40..e8702528 100644 --- a/src/ffi/lua/ext/mod.rs +++ b/src/ffi/lua/ext/mod.rs @@ -22,23 +22,3 @@ pub(crate) fn lua_parse_uuid(uuid: &str, pos: usize, name: &str) -> mlua::Result }), } } - -macro_rules! impl_lua_serde { - ($($t:ty)*) => { - $( - impl FromLua for $t { - fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<$t> { - lua.from_value(value) - } - } - - impl IntoLua for $t { - fn into_lua(self, lua: &Lua) -> LuaResult { - lua.to_value(&self) - } - } - )* - }; -} - -pub(crate) use impl_lua_serde; diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index 662cdc07..1e58d156 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -3,8 +3,6 @@ use mlua::prelude::*; use super::ext::a_sync::a_sync; -super::ext::impl_lua_serde! { CodempEvent CodempWorkspaceIdentifier } - impl LuaUserData for CodempWorkspace { fn add_methods>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { @@ -97,6 +95,6 @@ impl LuaUserData for CodempWorkspace { impl CodempWorkspace { fn lua_callback_id(&self) -> String { - format!("codemp-workspace({})-callback-registry", self.id()) + format!("codemp-workspace({}/{})-callback-registry", self.id().user, self.id().workspace) } } From 0abe0e1c593bfd089db264e71260191299953d31 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:29:24 +0100 Subject: [PATCH 082/121] fix: clone errors, some warns --- src/client.rs | 12 ++++-------- src/cursor/worker.rs | 3 --- src/ffi/js/buffer.rs | 2 +- src/ffi/js/mod.rs | 10 +++++----- src/ffi/lua/cursor.rs | 2 +- src/ffi/python/mod.rs | 6 +++--- src/workspace.rs | 7 ++++--- 7 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/client.rs b/src/client.rs index 82062af3..e29f2262 100644 --- a/src/client.rs +++ b/src/client.rs @@ -89,7 +89,7 @@ impl Client { }; let inner = Arc::new(ClientInner { - user: Arc::new(resp.user.into()), + user: Arc::new(resp.user), workspaces: DashMap::default(), poll_tx, events: tokio::sync::Mutex::new(ev_rx), @@ -255,7 +255,7 @@ impl Client { }) .await? .into_inner() - .into()) + ) } /// Join and return a [`Workspace`]. @@ -273,9 +273,7 @@ impl Client { let workspace = workspace.to_string(); let mut session_client = self.0.session.clone(); let token = session_client - .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from( - workspace_id.clone(), - )) + .get_workspace_token(workspace_id.clone()) .await? .into_inner(); @@ -314,9 +312,7 @@ impl Client { break; }; let new_credentials = session_client - .get_workspace_token(codemp_proto::session::WorkspaceIdentifier::from( - _workspace.clone(), - )) + .get_workspace_token(_workspace.clone()) .await? .into_inner(); workspace_claims.set(new_credentials); diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index c03a6e0c..109fe206 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -19,7 +19,6 @@ use super::controller::{CursorController, CursorControllerInner}; struct CursorWorker { workspace_id: crate::proto::session::WorkspaceIdentifier, op: mpsc::UnboundedReceiver, - map: Arc>, stream: mpsc::Receiver>>, poll: mpsc::UnboundedReceiver>, pollers: Vec>, @@ -37,7 +36,6 @@ impl CursorWorker { impl CursorController { pub(crate) fn spawn( - user_map: Arc>, tx: mpsc::Sender, rx: Streaming, workspace_id: crate::proto::session::WorkspaceIdentifier, @@ -62,7 +60,6 @@ impl CursorController { let worker = CursorWorker { workspace_id, op: op_rx, - map: user_map, stream: stream_rx, store: std::collections::VecDeque::default(), controller: weak, diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index d641daa6..3c8134d0 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -75,6 +75,6 @@ impl CodempBufferController { /// Get id of workspace containing this controller. #[napi(js_name = "workspaceId")] pub fn js_workspace_id(&self) -> CodempWorkspaceIdentifier { - self.workspace_id().clone().into() + self.workspace_id().clone() } } diff --git a/src/ffi/js/mod.rs b/src/ffi/js/mod.rs index 4b9d3eee..197417a1 100644 --- a/src/ffi/js/mod.rs +++ b/src/ffi/js/mod.rs @@ -1,8 +1,8 @@ -pub mod buffer; -pub mod client; -pub mod cursor; -pub mod ext; -pub mod workspace; +mod buffer; +mod client; +mod cursor; +mod ext; +mod workspace; impl From for napi::Error { fn from(value: crate::errors::ConnectionError) -> Self { diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index f5c26ff5..2b5521f9 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -14,7 +14,7 @@ impl LuaUserData for CodempCursorController { }); methods.add_method("list", |_, this, ()| a_sync! { - this => this.list().await?.into_iter().map(CodempCursorEvent::from).collect::>() + this => this.list().await? }); methods.add_method("send", |_, this, (cursor,): (CodempCursorUpdate,)| { diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index db315190..a3adc5b3 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -1,6 +1,6 @@ -pub mod client; -pub mod controllers; -pub mod workspace; +mod client; +mod controllers; +mod workspace; use crate::prelude::*; diff --git a/src/workspace.rs b/src/workspace.rs index 92cc0b3f..481b7486 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -104,7 +104,6 @@ impl Workspace { let users = Arc::new(DashMap::default()); let controller = cursor::Controller::spawn( - users.clone(), tx, cur_stream, id.clone(), @@ -153,6 +152,7 @@ impl Workspace { } /// drop arc, return true if was last + #[allow(unused)] // for now, until we solve the drop-behavior issue pub(crate) fn consume(self) -> bool { Arc::into_inner(self.0).is_some() } @@ -170,7 +170,7 @@ impl Workspace { // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( - path, + path.clone(), crate::proto::files::BufferNode { path: crate::proto::files::BufferPath::from(&path), ephemeral, @@ -436,6 +436,7 @@ impl WorkspaceWorker { break tracing::debug!("workspace worker clean exit"); }; tracing::debug!("received workspace event: {event:?}"); + let _event = event.clone(); match event.kind() { // TODO we should never get wrong optionals set but should we log if we do? WorkspaceEventKind::UserJoinWorkspace => { @@ -496,7 +497,7 @@ impl WorkspaceWorker { } } - if self.events.send(event).is_err() { + if self.events.send(_event).is_err() { tracing::warn!("no active controller to receive workspace event"); } self.pollers.drain(..).for_each(|x| { From 8900f242ee8d2540a401bbaee6c833b6a5f474ca Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 19:30:46 +0100 Subject: [PATCH 083/121] chore: one less clone --- src/client.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index e29f2262..f7c36ec2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -303,7 +303,6 @@ impl Client { let weak = Arc::downgrade(&ws.0); tokio::spawn(async move { - let _workspace = workspace_id.clone(); let fut = async move { loop { // TODO either configurable token refresh time or calculate depending on token lifetime @@ -312,7 +311,7 @@ impl Client { break; }; let new_credentials = session_client - .get_workspace_token(_workspace.clone()) + .get_workspace_token(workspace_id.clone()) .await? .into_inner(); workspace_claims.set(new_credentials); From 089d0609d8f35cbbac741169b2522025d39a33ce Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:44:46 +0100 Subject: [PATCH 084/121] docs(lua): update annotations --- dist/lua/annotations.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 08302d2b..441e3645 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -529,22 +529,25 @@ function BufferController:ack(version) end ---handle to a workspace's cursor channel, allowing send/recv operations local CursorController = {} ----a cursor selection span ----@class Selection ----@field start_row integer cursor position starting row in buffer ----@field start_col integer cursor position starting column in buffer ----@field end_row integer cursor position final row in buffer ----@field end_col integer cursor position final column in buffer +---a row+col tuple +---@class RowCol +---@field row integer current row +---@field col integer current column + +---an instant cursor position span +---@class CursorPosition +---@field start RowCol cursor position start in buffer +---@field finish RowCol cursor position end in buffer ---a cursor instantaneous state ----@class Cursor +---@class CursorUpdate ---@field buffer string path of buffer this cursor is on ----@field sel Selection[] the updated cursor selection(s) +---@field cursors CursorPosition[] the updated cursor position(s) ---an event that occurred about a user's cursor ---@class CursorEvent ---@field user string user who sent this cursor ----@field cursor Cursor cursor position data +---@field position CursorUpdate cursor position data ---@return WorkspaceIdentifier ---returns the workspace id this cursor controller belongs to @@ -556,7 +559,7 @@ function CursorController:workspace_id() end ---gets the current state of all user cursors function CursorController:list() end ----@param cursor Cursor cursor position to broadcast +---@param cursor CursorUpdate cursor position to broadcast ---update cursor position by sending a cursor event to server function CursorController:send(cursor) end From 5686812a4371e8d7e92455fc6e8ee6f749b70e18 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 19:46:24 +0100 Subject: [PATCH 085/121] fix(lua): use finish and not end --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cursor/controller.rs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 790114b9..f1fd1dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=a36cc56bd6aa675baa2f1ca26e0e00074bc6a389#a36cc56bd6aa675baa2f1ca26e0e00074bc6a389" +source = "git+https://github.com/hexedtech/codemp-proto?rev=c36b3595164b5e8c341ac1141dee62b31628ffae#c36b3595164b5e8c341ac1141dee62b31628ffae" dependencies = [ "jni", "jni-toolbox", diff --git a/Cargo.toml b/Cargo.toml index 1c91dec8..4ceba41b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "a36cc56bd6aa675baa2f1ca26e0e00074bc6a389", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c36b3595164b5e8c341ac1141dee62b31628ffae", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index ab6ae1e1..0c01a394 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -44,11 +44,11 @@ impl Controller for CursorController {} impl AsyncSender for CursorController { fn send(&self, mut cursor: CursorUpdate) -> ControllerResult<()> { for sel in cursor.cursors.iter_mut() { - if sel.start.row > sel.end.row - || (sel.start.row == sel.end.row && sel.start.col > sel.end.col) + if sel.start.row > sel.finish.row + || (sel.start.row == sel.finish.row && sel.start.col > sel.finish.col) { - std::mem::swap(&mut sel.start.row, &mut sel.end.row); - std::mem::swap(&mut sel.start.col, &mut sel.end.col); + std::mem::swap(&mut sel.start.row, &mut sel.finish.row); + std::mem::swap(&mut sel.start.col, &mut sel.finish.col); } } From bbabe0a00a8d2cf39d8f48e3391ee9b80fb40487 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 20:17:27 +0100 Subject: [PATCH 086/121] tests: fix doctests --- src/ffi/mod.rs | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 116f0c6e..bb4e459a 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -29,7 +29,7 @@ //! // wait for cursor movements //! loop { //! let event = workspace.cursor().recv().await?; -//! println!("user {} moved on buffer {}", event.user, event.cursor.buffer); +//! println!("user {} moved on buffer {}", event.user, event.position.buffer); //! } //! # Ok::<(),Box>(()) //! # }; diff --git a/src/lib.rs b/src/lib.rs index 7124e009..45a2551f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! use codemp::api::controller::{AsyncSender, AsyncReceiver}; // needed to access trait methods //! let cursor = workspace.cursor(); //! let event = cursor.recv().await.expect("disconnected while waiting for event!"); -//! println!("user {} moved on buffer {}", event.user, event.cursor.buffer); +//! println!("user {} moved on buffer {}", event.user, event.position.buffer); //! # }; //! ``` //! From 9a30c3314bb9c6da5fb12d6fab558d0d98bd90a5 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 20:34:38 +0100 Subject: [PATCH 087/121] fix: bump codemp-proto (enums start at 1) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1fd1dd6..4ff1ea7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=c36b3595164b5e8c341ac1141dee62b31628ffae#c36b3595164b5e8c341ac1141dee62b31628ffae" +source = "git+https://github.com/hexedtech/codemp-proto?rev=eda0969e86d0c592688c692a052b5659099277ae#eda0969e86d0c592688c692a052b5659099277ae" dependencies = [ "jni", "jni-toolbox", diff --git a/Cargo.toml b/Cargo.toml index 4ceba41b..40054e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c36b3595164b5e8c341ac1141dee62b31628ffae", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "eda0969e86d0c592688c692a052b5659099277ae", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From 52954850ac2549e734627e6e85c1c9f800cb38ac Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 20:34:50 +0100 Subject: [PATCH 088/121] docs(lua): update annotations --- dist/lua/annotations.lua | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 441e3645..085f22b2 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -99,6 +99,28 @@ function MaybeWorkspaceEventPromise:await() end function MaybeWorkspaceEventPromise:and_then(cb) end +---@class (exact) SessionEventPromise : Promise +local SessionEventPromise = {} +--- block until promise is ready and return value +--- @return SessionEvent +function SessionEventPromise:await() end +--- cancel promise execution +function SessionEventPromise:cancel() end +---@param cb fun(x: SessionEvent) callback to invoke +---invoke callback asynchronously as soon as promise is ready +function SessionEventPromise:and_then(cb) end + + +---@class (exact) MaybeSessionEventPromise : Promise +local MaybeSessionEventPromise = {} +--- block until promise is ready and return value +--- @return SessionEvent | nil +function MaybeSessionEventPromise:await() end +---@param cb fun(x: SessionEvent | nil) callback to invoke +---invoke callback asynchronously as soon as promise is ready +function MaybeSessionEventPromise:and_then(cb) end + + ---@class (exact) BufferControllerPromise : Promise local BufferControllerPromise = {} --- block until promise is ready and return value @@ -296,6 +318,31 @@ function Client:get_workspace(user, ws) end ---get full user info for given username from server function Client:get_user_info(user) end +---all enum types for session events +SessionEventKind = { + InvitationEvent = 1, + QuitEvent = 2, + AcceptEvent = 3, + RejectEvent = 4, +} + +---@class (exact) SessionEvent +---@field kind integer (SessionEventKind) event kind +---@field user string the user that created this event (sent invitation, rejected invite...) +---@field workspace WorkspaceIdentifier the workspace this event is related to + +---@return MaybeSessionEventPromise +---@async +---@nodiscard +---try to receive session events, returning nil if none is available +function Client:try_recv() end + +---@return SessionEventPromise +---@async +---@nodiscard +---block until next client event and return it +function Client:recv() end + ---@return NilPromise ---@async ---@nodiscard @@ -421,10 +468,23 @@ function Workspace:fetch_users(path) end ---fetch the list of users in the given buffer function Workspace:fetch_buffer_users(path) end +---all enum types for workspace events +WorkspaceEventKind = { + UserJoinWorkspace = 1, + UserLeaveWorkspace = 2, + FileCreate = 3, + FileRename = 4, + FileDelete = 5, + UserJoinBuffer = 6, + UserLeaveBuffer = 7, +} + ---@class (exact) WorkspaceEvent ----@field type string can be "UserJoin", "UserLeave" or "FileTreeUpdated" ----@field name? string present for "UserJoin" and "UserLeave" ----@field path? string present for "FileTreeUpdated" +---@field kind integer (WorkspaceEventKind) event kind +---@field user string? the user that joined/left (possibly a buffer) +---@field path string? path to relevant buffer (deleted/created/left by user...) +---@field ephemeral boolean? wheter relevant buffer is ephemeral +---@field after string? if this is a FileRename, new path will be here ---@return MaybeWorkspaceEventPromise ---@async From 167fb2dd3fede53168b1c0bd1a4ca13077d590c6 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 20:51:52 +0100 Subject: [PATCH 089/121] feat(java): client async recv glue --- dist/java/src/mp/code/BufferController.java | 3 +- dist/java/src/mp/code/Client.java | 59 ++++++++++++++++ dist/java/src/mp/code/CursorController.java | 5 +- dist/java/src/mp/code/proto/SessionEvent.java | 26 +++++++ .../src/mp/code/proto/SessionEventKind.java | 30 ++++++++ .../src/mp/code/proto/WorkspaceEventKind.java | 20 +++--- src/ffi/java/client.rs | 69 ++++++++++++++++++- 7 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 dist/java/src/mp/code/proto/SessionEvent.java create mode 100644 dist/java/src/mp/code/proto/SessionEventKind.java diff --git a/dist/java/src/mp/code/BufferController.java b/dist/java/src/mp/code/BufferController.java index dac3833e..cdcedff3 100644 --- a/dist/java/src/mp/code/BufferController.java +++ b/dist/java/src/mp/code/BufferController.java @@ -11,7 +11,8 @@ * Allows interaction with a CodeMP buffer, which in simple terms is a document * that multiple people can edit concurrently. *

- * It is generally safer to avoid storing this directly, see the api notes for {@link Workspace}. + * It is generally safer to avoid storing this directly, see the api notes for {@link Workspace}. + *

*/ public final class BufferController { private final long ptr; diff --git a/dist/java/src/mp/code/Client.java b/dist/java/src/mp/code/Client.java index cc415434..d1bfa3e6 100644 --- a/dist/java/src/mp/code/Client.java +++ b/dist/java/src/mp/code/Client.java @@ -1,12 +1,16 @@ package mp.code; import lombok.Getter; +import mp.code.exceptions.ControllerException; import mp.code.proto.Config; +import mp.code.proto.SessionEvent; import mp.code.proto.UserInfo; import mp.code.proto.WorkspaceIdentifier; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; +import java.util.function.Consumer; + /** * The main entrypoint of the library. * This is the only object you are expected to hold yourself; unlike all the others, @@ -196,6 +200,61 @@ public UserInfo getUserInfo(String user) throws ConnectionRemoteException { return get_user_info(this.ptr, user); } + private static native SessionEvent try_recv(long self) throws ControllerException; + + /** + * Tries to get a {@link SessionEvent} from the queue if any were present, null otherwise. + * @return the first session event in queue, if any are present + * @throws ControllerException if the controller was stopped + */ + public SessionEvent tryRecv() throws ControllerException { + return try_recv(this.ptr); + } + + private static native SessionEvent recv(long self) throws ControllerException; + + /** + * Blocks until a {@link SessionEvent} is available and returns it. + * @return the session event that occurred + * @throws ControllerException if the controller was stopped + */ + public SessionEvent recv() throws ControllerException { + return recv(this.ptr); + } + + private static native void callback(long self, Consumer cb); + + /** + * Registers a callback to be invoked whenever a {@link SessionEvent} occurs. + * This will not work unless a Java thread has been dedicated to the event loop. + * @param cb a {@link Consumer} that receives the controller when the change occurs; + * you should probably spawn a new thread in here, to avoid deadlocking + * @see Extensions#drive(boolean) + */ + public void callback(Consumer cb) { + callback(this.ptr, cb); + } + + private static native void clear_callback(long self); + + /** + * Clears the registered callback. + * @see #callback(Consumer) + */ + public void clearCallback() { + clear_callback(this.ptr); + } + + private static native void poll(long self) throws ControllerException; + + /** + * Blocks until a {@link SessionEvent} is available. + * @throws ControllerException if the controller was stopped + */ + public void poll() throws ControllerException { + poll(this.ptr); + } + private static native void refresh(long self) throws ConnectionRemoteException; /** diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index 3b927b8d..19dd03f5 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -10,7 +10,8 @@ /** * Allows interaction with the CodeMP cursor position tracking system. *

- * It is generally safer to avoid storing this directly, see the api notes for {@link Workspace}. + * It is generally safer to avoid storing this directly, see the api notes for {@link Workspace}. + *

*/ public final class CursorController { private final long ptr; @@ -34,7 +35,7 @@ public WorkspaceIdentifier workspaceId() { /** * Tries to get a {@link Cursor} update from the queue if any were present, null otherwise. - * @return the first cursor event in queue, if any are present + * @return the first cursor update in queue, if any are present * @throws ControllerException if the controller was stopped */ public Cursor tryRecv() throws ControllerException { diff --git a/dist/java/src/mp/code/proto/SessionEvent.java b/dist/java/src/mp/code/proto/SessionEvent.java new file mode 100644 index 00000000..f2b60747 --- /dev/null +++ b/dist/java/src/mp/code/proto/SessionEvent.java @@ -0,0 +1,26 @@ +package mp.code.proto; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class SessionEvent { + /** + * The underlying type of event, which will determine which fields are available. + * Always one of the constants from {@link SessionEventKind}. + */ + public final int kind; + + /** + * The user that joined or left, or null. + */ + public final String user; + + /** + * The {@link WorkspaceIdentifier} of the relevant workspace. + */ + public final WorkspaceIdentifier workspace; +} diff --git a/dist/java/src/mp/code/proto/SessionEventKind.java b/dist/java/src/mp/code/proto/SessionEventKind.java new file mode 100644 index 00000000..6e9756fe --- /dev/null +++ b/dist/java/src/mp/code/proto/SessionEventKind.java @@ -0,0 +1,30 @@ +package mp.code.proto; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** + * Represents the kind of session event. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class SessionEventKind { + /** + * Event that occurs when you get invited to a workspace. + */ + public static final int INVITATION_EVENT = 0; + + /** + * Event that occurs when a user quits a workspace. + */ + public static final int QUIT_EVENT = 1; + + /** + * Event that occurs when a user accepts an invitation to a workspace you are in. + */ + public static final int ACCEPT_EVENT = 2; + + /** + * Event that occurs when a user reject an invite. + */ + public static final int REJECT_EVENT = 3; +} diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java index ebe175fa..bd3d9de7 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEventKind.java +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -1,35 +1,39 @@ package mp.code.proto; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + /** * Represents the kind of workspace event. */ -public class WorkspaceEventKind { +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class WorkspaceEventKind { /** - * Somebody joined a workspace. + * Event that occurs when a user joins a workspace. */ public static final int USER_JOIN_WORKSPACE = 0; /** - * Somebody left a workspace. + * Event that occurs when a user leaves a workspace. */ public static final int USER_LEAVE_WORKSPACE = 1; /** - * A file was created. + * Event that occurs when a file is created in a workspace. */ public static final int FILE_CREATE = 2; /** - * A file was renamed. + * Event that occurs when a file is renamed in a workspace. */ public static final int FILE_RENAME = 3; /** - * A file was deleted. + * Event that occurs when a file is deleted in a workspace. */ public static final int FILE_DELETE = 4; /** - * Somebody joined a buffer. + * Event that occurs when a user joins a buffer. */ public static final int USER_JOIN_BUFFER = 5; /** - * Somebody left a buffer. + * Event that occurs when a user leaves a buffer. */ public static final int USER_LEAVE_BUFFER = 6; } diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 953df765..8ccbbdf1 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,9 +1,9 @@ use crate::{ Workspace, - api::Config, + api::{AsyncReceiver, Config}, client::Client, - errors::{ConnectionError, RemoteError}, - proto::{common::UserInfo, session::WorkspaceIdentifier} + errors::{ConnectionError, ControllerError, RemoteError}, + proto::{common::UserInfo, session::{SessionEvent, WorkspaceIdentifier}} }; use jni_toolbox::jni; @@ -97,6 +97,69 @@ fn get_user_info(client: &mut Client, user: String) -> Result Result, ControllerError> { + super::tokio().block_on(client.try_recv()) +} + +/// Block until it receives a [TextChange]. +#[jni(package = "mp.code", class = "Client")] +fn recv(client: &mut Client) -> Result { + super::tokio().block_on(client.recv()) +} + +/// Register a callback for client changes. +#[jni(package = "mp.code", class = "Client")] +fn callback<'local>( + env: &mut jni::Env<'local>, + client: &mut Client, + cb: jni::objects::JObject<'local>, +) -> Result<(), jni::errors::Error> { + if cb.is_null() { + return Err(jni::errors::Error::NullPtr("null pointer to buffer callback")); + } + + let cb_ref = env.new_global_ref(cb)?; + let jvm = env.get_java_vm()?; + + client.callback(move |controller: Client| { + let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { + env.with_local_frame(5, |env| { + use jni_toolbox::IntoJavaObject; + let jclient = controller.into_java_object(env)?; + env.call_method( + &cb_ref, + jni::jni_str!("accept"), + jni::jni_sig!((event: java.lang.Object) -> ()), + &[jni::objects::JValue::Object(&jclient)], + )?; + Ok::<(), jni::errors::Error>(()) + })?; + + Ok(()) + }); + + if let Err(e) = result { + tracing::error!("error invoking client callback: {e}"); + } + }); + + Ok(()) +} + +/// Clear the callback for client changes. +#[jni(package = "mp.code", class = "Client")] +fn clear_callback(client: &mut Client) { + client.clear_callback() +} + +/// Block until there is a new value available. +#[jni(package = "mp.code", class = "Client")] +fn poll(client: &mut Client) -> Result<(), ControllerError> { + super::tokio().block_on(client.poll()) +} + /// Refresh the client's session token. #[jni(package = "mp.code", class = "Client")] fn refresh(client: &mut Client) -> Result<(), RemoteError> { From 3761b0cdaeb49a54de9325676881fce60fccc08b Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Mar 2026 20:54:46 +0100 Subject: [PATCH 090/121] fix(java): enum kinds are 1 indexed --- dist/java/src/mp/code/proto/SessionEventKind.java | 8 ++++---- .../java/src/mp/code/proto/WorkspaceEventKind.java | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dist/java/src/mp/code/proto/SessionEventKind.java b/dist/java/src/mp/code/proto/SessionEventKind.java index 6e9756fe..8b99e435 100644 --- a/dist/java/src/mp/code/proto/SessionEventKind.java +++ b/dist/java/src/mp/code/proto/SessionEventKind.java @@ -11,20 +11,20 @@ public class SessionEventKind { /** * Event that occurs when you get invited to a workspace. */ - public static final int INVITATION_EVENT = 0; + public static final int INVITATION_EVENT = 1; /** * Event that occurs when a user quits a workspace. */ - public static final int QUIT_EVENT = 1; + public static final int QUIT_EVENT = 2; /** * Event that occurs when a user accepts an invitation to a workspace you are in. */ - public static final int ACCEPT_EVENT = 2; + public static final int ACCEPT_EVENT = 3; /** * Event that occurs when a user reject an invite. */ - public static final int REJECT_EVENT = 3; + public static final int REJECT_EVENT = 4; } diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java index bd3d9de7..1412918a 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEventKind.java +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -11,29 +11,29 @@ public final class WorkspaceEventKind { /** * Event that occurs when a user joins a workspace. */ - public static final int USER_JOIN_WORKSPACE = 0; + public static final int USER_JOIN_WORKSPACE = 1; /** * Event that occurs when a user leaves a workspace. */ - public static final int USER_LEAVE_WORKSPACE = 1; + public static final int USER_LEAVE_WORKSPACE = 2; /** * Event that occurs when a file is created in a workspace. */ - public static final int FILE_CREATE = 2; + public static final int FILE_CREATE = 3; /** * Event that occurs when a file is renamed in a workspace. */ - public static final int FILE_RENAME = 3; + public static final int FILE_RENAME = 4; /** * Event that occurs when a file is deleted in a workspace. */ - public static final int FILE_DELETE = 4; + public static final int FILE_DELETE = 5; /** * Event that occurs when a user joins a buffer. */ - public static final int USER_JOIN_BUFFER = 5; + public static final int USER_JOIN_BUFFER = 6; /** * Event that occurs when a user leaves a buffer. */ - public static final int USER_LEAVE_BUFFER = 6; + public static final int USER_LEAVE_BUFFER = 7; } From 564810f315ac3a78109342bbc868c421e54d6a92 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 22:51:08 +0100 Subject: [PATCH 091/121] fix(js): ty names are important, don't do renames --- dist/js/package.json | 6 +++--- src/ffi/js/buffer.rs | 20 +++++++++++--------- src/ffi/js/client.rs | 25 ++++++++++++++----------- src/ffi/js/cursor.rs | 18 ++++++++++-------- src/ffi/js/workspace.rs | 29 ++++++++++++++++------------- 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/dist/js/package.json b/dist/js/package.json index db4ea0fa..7ef0b2bd 100644 --- a/dist/js/package.json +++ b/dist/js/package.json @@ -21,11 +21,11 @@ ], "repository": "https://github.com/hexedtech/codemp", "devDependencies": { - "@napi-rs/cli": "^2.18.4" + "@napi-rs/cli": "^3.5.1" }, "napi": { - "name": "codemp", - "triples": { + "binaryName": "codemp", + "targets": { "defaults": false, "additional": [ "x86_64-unknown-linux-gnu", diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 3c8134d0..44234930 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,10 +1,12 @@ -use crate::api::controller::{AsyncReceiver, AsyncSender}; -use crate::prelude::*; +use crate::api::{BufferUpdate, TextChange, controller::{AsyncReceiver, AsyncSender}}; +use codemp_proto::session::WorkspaceIdentifier; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; +use crate::buffer::controller::BufferController; + #[napi] -impl CodempBufferController { +impl BufferController { /// Register a callback to be invoked every time a new event is available to consume /// There can only be one callback registered at any given time. #[napi( @@ -13,9 +15,9 @@ impl CodempBufferController { )] pub fn js_callback( &self, - fun: ThreadsafeFunction, + fun: ThreadsafeFunction, ) -> napi::Result<()> { - self.callback(move |controller: CodempBufferController| { + self.callback(move |controller: BufferController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this @@ -50,19 +52,19 @@ impl CodempBufferController { /// Return next buffer event if present #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { + pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } /// Wait for next buffer event and return it #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } /// Send a buffer update to workspace #[napi(js_name = "send")] - pub fn js_send(&self, op: CodempTextChange) -> napi::Result<()> { + pub fn js_send(&self, op: TextChange) -> napi::Result<()> { Ok(self.send(op)?) } @@ -74,7 +76,7 @@ impl CodempBufferController { /// Get id of workspace containing this controller. #[napi(js_name = "workspaceId")] - pub fn js_workspace_id(&self) -> CodempWorkspaceIdentifier { + pub fn js_workspace_id(&self) -> WorkspaceIdentifier { self.workspace_id().clone() } } diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index d7596049..91cafa3c 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,14 +1,17 @@ -use crate::prelude::*; +use codemp_proto::{common::UserInfo, session::WorkspaceIdentifier}; use napi_derive::napi; + #[napi] /// connect to codemp servers and return a client session pub async fn connect(config: crate::api::Config) -> napi::Result { Ok(crate::Client::connect(config).await?) } +use crate::{Client, Workspace}; + #[napi] -impl CodempClient { +impl Client { #[napi(js_name = "createWorkspace")] /// create workspace with given id, if able to pub async fn js_create_workspace(&self, workspace: String) -> napi::Result<()> { @@ -23,13 +26,13 @@ impl CodempClient { #[napi(js_name = "fetchOwnedWorkspaces")] /// fetch owned workspaces - pub async fn js_fetch_owned_workspaces(&self) -> napi::Result> { + pub async fn js_fetch_owned_workspaces(&self) -> napi::Result> { Ok(self.fetch_owned_workspaces().await?) } #[napi(js_name = "fetchJoinedWorkspaces")] /// fetch joined workspaces - pub async fn js_fetch_joined_workspaces(&self) -> napi::Result> { + pub async fn js_fetch_joined_workspaces(&self) -> napi::Result> { Ok(self.fetch_joined_workspaces().await?) } @@ -45,7 +48,7 @@ impl CodempClient { #[napi(js_name = "attachWorkspace")] /// join workspace with given id (will start its cursor controller) - pub async fn js_attach_workspace(&self, user: String, workspace: String) -> napi::Result { + pub async fn js_attach_workspace(&self, user: String, workspace: String) -> napi::Result { Ok(self.attach_workspace(&user, &workspace).await?) } @@ -57,19 +60,19 @@ impl CodempClient { #[napi(js_name = "getWorkspace")] /// get workspace with given id, if it exists - pub fn js_get_workspace(&self, user: String, workspace: String) -> Option { + pub fn js_get_workspace(&self, user: String, workspace: String) -> Option { self.get_workspace(&user, &workspace) } #[napi(js_name = "currentUser")] /// return current sessions's user id - pub fn js_current_user(&self) -> CodempUserInfo { - self.current_user().clone().into() + pub fn js_current_user(&self) -> UserInfo { + self.current_user().clone() } #[napi(js_name = "activeWorkspaces")] /// get list of all active workspaces - pub fn js_active_workspaces(&self) -> Vec { + pub fn js_active_workspaces(&self) -> Vec { self.active_workspaces() } @@ -87,8 +90,8 @@ impl CodempClient { /// Get the meta information for a user #[napi(js_name = "getUserInfo")] - pub async fn js_get_user_info(&self, user: String) -> napi::Result { - Ok(self.get_user_info(&user).await?.into()) + pub async fn js_get_user_info(&self, user: String) -> napi::Result { + Ok(self.get_user_info(&user).await?) } /// Quit a joined workspace. Cannot quit owned workspaces: must delete them diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 521ba130..09f25b2f 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,9 +1,11 @@ -use crate::prelude::*; +use codemp_proto::{cursor::{CursorEvent, CursorUpdate}, session::WorkspaceIdentifier}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; +use crate::{api::{AsyncReceiver, AsyncSender}, cursor::controller::CursorController}; + #[napi] -impl CodempCursorController { +impl CursorController { /// Register a callback to be called on receive. /// There can only be one callback registered at any given time. #[napi( @@ -12,9 +14,9 @@ impl CodempCursorController { )] pub fn js_callback( &self, - fun: ThreadsafeFunction, + fun: ThreadsafeFunction, ) -> napi::Result<()> { - self.callback(move |controller: CodempCursorController| { + self.callback(move |controller: CursorController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this @@ -31,25 +33,25 @@ impl CodempCursorController { /// Send a new cursor event to remote #[napi(js_name = "send")] - pub fn js_send(&self, sel: CodempCursorUpdate) -> napi::Result<()> { + pub fn js_send(&self, sel: CursorUpdate) -> napi::Result<()> { Ok(self.send(sel)?) } /// Get next cursor event if available without blocking #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { + pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } /// Block until next #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } /// Get id of workspace containing this controller. #[napi(js_name = "workspaceId")] - pub fn js_workspace_id(&self) -> CodempWorkspaceIdentifier { + pub fn js_workspace_id(&self) -> WorkspaceIdentifier { self.workspace_id().clone() } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index 8c289da2..727b699c 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,12 +1,15 @@ -use crate::prelude::*; +use codemp_proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; +use crate::{api::AsyncReceiver, buffer::controller::BufferController, cursor::controller::CursorController}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; +use crate::Workspace; + #[napi] -impl CodempWorkspace { +impl Workspace { /// Get the unique workspace id #[napi(js_name = "id")] - pub fn js_id(&self) -> CodempWorkspaceIdentifier { + pub fn js_id(&self) -> WorkspaceIdentifier { self.id().clone() } @@ -18,7 +21,7 @@ impl CodempWorkspace { /// List all user names currently in this workspace #[napi(js_name = "userList")] - pub fn js_user_list(&self) -> Vec { + pub fn js_user_list(&self) -> Vec { self.user_list() } @@ -30,13 +33,13 @@ impl CodempWorkspace { /// Get workspace's Cursor Controller #[napi(js_name = "cursor")] - pub fn js_cursor(&self) -> CodempCursorController { + pub fn js_cursor(&self) -> CursorController { self.cursor() } /// Get a buffer controller by its name (path) #[napi(js_name = "getBuffer")] - pub fn js_get_buffer(&self, path: String) -> Option { + pub fn js_get_buffer(&self, path: String) -> Option { self.get_buffer(&path) } @@ -48,7 +51,7 @@ impl CodempWorkspace { /// Attach to a workspace buffer, starting a BufferController #[napi(js_name = "attachBuffer")] - pub async fn js_attach_buffer(&self, path: String) -> napi::Result { + pub async fn js_attach_buffer(&self, path: String) -> napi::Result { Ok(self.attach_buffer(&path).await?) } @@ -59,12 +62,12 @@ impl CodempWorkspace { } #[napi(js_name = "recv")] - pub async fn js_recv(&self) -> napi::Result { + pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } #[napi(js_name = "tryRecv")] - pub async fn js_try_recv(&self) -> napi::Result> { + pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } @@ -81,9 +84,9 @@ impl CodempWorkspace { } #[napi(js_name = "callback", ts_args_type = "fun: (event: Workspace) => void")] - pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { - let tsfn: ThreadsafeFunction = fun; - self.callback(move |controller: CodempWorkspace| { + pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun; + self.callback(move |controller: Workspace| { tsfn.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); @@ -121,7 +124,7 @@ impl CodempWorkspace { /// Get all users currently attached to specified buffer #[napi(js_name = "bufferUserList")] - pub fn js_buffer_user_list(&self, path: String) -> Vec { + pub fn js_buffer_user_list(&self, path: String) -> Vec { self.buffer_user_list(&path) } From abcc53a449bb3bd16820b7254b69861dcd0fe60f Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 22:51:27 +0100 Subject: [PATCH 092/121] docs(lua): split annotations (for lsp) and enums (for runtime) --- dist/lua/annotations.lua | 19 ------------------- dist/lua/enums.lua | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 dist/lua/enums.lua diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 085f22b2..c484be91 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -318,14 +318,6 @@ function Client:get_workspace(user, ws) end ---get full user info for given username from server function Client:get_user_info(user) end ----all enum types for session events -SessionEventKind = { - InvitationEvent = 1, - QuitEvent = 2, - AcceptEvent = 3, - RejectEvent = 4, -} - ---@class (exact) SessionEvent ---@field kind integer (SessionEventKind) event kind ---@field user string the user that created this event (sent invitation, rejected invite...) @@ -468,17 +460,6 @@ function Workspace:fetch_users(path) end ---fetch the list of users in the given buffer function Workspace:fetch_buffer_users(path) end ----all enum types for workspace events -WorkspaceEventKind = { - UserJoinWorkspace = 1, - UserLeaveWorkspace = 2, - FileCreate = 3, - FileRename = 4, - FileDelete = 5, - UserJoinBuffer = 6, - UserLeaveBuffer = 7, -} - ---@class (exact) WorkspaceEvent ---@field kind integer (WorkspaceEventKind) event kind ---@field user string? the user that joined/left (possibly a buffer) diff --git a/dist/lua/enums.lua b/dist/lua/enums.lua new file mode 100644 index 00000000..83c2ccd9 --- /dev/null +++ b/dist/lua/enums.lua @@ -0,0 +1,24 @@ + +---all enum types for session events +local SessionEventKind = { + InvitationEvent = 1, + QuitEvent = 2, + AcceptEvent = 3, + RejectEvent = 4, +} + +---all enum types for workspace events +local WorkspaceEventKind = { + UserJoinWorkspace = 1, + UserLeaveWorkspace = 2, + FileCreate = 3, + FileRename = 4, + FileDelete = 5, + UserJoinBuffer = 6, + UserLeaveBuffer = 7, +} + +return { + SessionEventKind = SessionEventKind, + WorkspaceEventKind = WorkspaceEventKind, +} From 4e606eddac8029b44f9c2de8a67fd21fb3f36608 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 22:52:00 +0100 Subject: [PATCH 093/121] fix(lua): None is nil --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 40054e07..cfec872d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "eda0969e86d0c592688c692a052b5659099277ae", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "fb7acdb88364bd8932d5c975983ad9595fc85c4b", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From 3af5034bdebae65b3c4901d48085082f7163bac8 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 16 Mar 2026 22:52:14 +0100 Subject: [PATCH 094/121] ci(js): update publish workflow probably needs more work --- .github/workflows/javascript.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index c1436d5a..e0da451f 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -32,7 +32,7 @@ jobs: node-version: '20' - run: npm install working-directory: dist/js - - run: npx napi build --cargo-cwd=../.. --platform --release --features=js --strip + - run: npx napi build --manifest-path=../../Cargo.toml --output-dir=. --platform --release --features=js --strip working-directory: dist/js - uses: actions/upload-artifact@v4 with: @@ -57,7 +57,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm install working-directory: dist/js - - run: npx napi build --cargo-cwd=../.. --platform --features=js + - run: npx napi build --manifest-path=../../Cargo.toml --output-dir=. --platform --release --features=js --strip working-directory: dist/js - run: rm *.node working-directory: dist/js From 91eec309eee809fe314de7099c15c2eec5b45d0e Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 00:05:21 +0100 Subject: [PATCH 095/121] chore(java): cleanup enum leftovers, bump jni-toolbox --- Cargo.lock | 11 ++++++----- Cargo.toml | 6 +++--- src/ffi/java/mod.rs | 43 ------------------------------------------- 3 files changed, 9 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ff1ea7f..813c2181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=eda0969e86d0c592688c692a052b5659099277ae#eda0969e86d0c592688c692a052b5659099277ae" +source = "git+https://github.com/hexedtech/codemp-proto?rev=c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7#c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7" dependencies = [ "jni", "jni-toolbox", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "jni-toolbox" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c#9b54289d562f3ba0dfc59ab38342e7502aeb529c" dependencies = [ "jni", "jni-toolbox-macro", @@ -818,8 +818,9 @@ dependencies = [ [[package]] name = "jni-toolbox-macro" version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c#9b54289d562f3ba0dfc59ab38342e7502aeb529c" dependencies = [ + "convert_case", "proc-macro2", "quote", "syn 2.0.117", @@ -964,9 +965,9 @@ dependencies = [ [[package]] name = "mlua-serde-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c68d667cdb8a4106f37e339746d046d670f846ce18cc69e9d3319a8e509241f" +checksum = "c84dc28a39ade336b9c7e078121ba5cb95b2ebb55b3e77ac432f1bc5eacfb68a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index cfec872d..033f9dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "fb7acdb88364bd8932d5c975983ad9595fc85c4b", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api @@ -47,11 +47,11 @@ tracing-subscriber = { version = "0.3", optional = true } # glue (java) jni = { version = "0.22", features = ["invocation"], optional = true } #jni-toolbox = { version = "0.2", optional = true, features = ["uuid"] } -jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "c40e925510fd563763db5dec4df300615453054b", optional = true, features = ["uuid", "unsigned"] } +jni-toolbox = { git = "https://github.com/hexedtech/jni-toolbox", rev = "9b54289d562f3ba0dfc59ab38342e7502aeb529c", optional = true, features = ["uuid", "unsigned"] } # glue (lua) mlua = { version = "0.11", features = ["module", "serialize", "error-send"], optional = true } -mlua-serde-derive = { version = "0.1", optional = true } +mlua-serde-derive = { version = "0.1.1", optional = true } # glue (js) napi = { version = "3.1", features = ["full"], optional = true } diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 48d833d3..cb8807b8 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -144,46 +144,3 @@ into_java_ptr_class!(crate::Client, "mp/code/Client"); into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); - -/* TODO: reminder to come up with a way to make fancy enums in java -// #[allow(unsafe_code)] -impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::proto::workspace::WorkspaceEventKind { // TODO - const CLASS: &'static str = "mp/code/Workspace$Event"; - fn into_java_object( - self, - env: &mut jni::Env<'j>, - ) -> Result, jni::errors::Error> { - let (ordinal, user, buffer) = match self { - crate::api::Event::UserJoin { name } => (0, Some(name), None), - crate::api::Event::UserLeave { name } => (1, Some(name), None), - crate::api::Event::FileTreeUpdated { path } => (2, None, Some(path)), - crate::api::Event::UserJoinBuffer { name, buffer } => (3, Some(name), Some(buffer)), - crate::api::Event::UserLeaveBuffer { name, buffer } => (4, Some(name), Some(buffer)), - }; - - let type_class = env.find_class(jni::jni_str!("mp/code/Workspace$Event$Type"))?; - let variants = env - .call_method(type_class, jni::jni_str!("getEnumConstants"), jni::jni_sig!("()[Ljava/lang/Object;"), &[])? - .l()?; - let variants_array = jni::objects::JObjectArray::::cast_local(env, variants)?; - let event_type = variants_array.get_element(env, ordinal)?; - - let class_name = jni::strings::JNIString::new(Self::CLASS); - let event_class = env.find_class(class_name)?; - - let j_event_type = event_type.into_java_object(env)?; - let j_user = user.into_java_object(env)?; - let j_buffer = buffer.into_java_object(env)?; - - env.new_object( - event_class, - jni::jni_sig!("(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V"), - &[ - jni::JValue::Object(&j_event_type), - jni::JValue::Object(&j_user), - jni::JValue::Object(&j_buffer) - ] - ) - } -} -*/ From 8517744bbea079b86b96e8616c074d74126d6d59 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 00:14:27 +0100 Subject: [PATCH 096/121] docs(java): add missing javadocs --- dist/java/src/mp/code/Workspace.java | 1 + dist/java/src/mp/code/proto/SessionEvent.java | 3 +++ dist/java/src/mp/code/proto/WorkspaceEvent.java | 3 +++ 3 files changed, 7 insertions(+) diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 9b84e9b9..57898187 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -186,6 +186,7 @@ public void fetchBufferUsers(String path) throws ConnectionRemoteException { * The user must be attached to the buffer to perform this operation. * You can force-update the list with {@link #fetchBufferUsers(String)}. * @param path the path of the buffer to search + * @return the local list of users attached to the given buffer */ public UserInfo[] bufferUserList(String path) { return buffer_user_list(this.ptr, path); diff --git a/dist/java/src/mp/code/proto/SessionEvent.java b/dist/java/src/mp/code/proto/SessionEvent.java index f2b60747..4bf0c92f 100644 --- a/dist/java/src/mp/code/proto/SessionEvent.java +++ b/dist/java/src/mp/code/proto/SessionEvent.java @@ -4,6 +4,9 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; +/** + * An event concerning the user session. + */ @ToString @EqualsAndHashCode @RequiredArgsConstructor diff --git a/dist/java/src/mp/code/proto/WorkspaceEvent.java b/dist/java/src/mp/code/proto/WorkspaceEvent.java index 1a8a869d..0c94832c 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEvent.java +++ b/dist/java/src/mp/code/proto/WorkspaceEvent.java @@ -4,6 +4,9 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; +/** + * An event concerning workspaces. + */ @ToString @EqualsAndHashCode @RequiredArgsConstructor From 446725de13724bc0adac309e7635d47f6580ba40 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 17 Mar 2026 00:33:00 +0100 Subject: [PATCH 097/121] fix(js): callback receives error too --- dist/js/package.json | 2 +- src/ffi/js/buffer.rs | 2 +- src/ffi/js/cursor.rs | 2 +- src/ffi/js/workspace.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/js/package.json b/dist/js/package.json index 7ef0b2bd..730b418b 100644 --- a/dist/js/package.json +++ b/dist/js/package.json @@ -25,7 +25,7 @@ }, "napi": { "binaryName": "codemp", - "targets": { + "triplets": { "defaults": false, "additional": [ "x86_64-unknown-linux-gnu", diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 44234930..b19555d0 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -11,7 +11,7 @@ impl BufferController { /// There can only be one callback registered at any given time. #[napi( js_name = "callback", - ts_args_type = "fun: (event: BufferController) => void" + ts_args_type = "fun: (err: Error|null, event: BufferController) => void" )] pub fn js_callback( &self, diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 09f25b2f..bf2e21f9 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -10,7 +10,7 @@ impl CursorController { /// There can only be one callback registered at any given time. #[napi( js_name = "callback", - ts_args_type = "fun: (event: CursorController) => void" + ts_args_type = "fun: (err: Error|null, event: CursorController) => void" )] pub fn js_callback( &self, diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index 727b699c..a3fd1f53 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -83,7 +83,7 @@ impl Workspace { Ok(()) } - #[napi(js_name = "callback", ts_args_type = "fun: (event: Workspace) => void")] + #[napi(js_name = "callback", ts_args_type = "fun: (err: Error|null, event: Workspace) => void")] pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { let tsfn: ThreadsafeFunction = fun; self.callback(move |controller: Workspace| { From a10dcf2dcce487980db7ded0f1eb0945fcc4367e Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 17 Mar 2026 00:36:16 +0100 Subject: [PATCH 098/121] chore: bump proto --- Cargo.lock | 29 +++++++++++++++++++++++++---- Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 813c2181..6ebd5882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ dependencies = [ "dashmap", "diamond-types", "jni", - "jni-toolbox", + "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c)", "mlua", "mlua-serde-derive", "napi", @@ -188,10 +188,10 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7#c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7" +source = "git+https://github.com/hexedtech/codemp-proto?rev=fb7acdb88364bd8932d5c975983ad9595fc85c4b#fb7acdb88364bd8932d5c975983ad9595fc85c4b" dependencies = [ "jni", - "jni-toolbox", + "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", "mlua", "mlua-serde-derive", "napi", @@ -810,7 +810,18 @@ version = "0.2.2" source = "git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c#9b54289d562f3ba0dfc59ab38342e7502aeb529c" dependencies = [ "jni", - "jni-toolbox-macro", + "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c)", + "thiserror", + "uuid", +] + +[[package]] +name = "jni-toolbox" +version = "0.2.2" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +dependencies = [ + "jni", + "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", "thiserror", "uuid", ] @@ -826,6 +837,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jni-toolbox-macro" +version = "0.2.2" +source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "js-sys" version = "0.3.91" diff --git a/Cargo.toml b/Cargo.toml index 033f9dc8..72e1a731 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "fb7acdb88364bd8932d5c975983ad9595fc85c4b", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From ae2e02db8854df4d52fb8dc5a7f571e3dee1c882 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 03:16:47 +0100 Subject: [PATCH 099/121] chore(java): aligned signatures to rust side --- dist/java/src/mp/code/CursorController.java | 35 +++++++++--------- dist/java/src/mp/code/Workspace.java | 14 +++---- dist/java/src/mp/code/proto/Config.java | 3 +- dist/java/src/mp/code/proto/CursorEvent.java | 2 +- .../src/mp/code/proto/CursorPosition.java | 23 ++++++++++++ .../proto/{Cursor.java => CursorUpdate.java} | 4 +- dist/java/src/mp/code/proto/RowCol.java | 23 ++++++++++++ dist/java/src/mp/code/proto/Selection.java | 37 ------------------- 8 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 dist/java/src/mp/code/proto/CursorPosition.java rename dist/java/src/mp/code/proto/{Cursor.java => CursorUpdate.java} (84%) create mode 100644 dist/java/src/mp/code/proto/RowCol.java delete mode 100644 dist/java/src/mp/code/proto/Selection.java diff --git a/dist/java/src/mp/code/CursorController.java b/dist/java/src/mp/code/CursorController.java index 19dd03f5..f9aab761 100644 --- a/dist/java/src/mp/code/CursorController.java +++ b/dist/java/src/mp/code/CursorController.java @@ -1,7 +1,8 @@ package mp.code; -import mp.code.proto.Cursor; -import mp.code.proto.Selection; +import mp.code.proto.CursorUpdate; +import mp.code.proto.CursorEvent; +import mp.code.proto.CursorPosition; import mp.code.proto.WorkspaceIdentifier; import mp.code.exceptions.ControllerException; @@ -31,43 +32,43 @@ public WorkspaceIdentifier workspaceId() { return workspace_id(this.ptr); } - private static native Cursor try_recv(long self) throws ControllerException; + private static native CursorEvent try_recv(long self) throws ControllerException; /** - * Tries to get a {@link Cursor} update from the queue if any were present, null otherwise. - * @return the first cursor update in queue, if any are present + * Tries to get a {@link CursorEvent} from the queue if any were present, null otherwise. + * @return the first cursor event in queue, if any are present * @throws ControllerException if the controller was stopped */ - public Cursor tryRecv() throws ControllerException { + public CursorEvent tryRecv() throws ControllerException { return try_recv(this.ptr); } - private static native Cursor recv(long self) throws ControllerException; + private static native CursorUpdate recv(long self) throws ControllerException; /** - * Blocks until a {@link Cursor} update is available and returns it. - * @return the cursor update that occurred + * Blocks until a {@link CursorEvent} is available and returns it. + * @return the cursor event that occurred * @throws ControllerException if the controller was stopped */ - public Cursor recv() throws ControllerException { + public CursorUpdate recv() throws ControllerException { return recv(this.ptr); } - private static native void send(long self, Selection selection) throws ControllerException; + private static native void send(long self, CursorUpdate selection) throws ControllerException; /** - * Tries to send a {@link Selection} update. - * @param selection the update to send + * Tries to send a {@link CursorPosition} update. + * @param update the update to send * @throws ControllerException if the controller was stopped */ - public void send(Selection selection) throws ControllerException { - send(this.ptr, selection); + public void send(CursorUpdate update) throws ControllerException { + send(this.ptr, update); } private static native void callback(long self, Consumer cb); /** - * Registers a callback to be invoked whenever a {@link Cursor} update occurs. + * Registers a callback to be invoked whenever a {@link CursorUpdate} update occurs. * This will not work unless a Java thread has been dedicated to the event loop. * @param cb a {@link Consumer} that receives the controller when the change occurs; * you should probably spawn a new thread in here, to avoid deadlocking @@ -90,7 +91,7 @@ public void clearCallback() { private static native void poll(long self) throws ControllerException; /** - * Blocks until a {@link Cursor} update is available. + * Blocks until a {@link CursorUpdate} update is available. * @throws ControllerException if the controller was stopped */ public void poll() throws ControllerException { diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 57898187..f87fd601 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -1,6 +1,5 @@ package mp.code; -import java.util.Optional; import java.util.function.Consumer; import mp.code.proto.UserInfo; @@ -8,6 +7,7 @@ import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; import mp.code.proto.WorkspaceEvent; +import mp.code.proto.WorkspaceIdentifier; /** * Represents a CodeMP workspace, which broadly speaking is a collection @@ -25,13 +25,13 @@ public final class Workspace { Extensions.CLEANER.register(this, () -> free(ptr)); } - private static native String id(long self); + private static native WorkspaceIdentifier id(long self); /** * Gets the unique identifier of the current workspace. - * @return the identifier + * @return the {@link WorkspaceIdentifier} for this workspace */ - public String id() { + public WorkspaceIdentifier id() { return id(this.ptr); } @@ -51,10 +51,10 @@ public CursorController cursor() { * Looks for a {@link BufferController} with the given path within the * current workspace and returns it if it exists. * @param path the current path - * @return the {@link BufferController} with the given path, if it exists + * @return the {@link BufferController} with the given path, if it exists, null otherwise */ - public Optional getBuffer(String path) { - return Optional.ofNullable(get_buffer(this.ptr, path)); + public BufferController getBuffer(String path) { + return get_buffer(this.ptr, path); } private static native String[] search_buffers(long self, String filter); diff --git a/dist/java/src/mp/code/proto/Config.java b/dist/java/src/mp/code/proto/Config.java index 596670c4..465ca669 100644 --- a/dist/java/src/mp/code/proto/Config.java +++ b/dist/java/src/mp/code/proto/Config.java @@ -1,6 +1,5 @@ package mp.code.proto; -import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; @@ -10,7 +9,7 @@ */ @ToString @EqualsAndHashCode -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@RequiredArgsConstructor public class Config { /** The username to connect with. */ public final String username; diff --git a/dist/java/src/mp/code/proto/CursorEvent.java b/dist/java/src/mp/code/proto/CursorEvent.java index 0023d73b..893a96f6 100644 --- a/dist/java/src/mp/code/proto/CursorEvent.java +++ b/dist/java/src/mp/code/proto/CursorEvent.java @@ -19,5 +19,5 @@ public class CursorEvent { /** * The cursor position data. */ - public final Cursor cursor; + public final CursorUpdate position; } diff --git a/dist/java/src/mp/code/proto/CursorPosition.java b/dist/java/src/mp/code/proto/CursorPosition.java new file mode 100644 index 00000000..67b1db13 --- /dev/null +++ b/dist/java/src/mp/code/proto/CursorPosition.java @@ -0,0 +1,23 @@ +package mp.code.proto; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * A data class holding information about a cursor selection. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class CursorPosition { + /** + * The start of the cursor's position. + */ + public final RowCol start; + + /** + * The end of the cursor's position. + */ + public final RowCol end; +} diff --git a/dist/java/src/mp/code/proto/Cursor.java b/dist/java/src/mp/code/proto/CursorUpdate.java similarity index 84% rename from dist/java/src/mp/code/proto/Cursor.java rename to dist/java/src/mp/code/proto/CursorUpdate.java index c5588033..77c00799 100644 --- a/dist/java/src/mp/code/proto/Cursor.java +++ b/dist/java/src/mp/code/proto/CursorUpdate.java @@ -10,7 +10,7 @@ @ToString @EqualsAndHashCode @RequiredArgsConstructor -public class Cursor { +public class CursorUpdate { /** * The buffer the cursor is on. */ @@ -19,5 +19,5 @@ public class Cursor { /** * The associated selection updates. */ - public final Selection[] selection; + public final CursorPosition[] cursors; } diff --git a/dist/java/src/mp/code/proto/RowCol.java b/dist/java/src/mp/code/proto/RowCol.java new file mode 100644 index 00000000..106150e7 --- /dev/null +++ b/dist/java/src/mp/code/proto/RowCol.java @@ -0,0 +1,23 @@ +package mp.code.proto; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * A data class representing a position in a buffer. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class RowCol { + /** + * The row. If negative, it is clamped to 0. + */ + public final int row; + + /** + * The column. If negative, it is clamped to 0. + */ + public final int col; +} diff --git a/dist/java/src/mp/code/proto/Selection.java b/dist/java/src/mp/code/proto/Selection.java deleted file mode 100644 index 2ddb5642..00000000 --- a/dist/java/src/mp/code/proto/Selection.java +++ /dev/null @@ -1,37 +0,0 @@ -package mp.code.proto; - -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; -import lombok.ToString; - -/** - * A data class holding information about a cursor selection. - */ -@ToString -@EqualsAndHashCode -@RequiredArgsConstructor -public class Selection { - /** - * The starting row of the cursor position. - * If negative, it is clamped to 0. - */ - public final int startRow; - - /** - * The starting column of the cursor position. - * If negative, it is clamped to 0. - */ - public final int startCol; - - /** - * The ending row of the cursor position. - * If negative, it is clamped to 0. - */ - public final int endRow; - - /** - * The ending column of the cursor position. - * If negative, it is clamped to 0. - */ - public final int endCol; -} From 43725bd846e02f71154cc9540d5230ae6f2c9e2a Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 03:21:31 +0100 Subject: [PATCH 100/121] chore: re-bump the proto --- Cargo.lock | 29 ++++------------------------- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ebd5882..813c2181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,7 @@ dependencies = [ "dashmap", "diamond-types", "jni", - "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c)", + "jni-toolbox", "mlua", "mlua-serde-derive", "napi", @@ -188,10 +188,10 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=fb7acdb88364bd8932d5c975983ad9595fc85c4b#fb7acdb88364bd8932d5c975983ad9595fc85c4b" +source = "git+https://github.com/hexedtech/codemp-proto?rev=c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7#c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7" dependencies = [ "jni", - "jni-toolbox 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", + "jni-toolbox", "mlua", "mlua-serde-derive", "napi", @@ -810,18 +810,7 @@ version = "0.2.2" source = "git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c#9b54289d562f3ba0dfc59ab38342e7502aeb529c" dependencies = [ "jni", - "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=9b54289d562f3ba0dfc59ab38342e7502aeb529c)", - "thiserror", - "uuid", -] - -[[package]] -name = "jni-toolbox" -version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" -dependencies = [ - "jni", - "jni-toolbox-macro 0.2.2 (git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b)", + "jni-toolbox-macro", "thiserror", "uuid", ] @@ -837,16 +826,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-toolbox-macro" -version = "0.2.2" -source = "git+https://github.com/hexedtech/jni-toolbox?rev=c40e925510fd563763db5dec4df300615453054b#c40e925510fd563763db5dec4df300615453054b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "js-sys" version = "0.3.91" diff --git a/Cargo.toml b/Cargo.toml index 72e1a731..033f9dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "fb7acdb88364bd8932d5c975983ad9595fc85c4b", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api From 955197f4bbb96fde49e27037fe883b5a99a9e4be Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 03:45:32 +0100 Subject: [PATCH 101/121] fix: fix reference to WorkspaceIdentifier in e2e test --- src/tests/e2e/client.rs | 7 +++++-- src/tests/e2e/server.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tests/e2e/client.rs b/src/tests/e2e/client.rs index 73db628c..d943eabe 100644 --- a/src/tests/e2e/client.rs +++ b/src/tests/e2e/client.rs @@ -12,7 +12,10 @@ async fn test_workspace_creation_and_deletion() { client.create_workspace(workspace_name.clone()).await?; - let wsid = crate::api::WorkspaceIdentifier { user: client.current_user().name.clone(), workspace: workspace_name.clone() }; + let wsid = crate::proto::session::WorkspaceIdentifier { + user: client.current_user().name.clone(), + workspace: workspace_name.clone() + }; // we can't error, so we return empty vec which will be interpreted as err let workspace_list_before = client.fetch_owned_workspaces().await.unwrap_or_default(); @@ -77,7 +80,7 @@ async fn test_invite_user_to_workspace() { .expect("failed setting up bob's client"); let ws_name = uuid::Uuid::new_v4().to_string(); - let wsid = crate::api::WorkspaceIdentifier { + let wsid = crate::proto::session::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: ws_name.clone(), }; diff --git a/src/tests/e2e/server.rs b/src/tests/e2e/server.rs index 1dafdf04..32e92cc7 100644 --- a/src/tests/e2e/server.rs +++ b/src/tests/e2e/server.rs @@ -67,7 +67,7 @@ async fn test_workspace_interactions() { let client_alice = ClientFixture::of("alice").setup().await?; let client_bob = ClientFixture::of("bob").setup().await?; let workspace_name = format!("test-workspace-interactions-{}", uuid::Uuid::new_v4()); - let wsid = crate::api::WorkspaceIdentifier { + let wsid = crate::proto::session::WorkspaceIdentifier { user: client_alice.current_user().name.clone(), workspace: workspace_name.clone(), }; From 2148dd14db5a95316bac07bc3b4d5070193378ad Mon Sep 17 00:00:00 2001 From: zaaarf Date: Tue, 17 Mar 2026 04:00:04 +0100 Subject: [PATCH 102/121] docs: fixed broken doc links --- src/cursor/controller.rs | 4 ++-- src/lib.rs | 5 +++-- src/workspace.rs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 0c01a394..3caec9ce 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -1,5 +1,5 @@ //! ### Cursor Controller -//! A [Controller] implementation for [crate::api::Cursor] actions in a [crate::Workspace] +//! A [Controller] implementation for cursor actions in a [crate::Workspace]. use std::sync::Arc; @@ -12,7 +12,7 @@ use crate::{ }; use codemp_proto::cursor::{CursorEvent, CursorUpdate, cursor_client::CursorClient}; -/// A [Controller] for asynchronously sending and receiving [Cursor] event. +/// A [Controller] for asynchronously sending and receiving [CursorEvent]s. /// /// An unique [CursorController] exists for each active [crate::Workspace]. #[derive(Debug, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 45a2551f..7472938a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,8 +42,9 @@ //! # }; //! ``` //! -//! A [`Workspace`] handle can be used to acquire a [`cursor::Controller`] to track remote [`api::Cursor`]s -//! and one or more [`buffer::Controller`] to send and receive [`api::TextChange`]s. +//! A [`Workspace`] handle can be used to acquire a [`cursor::Controller`] to track remote +//! [`proto::cursor::CursorEvent`]s and one or more [`buffer::Controller`] to send and receive +//! [`api::TextChange`]s. //! //! ```no_run //! # async { diff --git a/src/workspace.rs b/src/workspace.rs index 481b7486..014c6fbd 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -310,7 +310,7 @@ impl Workspace { Ok(()) } - /// Fetch a list of the [User]s attached to a specific buffer. + /// Re-fetch the list of users attached to the given buffer.. pub async fn fetch_buffer_users(&self, path: impl ToString) -> RemoteResult<()> { let path = path.to_string(); let resp = self From 52da1a695f9f576fb438aa5ce3fddf0f5396f05d Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 17 Mar 2026 16:10:30 +0100 Subject: [PATCH 103/121] fix: newer proto --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/workspace.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 813c2181..9550267c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7#c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7" +source = "git+https://github.com/hexedtech/codemp-proto?rev=29fb391b4857ac2e3bd2f60ddebcc79494c77953#29fb391b4857ac2e3bd2f60ddebcc79494c77953" dependencies = [ "jni", "jni-toolbox", diff --git a/Cargo.toml b/Cargo.toml index 033f9dc8..f7ed0608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "c25c94bc71a3f32be203c40ca4c9dc03d2c0b9d7", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "29fb391b4857ac2e3bd2f60ddebcc79494c77953", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api diff --git a/src/workspace.rs b/src/workspace.rs index 014c6fbd..0d7b6dcd 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -288,7 +288,7 @@ impl Workspace { for b in resp.buffers { self.0 .filetree - .insert(b.path.clone().into(), b); + .insert(b.path.to_string(), b); } Ok(()) From 97e127fe36ad98aaa241ac905bcecbf4cc9d5cb0 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 17 Mar 2026 16:16:53 +0100 Subject: [PATCH 104/121] feat: search_buffers returns BufferNodes --- dist/java/src/mp/code/Workspace.java | 7 ++++--- dist/lua/annotations.lua | 6 +++++- src/ffi/java/workspace.rs | 3 ++- src/ffi/js/workspace.rs | 4 ++-- src/ffi/python/workspace.rs | 2 +- src/workspace.rs | 8 ++++---- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index f87fd601..8e857ef2 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -3,6 +3,7 @@ import java.util.function.Consumer; import mp.code.proto.UserInfo; +import mp.code.proto.BufferNode; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; @@ -57,14 +58,14 @@ public BufferController getBuffer(String path) { return get_buffer(this.ptr, path); } - private static native String[] search_buffers(long self, String filter); + private static native BufferNode[] search_buffers(long self, String filter); /** * Searches for buffers matching the filter in this workspace. * @param filter the filter to apply (may be null) - * @return an array containing file tree as flat paths + * @return an array containing file tree as {@link BufferNode}s */ - public String[] searchBuffers(String filter) { + public BufferNode[] searchBuffers(String filter) { return search_buffers(this.ptr, filter); } diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index c484be91..6951d10f 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -362,6 +362,10 @@ function Client:callback(cb) end ---@field user string username of workspace owner ---@field workspace string workspace name +---@class BufferNode +---represents a buffer and holds wheter it is ephemeral +---@field path string buffer path +---@field ephemeral boolean wheter this buffer is ephemeral @@ -428,7 +432,7 @@ function Workspace:attach_buffer(path) end function Workspace:detach_buffer(path) end ---@param filter? string apply a filter to the return elements ----@return string[] +---@return BufferNode[] ---return the list of available buffers in this workspace, as relative paths from workspace root function Workspace:search_buffers(filter) end diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 2a3cefbe..eb64ac86 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -4,6 +4,7 @@ use crate::{ errors::{ConnectionError, ControllerError, RemoteError}, proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent} }; +use codemp_proto::files::BufferNode; use jni::{Env, objects::JObject}; use jni_toolbox::jni; @@ -27,7 +28,7 @@ fn get_buffer(workspace: &mut Workspace, path: String) -> Option) -> Vec { +fn search_buffers(workspace: &mut Workspace, filter: Option) -> Vec { workspace.search_buffers(filter.as_deref()) } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index a3fd1f53..b3c21a4a 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,4 +1,4 @@ -use codemp_proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; +use codemp_proto::{common::UserInfo, files::BufferNode, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; use crate::{api::AsyncReceiver, buffer::controller::BufferController, cursor::controller::CursorController}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; @@ -15,7 +15,7 @@ impl Workspace { /// List all available buffers in this workspace #[napi(js_name = "searchBuffers")] - pub fn js_search_buffers(&self, filter: Option) -> Vec { + pub fn js_search_buffers(&self, filter: Option) -> Vec { self.search_buffers(filter.as_deref()) } diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index aec34e5b..ca06e9b6 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -83,7 +83,7 @@ impl CodempWorkspace { #[pyo3(name = "search_buffers")] #[pyo3(signature = (filter=None))] - fn pysearch_buffers(&self, filter: Option<&str>) -> Vec { + fn pysearch_buffers(&self, filter: Option<&str>) -> Vec { self.search_buffers(filter) } diff --git a/src/workspace.rs b/src/workspace.rs index 0d7b6dcd..4b4954dd 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -392,15 +392,15 @@ impl Workspace { /// Get the filetree as it is currently cached. /// A filter may be applied, and it works as a "starts_with" check. // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 - pub fn search_buffers(&self, filter: Option<&str>) -> Vec { + pub fn search_buffers(&self, filter: Option<&str>) -> Vec { let mut tree = self .0 .filetree .iter() .filter(|f| filter.is_none_or(|flt| f.key().starts_with(flt))) - .map(|f| f.key().clone()) - .collect::>(); - tree.sort(); + .map(|x| x.value().clone()) + .collect::>(); + tree.sort_by(|a, b| a.path.path.cmp(&b.path.path)); tree } } From 2a7a3b8b16ae3186a3fceb883f6430f8e9d10388 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 19 Mar 2026 17:50:06 +0100 Subject: [PATCH 105/121] feat: BufferAttributes (and event) --- Cargo.lock | 2 +- Cargo.toml | 2 +- dist/lua/annotations.lua | 6 +++++- dist/lua/enums.lua | 1 + src/ffi/java/workspace.rs | 6 +++--- src/ffi/js/workspace.rs | 6 +++--- src/ffi/lua/workspace.rs | 2 +- src/ffi/python/workspace.rs | 4 ++-- src/prelude.rs | 8 +++++--- src/workspace.rs | 33 +++++++++++++++++++++------------ 10 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9550267c..71aacfe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=29fb391b4857ac2e3bd2f60ddebcc79494c77953#29fb391b4857ac2e3bd2f60ddebcc79494c77953" +source = "git+https://github.com/hexedtech/codemp-proto?rev=0e3af02296d33563dde6fe4caf7fd8df237c9357#0e3af02296d33563dde6fe4caf7fd8df237c9357" dependencies = [ "jni", "jni-toolbox", diff --git a/Cargo.toml b/Cargo.toml index f7ed0608..59eb1def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "29fb391b4857ac2e3bd2f60ddebcc79494c77953", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "0e3af02296d33563dde6fe4caf7fd8df237c9357", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 6951d10f..c61e2051 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -362,10 +362,14 @@ function Client:callback(cb) end ---@field user string username of workspace owner ---@field workspace string workspace name +---@class BufferAttributes +---attributes and properties of a buffer +---@field ephemeral boolean wheter this buffer is ephemeral + ---@class BufferNode ---represents a buffer and holds wheter it is ephemeral ---@field path string buffer path ----@field ephemeral boolean wheter this buffer is ephemeral +---@field attributes BufferAttributes attributes of this buffer diff --git a/dist/lua/enums.lua b/dist/lua/enums.lua index 83c2ccd9..bcea9a7f 100644 --- a/dist/lua/enums.lua +++ b/dist/lua/enums.lua @@ -16,6 +16,7 @@ local WorkspaceEventKind = { FileDelete = 5, UserJoinBuffer = 6, UserLeaveBuffer = 7, + FileAttrsUpdated = 8, } return { diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index eb64ac86..2f01ce0c 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -4,7 +4,7 @@ use crate::{ errors::{ConnectionError, ControllerError, RemoteError}, proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent} }; -use codemp_proto::files::BufferNode; +use codemp_proto::files::{BufferAttributes, BufferNode}; use jni::{Env, objects::JObject}; use jni_toolbox::jni; @@ -46,8 +46,8 @@ fn user_list(workspace: &mut Workspace) -> Vec { /// Create a new buffer. #[jni(package = "mp.code", class = "Workspace")] -fn create_buffer(workspace: &mut Workspace, path: String, ephemeral: bool) -> Result<(), RemoteError> { - super::tokio().block_on(workspace.create_buffer(path, ephemeral)) +fn create_buffer(workspace: &mut Workspace, path: String, attributes: Option) -> Result<(), RemoteError> { + super::tokio().block_on(workspace.create_buffer(path, attributes)) } /// Pins an ephemeral buffer. diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index b3c21a4a..a53b119f 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,4 +1,4 @@ -use codemp_proto::{common::UserInfo, files::BufferNode, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; +use codemp_proto::{common::UserInfo, files::{BufferAttributes, BufferNode}, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; use crate::{api::AsyncReceiver, buffer::controller::BufferController, cursor::controller::CursorController}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; @@ -45,8 +45,8 @@ impl Workspace { /// Create a new buffer in the current workspace #[napi(js_name = "createBuffer")] - pub async fn js_create_buffer(&self, path: String, ephemeral: bool) -> napi::Result<()> { - Ok(self.create_buffer(&path, ephemeral).await?) + pub async fn js_create_buffer(&self, path: String, attributes: Option) -> napi::Result<()> { + Ok(self.create_buffer(&path, attributes).await?) } /// Attach to a workspace buffer, starting a BufferController diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index 1e58d156..4ab654a7 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -10,7 +10,7 @@ impl LuaUserData for CodempWorkspace { }); methods.add_method( "create_buffer", - |_, this, (name, ephemeral): (String, bool)| a_sync! { this => this.create_buffer(name, ephemeral).await? }, + |_, this, (name, attrs): (String, Option)| a_sync! { this => this.create_buffer(name, attrs).await? }, ); methods.add_method("pin_buffer", |_, this, (path,): (String,)| { diff --git a/src/ffi/python/workspace.rs b/src/ffi/python/workspace.rs index ca06e9b6..4374f837 100644 --- a/src/ffi/python/workspace.rs +++ b/src/ffi/python/workspace.rs @@ -9,9 +9,9 @@ use super::a_sync_detach; impl CodempWorkspace { // join a workspace #[pyo3(name = "create_buffer")] - fn pycreate_buffer(&self, py: Python, path: String, ephemeral: bool) -> PyResult { + fn pycreate_buffer(&self, py: Python, path: String, attrs: Option) -> PyResult { let this = self.clone(); - a_sync_detach!(py, this.create_buffer(path.as_str(), ephemeral).await) + a_sync_detach!(py, this.create_buffer(path.as_str(), attrs).await) } #[pyo3(name = "pin_buffer")] diff --git a/src/prelude.rs b/src/prelude.rs index 2c8c1822..5d440c81 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,11 +9,13 @@ pub use crate::api::{ }; pub use crate::proto::{ - files::BufferNode as CodempBufferNode, buffer::BufferEvent as CodempBufferEvent, + common::UserInfo as CodempUserInfo, + files::BufferNode as CodempBufferNode, files::BufferAttributes as CodempBufferAttributes, + buffer::BufferEvent as CodempBufferEvent, cursor::CursorEvent as CodempCursorEvent, cursor::CursorUpdate as CodempCursorUpdate, cursor::CursorPosition as CodempCursorPosition, cursor::RowCol as CodempRowCol, - session::SessionEvent as CodempSessionEvent, workspace::WorkspaceEvent as CodempWorkspaceEvent, - session::WorkspaceIdentifier as CodempWorkspaceIdentifier, common::UserInfo as CodempUserInfo, + session::SessionEvent as CodempSessionEvent, session::WorkspaceIdentifier as CodempWorkspaceIdentifier, + workspace::WorkspaceEvent as CodempWorkspaceEvent, }; pub use crate::{ diff --git a/src/workspace.rs b/src/workspace.rs index 4b4954dd..7ce8030b 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -15,7 +15,7 @@ use crate::{ use codemp_proto::{ common::Empty, - files::{BufferNode, BufferPath}, + files::{BufferAttributes, BufferNode, BufferPath}, workspace::{WorkspaceEvent, WorkspaceEventKind}, }; @@ -48,7 +48,7 @@ pub(crate) struct WorkspaceInner { cursor: cursor::Controller, buffers: DashMap, services: Services, - filetree: DashMap, + filetree: DashMap, buffer_users: DashMap>, users: Arc>, events: tokio::sync::Mutex>, @@ -158,22 +158,23 @@ impl Workspace { } /// Create a new buffer in the current workspace. - pub async fn create_buffer(&self, path: impl ToString, ephemeral: bool) -> RemoteResult<()> { + pub async fn create_buffer(&self, path: impl ToString, attributes: Option) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); let path = path.to_string(); + let attributes = attributes.unwrap_or_default(); workspace_client .create_buffer(tonic::Request::new(BufferNode { - path: crate::proto::files::BufferPath::from(&path), - ephemeral, + path: BufferPath::from(&path), + attributes, })) .await?; // add to filetree, not really necessary as we will get an event for it self.0.filetree.insert( path.clone(), - crate::proto::files::BufferNode { - path: crate::proto::files::BufferPath::from(&path), - ephemeral, + BufferNode { + path: BufferPath::from(&path), + attributes, }, ); @@ -467,11 +468,11 @@ impl WorkspaceWorker { }, WorkspaceEventKind::FileCreate => { - if let (Some(path), Some(ephemeral)) = (event.path, event.ephemeral) { + if let (Some(path), Some(attributes)) = (event.path, event.attributes) { inner.buffer_users.insert(path.clone(), Vec::new()); - inner.filetree.insert(path.clone(), crate::proto::files::BufferNode { - path: crate::proto::files::BufferPath::from(&path), - ephemeral + inner.filetree.insert(path.clone(), BufferNode { + path: BufferPath::from(&path), + attributes }); } } @@ -495,6 +496,14 @@ impl WorkspaceWorker { let _ = inner.buffers.remove(&path); } } + WorkspaceEventKind::FileAttrsUpdated => { + if let (Some(path), Some(attributes)) = (event.path, event.attributes) { + if let Some(mut r) = inner.filetree.get_mut(&path) { + r.attributes = attributes; + } + } + } + } if self.events.send(_event).is_err() { From 4b27119b40b2ce2f52d087332977838c2c6419f0 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 18:04:02 +0100 Subject: [PATCH 106/121] chore(java): update java-side glue --- dist/java/src/mp/code/Workspace.java | 13 +++++-------- .../src/mp/code/proto/BufferAttributes.java | 18 ++++++++++++++++++ dist/java/src/mp/code/proto/BufferNode.java | 4 ++-- .../java/src/mp/code/proto/WorkspaceEvent.java | 4 ++-- .../src/mp/code/proto/WorkspaceEventKind.java | 5 +++++ 5 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 dist/java/src/mp/code/proto/BufferAttributes.java diff --git a/dist/java/src/mp/code/Workspace.java b/dist/java/src/mp/code/Workspace.java index 8e857ef2..e5f21044 100644 --- a/dist/java/src/mp/code/Workspace.java +++ b/dist/java/src/mp/code/Workspace.java @@ -2,13 +2,10 @@ import java.util.function.Consumer; -import mp.code.proto.UserInfo; -import mp.code.proto.BufferNode; +import mp.code.proto.*; import mp.code.exceptions.ConnectionException; import mp.code.exceptions.ConnectionRemoteException; import mp.code.exceptions.ControllerException; -import mp.code.proto.WorkspaceEvent; -import mp.code.proto.WorkspaceIdentifier; /** * Represents a CodeMP workspace, which broadly speaking is a collection @@ -90,16 +87,16 @@ public UserInfo[] userList() { return user_list(this.ptr); } - private static native void create_buffer(long self, String path, boolean ephemeral) throws ConnectionRemoteException; + private static native void create_buffer(long self, String path, BufferAttributes attributes) throws ConnectionRemoteException; /** * Creates a buffer with the given path. * @param path the new buffer's path - * @param ephemeral whether the buffer should be ephemeral + * @param attributes the buffer's attributes (will use defaults if null) * @throws ConnectionRemoteException if an error occurs in communicating with the server */ - public void createBuffer(String path, boolean ephemeral) throws ConnectionRemoteException { - create_buffer(this.ptr, path, ephemeral); + public void createBuffer(String path, BufferAttributes attributes) throws ConnectionRemoteException { + create_buffer(this.ptr, path, attributes); } private static native void pin_buffer(long self, String path) throws ConnectionRemoteException; diff --git a/dist/java/src/mp/code/proto/BufferAttributes.java b/dist/java/src/mp/code/proto/BufferAttributes.java new file mode 100644 index 00000000..85c96ff8 --- /dev/null +++ b/dist/java/src/mp/code/proto/BufferAttributes.java @@ -0,0 +1,18 @@ +package mp.code.proto; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * The attributes of a buffer. + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public class BufferAttributes { + /** + * Whether this buffer gets auto-deleted once all users leave. + */ + public final boolean ephemeral; +} diff --git a/dist/java/src/mp/code/proto/BufferNode.java b/dist/java/src/mp/code/proto/BufferNode.java index ca6f987a..92e62dd9 100644 --- a/dist/java/src/mp/code/proto/BufferNode.java +++ b/dist/java/src/mp/code/proto/BufferNode.java @@ -17,7 +17,7 @@ public class BufferNode { public final String path; /** - * Whether this buffer gets auto-deleted once all users leave. + * The attributes of this buffer. */ - public final boolean ephemeral; + public final BufferAttributes attributes; } diff --git a/dist/java/src/mp/code/proto/WorkspaceEvent.java b/dist/java/src/mp/code/proto/WorkspaceEvent.java index 0c94832c..d5844f92 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEvent.java +++ b/dist/java/src/mp/code/proto/WorkspaceEvent.java @@ -28,9 +28,9 @@ public class WorkspaceEvent { public final String path; /** - * Whether the buffer is ephemeral, or null. + * The attributes of the buffer, or null. */ - public final Boolean ephemeral; + public final BufferAttributes attributes; /** * The new path of the buffer after the rename, or null. diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java index 1412918a..2f7423aa 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEventKind.java +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -36,4 +36,9 @@ public final class WorkspaceEventKind { * Event that occurs when a user leaves a buffer. */ public static final int USER_LEAVE_BUFFER = 7; + + /** + * Event that occurs when a buffer has one of its attributes changed. + */ + public static final int FILE_ATTRS_UPDATED = 8; } From 4ec7b02bc8cb176f1a6cf08524556d5b4ba33cf5 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 18:17:42 +0100 Subject: [PATCH 107/121] chore(java): use prelude imports --- src/ffi/java/buffer.rs | 18 +++++++++++------- src/ffi/java/client.rs | 30 +++++++++++++++++++++--------- src/ffi/java/cursor.rs | 26 +++++++++++++++++--------- src/ffi/java/mod.rs | 31 ++++++++++++++----------------- src/ffi/java/workspace.rs | 32 +++++++++++++++++++++----------- 5 files changed, 84 insertions(+), 53 deletions(-) diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 39b59abd..a02940cb 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -1,10 +1,12 @@ -use jni::{Env, objects::JObject}; use jni_toolbox::jni; use crate::{ - api::{AsyncReceiver, AsyncSender, BufferUpdate, TextChange}, errors::ControllerError, - proto::session::WorkspaceIdentifier + prelude::{ + CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, + CodempBufferUpdate as BufferUpdate, CodempTextChange as TextChange, + CodempWorkspaceIdentifier as WorkspaceIdentifier, + }, }; /// Get the name of the buffer. @@ -51,16 +53,18 @@ fn send( /// Register a callback for buffer changes. #[jni(package = "mp.code", class = "BufferController")] fn callback<'local>( - env: &mut Env<'local>, + env: &mut jni::Env<'local>, controller: &mut crate::buffer::Controller, - cb: JObject<'local>, + cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { - return Err(jni::errors::Error::NullPtr("null pointer to buffer callback")); + return Err(jni::errors::Error::NullPtr( + "null pointer to buffer callback", + )); } let cb_ref = env.new_global_ref(cb)?; - let jvm = env.get_java_vm()?; + let jvm = env.get_java_vm()?; controller.callback(move |controller: crate::buffer::Controller| { let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 8ccbbdf1..2b5f9596 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,11 +1,13 @@ +use jni_toolbox::jni; + use crate::{ - Workspace, - api::{AsyncReceiver, Config}, - client::Client, errors::{ConnectionError, ControllerError, RemoteError}, - proto::{common::UserInfo, session::{SessionEvent, WorkspaceIdentifier}} + prelude::{ + CodempAsyncReceiver as AsyncReceiver, CodempClient as Client, CodempConfig as Config, + CodempSessionEvent as SessionEvent, CodempUserInfo as UserInfo, + CodempWorkspace as Workspace, CodempWorkspaceIdentifier as WorkspaceIdentifier, + }, }; -use jni_toolbox::jni; /// Connect using the given credentials to the default server, and return a [Client] to interact with it. #[jni(package = "mp.code", class = "Client")] @@ -21,7 +23,11 @@ fn current_user(client: &mut Client) -> UserInfo { /// Join a [Workspace] and return a pointer to it. #[jni(package = "mp.code", class = "Client")] -fn attach_workspace(client: &mut Client, user: String, workspace: String) -> Result { +fn attach_workspace( + client: &mut Client, + user: String, + workspace: String, +) -> Result { super::tokio().block_on(client.attach_workspace(user, workspace)) } @@ -57,7 +63,11 @@ fn delete_workspace(client: &mut Client, workspace: String) -> Result<(), Remote /// Invite another user to an owned workspace. #[jni(package = "mp.code", class = "Client")] -fn invite_to_workspace(client: &mut Client, workspace: String, user: String) -> Result<(), RemoteError> { +fn invite_to_workspace( + client: &mut Client, + workspace: String, + user: String, +) -> Result<(), RemoteError> { super::tokio().block_on(client.invite_to_workspace(workspace, user)) } @@ -117,11 +127,13 @@ fn callback<'local>( cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { - return Err(jni::errors::Error::NullPtr("null pointer to buffer callback")); + return Err(jni::errors::Error::NullPtr( + "null pointer to buffer callback", + )); } let cb_ref = env.new_global_ref(cb)?; - let jvm = env.get_java_vm()?; + let jvm = env.get_java_vm()?; client.callback(move |controller: Client| { let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 8fe1878e..43fe0d50 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,10 +1,13 @@ +use jni_toolbox::jni; + use crate::{ - api::{AsyncReceiver, AsyncSender}, errors::ControllerError, - proto::{cursor::{CursorEvent, CursorUpdate}, session::WorkspaceIdentifier} + prelude::{ + CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, + CodempCursorEvent as CursorEvent, CodempCursorUpdate as CursorUpdate, + CodempWorkspaceIdentifier as WorkspaceIdentifier, + }, }; -use jni::{Env, objects::JObject}; -use jni_toolbox::jni; /// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. #[jni(package = "mp.code", class = "CursorController")] @@ -14,7 +17,9 @@ fn workspace_id(controller: &mut crate::cursor::Controller) -> WorkspaceIdentifi /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] -fn try_recv(controller: &mut crate::cursor::Controller) -> Result, ControllerError> { +fn try_recv( + controller: &mut crate::cursor::Controller, +) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } @@ -26,23 +31,26 @@ fn recv(controller: &mut crate::cursor::Controller) -> Result Result<(), ControllerError> { +fn send( + controller: &mut crate::cursor::Controller, + sel: CursorUpdate, +) -> Result<(), ControllerError> { controller.send(sel) } /// Register a callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] fn callback<'local>( - env: &mut Env<'local>, + env: &mut jni::Env<'local>, controller: &mut crate::cursor::Controller, - cb: JObject<'local>, + cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { return Err(jni::errors::Error::NullPtr("cursor callback is null")); } let cb_ref = env.new_global_ref(cb)?; - let jvm = env.get_java_vm()?; + let jvm = env.get_java_vm()?; controller.callback(move |controller: crate::cursor::Controller| { let res: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index cb8807b8..b75d3e75 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -90,9 +90,10 @@ impl jni_toolbox::IntoException for crate::errors::ControllerError { } } - -macro_rules! from_java_ptr { - ($type: ty) => { +/// Generates a [jni_toolbox::IntoJava] and [jni_toolbox::FromJava] implementations +/// for a class that is just a holder for a pointer. +macro_rules! java_ptr_class { + ($type: ty, $jclass: literal) => { impl<'j> jni_toolbox::FromJava<'j> for &mut $type { type From = jni::sys::jobject; #[allow(unsafe_code)] @@ -110,17 +111,7 @@ macro_rules! from_java_ptr { Self::from_java(env, value.l()?.into_raw()) } } - }; -} - -from_java_ptr!(crate::Client); -from_java_ptr!(crate::Workspace); -from_java_ptr!(crate::cursor::Controller); -from_java_ptr!(crate::buffer::Controller); -/// Generates a [JObjectify] implementation for a class that is just a holder for a pointer. -macro_rules! into_java_ptr_class { - ($type: ty, $jclass: literal) => { impl<'j> jni_toolbox::IntoJavaObject<'j> for $type { const CLASS: &'static str = $jclass; fn into_java_object( @@ -140,7 +131,13 @@ macro_rules! into_java_ptr_class { }; } -into_java_ptr_class!(crate::Client, "mp/code/Client"); -into_java_ptr_class!(crate::Workspace, "mp/code/Workspace"); -into_java_ptr_class!(crate::cursor::Controller, "mp/code/CursorController"); -into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); +java_ptr_class!(crate::prelude::CodempClient, "mp/code/Client"); +java_ptr_class!(crate::prelude::CodempWorkspace, "mp/code/Workspace"); +java_ptr_class!( + crate::prelude::CodempBufferController, + "mp/code/BufferController" +); +java_ptr_class!( + crate::prelude::CodempCursorController, + "mp/code/CursorController" +); diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 2f01ce0c..08c5af88 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,12 +1,13 @@ +use jni_toolbox::jni; + use crate::{ - Workspace, - api::controller::AsyncReceiver, errors::{ConnectionError, ControllerError, RemoteError}, - proto::{common::UserInfo, session::WorkspaceIdentifier, workspace::WorkspaceEvent} + prelude::{ + CodempAsyncReceiver as AsyncReceiver, CodempBufferAttributes as BufferAttributes, + CodempBufferNode as BufferNode, CodempUserInfo as UserInfo, CodempWorkspace as Workspace, + CodempWorkspaceEvent as WorkspaceEvent, CodempWorkspaceIdentifier as WorkspaceIdentifier, + }, }; -use codemp_proto::files::{BufferAttributes, BufferNode}; -use jni::{Env, objects::JObject}; -use jni_toolbox::jni; /// Get the workspace id. #[jni(package = "mp.code", class = "Workspace")] @@ -46,7 +47,11 @@ fn user_list(workspace: &mut Workspace) -> Vec { /// Create a new buffer. #[jni(package = "mp.code", class = "Workspace")] -fn create_buffer(workspace: &mut Workspace, path: String, attributes: Option) -> Result<(), RemoteError> { +fn create_buffer( + workspace: &mut Workspace, + path: String, + attributes: Option, +) -> Result<(), RemoteError> { super::tokio().block_on(workspace.create_buffer(path, attributes)) } @@ -64,7 +69,10 @@ fn un_pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr /// Attach to a buffer and return a pointer to its [`crate::buffer::Controller`]. #[jni(package = "mp.code", class = "Workspace")] -fn attach_buffer(workspace: &mut Workspace, path: String) -> Result { +fn attach_buffer( + workspace: &mut Workspace, + path: String, +) -> Result { super::tokio().block_on(workspace.attach_buffer(&path)) } @@ -131,12 +139,14 @@ fn clear_callback(workspace: &mut Workspace) { /// Register a callback for workspace events. #[jni(package = "mp.code", class = "Workspace")] fn callback<'local>( - env: &mut Env<'local>, + env: &mut jni::Env<'local>, controller: &mut crate::Workspace, - cb: JObject<'local>, + cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { - return Err(jni::errors::Error::NullPtr("null pointer to workspace callback")); + return Err(jni::errors::Error::NullPtr( + "null pointer to workspace callback", + )); } let cb_ref = env.new_global_ref(cb)?; From ce58d9853e19a735e5db3eff8399e5abcae6d71e Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 18:43:20 +0100 Subject: [PATCH 108/121] chore: update to new proto --- Cargo.lock | 2 +- Cargo.toml | 2 +- .../src/mp/code/proto/WorkspaceEventKind.java | 23 +++++++++---------- dist/lua/enums.lua | 12 +++++----- src/ffi/js/workspace.rs | 2 +- src/prelude.rs | 2 +- src/workspace.rs | 10 ++++---- 7 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71aacfe4..ae56b6f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "codemp-proto" version = "0.8.0" -source = "git+https://github.com/hexedtech/codemp-proto?rev=0e3af02296d33563dde6fe4caf7fd8df237c9357#0e3af02296d33563dde6fe4caf7fd8df237c9357" +source = "git+https://github.com/hexedtech/codemp-proto?rev=6b45fd7bd7c03ef60234880c59157481def4ca71#6b45fd7bd7c03ef60234880c59157481def4ca71" dependencies = [ "jni", "jni-toolbox", diff --git a/Cargo.toml b/Cargo.toml index 59eb1def..e36062c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ thiserror = "2.0" # crdt diamond-types = "1.0" # proto -codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "0e3af02296d33563dde6fe4caf7fd8df237c9357", features = ["client"] } +codemp-proto = { git = "https://github.com/hexedtech/codemp-proto", rev = "6b45fd7bd7c03ef60234880c59157481def4ca71", features = ["client"] } uuid = { version = "1.17", features = ["v4"] } tonic = { version = "0.14", features = ["tls-native-roots"] } # api diff --git a/dist/java/src/mp/code/proto/WorkspaceEventKind.java b/dist/java/src/mp/code/proto/WorkspaceEventKind.java index 2f7423aa..0f987a92 100644 --- a/dist/java/src/mp/code/proto/WorkspaceEventKind.java +++ b/dist/java/src/mp/code/proto/WorkspaceEventKind.java @@ -17,28 +17,27 @@ public final class WorkspaceEventKind { */ public static final int USER_LEAVE_WORKSPACE = 2; /** - * Event that occurs when a file is created in a workspace. + * Event that occurs when a user joins a buffer. */ - public static final int FILE_CREATE = 3; + public static final int USER_JOIN_BUFFER = 3; /** - * Event that occurs when a file is renamed in a workspace. + * Event that occurs when a user leaves a buffer. */ - public static final int FILE_RENAME = 4; + public static final int USER_LEAVE_BUFFER = 4; /** - * Event that occurs when a file is deleted in a workspace. + * Event that occurs when a buffer is created in a workspace. */ - public static final int FILE_DELETE = 5; + public static final int BUFFER_CREATE = 5; /** - * Event that occurs when a user joins a buffer. + * Event that occurs when a buffer is renamed in a workspace. */ - public static final int USER_JOIN_BUFFER = 6; + public static final int BUFFER_RENAME = 6; /** - * Event that occurs when a user leaves a buffer. + * Event that occurs when a buffer is deleted in a workspace. */ - public static final int USER_LEAVE_BUFFER = 7; - + public static final int BUFFER_DELETE = 7; /** * Event that occurs when a buffer has one of its attributes changed. */ - public static final int FILE_ATTRS_UPDATED = 8; + public static final int BUFFER_ATTRS_UPDATED = 8; } diff --git a/dist/lua/enums.lua b/dist/lua/enums.lua index bcea9a7f..63ee13e6 100644 --- a/dist/lua/enums.lua +++ b/dist/lua/enums.lua @@ -11,12 +11,12 @@ local SessionEventKind = { local WorkspaceEventKind = { UserJoinWorkspace = 1, UserLeaveWorkspace = 2, - FileCreate = 3, - FileRename = 4, - FileDelete = 5, - UserJoinBuffer = 6, - UserLeaveBuffer = 7, - FileAttrsUpdated = 8, + UserJoinBuffer = 3, + UserLeaveBuffer = 4, + BufferCreate = 5, + BufferRename = 6, + BufferDelete = 7, + BufferAttrsUpdated = 8, } return { diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index a53b119f..cd121414 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,4 +1,4 @@ -use codemp_proto::{common::UserInfo, files::{BufferAttributes, BufferNode}, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; +use codemp_proto::{common::UserInfo, buffer::{BufferAttributes, BufferNode}, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; use crate::{api::AsyncReceiver, buffer::controller::BufferController, cursor::controller::CursorController}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; diff --git a/src/prelude.rs b/src/prelude.rs index 5d440c81..08a5a710 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -10,7 +10,7 @@ pub use crate::api::{ pub use crate::proto::{ common::UserInfo as CodempUserInfo, - files::BufferNode as CodempBufferNode, files::BufferAttributes as CodempBufferAttributes, + buffer::BufferNode as CodempBufferNode, buffer::BufferAttributes as CodempBufferAttributes, buffer::BufferEvent as CodempBufferEvent, cursor::CursorEvent as CodempCursorEvent, cursor::CursorUpdate as CodempCursorUpdate, cursor::CursorPosition as CodempCursorPosition, cursor::RowCol as CodempRowCol, diff --git a/src/workspace.rs b/src/workspace.rs index 7ce8030b..e0df012c 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -14,8 +14,8 @@ use crate::{ }; use codemp_proto::{ + buffer::{BufferAttributes, BufferNode, BufferPath}, common::Empty, - files::{BufferAttributes, BufferNode, BufferPath}, workspace::{WorkspaceEvent, WorkspaceEventKind}, }; @@ -467,7 +467,7 @@ impl WorkspaceWorker { } }, - WorkspaceEventKind::FileCreate => { + WorkspaceEventKind::BufferCreate => { if let (Some(path), Some(attributes)) = (event.path, event.attributes) { inner.buffer_users.insert(path.clone(), Vec::new()); inner.filetree.insert(path.clone(), BufferNode { @@ -476,7 +476,7 @@ impl WorkspaceWorker { }); } } - WorkspaceEventKind::FileRename => { + WorkspaceEventKind::BufferRename => { if let (Some(before), Some(after)) = (event.path, event.after) { if let Some((_path, controller)) = inner.buffers.remove(&before) { inner.buffers.insert(after.clone(), controller); @@ -489,14 +489,14 @@ impl WorkspaceWorker { } } } - WorkspaceEventKind::FileDelete => { + WorkspaceEventKind::BufferDelete => { if let Some(path) = event.path { inner.filetree.remove(&path); inner.buffer_users.remove(&path); let _ = inner.buffers.remove(&path); } } - WorkspaceEventKind::FileAttrsUpdated => { + WorkspaceEventKind::BufferAttrsUpdated => { if let (Some(path), Some(attributes)) = (event.path, event.attributes) { if let Some(mut r) = inner.filetree.get_mut(&path) { r.attributes = attributes; From 6c9592170f239edb5296940b8bee7a7ec73fd5be Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 18:57:25 +0100 Subject: [PATCH 109/121] docs(js): added missing rustdocs --- src/ffi/js/client.rs | 3 ++- src/ffi/js/ext.rs | 2 ++ src/ffi/js/mod.rs | 3 +++ src/ffi/js/workspace.rs | 6 ++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 91cafa3c..45a12442 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -2,8 +2,9 @@ use codemp_proto::{common::UserInfo, session::WorkspaceIdentifier}; use napi_derive::napi; -#[napi] /// connect to codemp servers and return a client session +#[allow(dead_code)] +#[napi] pub async fn connect(config: crate::api::Config) -> napi::Result { Ok(crate::Client::connect(config).await?) } diff --git a/src/ffi/js/ext.rs b/src/ffi/js/ext.rs index 52fbe3d8..a4a9ad82 100644 --- a/src/ffi/js/ext.rs +++ b/src/ffi/js/ext.rs @@ -1,12 +1,14 @@ use napi_derive::napi; /// Hash function +#[allow(dead_code)] #[napi(js_name = "hash")] pub fn js_hash(data: String) -> i64 { crate::ext::hash(data) } /// Get the current version of the client +#[allow(dead_code)] #[napi(js_name = "version")] pub fn js_version() -> &'static str { crate::version() diff --git a/src/ffi/js/mod.rs b/src/ffi/js/mod.rs index 197417a1..9b32ae57 100644 --- a/src/ffi/js/mod.rs +++ b/src/ffi/js/mod.rs @@ -24,11 +24,13 @@ impl From for napi::Error { use napi_derive::napi; +/// A napi-friendly representation of a logger. #[napi] pub struct JsLogger(std::sync::Arc>>); #[napi] impl JsLogger { + /// Creates a new [JsLogger]. #[napi(constructor)] pub fn new(debug: Option) -> JsLogger { let (tx, rx) = tokio::sync::mpsc::channel(256); @@ -56,6 +58,7 @@ impl JsLogger { JsLogger(std::sync::Arc::new(tokio::sync::Mutex::new(rx))) } + /// Gets a message from the logger. #[napi] pub async fn message(&self) -> Option { self.0.lock().await.recv().await diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index cd121414..b9bec6a5 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -61,28 +61,34 @@ impl Workspace { Ok(self.delete_buffer(&path).await?) } + /// Wait for next workspace event and return it #[napi(js_name = "recv")] pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } + /// Return next workspace event if present #[napi(js_name = "tryRecv")] pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } + /// Block until next workspace event without returning it #[napi(js_name = "poll")] pub async fn js_poll(&self) -> napi::Result<()> { self.poll().await?; Ok(()) } + /// Remove registered workspace callback #[napi(js_name = "clearCallback")] pub fn js_clear_callback(&self) -> napi::Result<()> { self.clear_callback(); Ok(()) } + /// Register a callback to be invoked every time a new event is available to consume + /// There can only be one callback registered at any given time. #[napi(js_name = "callback", ts_args_type = "fun: (err: Error|null, event: Workspace) => void")] pub fn js_callback(&self, fun: ThreadsafeFunction) -> napi::Result<()> { let tsfn: ThreadsafeFunction = fun; From cc79d33609c3e00bed16b58ad979e5e8dc8cb7d3 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 18:57:36 +0100 Subject: [PATCH 110/121] docs(py): added missing rustdocs --- src/ffi/python/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index a3adc5b3..262b3a62 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -197,6 +197,7 @@ fn init() -> PyResult { #[pymethods] impl CodempConfig { + /// Creates a new config, handling defaulted values. #[new] #[pyo3(signature = (*, username, password, **kwds))] pub fn pynew( From 708aadd3bdf02e82c4b8f2e9e051af81b960af56 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 19:04:38 +0100 Subject: [PATCH 111/121] chore(java): forgot a couple of preludes --- src/ffi/java/buffer.rs | 33 ++++++++++++++------------------- src/ffi/java/cursor.rs | 27 +++++++++++---------------- src/ffi/java/workspace.rs | 20 +++++++++++--------- 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index a02940cb..af777455 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -4,49 +4,44 @@ use crate::{ errors::ControllerError, prelude::{ CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, - CodempBufferUpdate as BufferUpdate, CodempTextChange as TextChange, - CodempWorkspaceIdentifier as WorkspaceIdentifier, + CodempBufferController as BufferController, CodempBufferUpdate as BufferUpdate, + CodempTextChange as TextChange, CodempWorkspaceIdentifier as WorkspaceIdentifier, }, }; /// Get the name of the buffer. #[jni(package = "mp.code", class = "BufferController")] -fn path(controller: &mut crate::buffer::Controller) -> String { +fn path(controller: &mut BufferController) -> String { controller.path().to_string() } /// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. #[jni(package = "mp.code", class = "BufferController")] -fn workspace_id(controller: &mut crate::buffer::Controller) -> WorkspaceIdentifier { +fn workspace_id(controller: &mut BufferController) -> WorkspaceIdentifier { controller.workspace_id().clone() } /// Get the contents of the buffers. #[jni(package = "mp.code", class = "BufferController")] -fn content(controller: &mut crate::buffer::Controller) -> Result { +fn content(controller: &mut BufferController) -> Result { super::tokio().block_on(controller.content()) } /// Try to fetch a [TextChange], or return null if there's nothing. #[jni(package = "mp.code", class = "BufferController")] -fn try_recv( - controller: &mut crate::buffer::Controller, -) -> Result, ControllerError> { +fn try_recv(controller: &mut BufferController) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } /// Block until it receives a [TextChange]. #[jni(package = "mp.code", class = "BufferController")] -fn recv(controller: &mut crate::buffer::Controller) -> Result { +fn recv(controller: &mut BufferController) -> Result { super::tokio().block_on(controller.recv()) } /// Send a [TextChange] to the server. #[jni(package = "mp.code", class = "BufferController")] -fn send( - controller: &mut crate::buffer::Controller, - change: TextChange, -) -> Result<(), ControllerError> { +fn send(controller: &mut BufferController, change: TextChange) -> Result<(), ControllerError> { controller.send(change) } @@ -54,7 +49,7 @@ fn send( #[jni(package = "mp.code", class = "BufferController")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut crate::buffer::Controller, + controller: &mut BufferController, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -66,7 +61,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |controller: crate::buffer::Controller| { + controller.callback(move |controller: BufferController| { let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -93,19 +88,19 @@ fn callback<'local>( /// Clear the callback for buffer changes. #[jni(package = "mp.code", class = "BufferController")] -fn clear_callback(controller: &mut crate::buffer::Controller) { +fn clear_callback(controller: &mut BufferController) { controller.clear_callback() } /// Block until there is a new value available. #[jni(package = "mp.code", class = "BufferController")] -fn poll(controller: &mut crate::buffer::Controller) -> Result<(), ControllerError> { +fn poll(controller: &mut BufferController) -> Result<(), ControllerError> { super::tokio().block_on(controller.poll()) } /// Acknowledge that a change has been correctly applied. #[jni(package = "mp.code", class = "BufferController")] -fn ack(controller: &mut crate::buffer::Controller, version: Vec) { +fn ack(controller: &mut BufferController, version: Vec) { controller.ack(version) } @@ -113,5 +108,5 @@ fn ack(controller: &mut crate::buffer::Controller, version: Vec) { #[allow(unsafe_code)] #[jni(package = "mp.code", class = "BufferController")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut crate::buffer::Controller) }; + let _ = unsafe { Box::from_raw(input as *mut BufferController) }; } diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 43fe0d50..3f592356 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -4,37 +4,32 @@ use crate::{ errors::ControllerError, prelude::{ CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, - CodempCursorEvent as CursorEvent, CodempCursorUpdate as CursorUpdate, - CodempWorkspaceIdentifier as WorkspaceIdentifier, + CodempCursorController as CursorController, CodempCursorEvent as CursorEvent, + CodempCursorUpdate as CursorUpdate, CodempWorkspaceIdentifier as WorkspaceIdentifier, }, }; /// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. #[jni(package = "mp.code", class = "CursorController")] -fn workspace_id(controller: &mut crate::cursor::Controller) -> WorkspaceIdentifier { +fn workspace_id(controller: &mut CursorController) -> WorkspaceIdentifier { controller.workspace_id().clone() } /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] -fn try_recv( - controller: &mut crate::cursor::Controller, -) -> Result, ControllerError> { +fn try_recv(controller: &mut CursorController) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } /// Block until it receives a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn recv(controller: &mut crate::cursor::Controller) -> Result { +fn recv(controller: &mut CursorController) -> Result { super::tokio().block_on(controller.recv()) } /// Receive from Java, converts and sends a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn send( - controller: &mut crate::cursor::Controller, - sel: CursorUpdate, -) -> Result<(), ControllerError> { +fn send(controller: &mut CursorController, sel: CursorUpdate) -> Result<(), ControllerError> { controller.send(sel) } @@ -42,7 +37,7 @@ fn send( #[jni(package = "mp.code", class = "CursorController")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut crate::cursor::Controller, + controller: &mut CursorController, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -52,7 +47,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |controller: crate::cursor::Controller| { + controller.callback(move |controller: CursorController| { let res: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -79,13 +74,13 @@ fn callback<'local>( /// Clear the callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] -fn clear_callback(controller: &mut crate::cursor::Controller) { +fn clear_callback(controller: &mut CursorController) { controller.clear_callback() } /// Block until there is a new value available. #[jni(package = "mp.code", class = "CursorController")] -fn poll(controller: &mut crate::cursor::Controller) -> Result<(), ControllerError> { +fn poll(controller: &mut CursorController) -> Result<(), ControllerError> { super::tokio().block_on(controller.poll()) } @@ -93,5 +88,5 @@ fn poll(controller: &mut crate::cursor::Controller) -> Result<(), ControllerErro #[allow(unsafe_code)] #[jni(package = "mp.code", class = "CursorController")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut crate::cursor::Controller) }; + let _ = unsafe { Box::from_raw(input as *mut CursorController) }; } diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 08c5af88..81b08e81 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -4,8 +4,10 @@ use crate::{ errors::{ConnectionError, ControllerError, RemoteError}, prelude::{ CodempAsyncReceiver as AsyncReceiver, CodempBufferAttributes as BufferAttributes, - CodempBufferNode as BufferNode, CodempUserInfo as UserInfo, CodempWorkspace as Workspace, - CodempWorkspaceEvent as WorkspaceEvent, CodempWorkspaceIdentifier as WorkspaceIdentifier, + CodempBufferController as BufferController, CodempBufferNode as BufferNode, + CodempCursorController as CursorController, CodempUserInfo as UserInfo, + CodempWorkspace as Workspace, CodempWorkspaceEvent as WorkspaceEvent, + CodempWorkspaceIdentifier as WorkspaceIdentifier, }, }; @@ -17,13 +19,13 @@ fn id(workspace: &mut Workspace) -> WorkspaceIdentifier { /// Get a cursor controller by name and returns a pointer to it. #[jni(package = "mp.code", class = "Workspace")] -fn cursor(workspace: &mut Workspace) -> crate::cursor::Controller { +fn cursor(workspace: &mut Workspace) -> CursorController { workspace.cursor() } /// Get a buffer controller by name and returns a pointer to it. #[jni(package = "mp.code", class = "Workspace")] -fn get_buffer(workspace: &mut Workspace, path: String) -> Option { +fn get_buffer(workspace: &mut Workspace, path: String) -> Option { workspace.get_buffer(&path) } @@ -67,12 +69,12 @@ fn un_pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr super::tokio().block_on(workspace.un_pin_buffer(path)) } -/// Attach to a buffer and return a pointer to its [`crate::buffer::Controller`]. +/// Attach to a buffer and return a pointer to its [`BufferController`]. #[jni(package = "mp.code", class = "Workspace")] fn attach_buffer( workspace: &mut Workspace, path: String, -) -> Result { +) -> Result { super::tokio().block_on(workspace.attach_buffer(&path)) } @@ -140,7 +142,7 @@ fn clear_callback(workspace: &mut Workspace) { #[jni(package = "mp.code", class = "Workspace")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut crate::Workspace, + controller: &mut Workspace, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -152,7 +154,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |workspace: crate::Workspace| { + controller.callback(move |workspace: Workspace| { let out: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -180,5 +182,5 @@ fn callback<'local>( #[allow(unsafe_code)] #[jni(package = "mp.code", class = "Workspace")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut crate::Workspace) }; + let _ = unsafe { Box::from_raw(input as *mut Workspace) }; } From 59b58cd85745d72cd0907684e58bfa4584019fd4 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 19:42:09 +0100 Subject: [PATCH 112/121] chore(js+java): prelude imports --- src/ffi/java/buffer.rs | 30 +++++++++----------- src/ffi/java/client.rs | 56 ++++++++++++++++++------------------- src/ffi/java/cursor.rs | 24 +++++++--------- src/ffi/java/workspace.rs | 58 ++++++++++++++++++--------------------- src/ffi/js/buffer.rs | 13 ++++++--- src/ffi/js/client.rs | 14 ++++++---- src/ffi/js/cursor.rs | 10 +++++-- src/ffi/js/workspace.rs | 14 ++++++++-- 8 files changed, 112 insertions(+), 107 deletions(-) diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index af777455..027a194b 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -2,46 +2,42 @@ use jni_toolbox::jni; use crate::{ errors::ControllerError, - prelude::{ - CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, - CodempBufferController as BufferController, CodempBufferUpdate as BufferUpdate, - CodempTextChange as TextChange, CodempWorkspaceIdentifier as WorkspaceIdentifier, - }, + prelude::* }; /// Get the name of the buffer. #[jni(package = "mp.code", class = "BufferController")] -fn path(controller: &mut BufferController) -> String { +fn path(controller: &mut CodempBufferController) -> String { controller.path().to_string() } /// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. #[jni(package = "mp.code", class = "BufferController")] -fn workspace_id(controller: &mut BufferController) -> WorkspaceIdentifier { +fn workspace_id(controller: &mut CodempBufferController) -> CodempWorkspaceIdentifier { controller.workspace_id().clone() } /// Get the contents of the buffers. #[jni(package = "mp.code", class = "BufferController")] -fn content(controller: &mut BufferController) -> Result { +fn content(controller: &mut CodempBufferController) -> Result { super::tokio().block_on(controller.content()) } /// Try to fetch a [TextChange], or return null if there's nothing. #[jni(package = "mp.code", class = "BufferController")] -fn try_recv(controller: &mut BufferController) -> Result, ControllerError> { +fn try_recv(controller: &mut CodempBufferController) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } /// Block until it receives a [TextChange]. #[jni(package = "mp.code", class = "BufferController")] -fn recv(controller: &mut BufferController) -> Result { +fn recv(controller: &mut CodempBufferController) -> Result { super::tokio().block_on(controller.recv()) } /// Send a [TextChange] to the server. #[jni(package = "mp.code", class = "BufferController")] -fn send(controller: &mut BufferController, change: TextChange) -> Result<(), ControllerError> { +fn send(controller: &mut CodempBufferController, change: CodempTextChange) -> Result<(), ControllerError> { controller.send(change) } @@ -49,7 +45,7 @@ fn send(controller: &mut BufferController, change: TextChange) -> Result<(), Con #[jni(package = "mp.code", class = "BufferController")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut BufferController, + controller: &mut CodempBufferController, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -61,7 +57,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |controller: BufferController| { + controller.callback(move |controller: CodempBufferController| { let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -88,19 +84,19 @@ fn callback<'local>( /// Clear the callback for buffer changes. #[jni(package = "mp.code", class = "BufferController")] -fn clear_callback(controller: &mut BufferController) { +fn clear_callback(controller: &mut CodempBufferController) { controller.clear_callback() } /// Block until there is a new value available. #[jni(package = "mp.code", class = "BufferController")] -fn poll(controller: &mut BufferController) -> Result<(), ControllerError> { +fn poll(controller: &mut CodempBufferController) -> Result<(), ControllerError> { super::tokio().block_on(controller.poll()) } /// Acknowledge that a change has been correctly applied. #[jni(package = "mp.code", class = "BufferController")] -fn ack(controller: &mut BufferController, version: Vec) { +fn ack(controller: &mut CodempBufferController, version: Vec) { controller.ack(version) } @@ -108,5 +104,5 @@ fn ack(controller: &mut BufferController, version: Vec) { #[allow(unsafe_code)] #[jni(package = "mp.code", class = "BufferController")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut BufferController) }; + let _ = unsafe { Box::from_raw(input as *mut CodempBufferController) }; } diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index 2b5f9596..220c2f59 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -2,69 +2,65 @@ use jni_toolbox::jni; use crate::{ errors::{ConnectionError, ControllerError, RemoteError}, - prelude::{ - CodempAsyncReceiver as AsyncReceiver, CodempClient as Client, CodempConfig as Config, - CodempSessionEvent as SessionEvent, CodempUserInfo as UserInfo, - CodempWorkspace as Workspace, CodempWorkspaceIdentifier as WorkspaceIdentifier, - }, + prelude::* }; /// Connect using the given credentials to the default server, and return a [Client] to interact with it. #[jni(package = "mp.code", class = "Client")] -fn connect(config: Config) -> Result { - super::tokio().block_on(Client::connect(config)) +fn connect(config: CodempConfig) -> Result { + super::tokio().block_on(CodempClient::connect(config)) } /// Gets the [UserInfo] for the current user. #[jni(package = "mp.code", class = "Client")] -fn current_user(client: &mut Client) -> UserInfo { +fn current_user(client: &mut CodempClient) -> CodempUserInfo { client.current_user().clone() } /// Join a [Workspace] and return a pointer to it. #[jni(package = "mp.code", class = "Client")] fn attach_workspace( - client: &mut Client, + client: &mut CodempClient, user: String, workspace: String, -) -> Result { +) -> Result { super::tokio().block_on(client.attach_workspace(user, workspace)) } /// Accepts an invitation to a workspace. #[jni(package = "mp.code", class = "Client")] -fn accept_invite(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { +fn accept_invite(client: &mut CodempClient, user: String, workspace: String) -> Result<(), RemoteError> { super::tokio().block_on(client.accept_invite(user, workspace)) } /// Rejects an invitation to a workspace. #[jni(package = "mp.code", class = "Client")] -fn reject_invite(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { +fn reject_invite(client: &mut CodempClient, user: String, workspace: String) -> Result<(), RemoteError> { super::tokio().block_on(client.reject_invite(user, workspace)) } /// Quit a joined [Workspace]. #[jni(package = "mp.code", class = "Client")] -fn quit_workspace(client: &mut Client, user: String, workspace: String) -> Result<(), RemoteError> { +fn quit_workspace(client: &mut CodempClient, user: String, workspace: String) -> Result<(), RemoteError> { super::tokio().block_on(client.quit_workspace(user, workspace)) } /// Create a workspace on server, if allowed to. #[jni(package = "mp.code", class = "Client")] -fn create_workspace(client: &mut Client, workspace: String) -> Result<(), RemoteError> { +fn create_workspace(client: &mut CodempClient, workspace: String) -> Result<(), RemoteError> { super::tokio().block_on(client.create_workspace(workspace)) } /// Delete a workspace on server, if allowed to. #[jni(package = "mp.code", class = "Client")] -fn delete_workspace(client: &mut Client, workspace: String) -> Result<(), RemoteError> { +fn delete_workspace(client: &mut CodempClient, workspace: String) -> Result<(), RemoteError> { super::tokio().block_on(client.delete_workspace(workspace)) } /// Invite another user to an owned workspace. #[jni(package = "mp.code", class = "Client")] fn invite_to_workspace( - client: &mut Client, + client: &mut CodempClient, workspace: String, user: String, ) -> Result<(), RemoteError> { @@ -73,49 +69,49 @@ fn invite_to_workspace( /// List owned workspaces. #[jni(package = "mp.code", class = "Client")] -fn fetch_owned_workspaces(client: &mut Client) -> Result, RemoteError> { +fn fetch_owned_workspaces(client: &mut CodempClient) -> Result, RemoteError> { super::tokio().block_on(client.fetch_owned_workspaces()) } /// List joined workspaces. #[jni(package = "mp.code", class = "Client")] -fn fetch_joined_workspaces(client: &mut Client) -> Result, RemoteError> { +fn fetch_joined_workspaces(client: &mut CodempClient) -> Result, RemoteError> { super::tokio().block_on(client.fetch_joined_workspaces()) } /// List available workspaces. #[jni(package = "mp.code", class = "Client")] -fn active_workspaces(client: &mut Client) -> Vec { +fn active_workspaces(client: &mut CodempClient) -> Vec { client.active_workspaces() } /// Leave a [Workspace] and return whether or not the client was in such workspace. #[jni(package = "mp.code", class = "Client")] -fn leave_workspace(client: &mut Client, user: String, workspace: String) -> bool { +fn leave_workspace(client: &mut CodempClient, user: String, workspace: String) -> bool { client.leave_workspace(user, workspace) } /// Get a [Workspace] by name and returns a pointer to it. #[jni(package = "mp.code", class = "Client")] -fn get_workspace(client: &mut Client, user: String, workspace: String) -> Option { +fn get_workspace(client: &mut CodempClient, user: String, workspace: String) -> Option { client.get_workspace(user, workspace) } /// Fetches information about a user. #[jni(package = "mp.code", class = "Client")] -fn get_user_info(client: &mut Client, user: String) -> Result { +fn get_user_info(client: &mut CodempClient, user: String) -> Result { super::tokio().block_on(client.get_user_info(user)) } /// Try to fetch a [TextChange], or return null if there's nothing. #[jni(package = "mp.code", class = "Client")] -fn try_recv(client: &mut Client) -> Result, ControllerError> { +fn try_recv(client: &mut CodempClient) -> Result, ControllerError> { super::tokio().block_on(client.try_recv()) } /// Block until it receives a [TextChange]. #[jni(package = "mp.code", class = "Client")] -fn recv(client: &mut Client) -> Result { +fn recv(client: &mut CodempClient) -> Result { super::tokio().block_on(client.recv()) } @@ -123,7 +119,7 @@ fn recv(client: &mut Client) -> Result { #[jni(package = "mp.code", class = "Client")] fn callback<'local>( env: &mut jni::Env<'local>, - client: &mut Client, + client: &mut CodempClient, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -135,7 +131,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - client.callback(move |controller: Client| { + client.callback(move |controller: CodempClient| { let result: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -162,19 +158,19 @@ fn callback<'local>( /// Clear the callback for client changes. #[jni(package = "mp.code", class = "Client")] -fn clear_callback(client: &mut Client) { +fn clear_callback(client: &mut CodempClient) { client.clear_callback() } /// Block until there is a new value available. #[jni(package = "mp.code", class = "Client")] -fn poll(client: &mut Client) -> Result<(), ControllerError> { +fn poll(client: &mut CodempClient) -> Result<(), ControllerError> { super::tokio().block_on(client.poll()) } /// Refresh the client's session token. #[jni(package = "mp.code", class = "Client")] -fn refresh(client: &mut Client) -> Result<(), RemoteError> { +fn refresh(client: &mut CodempClient) -> Result<(), RemoteError> { super::tokio().block_on(client.refresh()) } @@ -182,5 +178,5 @@ fn refresh(client: &mut Client) -> Result<(), RemoteError> { #[allow(unsafe_code)] #[jni(package = "mp.code", class = "Client")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut Client) }; + let _ = unsafe { Box::from_raw(input as *mut CodempClient) }; } diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 3f592356..beb3299f 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -2,34 +2,30 @@ use jni_toolbox::jni; use crate::{ errors::ControllerError, - prelude::{ - CodempAsyncReceiver as AsyncReceiver, CodempAsyncSender as AsyncSender, - CodempCursorController as CursorController, CodempCursorEvent as CursorEvent, - CodempCursorUpdate as CursorUpdate, CodempWorkspaceIdentifier as WorkspaceIdentifier, - }, + prelude::*, }; /// Get the [WorkspaceIdentifier] of the workspace that contains this buffer. #[jni(package = "mp.code", class = "CursorController")] -fn workspace_id(controller: &mut CursorController) -> WorkspaceIdentifier { +fn workspace_id(controller: &mut CodempCursorController) -> CodempWorkspaceIdentifier { controller.workspace_id().clone() } /// Try to fetch a [Cursor], or returns null if there's nothing. #[jni(package = "mp.code", class = "CursorController")] -fn try_recv(controller: &mut CursorController) -> Result, ControllerError> { +fn try_recv(controller: &mut CodempCursorController) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } /// Block until it receives a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn recv(controller: &mut CursorController) -> Result { +fn recv(controller: &mut CodempCursorController) -> Result { super::tokio().block_on(controller.recv()) } /// Receive from Java, converts and sends a [Cursor]. #[jni(package = "mp.code", class = "CursorController")] -fn send(controller: &mut CursorController, sel: CursorUpdate) -> Result<(), ControllerError> { +fn send(controller: &mut CodempCursorController, sel: CodempCursorUpdate) -> Result<(), ControllerError> { controller.send(sel) } @@ -37,7 +33,7 @@ fn send(controller: &mut CursorController, sel: CursorUpdate) -> Result<(), Cont #[jni(package = "mp.code", class = "CursorController")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut CursorController, + controller: &mut CodempCursorController, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -47,7 +43,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |controller: CursorController| { + controller.callback(move |controller: CodempCursorController| { let res: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -74,13 +70,13 @@ fn callback<'local>( /// Clear the callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] -fn clear_callback(controller: &mut CursorController) { +fn clear_callback(controller: &mut CodempCursorController) { controller.clear_callback() } /// Block until there is a new value available. #[jni(package = "mp.code", class = "CursorController")] -fn poll(controller: &mut CursorController) -> Result<(), ControllerError> { +fn poll(controller: &mut CodempCursorController) -> Result<(), ControllerError> { super::tokio().block_on(controller.poll()) } @@ -88,5 +84,5 @@ fn poll(controller: &mut CursorController) -> Result<(), ControllerError> { #[allow(unsafe_code)] #[jni(package = "mp.code", class = "CursorController")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut CursorController) }; + let _ = unsafe { Box::from_raw(input as *mut CodempCursorController) }; } diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index 81b08e81..8e56cf4d 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -2,139 +2,133 @@ use jni_toolbox::jni; use crate::{ errors::{ConnectionError, ControllerError, RemoteError}, - prelude::{ - CodempAsyncReceiver as AsyncReceiver, CodempBufferAttributes as BufferAttributes, - CodempBufferController as BufferController, CodempBufferNode as BufferNode, - CodempCursorController as CursorController, CodempUserInfo as UserInfo, - CodempWorkspace as Workspace, CodempWorkspaceEvent as WorkspaceEvent, - CodempWorkspaceIdentifier as WorkspaceIdentifier, - }, + prelude::*, }; /// Get the workspace id. #[jni(package = "mp.code", class = "Workspace")] -fn id(workspace: &mut Workspace) -> WorkspaceIdentifier { +fn id(workspace: &mut CodempWorkspace) -> CodempWorkspaceIdentifier { workspace.id().clone() } /// Get a cursor controller by name and returns a pointer to it. #[jni(package = "mp.code", class = "Workspace")] -fn cursor(workspace: &mut Workspace) -> CursorController { +fn cursor(workspace: &mut CodempWorkspace) -> CodempCursorController { workspace.cursor() } /// Get a buffer controller by name and returns a pointer to it. #[jni(package = "mp.code", class = "Workspace")] -fn get_buffer(workspace: &mut Workspace, path: String) -> Option { +fn get_buffer(workspace: &mut CodempWorkspace, path: String) -> Option { workspace.get_buffer(&path) } /// Searches for buffers matching the filter. #[jni(package = "mp.code", class = "Workspace")] -fn search_buffers(workspace: &mut Workspace, filter: Option) -> Vec { +fn search_buffers(workspace: &mut CodempWorkspace, filter: Option) -> Vec { workspace.search_buffers(filter.as_deref()) } /// Gets a list of the active buffers. #[jni(package = "mp.code", class = "Workspace")] -fn active_buffers(workspace: &mut Workspace) -> Vec { +fn active_buffers(workspace: &mut CodempWorkspace) -> Vec { workspace.active_buffers() } /// Gets a list of the active buffers. #[jni(package = "mp.code", class = "Workspace")] -fn user_list(workspace: &mut Workspace) -> Vec { +fn user_list(workspace: &mut CodempWorkspace) -> Vec { workspace.user_list() } /// Create a new buffer. #[jni(package = "mp.code", class = "Workspace")] fn create_buffer( - workspace: &mut Workspace, + workspace: &mut CodempWorkspace, path: String, - attributes: Option, + attributes: Option, ) -> Result<(), RemoteError> { super::tokio().block_on(workspace.create_buffer(path, attributes)) } /// Pins an ephemeral buffer. #[jni(package = "mp.code", class = "Workspace")] -fn pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { +fn pin_buffer(workspace: &mut CodempWorkspace, path: String) -> Result<(), RemoteError> { super::tokio().block_on(workspace.pin_buffer(path)) } /// Un-pins an ephemeral buffer. #[jni(package = "mp.code", class = "Workspace")] -fn un_pin_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { +fn un_pin_buffer(workspace: &mut CodempWorkspace, path: String) -> Result<(), RemoteError> { super::tokio().block_on(workspace.un_pin_buffer(path)) } /// Attach to a buffer and return a pointer to its [`BufferController`]. #[jni(package = "mp.code", class = "Workspace")] fn attach_buffer( - workspace: &mut Workspace, + workspace: &mut CodempWorkspace, path: String, -) -> Result { +) -> Result { super::tokio().block_on(workspace.attach_buffer(&path)) } /// Detach from a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn detach_buffer(workspace: &mut Workspace, path: String) -> bool { +fn detach_buffer(workspace: &mut CodempWorkspace, path: String) -> bool { workspace.detach_buffer(&path) } /// Update the local buffer list. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_buffers(workspace: &mut Workspace) -> Result<(), RemoteError> { +fn fetch_buffers(workspace: &mut CodempWorkspace) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_buffers()) } /// Update the local user list. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_users(workspace: &mut Workspace) -> Result<(), RemoteError> { +fn fetch_users(workspace: &mut CodempWorkspace) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_users()) } /// Fetch users attached to a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn fetch_buffer_users(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { +fn fetch_buffer_users(workspace: &mut CodempWorkspace, path: String) -> Result<(), RemoteError> { super::tokio().block_on(workspace.fetch_buffer_users(&path)) } /// Fetch users attached to a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn buffer_user_list(workspace: &mut Workspace, path: String) -> Vec { +fn buffer_user_list(workspace: &mut CodempWorkspace, path: String) -> Vec { workspace.buffer_user_list(&path) } /// Delete a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn delete_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteError> { +fn delete_buffer(workspace: &mut CodempWorkspace, path: String) -> Result<(), RemoteError> { super::tokio().block_on(workspace.delete_buffer(&path)) } /// Block and receive a workspace event. #[jni(package = "mp.code", class = "Workspace")] -fn recv(workspace: &mut Workspace) -> Result { +fn recv(workspace: &mut CodempWorkspace) -> Result { super::tokio().block_on(workspace.recv()) } /// Receive a workspace event if present. #[jni(package = "mp.code", class = "Workspace")] -fn try_recv(workspace: &mut Workspace) -> Result, ControllerError> { +fn try_recv(workspace: &mut CodempWorkspace) -> Result, ControllerError> { super::tokio().block_on(workspace.try_recv()) } /// Block until a workspace event is available. #[jni(package = "mp.code", class = "Workspace")] -fn poll(workspace: &mut Workspace) -> Result<(), ControllerError> { +fn poll(workspace: &mut CodempWorkspace) -> Result<(), ControllerError> { super::tokio().block_on(workspace.poll()) } /// Clear previously registered callback. #[jni(package = "mp.code", class = "Workspace")] -fn clear_callback(workspace: &mut Workspace) { +fn clear_callback(workspace: &mut CodempWorkspace) { workspace.clear_callback(); } @@ -142,7 +136,7 @@ fn clear_callback(workspace: &mut Workspace) { #[jni(package = "mp.code", class = "Workspace")] fn callback<'local>( env: &mut jni::Env<'local>, - controller: &mut Workspace, + controller: &mut CodempWorkspace, cb: jni::objects::JObject<'local>, ) -> Result<(), jni::errors::Error> { if cb.is_null() { @@ -154,7 +148,7 @@ fn callback<'local>( let cb_ref = env.new_global_ref(cb)?; let jvm = env.get_java_vm()?; - controller.callback(move |workspace: Workspace| { + controller.callback(move |workspace: CodempWorkspace| { let out: Result<(), jni::errors::Error> = jvm.attach_current_thread(|env| { env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -182,5 +176,5 @@ fn callback<'local>( #[allow(unsafe_code)] #[jni(package = "mp.code", class = "Workspace")] fn free(input: jni::sys::jlong) { - let _ = unsafe { Box::from_raw(input as *mut Workspace) }; + let _ = unsafe { Box::from_raw(input as *mut CodempWorkspace) }; } diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index b19555d0..5f179784 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,9 +1,14 @@ -use crate::api::{BufferUpdate, TextChange, controller::{AsyncReceiver, AsyncSender}}; -use codemp_proto::session::WorkspaceIdentifier; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use crate::buffer::controller::BufferController; +use crate::prelude::{ + CodempAsyncReceiver as AsyncReceiver, + CodempAsyncSender as AsyncSender, + CodempBufferController as BufferController, + CodempBufferUpdate as BufferUpdate, + CodempTextChange as TextChange, + CodempWorkspaceIdentifier as WorkspaceIdentifier, +}; #[napi] impl BufferController { @@ -19,7 +24,7 @@ impl BufferController { ) -> napi::Result<()> { self.callback(move |controller: BufferController| { fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); - //check this with tracing also we could use Ok(event) to get the error + // check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this }); diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 45a12442..1001f230 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,16 +1,20 @@ -use codemp_proto::{common::UserInfo, session::WorkspaceIdentifier}; use napi_derive::napi; +use crate::prelude::{ + CodempConfig as Config, + CodempClient as Client, + CodempUserInfo as UserInfo, + CodempWorkspace as Workspace, + CodempWorkspaceIdentifier as WorkspaceIdentifier, +}; /// connect to codemp servers and return a client session #[allow(dead_code)] #[napi] -pub async fn connect(config: crate::api::Config) -> napi::Result { - Ok(crate::Client::connect(config).await?) +pub async fn connect(config: Config) -> napi::Result { + Ok(Client::connect(config).await?) } -use crate::{Client, Workspace}; - #[napi] impl Client { #[napi(js_name = "createWorkspace")] diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index bf2e21f9..6f4618d5 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -1,8 +1,14 @@ -use codemp_proto::{cursor::{CursorEvent, CursorUpdate}, session::WorkspaceIdentifier}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use crate::{api::{AsyncReceiver, AsyncSender}, cursor::controller::CursorController}; +use crate::prelude::{ + CodempAsyncReceiver as AsyncReceiver, + CodempAsyncSender as AsyncSender, + CodempCursorController as CursorController, + CodempCursorEvent as CursorEvent, + CodempCursorUpdate as CursorUpdate, + CodempWorkspaceIdentifier as WorkspaceIdentifier, +}; #[napi] impl CursorController { diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index b9bec6a5..a13e972d 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,9 +1,17 @@ -use codemp_proto::{common::UserInfo, buffer::{BufferAttributes, BufferNode}, session::WorkspaceIdentifier, workspace::WorkspaceEvent}; -use crate::{api::AsyncReceiver, buffer::controller::BufferController, cursor::controller::CursorController}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use crate::Workspace; +use crate::prelude::{ + CodempAsyncReceiver as AsyncReceiver, + CodempBufferAttributes as BufferAttributes, + CodempBufferNode as BufferNode, + CodempBufferController as BufferController, + CodempCursorController as CursorController, + CodempUserInfo as UserInfo, + CodempWorkspace as Workspace, + CodempWorkspaceEvent as WorkspaceEvent, + CodempWorkspaceIdentifier as WorkspaceIdentifier, +}; #[napi] impl Workspace { From 2f524626e729288b796761a6aa22516d44385f00 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 19 Mar 2026 19:46:02 +0100 Subject: [PATCH 113/121] feat(js): coverage --- src/ffi/js/client.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 1001f230..2eaf9332 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,9 +1,12 @@ +use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; use crate::prelude::{ + CodempAsyncReceiver as AsyncReceiver, CodempConfig as Config, CodempClient as Client, CodempUserInfo as UserInfo, + CodempSessionEvent as SessionEvent, CodempWorkspace as Workspace, CodempWorkspaceIdentifier as WorkspaceIdentifier, }; @@ -111,5 +114,46 @@ impl Client { Ok(self.reject_invite(&user, &workspace).await?) } + /// Register a callback to be called on receive. + /// There can only be one callback registered at any given time. + #[napi( + js_name = "callback", + ts_args_type = "fun: (err: Error|null, event: Client) => void" + )] + pub fn js_callback( + &self, + fun: ThreadsafeFunction, + ) -> napi::Result<()> { + self.callback(move |controller: Client| { + fun.call(Ok(controller.clone()), ThreadsafeFunctionCallMode::Blocking); + //check this with tracing also we could use Ok(event) to get the error + // If it blocks the main thread too many time we have to change this + }); + + Ok(()) + } + + /// Clear the registered callback + #[napi(js_name = "clearCallback")] + pub fn js_clear_callback(&self) { + self.clear_callback(); + } + /// Get next session event if available without blocking + #[napi(js_name = "tryRecv")] + pub async fn js_try_recv(&self) -> napi::Result> { + Ok(self.try_recv().await?) + } + + /// Block until next session event + #[napi(js_name = "recv")] + pub async fn js_recv(&self) -> napi::Result { + Ok(self.recv().await?) + } + + /// Block until next session event without returning it + #[napi(js_name = "poll")] + pub async fn js_poll(&self) -> napi::Result<()> { + Ok(self.poll().await?) + } } From f66942965fc04fd4e79e8841c65f7cc550cb023a Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 19 Mar 2026 19:50:08 +0100 Subject: [PATCH 114/121] fix: add re-polling to workers so they quit, log how many refs remain --- src/buffer/worker.rs | 6 ++++-- src/client.rs | 16 +++++++++++++--- src/workspace.rs | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index 604ed9db..168714d0 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -106,7 +106,7 @@ impl BufferController { loop { if worker.controller.upgrade().is_none() { break tracing::debug!("buffer worker clean exit"); - }; + } // block until one of these is ready tokio::select! { @@ -159,7 +159,9 @@ impl BufferController { tx.send(content) .unwrap_or_warn("checkout request dropped"); }, - } + }, + + _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {}, } } diff --git a/src/client.rs b/src/client.rs index f7c36ec2..dc7d0342 100644 --- a/src/client.rs +++ b/src/client.rs @@ -332,9 +332,13 @@ impl Client { /// Leave the [`Workspace`] with the given name. pub fn leave_workspace(&self, user: impl AsRef, workspace: impl AsRef) -> bool { - if let Some(wss) = self.0.workspaces.get_mut(user.as_ref()) { - if wss.remove(workspace.as_ref()).is_some() { - return true; + if let Some(intermediate) = self.0.workspaces.get_mut(user.as_ref()) { + if let Some((_name, ws)) = intermediate.remove(workspace.as_ref()) { + let count = Arc::strong_count(&ws.0); + tracing::debug!("there are {} more references to this workspace", count - 1); + if Arc::into_inner(ws.0).is_some() { + return true; + } } } @@ -417,7 +421,13 @@ impl ClientWorker { ) { tracing::debug!("client worker starting"); loop { + if weak.upgrade().is_none() { + break tracing::debug!("client worker clean exit"); + } + tokio::select! { + _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => {}, + res = self.poll_rx.recv() => match res { None => break tracing::debug!("pollers channel closed: client has been dropped"), Some(x) => self.pollers.push(x), diff --git a/src/workspace.rs b/src/workspace.rs index e0df012c..d2636968 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -273,9 +273,13 @@ impl Workspace { pub fn detach_buffer(&self, path: impl AsRef) -> bool { match self.0.buffers.remove(path.as_ref()) { None => true, // noop: we werent attached in the first place - Some((_name, controller)) => match Arc::into_inner(controller.0) { - None => false, // dangling ref! we can't drop this - Some(_) => true, // dropping it now + Some((_name, controller)) => { + let count = Arc::strong_count(&controller.0); + tracing::debug!("there are {} more references to this buffer controller", count - 1); + match Arc::into_inner(controller.0) { + Some(_) => true, // dropping it now + None => false, // dangling ref! we can't drop this + } }, } } @@ -423,7 +427,14 @@ impl WorkspaceWorker { ) { tracing::debug!("workspace worker starting"); loop { + if weak.upgrade().is_none() { + break tracing::debug!("workspace worker clean exit"); + } + tokio::select! { + // re-poll every 10s + _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {}, + res = self.poll_rx.recv() => match res { None => break tracing::debug!("pollers channel closed: workspace has been dropped"), Some(x) => self.pollers.push(x), From b991a826387b84894c7137c29fcf15378cc311bd Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 19 Mar 2026 19:52:46 +0100 Subject: [PATCH 115/121] fix(lua): update annotations --- dist/lua/annotations.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index c61e2051..1efcbcc7 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -390,12 +390,12 @@ function Workspace:active_buffers() end function Workspace:cursor() end ---@param path string relative path ("name") of new buffer ----@param ephemeral boolean wether this buffer is ephemeral (auto deletes) +---@param attributes BufferAttributes? buffer attributes for this new buffer ---@return NilPromise ---@async ---@nodiscard ---create a new empty buffer -function Workspace:create_buffer(path, ephemeral) end +function Workspace:create_buffer(path, attributes) end ---@param path string relative path ("name") of buffer to delete ---@return NilPromise From 7b2a89bb5d2de28e0451ec40766a933c1f5d87f1 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 19 Mar 2026 23:51:12 +0100 Subject: [PATCH 116/121] fix: avoid tracing whole ops buffer --- src/buffer/worker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index 168714d0..6639e6ab 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -211,7 +211,7 @@ impl BufferWorker { } } - #[tracing::instrument(skip(self))] + #[tracing::instrument(skip(self, change), fields(user = change.user))] async fn handle_server_change(&mut self, change: BufferEvent) -> bool { match self.controller.upgrade() { None => { From d1c6d7d7a2eb3cbac33c67099bef8d5f3f706102 Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 19 Mar 2026 23:04:48 +0100 Subject: [PATCH 117/121] py automatic hints --- Cargo.lock | 50 ++++++++++++++++++ Cargo.toml | 7 ++- build.rs | 68 +++++++++++++++++++++++++ src/ffi/python/mod.rs | 116 ++++++++++++++++++++++++++++++++---------- 4 files changed, 211 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae56b6f0..a95591ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,7 @@ dependencies = [ "napi-derive", "pyo3", "pyo3-build-config", + "pyo3-introspection", "regex", "serde", "syn 2.0.117", @@ -525,6 +526,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "goblin" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "h2" version = "0.4.13" @@ -1209,6 +1221,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1361,6 +1379,18 @@ dependencies = [ "pyo3-build-config", ] +[[package]] +name = "pyo3-introspection" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11f40a1f5ec62a36963d4b4b0c051fac90c879c640baa975f45cd01afd3c38" +dependencies = [ + "anyhow", + "goblin", + "serde", + "serde_json", +] + [[package]] name = "pyo3-macros" version = "0.28.2" @@ -1596,6 +1626,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "security-framework" version = "3.7.0" diff --git a/Cargo.toml b/Cargo.toml index e36062c5..bd2240a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,19 +58,22 @@ napi = { version = "3.1", features = ["full"], optional = true } napi-derive = { version="3.1", optional = true} # glue (python) -pyo3 = { version = "0.28", features = ["multiple-pymethods", "uuid"], optional = true} +pyo3 = { version = "0.28", features = ["multiple-pymethods", "experimental-inspect"], optional = true} + # extra async-trait = { version = "0.1", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } -syn = { version = "2.0.117", features = ["full", "visit"], optional = true } +syn = { version = "2.0", features = ["full"], optional = true } regex = { version = "1.12", optional = true } + [build-dependencies] # glue (js) napi-build = { version = "2.2", optional = true } # glue (python) pyo3-build-config = { version = "0.28", optional = true } +pyo3-introspection = { version = "0.28", optional = true } [features] default = ["lua-jit", "py-abi3", "py-extmod"] diff --git a/build.rs b/build.rs index dab48246..78ac0698 100644 --- a/build.rs +++ b/build.rs @@ -7,6 +7,9 @@ extern crate napi_build; #[cfg(feature = "py")] extern crate pyo3_build_config; +#[cfg(feature = "py")] +extern crate pyo3_introspection; + /// The main method of the buildscript, required by some glue modules. fn main() { #[cfg(feature = "js")] @@ -17,6 +20,71 @@ fn main() { #[cfg(feature = "py")] { pyo3_build_config::add_extension_module_link_args(); + + // The Python introspection step requires an already-built cdylib, which + // does not exist during the first clean build. Make it an opt-in second pass. + println!("cargo:rerun-if-env-changed=CODEMP_PY_GENHINTS"); + if std::env::var("CODEMP_PY_GENHINTS").as_deref() != Ok("1") { + return; + } + + let out_dir = std::env::var("OUT_DIR").expect("unreachable"); + let dylib = if let Ok("windows") = std::env::var("CARGO_CFG_TARGET_OS").as_deref() { + "libcodemp.dll" + } else if let Ok("macos") = std::env::var("CARGO_CFG_TARGET_OS").as_deref() { + "libcodemp.dylib" + } else { + "libcodemp.so" + }; + + let lib_path = std::path::Path::new(&out_dir) + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join(dylib); + if !lib_path.exists() { + println!( + "cargo:warning=skipping python postprocess, cdylib missing at {}", + lib_path.display() + ); + return; + } + + let module_hints = pyo3_introspection::introspect_cdylib(lib_path, "codemplib") + .expect("could not extract embedded type hints."); + let output = pyo3_introspection::module_stub_files(&module_hints); + println!("{output:?}"); + + let root_dir = std::env::var("CARGO_MANIFEST_DIR").expect("unreachable"); + let pydist = std::path::Path::new(&root_dir) + .join("dist") + .join("py") + .join("src") + .join("autocodemp"); + + let _: Vec<_> = output + .into_iter() + .map(|(p, content)| { + let outpath = pydist.join(p); + if let Some(parent) = outpath.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|_| { + panic!( + "failed to create dir {:?}", + parent.to_str().expect("invalid path.") + ) + }); + } + std::fs::write(&outpath, content).unwrap_or_else(|_| { + panic!( + "failed to create file {:?}", + outpath.to_str().expect("invalid path.") + ) + }); + }) + .collect(); } #[cfg(feature = "lua")] diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index 262b3a62..b3250a18 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -163,19 +163,19 @@ fn init() -> PyResult { // fn get_name(&self) -> pyo3::PyResult { // Ok(self.name.clone()) // } -// +// // #[getter] // fn get_display_name(&self) -> pyo3::PyResult { // Ok(self.display_name.clone().unwrap_or(self.name.clone())) // } -// +// // #[setter] // fn set_display_name(&mut self, value: String) -> pyo3::PyResult<()> { // self.display_name.replace(value); -// +// // Ok(()) // } -// +// // #[getter] // fn get_description(&self) -> pyo3::PyResult { // Ok(self @@ -183,13 +183,13 @@ fn init() -> PyResult { // .clone() // .unwrap_or("No description.".to_string())) // } -// +// // #[setter] // fn set_description(&mut self, value: String) -> pyo3::PyResult<()> { // self.description.replace(value); // Ok(()) // } -// +// // fn __str__(&self) -> String { // format!("{self:?}") // } @@ -261,7 +261,7 @@ impl CodempConfig { // }) // } // } -// +// // fn __str__(&self) -> String { // format!("{self:?}") // } @@ -298,7 +298,7 @@ impl CodempConfig { // }) // } // } -// +// // fn __str__(&self) -> String { // format!("{self:?}") // } @@ -375,29 +375,89 @@ impl From for PyErr { } } -#[pymodule] -fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(version, m)?)?; - m.add_function(wrap_pyfunction!(init, m)?)?; - m.add_function(wrap_pyfunction!(connect, m)?)?; - m.add_function(wrap_pyfunction!(set_logger, m)?)?; - m.add_class::()?; +// #[pymodule] +// fn codemp(m: &Bound<'_, PyModule>) -> PyResult<()> { +// m.add_function(wrap_pyfunction!(version, m)?)?; +// m.add_function(wrap_pyfunction!(init, m)?)?; +// m.add_function(wrap_pyfunction!(connect, m)?)?; +// m.add_function(wrap_pyfunction!(set_logger, m)?)?; +// m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; - m.add_class::()?; +// m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; +// m.add_class::()?; - Ok(()) +// Ok(()) +// } + +#[pymodule(name = "codemplib")] +mod pycodemp { + #[pymodule_export] + use super::version; + + #[pymodule_export] + use super::init; + + #[pymodule_export] + use super::connect; + + #[pymodule_export] + use super::set_logger; + + #[pymodule_export] + use super::Driver; + + #[pymodule_export] + use super::Promise; + + #[pymodule_export] + use super::CodempBufferController; + + #[pymodule_export] + use super::CodempBufferUpdate; + + #[pymodule_export] + use super::CodempTextChange; + + #[pymodule_export] + use super::CodempCursorPosition; + + #[pymodule_export] + use super::CodempCursorUpdate; + + #[pymodule_export] + use super::CodempCursorController; + + #[pymodule_export] + use super::CodempUserInfo; + + #[pymodule_export] + use super::CodempWorkspace; + + #[pymodule_export] + use super::WorkspaceIdentifier; + + #[pymodule_export] + use super::CodempWorkspaceEvent; + + #[pymodule_export] + use super::CodempSessionEvent; + + #[pymodule_export] + use super::CodempClient; + + #[pymodule_export] + use super::CodempConfig; } From 722d9db1bcf7114faf581ea41b6c27b635c0b575 Mon Sep 17 00:00:00 2001 From: cschen Date: Fri, 20 Mar 2026 00:23:41 +0100 Subject: [PATCH 118/121] (py): fix build script to create correct structure for maturin --- Cargo.toml | 2 +- build.rs | 34 +++-- dist/py/src/autocodemp/__init__.py | 6 + dist/py/src/autocodemp/codemplib.pyi | 182 +++++++++++++++++++++++++++ dist/py/src/codemp/__init__.py | 8 +- src/ffi/python/mod.rs | 4 +- 6 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 dist/py/src/autocodemp/__init__.py create mode 100644 dist/py/src/autocodemp/codemplib.pyi diff --git a/Cargo.toml b/Cargo.toml index bd2240a8..90e46b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ test-coverage = ["dep:syn", "dep:regex"] # ffi java = ["dep:jni", "dep:tracing-subscriber", "dep:jni-toolbox", "codemp-proto/java"] js = ["dep:napi-build", "dep:tracing-subscriber", "dep:napi", "dep:napi-derive", "codemp-proto/js"] -py = ["dep:pyo3", "dep:tracing-subscriber", "dep:pyo3-build-config", "codemp-proto/py"] +py = ["dep:pyo3", "dep:tracing-subscriber", "dep:pyo3-build-config", "dep:pyo3-introspection", "codemp-proto/py"] lua = ["serialize", "dep:mlua", "dep:mlua-serde-derive", "dep:tracing-subscriber", "codemp-proto/lua"] # ffi variants lua-jit = ["mlua?/luajit"] diff --git a/build.rs b/build.rs index 78ac0698..8d0dce3b 100644 --- a/build.rs +++ b/build.rs @@ -65,26 +65,20 @@ fn main() { .join("src") .join("autocodemp"); - let _: Vec<_> = output - .into_iter() - .map(|(p, content)| { - let outpath = pydist.join(p); - if let Some(parent) = outpath.parent() { - std::fs::create_dir_all(parent).unwrap_or_else(|_| { - panic!( - "failed to create dir {:?}", - parent.to_str().expect("invalid path.") - ) - }); - } - std::fs::write(&outpath, content).unwrap_or_else(|_| { - panic!( - "failed to create file {:?}", - outpath.to_str().expect("invalid path.") - ) - }); - }) - .collect(); + let pyicontent = output + .get_key_value(std::path::Path::new("__init__.pyi")) + .unwrap() + .1; + + let pyinit = "\ +from .codemplib import * + +__doc__ = codemplib.__doc__ +if hasattr(codemplib, '__all__'): +__all__ = codemplib.__all__"; + + let _ = std::fs::write(pydist.join("__init__.py"), pyinit); + let _ = std::fs::write(pydist.join("codemplib.pyi"), pyicontent); } #[cfg(feature = "lua")] diff --git a/dist/py/src/autocodemp/__init__.py b/dist/py/src/autocodemp/__init__.py new file mode 100644 index 00000000..dbf9696c --- /dev/null +++ b/dist/py/src/autocodemp/__init__.py @@ -0,0 +1,6 @@ + +from .codemplib import * + +__doc__ = codemplib.__doc__ +if hasattr(codemplib, '__all__'): +__all__ = codemplib.__all__ \ No newline at end of file diff --git a/dist/py/src/autocodemp/codemplib.pyi b/dist/py/src/autocodemp/codemplib.pyi new file mode 100644 index 00000000..4c1d4465 --- /dev/null +++ b/dist/py/src/autocodemp/codemplib.pyi @@ -0,0 +1,182 @@ +from collections.abc import Sequence +from typing import Any, final + +@final +class BufferController: + def ack(self, /, v: Sequence[int]) -> None: ... + def callback(self, /, cb: Any) -> None: ... + def clear_callback(self, /) -> None: ... + def content(self, /) -> Promise: ... + def path(self, /) -> str: ... + def poll(self, /) -> Promise: ... + def recv(self, /) -> Promise: ... + def send(self, /, op: TextChange) -> None: ... + def try_recv(self, /) -> Promise: ... + def workspace_id(self, /) -> WorkspaceIdentifier: ... + +@final +class BufferUpdate: + @property + def change(self, /) -> TextChange: ... + @property + def hash(self, /) -> int |None: ... + @property + def version(self, /) -> list[int]: ... + +@final +class Client: + def accept_invite(self, /, user: str, workspace: str) -> Promise: ... + def active_workspaces(self, /) -> list[WorkspaceIdentifier]: ... + def attach_workspace(self, /, user: str, workspace: str) -> Promise: ... + def create_workspace(self, /, workspace: str) -> Promise: ... + def current_user(self, /) -> UserInfo: ... + def delete_workspace(self, /, workspace: str) -> Promise: ... + def fetch_joined_workspaces(self, /) -> Promise: ... + def fetch_owned_workspaces(self, /) -> Promise: ... + def get_user_info(self, /, user: str) -> Promise: ... + def get_workspace(self, /, user: str, workspace: str) -> Workspace |None: ... + def invite_to_workspace(self, /, workspace: str, user: str) -> Promise: ... + def leave_workspace(self, /, user: str, workspace: str) -> bool: ... + def quit_workspace(self, /, user: str, workspace: str) -> Promise: ... + def refresh(self, /) -> Promise: ... + def reject_invite(self, /, user: str, workspace: str) -> Promise: ... + +@final +class Config: + def __new__(cls, /, *, username: str, password: str, **kwds) -> Config: ... + def __str__(self, /) -> str: ... + @property + def host(self, /) -> str |None: ... + @host.setter + def host(self, /, value: str |None) -> None: ... + @property + def password(self, /) -> str: ... + @password.setter + def password(self, /, value: str) -> None: ... + @property + def port(self, /) -> int |None: ... + @port.setter + def port(self, /, value: int |None) -> None: ... + @property + def tls(self, /) -> bool |None: ... + @tls.setter + def tls(self, /, value: bool |None) -> None: ... + @property + def username(self, /) -> str: ... + @username.setter + def username(self, /, value: str) -> None: ... + +@final +class CursorController: + def callback(self, /, cb: Any) -> None: ... + def clear_callback(self, /) -> None: ... + def poll(self, /) -> Promise: ... + def recv(self, /) -> Promise: ... + def send(self, /, pos: CursorUpdate) -> None: ... + def try_recv(self, /) -> Promise: ... + def workspace_id(self, /) -> WorkspaceIdentifier: ... + +@final +class CursorPosition: + @property + def finish(self, /) -> Any: ... + @property + def start(self, /) -> Any: ... + +@final +class CursorUpdate: + @property + def buffer(self, /) -> str: ... + @property + def cursors(self, /) -> list[CursorPosition]: ... + +@final +class Driver: + def stop(self, /) -> None: ... + +@final +class Promise: + def done(self, /) -> bool: ... + def wait(self, /) -> Any: ... + +@final +class SessionEvent: + @property + def kind(self, /) -> int: ... + @property + def user(self, /) -> str: ... + @property + def workspace(self, /) -> WorkspaceIdentifier: ... + +@final +class TextChange: + def apply(self, /, txt: str) -> str: ... + @property + def content(self, /) -> str: ... + @property + def end_idx(self, /) -> int: ... + def is_delete(self, /) -> bool: ... + def is_empty(self, /) -> bool: ... + def is_insert(self, /) -> bool: ... + @property + def start_idx(self, /) -> int: ... + +@final +class UserInfo: + @property + def avatar(self, /) -> bytes |None: ... + @property + def description(self, /) -> str |None: ... + @property + def display_name(self, /) -> str |None: ... + @property + def name(self, /) -> str: ... + +@final +class Workspace: + def active_buffers(self, /) -> list[str]: ... + def attach_buffer(self, /, path: str) -> Promise: ... + def buffer_user_list(self, /, path: str) -> list[UserInfo]: ... + def callback(self, /, cb: Any) -> None: ... + def clear_callback(self, /) -> None: ... + def create_buffer(self, /, path: str, attrs: Any |None) -> Promise: ... + def cursor(self, /) -> CursorController: ... + def delete_buffer(self, /, path: str) -> Promise: ... + def detach_buffer(self, /, path: str) -> bool: ... + def fetch_buffer_users(self, /, path: str) -> Promise: ... + def fetch_buffers(self, /) -> Promise: ... + def fetch_users(self, /) -> Promise: ... + def get_buffer(self, /, path: str) -> BufferController |None: ... + def id(self, /) -> WorkspaceIdentifier: ... + def pin_buffer(self, /, path: str) -> Promise: ... + def poll(self, /) -> Promise: ... + def recv(self, /) -> Promise: ... + def search_buffers(self, /, filter: str |None = None) -> list[Any]: ... + def try_recv(self, /) -> Promise: ... + def un_pin_buffer(self, /, path: str) -> Promise: ... + def user_list(self, /) -> list[UserInfo]: ... + +@final +class WorkspaceEvent: + @property + def after(self, /) -> str |None: ... + @property + def attributes(self, /) -> Any |None: ... + @property + def kind(self, /) -> int: ... + @property + def path(self, /) -> str |None: ... + @property + def user(self, /) -> str |None: ... + +@final +class WorkspaceIdentifier: + @property + def user(self, /) -> str: ... + @property + def workspace(self, /) -> str: ... + +def connect(config: Config) -> Promise: ... +def init() -> Driver: ... +def set_logger(logging_cb: Any, debug: bool) -> bool: ... +def version() -> str: ... diff --git a/dist/py/src/codemp/__init__.py b/dist/py/src/codemp/__init__.py index 2ac1e90c..669f9e64 100644 --- a/dist/py/src/codemp/__init__.py +++ b/dist/py/src/codemp/__init__.py @@ -1,5 +1,5 @@ -from .codemp import * +from .codemplib import * -__doc__ = codemp.__doc__ -if hasattr(codemp, "__all__"): - __all__ = codemp.__all__ +__doc__ = codemplib.__doc__ +if hasattr(codemplib, "__all__"): + __all__ = codemplib.__all__ diff --git a/src/ffi/python/mod.rs b/src/ffi/python/mod.rs index b3250a18..9d692e3a 100644 --- a/src/ffi/python/mod.rs +++ b/src/ffi/python/mod.rs @@ -45,7 +45,7 @@ pub fn tokio() -> &'static tokio::runtime::Runtime { /// Implements a simple future like object between python and rust to allow for async operations /// between the two runtimes. -#[pyclass] +#[pyclass] // once we go past pytho 3.8 we can use pyclass(generic) pub struct Promise(Option>>>); #[pymethods] @@ -447,7 +447,7 @@ mod pycodemp { use super::CodempWorkspace; #[pymodule_export] - use super::WorkspaceIdentifier; + use super::CodempWorkspaceIdentifier; #[pymodule_export] use super::CodempWorkspaceEvent; From eb481e67340e1c53a33800bb492e41a0dd2930c1 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 20 Mar 2026 10:17:49 +0100 Subject: [PATCH 119/121] fix: doctests --- src/ffi/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index bb4e459a..8210e6de 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -17,7 +17,7 @@ //! let workspace = client.attach_workspace("my-username", "some-workspace").await?; //! //! // create a new buffer in this workspace and attach to it -//! workspace.create_buffer("/my/file.txt", false).await?; +//! workspace.create_buffer("/my/file.txt", None).await?; //! let buffer = workspace.attach_buffer("/my/file.txt").await?; //! //! // write `hello!` at the beginning of this buffer From 8e014e8620ba4c84eed481234e31bdb94c046ca2 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 21 Mar 2026 12:15:22 +0100 Subject: [PATCH 120/121] fix(js): leave_workspace is not async --- src/ffi/js/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 2eaf9332..914d86d4 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -62,7 +62,7 @@ impl Client { #[napi(js_name = "leaveWorkspace")] /// leave workspace and disconnect, returns true if workspace was active - pub async fn js_leave_workspace(&self, user: String, workspace: String) -> bool { + pub fn js_leave_workspace(&self, user: String, workspace: String) -> bool { self.leave_workspace(&user, workspace) } From dcd9d59abc16c5abd49317d05d39158b55c16c7e Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 21 Mar 2026 12:15:34 +0100 Subject: [PATCH 121/121] fix(lua): annotations --- dist/lua/annotations.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index 1efcbcc7..d31e2c3e 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -355,7 +355,7 @@ function Client:callback(cb) end ---@field name string user unique, immutable name ---@field display_name string? display name, mutable and not guaranteed to be unique ---@field description string? user description, maybe containing contact info ----@field avatar data? user avatar image, as bytes +---@field avatar integer[]? user avatar image, as bytes ---@class WorkspaceIdentifier ---uniquely identifies a workspace, by its owner and workspace name @@ -366,9 +366,14 @@ function Client:callback(cb) end ---attributes and properties of a buffer ---@field ephemeral boolean wheter this buffer is ephemeral +---@class BufferPath +---a wrapper around a buffer path string +---@field path string the underlying path +--TODO this should go + ---@class BufferNode ---represents a buffer and holds wheter it is ephemeral ----@field path string buffer path +---@field path BufferPath buffer path ---@field attributes BufferAttributes attributes of this buffer