docs: improved cargo docs, rewrote readme

Co-authored-by: alemi <me@alemi.dev>
This commit is contained in:
zaaarf 2024-09-05 01:45:48 +02:00
parent bfe84c45e0
commit d25e744a37
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
21 changed files with 236 additions and 190 deletions

4
.gitignore vendored
View file

@ -3,9 +3,13 @@
.vscode/ .vscode/
*.sublime-* *.sublime-*
# python
.venv .venv
wheels/ wheels/
# lua
dist/lua/*.so
# js # js
node_modules/ node_modules/
package-lock.json package-lock.json

9
Cargo.lock generated
View file

@ -857,8 +857,7 @@ dependencies = [
[[package]] [[package]]
name = "mlua" name = "mlua"
version = "0.10.0-beta.1" version = "0.10.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
checksum = "49976b1ca7e2538314441ba370636b8c80891438e3c255636a87594079362c4f"
dependencies = [ dependencies = [
"bstr", "bstr",
"mlua-sys", "mlua-sys",
@ -871,8 +870,7 @@ dependencies = [
[[package]] [[package]]
name = "mlua-sys" name = "mlua-sys"
version = "0.6.2" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
@ -882,8 +880,7 @@ dependencies = [
[[package]] [[package]]
name = "mlua_derive" name = "mlua_derive"
version = "0.9.3" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/mlua-rs/mlua?rev=ece66c46bfdc62685b758ffff16286f2806b2662#ece66c46bfdc62685b758ffff16286f2806b2662"
checksum = "09697a6cec88e7f58a02c7ab5c18c611c6907c8654613df9cc0192658a4fb859"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

124
README.md
View file

@ -1,102 +1,74 @@
# codemp # 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, 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. 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_: 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 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. and low network footprint, meaning even slow connections can provide stable real-time editing.
# using this project The full documentation is available on [docs.rs](https://docs.rs/codemp).
CodeMP is available for many editors as plugins.
Currently we support: # Usage
- [VSCode](https://github.com/hexedtech/codemp-vscode) `codemp` is primarily used as a plugin in your editor of choice.
- [Intellij](https://github.com/hexedtech/codemp-intellij)
## 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) - [NeoVim](https://github.com/hexedtech/codemp-nvim)
- [VSCode](https://github.com/hexedtech/codemp-vscode)
- [Sublime Text](https://github.com/hexedtech/codemp-sublime) - [Sublime Text](https://github.com/hexedtech/codemp-sublime)
<!-- - [IntelliJ Platform](https://github.com/hexedtech/codemp-intellij) -->
# using this library ## Registration
This is the main client library for codemp. It exposes functions to interact with the codemp client itself, its workspaces and buffers. 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 During the initial closed beta, registrations will require an invite code. Get in contact if interested.
This library is primarily a rust crate, so rust applications will get the best possible integration.
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. Just `cargo add codemp` and check the docs for some examples.
### from supported languages ### From supported languages
This library provides first-class bindings for: We provide first-class bindings for:
- java - [JavaScript](./dist/js/README.md): available from `npm` as [`codemp`](https://npmjs.org/package/codemp)
- javascript - [Python](./dist/lua/README.md): available from `PyPI` as [`codemp`](https://pypi.org/project/codemp)
- python - [Lua](./dist/lua/README.md): run `cargo build --features=lua`
- 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] > [!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. Any other language with C FFI capabilities will be able to use `codemp` via its bare C bindings.
This will be more complex and may require wrapping the native calls underneath. This may 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).
# Contributing
If you find bugs or would like to see new features implemented, be sure to open an issue on this repository. 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
View 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
View 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
View 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
View 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.

View file

@ -1,38 +1,41 @@
//! # TextChange //! # TextChange
//! //! A high-level representation of a change within a given buffer.
//! an editor-friendly representation of a text change in a buffer
//! to easily interface with codemp from various editors
/// 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 /// Bulky and large operations will result in a single [`TextChange`] effectively sending the whole
/// replaced to it, allowing to represent any combination of deletions, insertions or replacements /// 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 /// [`TextChange`] contains an optional `hash` field. This is used for error correction: if
/// buffer, but small changes are efficient and easy to create or apply /// 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 /// ### Examples
/// to insert 'a' after 4th character we should send a /// To insert 'a' after 4th character we should send a.
/// `TextChange { span: 4..4, content: "a".into() }` /// `TextChange { start: 4, end: 4, content: "a".into(), hash: None }`
/// ///
/// to delete a the fourth character we should send a /// To delete a the fourth character we should send a.
/// `TextChange { span: 3..4, content: "".into() }` /// `TextChange { start: 3, end: 4, content: "".into(), hash: None }`
/// ///
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "js", napi_derive::napi(object))] #[cfg_attr(feature = "js", napi_derive::napi(object))]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))] #[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct TextChange { pub struct TextChange {
/// range start of text change, as char indexes in buffer previous state /// Range start of text change, as char indexes in buffer previous state.
pub start: u32, pub start: u32,
/// 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, pub end: u32,
/// new content of text inside span /// New content of text inside span.
pub content: String, pub content: String,
/// optional content hash after applying this change /// Optional content hash after applying this change.
pub hash: Option<i64>, pub hash: Option<i64>,
} }
impl TextChange { impl TextChange {
/// Returns the [`std::ops::Range`] representing this change's span.
pub fn span(&self) -> std::ops::Range<usize> { pub fn span(&self) -> std::ops::Range<usize> {
self.start as usize..self.end as usize self.start as usize..self.end as usize
} }
@ -40,24 +43,26 @@ impl TextChange {
#[cfg_attr(feature = "python", pyo3::pymethods)] #[cfg_attr(feature = "python", pyo3::pymethods)]
impl TextChange { impl TextChange {
/// A change is a "deletion" if the change range span is bigger than zero. /// Returns true if this [`TextChange`] deletes existing text.
/// This is not exclusive, a change can be both an insertion and a deletion. ///
/// Note that this is is **not** mutually exclusive with [TextChange::is_insert].
pub fn is_delete(&self) -> bool { pub fn is_delete(&self) -> bool {
self.start < self.end self.start < self.end
} }
/// A change is an "Insertion" if the content is not empty. /// Returns true if this [`TextChange`] adds new text.
/// This is not exclusive, a change can be both an insertion and a deletion. ///
/// Note that this is is **not** mutually exclusive with [TextChange::is_delete].
pub fn is_insert(&self) -> bool { pub fn is_insert(&self) -> bool {
!self.content.is_empty() !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 { pub fn is_empty(&self) -> bool {
!self.is_delete() && !self.is_insert() !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 { pub fn apply(&self, txt: &str) -> String {
let pre_index = std::cmp::min(self.start as usize, txt.len()); let pre_index = std::cmp::min(self.start as usize, txt.len());
let pre = txt.get(..pre_index).unwrap_or("").to_string(); let pre = txt.get(..pre_index).unwrap_or("").to_string();

View file

@ -1,7 +1,7 @@
//! # Controller //! # Controller
//! //!
//! an bidirectional stream handler to easily manage async operations across local buffers and the //! A bidirectional stream handler to easily manage asynchronous operations between local buffers
//! server //! and the server.
use crate::errors::ControllerResult; 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 // 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 // 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. /// 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. /// Events can be enqueued for dispatching without blocking with [`Controller::send`].
/// ///
/// * if possible, prefer a pure [Controller::recv] consumer, awaiting for events /// For receiving events from the server, an asynchronous API with [`Controller::recv`] is
/// * if async is not feasible a [Controller::poll]/[Controller::try_recv] approach is possible /// 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] #[async_trait::async_trait]
pub trait Controller<T : Sized + Send + Sync> : Sized + Send + Sync { pub trait Controller<T : Sized + Send + Sync> : Sized + Send + Sync {
/// enqueue a new value to be sent to all other users /// 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
async fn send(&self, x: T) -> ControllerResult<()>; async fn send(&self, x: T) -> ControllerResult<()>;
/// get next value from other users, blocking until one is available /// Block until a value is available and returns it.
///
/// this is just an async trait function wrapped by `async_trait`:
///
/// `async fn recv(&self) -> codemp::ControllerResult<T>;`
async fn recv(&self) -> ControllerResult<T> { async fn recv(&self) -> ControllerResult<T> {
loop { loop {
self.poll().await?; 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>>); fn callback(&self, cb: impl Into<ControllerCallback<Self>>);
/// clears the currently registered callback. /// Clear the currently registered callback.
fn clear_callback(&self); fn clear_callback(&self);
/// block until next value is available without consuming it /// Block until a value is available, without consuming it.
///
/// this is just an async trait function wrapped by `async_trait`:
///
/// `async fn poll(&self) -> codemp::ControllerResult<()>;`
async fn poll(&self) -> ControllerResult<()>; 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>>; 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, /// After this is called, nothing can be received or sent anymore; however, existing
/// but existing controllers will still be accessible until all are dropped /// controllers will still be accessible until all handles are dropped.
/// ///
/// returns true if stop signal was sent, false if channel is closed /// Returns true if the stop signal was successfully sent, false if channel was
/// (likely if worker is already stopped) /// closed (probably because worker had already been stopped).
fn stop(&self) -> bool; 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)>); pub struct ControllerCallback<T>(pub Box<dyn Sync + Send + Fn(T)>);
impl<T> ControllerCallback<T> { impl<T> ControllerCallback<T> {
pub fn call(&self, x: T) { pub(crate) fn call(&self, x: T) {
self.0(x) // lmao at this syntax self.0(x)
} }
} }

View file

@ -1,5 +1,5 @@
//! ### Cursor //! ### 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 codemp_proto as proto;
use uuid::Uuid; use uuid::Uuid;
@ -12,14 +12,14 @@ use pyo3::prelude::*;
#[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "python", pyclass)]
// #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))] // #[cfg_attr(feature = "python", pyo3(crate = "reexported::pyo3"))]
pub struct Cursor { pub struct Cursor {
/// Cursor start position in buffer, as 0-indexed row-column tuple /// Cursor start position in buffer, as 0-indexed row-column tuple.
pub start: (i32, i32), pub start: (i32, i32),
/// 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), pub end: (i32, i32),
/// Path of buffer this cursor is on /// Path of buffer this cursor is on.
pub buffer: String, pub buffer: String,
/// User display name, if provided /// User display name, if provided.
pub user: Option<Uuid>, pub user: Option<Uuid>, // TODO this should be a string, not the UUID!
} }
impl From<proto::cursor::CursorPosition> for Cursor { impl From<proto::cursor::CursorPosition> for Cursor {

View file

@ -1,17 +1,17 @@
//! # Event //! # 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; use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner;
/// Event in a [crate::Workspace] /// Event in a [crate::Workspace].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
pub enum Event { pub enum Event {
/// Fired when the file tree changes /// Fired when the file tree changes.
/// containes the modified buffer path (deleted or created or renamed) /// Contains the modified buffer path (deleted, created or renamed).
FileTreeUpdated(String), FileTreeUpdated(String),
/// Fired when an user joins the current workspace /// Fired when an user joins the current workspace.
UserJoin(String), UserJoin(String),
/// Fired when an user leaves the current workspace /// Fired when an user leaves the current workspace.
UserLeave(String), UserLeave(String),
} }

View file

@ -7,9 +7,9 @@ use uuid::Uuid;
/// Represents a service user /// Represents a service user
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
/// User unique identifier, should never change /// User unique identifier, should never change.
pub id: Uuid, pub id: Uuid,
/// User name, can change but should be unique /// User name, can change but should be unique.
pub name: String, pub name: String,
} }

View file

@ -14,10 +14,10 @@ use crate::ext::InternallyMutable;
use super::worker::DeltaRequest; 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 /// 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)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi)] #[cfg_attr(feature = "js", napi_derive::napi)]

View file

@ -3,9 +3,9 @@
//! It is built on top of [diamond_types] CRDT, guaranteeing that all peers which have received the //! 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. //! same set of operations will converge to the same content.
/// buffer controller implementation /// controller worker implementation
pub mod controller;
pub(crate) mod worker; pub(crate) mod worker;
/// buffer controller implementation
pub mod controller;
pub use controller::BufferController as Controller; pub use controller::BufferController as Controller;

View file

@ -9,7 +9,7 @@ use tonic::async_trait;
use crate::{api::{controller::ControllerCallback, Controller, Cursor}, errors::ControllerResult}; use crate::{api::{controller::ControllerCallback, Controller, Cursor}, errors::ControllerResult};
use codemp_proto::cursor::CursorPosition; 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]. /// An unique [CursorController] exists for each active [crate::Workspace].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -2,9 +2,9 @@
//! Each user in a [crate::Workspace] holds a cursor and can move it across multiple buffers. //! 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. //! A cursor spans zero or more characters across one or more lines.
/// cursor worker implementation
pub(crate) mod worker; pub(crate) mod worker;
/// cursor controller implementation /// cursor controller implementation
pub mod controller; pub mod controller;
pub use controller::CursorController as Controller; pub use controller::CursorController as Controller;

View file

@ -4,7 +4,7 @@
use crate::{api::Controller, errors::ControllerResult}; use crate::{api::Controller, errors::ControllerResult};
use tokio::sync::mpsc; 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. /// 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. /// 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`]. /// Currently, it uses [`xxhash_rust::xxh3::xxh3_64`].
pub fn hash(data: impl AsRef<[u8]>) -> i64 { pub fn hash(data: impl AsRef<[u8]>) -> i64 {

View file

@ -1,4 +1,4 @@
//! ### lua //! ### Lua
//! Using [mlua] it's possible to map almost perfectly the entirety of `codemp` API. //! 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 //! Notable outliers are functions that receive `codemp` objects: these instead receive arguments
//! to build the object instead (such as [`crate::api::Controller::send`]) //! 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` //! necessary to drive it. A separate driver thread can be spawned with `spawn_runtime_driver`
//! function. //! 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 //! `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::io::Write;
use std::sync::Mutex; use std::sync::Mutex;

View file

@ -1,8 +1,9 @@
//! ### FFI //! ### 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 //! 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. //! available to simplify dependency management and provide type hints in editor.
/// java bindings, built with [jni] /// java bindings, built with [jni]
#[cfg(feature = "java")] #[cfg(feature = "java")]

View file

@ -6,8 +6,10 @@
//! workspaces each containing any number of buffers. //! workspaces each containing any number of buffers.
//! //!
//! The [`Client`] is completely managed by the library itself, making its use simple across async //! 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`], //! contexts and FFI boundaries. All memory is managed by the library itself, which gives out always
//! providing an unopinionated approach with both callback-based and blocking-based APIs. //! 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, //! The library also provides ready-to-use bindings in a growing number of other programming languages,
//! to support a potentially infinite number of editors. //! to support a potentially infinite number of editors.
@ -81,7 +83,7 @@
//! * `python` //! * `python`
//! //!
//! For some of these, ready-to-use packages are available in various registries: //! 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) //! * [npm (javascript)](https://www.npmjs.com/package/codemp)
//! //!

View file

@ -96,7 +96,7 @@ impl Workspace {
Ok(ws) 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<()> { pub async fn create(&self, path: &str) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
workspace_client workspace_client
@ -114,10 +114,7 @@ impl Workspace {
Ok(()) Ok(())
} }
/// attach to a buffer, starting a buffer controller and returning a new reference to it /// Attach to a buffer and return a handle to it.
///
/// to interact with such buffer use [crate::api::Controller::send] or
/// [crate::api::Controller::recv] to exchange [crate::api::TextChange]
pub async fn attach(&self, path: &str) -> ConnectionResult<buffer::Controller> { pub async fn attach(&self, path: &str) -> ConnectionResult<buffer::Controller> {
let mut worskspace_client = self.0.services.ws(); let mut worskspace_client = self.0.services.ws();
let request = tonic::Request::new(BufferNode { let request = tonic::Request::new(BufferNode {
@ -148,11 +145,11 @@ impl Workspace {
Ok(controller) 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 /// This option will be carried in background. [`buffer::worker::BufferWorker`] will be stopped and dropped.
/// may still be some events enqueued in buffers to poll, but the [buffer::Controller] itself won't be /// There may still be some events enqueued in buffers to poll, but the [buffer::Controller] itself won't be
/// accessible anymore from [Workspace]. /// accessible anymore from [`Workspace`].
pub fn detach(&self, path: &str) -> DetachResult { pub fn detach(&self, path: &str) -> DetachResult {
match self.0.buffers.remove(path) { match self.0.buffers.remove(path) {
None => DetachResult::NotAttached, 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? // TODO this method is weird and ugly, can we make it more standard?
pub async fn event(&self) -> ControllerResult<Event> { pub async fn event(&self) -> ControllerResult<Event> {
self.0 self.0
@ -178,7 +175,7 @@ impl Workspace {
.ok_or(crate::errors::ControllerError::Unfulfilled) .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<()> { pub async fn fetch_buffers(&self) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
let buffers = workspace_client let buffers = workspace_client
@ -195,7 +192,7 @@ impl Workspace {
Ok(()) 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<()> { pub async fn fetch_users(&self) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
let users = BTreeSet::from_iter( let users = BTreeSet::from_iter(
@ -216,7 +213,7 @@ impl Workspace {
Ok(()) 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>> { pub async fn list_buffer_users(&self, path: &str) -> RemoteResult<Vec<User>> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
let buffer_users = workspace_client let buffer_users = workspace_client
@ -233,7 +230,7 @@ impl Workspace {
Ok(buffer_users) Ok(buffer_users)
} }
/// Delete a buffer /// Delete a buffer.
pub async fn delete(&self, path: &str) -> RemoteResult<()> { pub async fn delete(&self, path: &str) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
workspace_client workspace_client
@ -251,25 +248,25 @@ impl Workspace {
Ok(()) 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 // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn id(&self) -> String { pub fn id(&self) -> String {
self.0.name.clone() 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 // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn cursor(&self) -> cursor::Controller { pub fn cursor(&self) -> cursor::Controller {
self.0.cursor.clone() 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 // #[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> { pub fn buffer_by_name(&self, path: &str) -> Option<buffer::Controller> {
self.0.buffers.get(path).map(|x| x.clone()) 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 // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn buffer_list(&self) -> Vec<String> { pub fn buffer_list(&self) -> Vec<String> {
self.0 self.0
@ -279,7 +276,7 @@ impl Workspace {
.collect() .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 // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn filetree(&self, filter: Option<&str>) -> Vec<String> { pub fn filetree(&self, filter: Option<&str>) -> Vec<String> {
self.0.filetree.iter() self.0.filetree.iter()