mirror of
https://github.com/hexedtech/codemp.git
synced 2024-12-22 21:04:53 +01:00
docs: improved cargo docs, rewrote readme
Co-authored-by: alemi <me@alemi.dev>
This commit is contained in:
parent
bfe84c45e0
commit
d25e744a37
21 changed files with 236 additions and 190 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,9 +3,13 @@
|
|||
.vscode/
|
||||
*.sublime-*
|
||||
|
||||
# python
|
||||
.venv
|
||||
wheels/
|
||||
|
||||
# lua
|
||||
dist/lua/*.so
|
||||
|
||||
# js
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -857,8 +857,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.10.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49976b1ca7e2538314441ba370636b8c80891438e3c255636a87594079362c4f"
|
||||
source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"mlua-sys",
|
||||
|
@ -871,8 +870,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29"
|
||||
source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
|
@ -882,8 +880,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mlua_derive"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09697a6cec88e7f58a02c7ab5c18c611c6907c8654613df9cc0192658a4fb859"
|
||||
source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
124
README.md
124
README.md
|
@ -1,102 +1,74 @@
|
|||
# codemp
|
||||
<p align="center"><a href="https://codemp.dev"><img alt="codemp logo" src="https://codemp.dev/static/codemp-banner.png" height="100"/></a></p>
|
||||
<!-- I know it's not going to work on a lot of markdown renderers, but it'll look alright even if it's rendered on the right. -->
|
||||
|
||||
<a href="https://codemp.dev"><img alt="codemp logo" align="center" src="https://codemp.dev/codemp-t.png" height="100" /></a>
|
||||
> `codemp` is a **collaborative** text editing solution to work remotely.
|
||||
|
||||
### code multiplexer
|
||||
|
||||
> CodeMP is a **collaborative** text editing plugin to work remotely.
|
||||
It seamlessly integrates in your editor providing remote cursors and instant text synchronization,
|
||||
as well as a remote virtual workspace for you and your team.
|
||||
|
||||
> CodeMP is build with state-of-the-art CRDT technology, guaranteeing eventual consistency.
|
||||
> `codemp` is build with state-of-the-art CRDT technology, guaranteeing eventual consistency.
|
||||
|
||||
This means everyone in a workspace will always be working on the exact same file _eventually_:
|
||||
even under unreliable networks or constrained resources, the underlying CRDT will always reach a
|
||||
convergent state across all users. Even with this baseline, CodeMP's proto is optimized for speed
|
||||
convergent state across all users. Even with this baseline, `codemp`'s protocol is optimized for speed
|
||||
and low network footprint, meaning even slow connections can provide stable real-time editing.
|
||||
|
||||
# using this project
|
||||
CodeMP is available for many editors as plugins.
|
||||
The full documentation is available on [docs.rs](https://docs.rs/codemp).
|
||||
|
||||
Currently we support:
|
||||
- [VSCode](https://github.com/hexedtech/codemp-vscode)
|
||||
- [Intellij](https://github.com/hexedtech/codemp-intellij)
|
||||
# Usage
|
||||
`codemp` is primarily used as a plugin in your editor of choice.
|
||||
|
||||
## Installation
|
||||
> [!WARNING]
|
||||
> The editor plugins are in active development. Expect frequent changes.
|
||||
|
||||
`codemp` is available as a plugin for a growing number of text editors. Currently we support:
|
||||
- [NeoVim](https://github.com/hexedtech/codemp-nvim)
|
||||
- [VSCode](https://github.com/hexedtech/codemp-vscode)
|
||||
- [Sublime Text](https://github.com/hexedtech/codemp-sublime)
|
||||
<!-- - [IntelliJ Platform](https://github.com/hexedtech/codemp-intellij) -->
|
||||
|
||||
# using this library
|
||||
This is the main client library for codemp. It exposes functions to interact with the codemp client itself, its workspaces and buffers.
|
||||
## Registration
|
||||
The `codemp` protocol is [openly available](https://github.com/hexedtech/codemp-proto/) and servers may be freely developed with it.
|
||||
|
||||
All memory is managed by the library itself, which gives out always atomic reference-counted pointers to internally mutable objects. The host program needs only to connect a client first, and from that reference can retrieve every other necessary component.
|
||||
A reference instance is provided by hexed.technology at [codemp.dev](https://codemp.dev). You may create an account for it [here](https://codemp.dev/register).
|
||||
|
||||
### from rust
|
||||
This library is primarily a rust crate, so rust applications will get the best possible integration.
|
||||
During the initial closed beta, registrations will require an invite code. Get in contact if interested.
|
||||
|
||||
An open beta is going to follow with free access to a single workspace. After the open beta period, the [codemp.dev] will switch to a subscription-based model.
|
||||
|
||||
# Development
|
||||
This is the main client library for `codemp`. It provides a batteries-included fully-featured `Client`, managed by the library itself, and exposes a number of functions to interact with it. The host program can obtain a `Client` handle by connecting, and from that reference can retrieve every other necessary component.
|
||||
|
||||
`codemp` is primarily a rlib and can be used as such, but is also available in other languages via FFI.
|
||||
|
||||
Adding a dependency on `codemp` is **easy**:
|
||||
|
||||
### From Rust
|
||||
Just `cargo add codemp` and check the docs for some examples.
|
||||
|
||||
### from supported languages
|
||||
This library provides first-class bindings for:
|
||||
- java
|
||||
- javascript
|
||||
- python
|
||||
- lua
|
||||
### From supported languages
|
||||
We provide first-class bindings for:
|
||||
- [JavaScript](./dist/js/README.md): available from `npm` as [`codemp`](https://npmjs.org/package/codemp)
|
||||
- [Python](./dist/lua/README.md): available from `PyPI` as [`codemp`](https://pypi.org/project/codemp)
|
||||
- [Lua](./dist/lua/README.md): run `cargo build --features=lua`
|
||||
- [Java](./dist/java/README.md): run `gradle build` in `dist/java/` (requires Gradle)
|
||||
|
||||
For any of these languages, just add `codemp` as a dependency in your project.
|
||||
As a design philosophy, our binding APIs attempt to perfectly mimic their Rust counterparts, so the main documentation can still be referenced as source of truth.
|
||||
Refer to specific language documentation for specifics, differences and quirks.
|
||||
|
||||
The API should perfectly mimic what rust exposes underneath, so the main rust docs can still be used as reference for available methods and objects.
|
||||
|
||||
### from other languages
|
||||
### From other languages
|
||||
> [!WARNING]
|
||||
> The common C bindings are still not available
|
||||
> The common C bindings are not available yet!
|
||||
|
||||
Any other language with C ffi capabilities can use codemp via its bare C bindings.
|
||||
This will be more complex and may require wrapping the native calls underneath.
|
||||
|
||||
# documentation
|
||||
This project is mainly a rust crate, so the most up-to-date and extended documentation will be found on docs.rs.
|
||||
- Check [docs.rs/codemp](https://docs.rs/codemp) for our full documentation!
|
||||
|
||||
# architecture
|
||||
CodeMP is built from scratch to guarantee impeccable performance and accuracy.
|
||||
The following architectural choices are driven by this very strict requirement.
|
||||
|
||||
## interop: FFI
|
||||
The first challenge of developing such a system is adoption: getting all your colleagues to switch to your editor is not going to happen. Supporting a multitude of plugins in different languages and possibly different architectures however is a daunting task even for larger teams.
|
||||
|
||||
Our solution is a single common native library, developed in safe and performant Rust, which can be used by any plugin with a thin layer of glue code to provide native bindings.
|
||||
|
||||
This allows us to maintain a single client codebase and multiple plugins, rather than multiple clients and plugins, with the cost of FFI complexity.
|
||||
|
||||
We took a gamble which paid off: our team was capable enough to handle cross compiling and multiple bindings, and can now focus on first-class integration in each editor API.
|
||||
|
||||
## synchronization: CRDT
|
||||
Our investigations in the field of text synchronization for multi agent editing showed that there are mostly two approached to solve the problem: Operational Transforms (older, more used) and Conflict-free Replicated Data Structures (CRDTs, a newer technology)
|
||||
|
||||
While initial prototypes used OT to achieve syncrhonization, we quickly found issues. The editor is not under our plugin's control, and could always apply new insertions/deletions while processing remote changes. This was a huge issue with OTs, as it would require control over the integration process.
|
||||
|
||||
We introduced CRDTs first with a hand-crafted naive approach, and were very impressed by the results. Because of the nature of CRDTs, we have an internal state which is always kept in sync with the server (and all other peers), and this state can then be finely synchronized with the effective editor state. Edits coming while integrating just branch more, and our inner CRDT merges those seemlessly.
|
||||
|
||||
We recently swapped our internal library for a production-grade solution: [diamond-types](https://github.com/josephg/diamond-types), with even more impressive results: we jumped from processing ~2 thousand operations per second to an astonishing **~8 million**, a `1000x` improvement!
|
||||
|
||||
## layout: star (client/server)
|
||||
Network layout posed a challenging decision: a distributed system could provide lower latency but a centralized arbiter could dramatically reduce necessary resources for each peer.
|
||||
|
||||
We want codemp to be a viable solution on low power devices in unreliable networks, so opted to a centralized approach.
|
||||
|
||||
While for small work groups the benefits are negligible, bigger sessions dramatically benefit from having a central server which handles reduntant merging and skips irrelevant operations, while masking IPs and removing the problem of punching through NATs.
|
||||
|
||||
We hope to provide a solution capable of scaling to hundreds or thousands of concurrent users, in order to open new uses in conferences, competitions, teaching and live entertainment.
|
||||
|
||||
## protocol: streams (grpc)
|
||||
The underlying network structure is really important to achieve good performance. We need a binary stream to quickly beam back and forth operations.
|
||||
|
||||
GRPC provides this, encapsulating is convenient to use primitives, while also providing request/response procedures.
|
||||
|
||||
We plan to experiment with laminar and capnproto for the fast cursor and operation streams, but we will probably retain an http-based approach for workspace management and authentication.
|
||||
|
||||
# contributing
|
||||
> [!NOTE]
|
||||
> This project is maintained by [hexedtech](https://hexed.technology).
|
||||
Any other language with C FFI capabilities will be able to use `codemp` via its bare C bindings.
|
||||
This may be more complex and may require wrapping the native calls underneath.
|
||||
|
||||
# Contributing
|
||||
If you find bugs or would like to see new features implemented, be sure to open an issue on this repository.
|
||||
|
||||
In case you wished to contribute code, that's great! We love external contributions, but we require you to **sign our CLA first** (which is not yet ready, TODO!)
|
||||
> [!WARNING]
|
||||
> The CLA necessary for code contributions is not yet available!
|
||||
|
||||
In case you wish to contribute code, that's great! We love external contributions, but we require you to sign our CLA first (available soon).
|
||||
|
|
17
dist/java/README.md
vendored
Normal file
17
dist/java/README.md
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Java bindings
|
||||
`codemp`'s Java bindings are implemented using the [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/).
|
||||
|
||||
On the Rust side, all Java-related code is gated behind the `java` feature, and is implemented using[`jni-rs`](https://github.com/jni-rs/jni-rs).
|
||||
Unlike other languages, Java requires glue code on both sides: as a result, a Java component is necessary.
|
||||
|
||||
## Building
|
||||
This is a [Gradle](https://gradle.org/) project: building requires having both Gradle and Cargo installed, as well as the JDK (any non-abandoned version).
|
||||
Once you have all the requirements, building is as simple as running `gradle build`: the output is going to be a JAR under `build/libs`, which you can import into your classpath with your IDE of choice.
|
||||
|
||||
## Development
|
||||
The Java bindings have no known major quirk. However, here are a list of facts that are useful to know when developing with these:
|
||||
|
||||
* Memory management is entirely delegated to the JVM's garbage collector.
|
||||
* A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually.
|
||||
* Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`.
|
||||
* `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug.
|
18
dist/js/README.md
vendored
Normal file
18
dist/js/README.md
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
# JavaScript bindings
|
||||
NodeJS allows directly `require`ing properly formed shared objects, so the glue can live mostly on the Rust side.
|
||||
|
||||
Our JavaScript glue is built with [`napi`](https://napi.rs).
|
||||
|
||||
To get a usable shared object just `cargo build --release --features=js`, however preparing a proper javascript package to be included as dependency requires more steps.
|
||||
|
||||
## `npm`
|
||||
|
||||
`codemp` is directly available on `npm` as [`codemp`](https://npmjs.org/package/codemp).
|
||||
|
||||
## Building
|
||||
|
||||
To build a node package, `napi-cli` must first be installed: `npm install napi-cli`.
|
||||
|
||||
You can then `npx napi build` in the project root to compile the native extension and create the type annotations (`index.d.ts`).
|
||||
A package.json is provided for publishing, but will require some tweaking.
|
||||
|
27
dist/lua/README.md
vendored
Normal file
27
dist/lua/README.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Lua bindings
|
||||
Lua allows directly `require`ing properly constructed shared objects, so glue code can live completely on the Rust side.
|
||||
|
||||
The Lua-compatible wrappers are built with [`mlua`](https://github.com/mlua-rs/mlua).
|
||||
|
||||
To build, just `cargo build --release --features=lua` and rename the resulting `libcodemp.so` / `codemp.dll` / `codemp.dylib` in `codemp_native.so/dll/dylib`.
|
||||
This is important because Lua looks up the constructor symbol based on filename.
|
||||
|
||||
Type hints are provided in `annotations.lua`, just include them in your language server: `---@module 'annotations'`.
|
||||
|
||||
## Example loader
|
||||
A simple loader is provided here:
|
||||
|
||||
```lua
|
||||
---@module 'annotations'
|
||||
|
||||
---@return Codemp
|
||||
local function load()
|
||||
local native, _ = require("codemp.native")
|
||||
return native
|
||||
end
|
||||
|
||||
return {
|
||||
load = load,
|
||||
}
|
||||
```
|
||||
|
14
dist/py/README.md
vendored
Normal file
14
dist/py/README.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Python bindings
|
||||
Python allows directly `import`ing properly formed shared objects, so the glue can live mostly on the Rust side.
|
||||
|
||||
Our Python glue is built with [`PyO3`](https://pyo3.rs).
|
||||
|
||||
To get a usable shared object just `cargo build --release --features=python`, however preparing a proper python package to be included as dependency requires more steps.
|
||||
|
||||
## `PyPI`
|
||||
|
||||
`codemp` is directly available on `PyPI` as [`codemp`](https://pypi.org/project/codemp).
|
||||
|
||||
## Building
|
||||
To distribute the native extension we can leverage python wheels. It will be necessary to build the relevant wheels with [`maturin`](https://github.com/PyO3/maturin).
|
||||
After installing with `pip install maturin`, run `maturin build` to obtain an `import`able package.
|
|
@ -1,38 +1,41 @@
|
|||
//! # TextChange
|
||||
//!
|
||||
//! an editor-friendly representation of a text change in a buffer
|
||||
//! to easily interface with codemp from various editors
|
||||
//! A high-level representation of a change within a given buffer.
|
||||
|
||||
/// an editor-friendly representation of a text change in a buffer
|
||||
/// An editor-friendly representation of a text change in a given buffer.
|
||||
///
|
||||
/// It's expressed with a range of characters and a string of content that should replace them,
|
||||
/// allowing representation of any combination of deletions, insertions or replacements.
|
||||
///
|
||||
/// this represent a range in the previous state of the string and a new content which should be
|
||||
/// replaced to it, allowing to represent any combination of deletions, insertions or replacements
|
||||
/// Bulky and large operations will result in a single [`TextChange`] effectively sending the whole
|
||||
/// new buffer, but smaller changes are efficient and easy to create or apply.
|
||||
///
|
||||
/// bulk and widespread operations will result in a TextChange effectively sending the whole new
|
||||
/// buffer, but small changes are efficient and easy to create or apply
|
||||
/// [`TextChange`] contains an optional `hash` field. This is used for error correction: if
|
||||
/// provided, it should match the hash of the buffer content **after** applying this change.
|
||||
/// Note that the `hash` field will not necessarily be provided every time.
|
||||
///
|
||||
/// ### examples
|
||||
/// to insert 'a' after 4th character we should send a
|
||||
/// `TextChange { span: 4..4, content: "a".into() }`
|
||||
/// ### Examples
|
||||
/// To insert 'a' after 4th character we should send a.
|
||||
/// `TextChange { start: 4, end: 4, content: "a".into(), hash: None }`
|
||||
///
|
||||
/// to delete a the fourth character we should send a
|
||||
/// `TextChange { span: 3..4, content: "".into() }`
|
||||
/// To delete a the fourth character we should send a.
|
||||
/// `TextChange { start: 3, end: 4, content: "".into(), hash: None }`
|
||||
///
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
|
||||
pub struct TextChange {
|
||||
/// range start of text change, as char indexes in buffer previous state
|
||||
/// Range start of text change, as char indexes in buffer previous state.
|
||||
pub start: u32,
|
||||
/// range end of text change, as char indexes in buffer previous state
|
||||
/// Range end of text change, as char indexes in buffer previous state.
|
||||
pub end: u32,
|
||||
/// new content of text inside span
|
||||
/// New content of text inside span.
|
||||
pub content: String,
|
||||
/// optional content hash after applying this change
|
||||
/// Optional content hash after applying this change.
|
||||
pub hash: Option<i64>,
|
||||
}
|
||||
|
||||
impl TextChange {
|
||||
/// Returns the [`std::ops::Range`] representing this change's span.
|
||||
pub fn span(&self) -> std::ops::Range<usize> {
|
||||
self.start as usize..self.end as usize
|
||||
}
|
||||
|
@ -40,24 +43,26 @@ impl TextChange {
|
|||
|
||||
#[cfg_attr(feature = "python", pyo3::pymethods)]
|
||||
impl TextChange {
|
||||
/// A change is a "deletion" if the change range span is bigger than zero.
|
||||
/// This is not exclusive, a change can be both an insertion and a deletion.
|
||||
/// Returns true if this [`TextChange`] deletes existing text.
|
||||
///
|
||||
/// Note that this is is **not** mutually exclusive with [TextChange::is_insert].
|
||||
pub fn is_delete(&self) -> bool {
|
||||
self.start < self.end
|
||||
}
|
||||
|
||||
/// A change is an "Insertion" if the content is not empty.
|
||||
/// This is not exclusive, a change can be both an insertion and a deletion.
|
||||
/// Returns true if this [`TextChange`] adds new text.
|
||||
///
|
||||
/// Note that this is is **not** mutually exclusive with [TextChange::is_delete].
|
||||
pub fn is_insert(&self) -> bool {
|
||||
!self.content.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true if this TextChange is effectively as no-op
|
||||
/// Returns true if this [`TextChange`] is effectively as no-op.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.is_delete() && !self.is_insert()
|
||||
}
|
||||
|
||||
/// Applies this text change to given text, returning a new string
|
||||
/// Applies this text change to given text, returning a new string.
|
||||
pub fn apply(&self, txt: &str) -> String {
|
||||
let pre_index = std::cmp::min(self.start as usize, txt.len());
|
||||
let pre = txt.get(..pre_index).unwrap_or("").to_string();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! # Controller
|
||||
//!
|
||||
//! an bidirectional stream handler to easily manage async operations across local buffers and the
|
||||
//! server
|
||||
//! A bidirectional stream handler to easily manage asynchronous operations between local buffers
|
||||
//! and the server.
|
||||
|
||||
use crate::errors::ControllerResult;
|
||||
|
||||
|
@ -18,27 +18,23 @@ pub(crate) trait ControllerWorker<T : Sized + Send + Sync> {
|
|||
// note that we don't use thiserror's #[from] because we don't want the error structs to contain
|
||||
// these foreign types, and also we want these to be easily constructable
|
||||
|
||||
/// async and threadsafe handle to a generic bidirectional stream
|
||||
/// Asynchronous and thread-safe handle to a generic bidirectional stream.
|
||||
///
|
||||
/// this generic trait is implemented by actors managing stream procedures.
|
||||
/// events can be enqueued for dispatching without blocking ([Controller::send]), and an async blocking
|
||||
/// api ([Controller::recv]) is provided to wait for server events.
|
||||
/// This generic trait is implemented by actors managing stream procedures.
|
||||
///
|
||||
/// Events can be enqueued for dispatching without blocking with [`Controller::send`].
|
||||
///
|
||||
/// * if possible, prefer a pure [Controller::recv] consumer, awaiting for events
|
||||
/// * if async is not feasible a [Controller::poll]/[Controller::try_recv] approach is possible
|
||||
/// For receiving events from the server, an asynchronous API with [`Controller::recv`] is
|
||||
/// provided; if that is not feasible, consider using [`Controller::callback`] or, alternatively,
|
||||
/// [`Controller::poll`] combined with [`Controller::try_recv`].
|
||||
///
|
||||
/// [`crate::ext::select_buffer`] may provide a useful helper for managing multiple controllers.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Controller<T : Sized + Send + Sync> : Sized + Send + Sync {
|
||||
/// enqueue a new value to be sent to all other users
|
||||
///
|
||||
/// success or failure of this function does not imply validity of sent operation,
|
||||
/// because it's integrated asynchronously on the background worker
|
||||
/// Enqueue a new value to be sent to all other users.
|
||||
async fn send(&self, x: T) -> ControllerResult<()>;
|
||||
|
||||
/// get next value from other users, blocking until one is available
|
||||
///
|
||||
/// this is just an async trait function wrapped by `async_trait`:
|
||||
///
|
||||
/// `async fn recv(&self) -> codemp::ControllerResult<T>;`
|
||||
/// Block until a value is available and returns it.
|
||||
async fn recv(&self) -> ControllerResult<T> {
|
||||
loop {
|
||||
self.poll().await?;
|
||||
|
@ -48,41 +44,37 @@ pub trait Controller<T : Sized + Send + Sync> : Sized + Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
/// registers a callback to be called on receive.
|
||||
/// Register a callback to be called on receive.
|
||||
///
|
||||
/// there can only be one callback at any given time.
|
||||
/// There can only be one callback registered at any given time.
|
||||
fn callback(&self, cb: impl Into<ControllerCallback<Self>>);
|
||||
|
||||
/// clears the currently registered callback.
|
||||
/// Clear the currently registered callback.
|
||||
fn clear_callback(&self);
|
||||
|
||||
/// block until next value is available without consuming it
|
||||
///
|
||||
/// this is just an async trait function wrapped by `async_trait`:
|
||||
///
|
||||
/// `async fn poll(&self) -> codemp::ControllerResult<()>;`
|
||||
/// Block until a value is available, without consuming it.
|
||||
async fn poll(&self) -> ControllerResult<()>;
|
||||
|
||||
/// attempt to receive a value without blocking, return None if nothing is available
|
||||
/// Attempt to receive a value, return None if nothing is currently available.
|
||||
async fn try_recv(&self) -> ControllerResult<Option<T>>;
|
||||
|
||||
/// stop underlying worker
|
||||
/// Stop underlying worker.
|
||||
///
|
||||
/// note that this will mean no more values can be received nor sent,
|
||||
/// but existing controllers will still be accessible until all are dropped
|
||||
/// After this is called, nothing can be received or sent anymore; however, existing
|
||||
/// controllers will still be accessible until all handles are dropped.
|
||||
///
|
||||
/// returns true if stop signal was sent, false if channel is closed
|
||||
/// (likely if worker is already stopped)
|
||||
/// Returns true if the stop signal was successfully sent, false if channel was
|
||||
/// closed (probably because worker had already been stopped).
|
||||
fn stop(&self) -> bool;
|
||||
}
|
||||
|
||||
|
||||
/// type wrapper for Boxed dyn callback
|
||||
/// Type wrapper for Boxed dynamic callback.
|
||||
pub struct ControllerCallback<T>(pub Box<dyn Sync + Send + Fn(T)>);
|
||||
|
||||
impl<T> ControllerCallback<T> {
|
||||
pub fn call(&self, x: T) {
|
||||
self.0(x) // lmao at this syntax
|
||||
pub(crate) fn call(&self, x: T) {
|
||||
self.0(x)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//! ### Cursor
|
||||
//! Represents the position of a remote user's cursor, with their display name
|
||||
//! Represents the position of a remote user's cursor.
|
||||
|
||||
use codemp_proto as proto;
|
||||
use uuid::Uuid;
|
||||
|
@ -12,14 +12,14 @@ use pyo3::prelude::*;
|
|||
#[cfg_attr(feature = "python", pyclass)]
|
||||
// #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))]
|
||||
pub struct Cursor {
|
||||
/// Cursor start position in buffer, as 0-indexed row-column tuple
|
||||
/// Cursor start position in buffer, as 0-indexed row-column tuple.
|
||||
pub start: (i32, i32),
|
||||
/// Cursor end position in buffer, as 0-indexed row-column tuple
|
||||
/// Cursor end position in buffer, as 0-indexed row-column tuple.
|
||||
pub end: (i32, i32),
|
||||
/// Path of buffer this cursor is on
|
||||
/// Path of buffer this cursor is on.
|
||||
pub buffer: String,
|
||||
/// User display name, if provided
|
||||
pub user: Option<Uuid>,
|
||||
/// User display name, if provided.
|
||||
pub user: Option<Uuid>, // TODO this should be a string, not the UUID!
|
||||
}
|
||||
|
||||
impl From<proto::cursor::CursorPosition> for Cursor {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
//! # Event
|
||||
//! Real time notification of changes in a workspace, to either users or buffers
|
||||
//! Real time notification of changes in a workspace, to either users or buffers.
|
||||
use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner;
|
||||
|
||||
/// Event in a [crate::Workspace]
|
||||
/// Event in a [crate::Workspace].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
pub enum Event {
|
||||
/// Fired when the file tree changes
|
||||
/// containes the modified buffer path (deleted or created or renamed)
|
||||
/// Fired when the file tree changes.
|
||||
/// Contains the modified buffer path (deleted, created or renamed).
|
||||
FileTreeUpdated(String),
|
||||
/// Fired when an user joins the current workspace
|
||||
/// Fired when an user joins the current workspace.
|
||||
UserJoin(String),
|
||||
/// Fired when an user leaves the current workspace
|
||||
/// Fired when an user leaves the current workspace.
|
||||
UserLeave(String),
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ use uuid::Uuid;
|
|||
/// Represents a service user
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
/// User unique identifier, should never change
|
||||
/// User unique identifier, should never change.
|
||||
pub id: Uuid,
|
||||
/// User name, can change but should be unique
|
||||
/// User name, can change but should be unique.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,10 @@ use crate::ext::InternallyMutable;
|
|||
|
||||
use super::worker::DeltaRequest;
|
||||
|
||||
/// A [Controller] to asyncrhonously interact with remote buffers
|
||||
/// A [Controller] to asynchronously interact with remote buffers.
|
||||
///
|
||||
/// Each buffer controller internally tracks the last acknowledged state, remaining always in sync
|
||||
/// with the server while allowing to procedurally receiving changes while still sending new ones.
|
||||
/// with the server while allowing to procedurally receive changes while still sending new ones.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
||||
#[cfg_attr(feature = "js", napi_derive::napi)]
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
//! It is built on top of [diamond_types] CRDT, guaranteeing that all peers which have received the
|
||||
//! same set of operations will converge to the same content.
|
||||
|
||||
/// buffer controller implementation
|
||||
pub mod controller;
|
||||
|
||||
/// controller worker implementation
|
||||
pub(crate) mod worker;
|
||||
|
||||
/// buffer controller implementation
|
||||
pub mod controller;
|
||||
pub use controller::BufferController as Controller;
|
||||
|
|
|
@ -9,7 +9,7 @@ use tonic::async_trait;
|
|||
use crate::{api::{controller::ControllerCallback, Controller, Cursor}, errors::ControllerResult};
|
||||
use codemp_proto::cursor::CursorPosition;
|
||||
|
||||
/// A [Controller] for asynchronously sending and receiving [Cursor] events
|
||||
/// A [Controller] for asynchronously sending and receiving [Cursor] event.
|
||||
///
|
||||
/// An unique [CursorController] exists for each active [crate::Workspace].
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
//! Each user in a [crate::Workspace] holds a cursor and can move it across multiple buffers.
|
||||
//! A cursor spans zero or more characters across one or more lines.
|
||||
|
||||
/// cursor worker implementation
|
||||
pub(crate) mod worker;
|
||||
|
||||
/// cursor controller implementation
|
||||
pub mod controller;
|
||||
|
||||
pub use controller::CursorController as Controller;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use crate::{api::Controller, errors::ControllerResult};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Polls all given buffer controllers and waits, returning the first one ready.
|
||||
/// Poll all given buffer controllers and wait, returning the first one ready.
|
||||
///
|
||||
/// It will spawn tasks blocked on [`Controller::poll`] for each buffer controller.
|
||||
/// As soon as one finishes, its controller is returned and all other tasks are canceled.
|
||||
|
@ -51,7 +51,7 @@ pub async fn select_buffer(
|
|||
}
|
||||
}
|
||||
|
||||
/// Hashes a given byte array with the internally used algorithm.
|
||||
/// Hash a given byte array with the internally used algorithm.
|
||||
///
|
||||
/// Currently, it uses [`xxhash_rust::xxh3::xxh3_64`].
|
||||
pub fn hash(data: impl AsRef<[u8]>) -> i64 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! ### lua
|
||||
//! ### Lua
|
||||
//! Using [mlua] it's possible to map almost perfectly the entirety of `codemp` API.
|
||||
//! Notable outliers are functions that receive `codemp` objects: these instead receive arguments
|
||||
//! to build the object instead (such as [`crate::api::Controller::send`])
|
||||
|
@ -7,9 +7,9 @@
|
|||
//! necessary to drive it. A separate driver thread can be spawned with `spawn_runtime_driver`
|
||||
//! function.
|
||||
//!
|
||||
//! To make callbacks work, the main lua thread must periodically stop and poll for callbacks via
|
||||
//! To work with callbacks, the main Lua thread must periodically stop and poll for callbacks via
|
||||
//! `poll_callback`, otherwise those will never run. This is necessary to allow safe concurrent
|
||||
//! access to the global Lua state, so minimize runtime inside callbacks as much as possile.
|
||||
//! access to the global Lua state, so minimize callback execution time as much as possible.
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//! ### FFI
|
||||
//! Foreign-Function-Interface glue code, each gated behind feature flags
|
||||
//! The glue code for FFI (Foreign Function Interface) in various languages, each gated behind
|
||||
//! a feature flag.
|
||||
//!
|
||||
//! For all except java, the resulting shared object is ready to use, but external packages are
|
||||
//! available to simplify the dependancy and provide type hints in editor.
|
||||
//! For all except Java, the resulting shared object is ready to use, but external packages are
|
||||
//! available to simplify dependency management and provide type hints in editor.
|
||||
|
||||
/// java bindings, built with [jni]
|
||||
#[cfg(feature = "java")]
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
//! workspaces each containing any number of buffers.
|
||||
//!
|
||||
//! The [`Client`] is completely managed by the library itself, making its use simple across async
|
||||
//! contexts and FFI boundaries. Asynchronous actions are abstracted away by the [`api::Controller`],
|
||||
//! providing an unopinionated approach with both callback-based and blocking-based APIs.
|
||||
//! contexts and FFI boundaries. All memory is managed by the library itself, which gives out always
|
||||
//! atomic reference-counted pointers to internally mutable objects. Asynchronous actions are
|
||||
//! abstracted away by the [`api::Controller`], providing an unopinionated approach with both
|
||||
//! callback-based and blocking-based APIs.
|
||||
//!
|
||||
//! The library also provides ready-to-use bindings in a growing number of other programming languages,
|
||||
//! to support a potentially infinite number of editors.
|
||||
|
@ -81,7 +83,7 @@
|
|||
//! * `python`
|
||||
//!
|
||||
//! For some of these, ready-to-use packages are available in various registries:
|
||||
//! * [pypi (python)](https://pypi.org/project/codemp)
|
||||
//! * [PyPI (python)](https://pypi.org/project/codemp)
|
||||
//! * [npm (javascript)](https://www.npmjs.com/package/codemp)
|
||||
//!
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ impl Workspace {
|
|||
Ok(ws)
|
||||
}
|
||||
|
||||
/// create a new buffer in current workspace
|
||||
/// Create a new buffer in the current workspace.
|
||||
pub async fn create(&self, path: &str) -> RemoteResult<()> {
|
||||
let mut workspace_client = self.0.services.ws();
|
||||
workspace_client
|
||||
|
@ -114,10 +114,7 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// attach to a buffer, starting a buffer controller and returning a new reference to it
|
||||
///
|
||||
/// to interact with such buffer use [crate::api::Controller::send] or
|
||||
/// [crate::api::Controller::recv] to exchange [crate::api::TextChange]
|
||||
/// Attach to a buffer and return a handle to it.
|
||||
pub async fn attach(&self, path: &str) -> ConnectionResult<buffer::Controller> {
|
||||
let mut worskspace_client = self.0.services.ws();
|
||||
let request = tonic::Request::new(BufferNode {
|
||||
|
@ -148,11 +145,11 @@ impl Workspace {
|
|||
Ok(controller)
|
||||
}
|
||||
|
||||
/// detach from an active buffer
|
||||
/// Detach from an active buffer.
|
||||
///
|
||||
/// this option will be carried in background: [buffer::worker::BufferWorker] will be stopped and dropped. there
|
||||
/// may still be some events enqueued in buffers to poll, but the [buffer::Controller] itself won't be
|
||||
/// accessible anymore from [Workspace].
|
||||
/// This option will be carried in background. [`buffer::worker::BufferWorker`] will be stopped and dropped.
|
||||
/// There may still be some events enqueued in buffers to poll, but the [buffer::Controller] itself won't be
|
||||
/// accessible anymore from [`Workspace`].
|
||||
pub fn detach(&self, path: &str) -> DetachResult {
|
||||
match self.0.buffers.remove(path) {
|
||||
None => DetachResult::NotAttached,
|
||||
|
@ -166,7 +163,7 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
/// Await next workspace [Event] and return it
|
||||
/// Await next workspace [Event] and return it when it arrives.
|
||||
// TODO this method is weird and ugly, can we make it more standard?
|
||||
pub async fn event(&self) -> ControllerResult<Event> {
|
||||
self.0
|
||||
|
@ -178,7 +175,7 @@ impl Workspace {
|
|||
.ok_or(crate::errors::ControllerError::Unfulfilled)
|
||||
}
|
||||
|
||||
/// Re-fetch list of all buffers in a 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 buffers = workspace_client
|
||||
|
@ -195,7 +192,7 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-fetch list of all users in a workspace
|
||||
/// Re-fetch the list of all users in the workspace.
|
||||
pub async fn fetch_users(&self) -> RemoteResult<()> {
|
||||
let mut workspace_client = self.0.services.ws();
|
||||
let users = BTreeSet::from_iter(
|
||||
|
@ -216,7 +213,7 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a list of the [User]s attached to a specific buffer
|
||||
/// Get a list of the [User]s attached to a specific buffer.
|
||||
pub async fn list_buffer_users(&self, path: &str) -> RemoteResult<Vec<User>> {
|
||||
let mut workspace_client = self.0.services.ws();
|
||||
let buffer_users = workspace_client
|
||||
|
@ -233,7 +230,7 @@ impl Workspace {
|
|||
Ok(buffer_users)
|
||||
}
|
||||
|
||||
/// Delete a buffer
|
||||
/// Delete a buffer.
|
||||
pub async fn delete(&self, path: &str) -> RemoteResult<()> {
|
||||
let mut workspace_client = self.0.services.ws();
|
||||
workspace_client
|
||||
|
@ -251,25 +248,25 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the workspace unique id
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Return a handle to workspace cursor controller
|
||||
/// Return a handle to the [`cursor::Controller`].
|
||||
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
|
||||
pub fn cursor(&self) -> cursor::Controller {
|
||||
self.0.cursor.clone()
|
||||
}
|
||||
|
||||
/// Get a [buffer::Controller] by path, if any is active on given path
|
||||
/// 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 buffer_by_name(&self, path: &str) -> Option<buffer::Controller> {
|
||||
self.0.buffers.get(path).map(|x| x.clone())
|
||||
}
|
||||
|
||||
/// Get a list of all the currently attached buffers
|
||||
/// Get a list of all the currently attached buffers.
|
||||
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
|
||||
pub fn buffer_list(&self) -> Vec<String> {
|
||||
self.0
|
||||
|
@ -279,7 +276,7 @@ impl Workspace {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// get the currently cached "filetree"
|
||||
/// Get the filetree as it is currently cached.
|
||||
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
|
||||
pub fn filetree(&self, filter: Option<&str>) -> Vec<String> {
|
||||
self.0.filetree.iter()
|
||||
|
|
Loading…
Reference in a new issue