From 2ad5d4f6b0abd030c0ca7c1304c98728e76cdba0 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 20 Aug 2023 00:46:55 +0200 Subject: [PATCH] docs: a ton of documentation --- src/buffer/controller.rs | 22 ++++++++++ src/buffer/factory.rs | 16 +++++++ src/buffer/mod.rs | 14 ++++++ src/client.rs | 26 +++++++++++ src/cursor/controller.rs | 32 +++++++------- src/cursor/mod.rs | 9 ++++ src/errors.rs | 14 +++++- src/lib.rs | 95 +++++++++++++++++++++++++++++++++++++--- src/prelude.rs | 4 ++ 9 files changed, 210 insertions(+), 22 deletions(-) diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index c42477a..6042e1d 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -1,3 +1,7 @@ +//! ### controller +//! +//! a controller implementation for buffer actions + use operational_transform::OperationSeq; use tokio::sync::{watch, mpsc, broadcast, Mutex}; use tonic::async_trait; @@ -8,6 +12,22 @@ use crate::buffer::factory::{leading_noop, tailing_noop, OperationFactory}; use super::TextChange; +/// the buffer controller implementation +/// +/// this contains +/// * a watch channel which always contains an updated view of the buffer content +/// * a sink to send buffer operations into +/// * a mutexed broadcast receiver for buffer operations +/// * a channel to stop the associated worker +/// +/// for each controller a worker exists, managing outgoing and inbound +/// queues, transforming outbound delayed ops and applying remote changes +/// to the local buffer +/// +/// this controller implements [crate::buffer::OperationFactory], allowing to produce +/// Operation Sequences easily +/// +/// upon dropping this handle will stop the associated worker pub struct BufferController { content: watch::Receiver, operations: mpsc::UnboundedSender, @@ -43,6 +63,7 @@ impl OperationFactory for BufferController { impl Controller for BufferController { type Input = OperationSeq; + /// receive an operation seq and transform it into a TextChange from buffer content async fn recv(&self) -> Result { let op = self.stream.lock().await.recv().await?; let after = self.content.borrow().clone(); @@ -54,6 +75,7 @@ impl Controller for BufferController { Ok(TextChange { span, content }) } + /// enqueue an opseq for processing fn send(&self, op: OperationSeq) -> Result<(), Error> { Ok(self.operations.send(op)?) } diff --git a/src/buffer/factory.rs b/src/buffer/factory.rs index 4c175e0..26a903b 100644 --- a/src/buffer/factory.rs +++ b/src/buffer/factory.rs @@ -1,9 +1,17 @@ +//! ### factory +//! +//! a helper trait to produce Operation Sequences, knowing the current +//! state of the buffer + use std::ops::Range; use operational_transform::{OperationSeq, Operation}; use similar::{TextDiff, ChangeTag}; +/// calculate leading no-ops in given opseq pub const fn leading_noop(seq: &[Operation]) -> u64 { count_noop(seq.first()) } + +/// calculate tailing no-ops in given opseq pub const fn tailing_noop(seq: &[Operation]) -> u64 { count_noop(seq.last()) } const fn count_noop(op: Option<&Operation>) -> u64 { @@ -14,19 +22,24 @@ const fn count_noop(op: Option<&Operation>) -> u64 { } } +/// return the range on which the operation seq is actually applying its changes pub fn op_effective_range(op: &OperationSeq) -> Range { let first = leading_noop(op.ops()); let last = op.base_len() as u64 - tailing_noop(op.ops()); first..last } +/// a helper trait that any string container can implement, which generates opseqs pub trait OperationFactory { + /// the current content of the buffer fn content(&self) -> String; + /// completely replace the buffer with given text fn replace(&self, txt: &str) -> Option { self.delta(0, txt, self.content().len()) } + /// transform buffer in range [start..end] with given text fn delta(&self, start: usize, txt: &str, end: usize) -> Option { let mut out = OperationSeq::default(); let content = self.content(); @@ -55,6 +68,7 @@ pub trait OperationFactory { Some(out) } + /// insert given chars at target position fn insert(&self, txt: &str, pos: u64) -> OperationSeq { let mut out = OperationSeq::default(); let total = self.content().len() as u64; @@ -64,6 +78,7 @@ pub trait OperationFactory { out } + /// delete n characters forward at given position fn delete(&self, pos: u64, count: u64) -> OperationSeq { let mut out = OperationSeq::default(); let len = self.content().len() as u64; @@ -73,6 +88,7 @@ pub trait OperationFactory { out } + /// delete n characters backwards at given position fn cancel(&self, pos: u64, count: u64) -> OperationSeq { let mut out = OperationSeq::default(); let len = self.content().len() as u64; diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 13beed4..cbe072f 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -1,16 +1,30 @@ +//! ### buffer +//! +//! a buffer is a container fo text edited by users. +//! this module contains buffer-related operations and helpers to create Operation Sequences +//! (the underlying chunks of changes sent over the wire) + use std::ops::Range; pub(crate) mod worker; + +/// buffer controller implementation pub mod controller; + +/// operation factory, with helper functions to produce opseqs pub mod factory; pub use factory::OperationFactory; pub use controller::BufferController as Controller; +/// an editor-friendly representation of a text change in a buffer +/// /// TODO move in proto #[derive(Debug)] pub struct TextChange { + /// range of text change, as byte indexes in buffer pub span: Range, + /// content of text change, as string pub content: String, } diff --git a/src/client.rs b/src/client.rs index 60c911b..2f0341b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,7 @@ +//! ### client +//! +//! codemp client manager, containing grpc services + use std::{sync::Arc, collections::BTreeMap}; use tonic::transport::Channel; @@ -11,6 +15,11 @@ use crate::{ }; +/// codemp client manager +/// +/// contains all required grpc services and the unique user id +/// will disconnect when dropped +/// can be used to interact with server pub struct Client { id: String, client: Services, @@ -29,6 +38,7 @@ struct Workspace { impl Client { + /// instantiate and connect a new client pub async fn new(dst: &str) -> Result { let buffer = BufferClient::connect(dst.to_string()).await?; let cursor = CursorClient::connect(dst.to_string()).await?; @@ -37,15 +47,18 @@ impl Client { Ok(Client { id, client: Services { buffer, cursor}, workspace: None }) } + /// return a reference to current cursor controller, if currently in a workspace pub fn get_cursor(&self) -> Option> { Some(self.workspace.as_ref()?.cursor.clone()) } + /// leave current workspace if in one, disconnecting buffer and cursor controllers pub fn leave_workspace(&mut self) { // TODO need to stop tasks? self.workspace = None } + /// disconnect from a specific buffer pub fn disconnect_buffer(&mut self, path: &str) -> bool { match &mut self.workspace { Some(w) => w.buffers.remove(path).is_some(), @@ -53,10 +66,15 @@ impl Client { } } + /// get a new reference to a buffer controller, if any is active to given path pub fn get_buffer(&self, path: &str) -> Option> { self.workspace.as_ref()?.buffers.get(path).cloned() } + /// join a workspace, starting a cursorcontroller and returning a new reference to it + /// + /// to interact with such workspace [crate::Controller::send] cursor events or + /// [crate::Controller::recv] for events on the associated [crate::cursor::Controller]. pub async fn join(&mut self, _session: &str) -> Result, Error> { // TODO there is no real workspace handling in codemp server so it behaves like one big global // session. I'm still creating this to start laying out the proper use flow @@ -83,6 +101,7 @@ impl Client { Ok(handle) } + /// create a new buffer in current workspace, with optional given content pub async fn create(&mut self, path: &str, content: Option<&str>) -> Result<(), Error> { if let Some(_workspace) = &self.workspace { self.client.buffer @@ -98,6 +117,13 @@ impl Client { } } + /// attach to a buffer, starting a buffer controller and returning a new reference to it + /// + /// to interact with such buffer [crate::Controller::send] operation sequences + /// or [crate::Controller::recv] for text events using its [crate::buffer::Controller]. + /// to generate operation sequences use the [crate::buffer::OperationFactory] + /// methods, which are implemented on [crate::buffer::Controller], such as + /// [crate::buffer::OperationFactory::delta]. pub async fn attach(&mut self, path: &str) -> Result, Error> { if let Some(workspace) = &mut self.workspace { let mut client = self.client.buffer.clone(); diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index ae5239a..2374161 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -1,8 +1,23 @@ +//! ### controller +//! +//! a controller implementation for cursor actions + use tokio::sync::{mpsc, broadcast::{self, error::RecvError}, Mutex}; use tonic::async_trait; use crate::{proto::{CursorPosition, CursorEvent}, Error, Controller, errors::IgnorableError}; +/// the cursor controller implementation +/// +/// this contains +/// * the unique identifier of current user +/// * a sink to send movements into +/// * a mutex over a stream of inbound cursor events +/// * a channel to stop the associated worker +/// +/// for each controller a worker exists, managing outgoing and inbound event queues +/// +/// upon dropping this handle will stop the associated worker pub struct CursorController { uid: String, op: mpsc::UnboundedSender, @@ -31,6 +46,7 @@ impl CursorController { impl Controller for CursorController { type Input = CursorPosition; + /// enqueue a cursor event to be broadcast to current workspace fn send(&self, cursor: CursorPosition) -> Result<(), Error> { Ok(self.op.send(CursorEvent { user: self.uid.clone(), @@ -40,6 +56,7 @@ impl Controller for CursorController { // TODO is this cancelable? so it can be used in tokio::select! // TODO is the result type overkill? should be an option? + /// get next cursor event from current workspace, or block until one is available async fn recv(&self) -> Result { let mut stream = self.stream.lock().await; match stream.recv().await { @@ -51,19 +68,4 @@ impl Controller for CursorController { } } } - - // fn try_poll(&self) -> Option> { - // match self.stream.try_lock() { - // Err(_) => None, - // Ok(mut x) => match x.try_recv() { - // Ok(x) => Some(Some(x)), - // Err(TryRecvError::Empty) => None, - // Err(TryRecvError::Closed) => Some(None), - // Err(TryRecvError::Lagged(n)) => { - // tracing::error!("cursor channel lagged behind, skipping {} events", n); - // Some(Some(x.try_recv().expect("could not receive after lagging"))) - // } - // } - // } - // } } diff --git a/src/cursor/mod.rs b/src/cursor/mod.rs index b989418..4f8623c 100644 --- a/src/cursor/mod.rs +++ b/src/cursor/mod.rs @@ -1,4 +1,11 @@ +//! ### cursor +//! +//! each user holds a cursor, which consists of multiple highlighted region +//! on a specific buffer + pub(crate) mod worker; + +/// cursor controller implementation pub mod controller; pub use controller::CursorController as Controller; @@ -18,10 +25,12 @@ impl From::<(i32, i32)> for RowCol { } impl CursorPosition { + /// extract start position, defaulting to (0,0) pub fn start(&self) -> RowCol { self.start.clone().unwrap_or((0, 0).into()) } + /// extract end position, defaulting to (0,0) pub fn end(&self) -> RowCol { self.end.clone().unwrap_or((0, 0).into()) } diff --git a/src/errors.rs b/src/errors.rs index 523c406..6c0bbbb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,9 +1,14 @@ +//! ### Errors +//! +//! library error helpers and types + use std::{result::Result as StdResult, error::Error as StdError, fmt::Display}; use tokio::sync::{mpsc, broadcast}; use tonic::{Status, Code}; use tracing::warn; +/// an error which can be ignored with just a warning entry pub trait IgnorableError { fn unwrap_or_warn(self, msg: &str); } @@ -18,24 +23,29 @@ where E : std::fmt::Display { } } +/// result type for codemp errors pub type Result = StdResult; // TODO split this into specific errors for various parts of the library +/// codemp error type for library issues #[derive(Debug)] pub enum Error { + /// errors caused by tonic http layer Transport { status: Code, message: String, }, + /// errors caused by async channels Channel { send: bool }, + /// errors caused by wrong usage of library objects InvalidState { msg: String, }, - // TODO filler error, remove later - Filler { + /// if you see these errors someone is being lazy (: + Filler { // TODO filler error, remove later message: String, }, } diff --git a/src/lib.rs b/src/lib.rs index 7876eff..1c6bfa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,18 +3,102 @@ //! This is the core library of the codemp project. //! //! ## Structure -//! The main entrypoint is the [client::Client] object, that maintains a connection and can +//! The main entrypoint is the [Client] object, that maintains a connection and can //! be used to join workspaces or attach to buffers. //! //! Some actions will return structs implementing the [Controller] trait. These can be polled -//! for new stream events, which will be returned in order. Blocking and callback variants are also -//! implemented. The [Controller] can also be used to send new events to the server. +//! for new stream events ([Controller::recv]), which will be returned in order. Blocking and +//! callback variants are also implemented. The [Controller] can also be used to send new +//! events to the server ([Controller::send]). +//! +//! Each operation on a buffer is represented as an [ot::OperationSeq]. +//! A visualization about how OperationSeqs work is available +//! [here](http://operational-transformation.github.io/index.html), +//! but to use this library it's only sufficient to know that they can only +//! be applied on buffers of some length and are transformable to be able to be +//! applied in a different order while maintaining the same result. +//! +//! To generate Operation Sequences use helper methods from the trait [buffer::OperationFactory]. //! //! ## Features //! * `proto` : include GRCP protocol definitions under [proto] (default) //! * `global`: provide a lazy_static global INSTANCE in [instance::global] //! * `sync` : wraps the [instance::a_sync::Instance] holder into a sync variant: [instance::sync::Instance] //! +//! ## Example +//! library can be used both sync and async depending on wether the `sync` feature flag has been +//! enabled. a global `INSTANCE` static reference can also be made available with the `global` +//! flag. +//! +//! ### Async +//! ```no_run +//! async fn async_example() -> codemp::Result<()> { +//! // create global instance +//! let codemp = codemp::Instance::default(); +//! +//! // connect to a remote server +//! codemp.connect("http://alemi.dev:50051").await?; +//! +//! // join a remote workspace, obtaining a cursor controller +//! let cursor = codemp.join("some_workspace").await?; +//! +//! // move cursor +//! cursor.send( +//! codemp::proto::CursorPosition { +//! buffer: "test.txt".into(), +//! start: Some(codemp::proto::RowCol { row: 0, col: 0 }), +//! end: Some(codemp::proto::RowCol { row: 0, col: 1 }), +//! } +//! )?; +//! +//! // listen for event +//! let op = cursor.recv().await?; +//! println!("received cursor event: {:?}", op); +//! +//! // create a new buffer +//! codemp.create("test.txt", None).await?; +//! +//! // attach to created buffer +//! let buffer = codemp.attach("test.txt").await?; +//! +//! // sending operation +//! if let Some(delta) = buffer.delta(0, "hello", 0) { +//! buffer.send(delta).expect("could not enqueue operation"); +//! } +//! +//! if let Some(delta) = buffer.delta(4, "o world", 5) { +//! buffer.send(delta).expect("could not enqueue operation"); +//! } +//! +//! Ok(()) +//! } +//! ``` +//! +//! ### Sync +//! +//! ```no_run +//! // activate feature "global" to access static CODEMP_INSTANCE +//! use codemp::instance::global::INSTANCE; +//! +//! fn sync_example() -> Result<()> { +//! // connect to remote server +//! INSTANCE.connect("http://alemi.dev:50051")?; +//! +//! // join workspace +//! let cursor = INSTANCE.join("some_workspace")?; +//! +//! let (stop, stop_rx) = tokio::sync::mpsc::unbounded_channel(); +//! Arc::new(cursor).callback( +//! INSTANCE.rt(), +//! stop_rx, +//! | cursor_event | { +//! println!("received cursor event: {:?}", cursor_event); +//! } +//! ); +//! +//! Ok(()) +//! } +//! ``` /// cursor related types and controller @@ -99,7 +183,7 @@ pub trait Controller : Sized + Send + Sync { /// preventing it from being dropped (and likely disconnecting). using the stop channel is /// important for proper cleanup fn callback( - self: Arc, + self: &Arc, rt: &tokio::runtime::Runtime, mut stop: tokio::sync::mpsc::UnboundedReceiver<()>, mut cb: F @@ -107,10 +191,11 @@ pub trait Controller : Sized + Send + Sync { Self : 'static, F : FnMut(T) + Sync + Send + 'static { + let _self = self.clone(); rt.spawn(async move { loop { tokio::select! { - Ok(data) = self.recv() => cb(data), + Ok(data) = _self.recv() => cb(data), Some(()) = stop.recv() => break, else => break, } diff --git a/src/prelude.rs b/src/prelude.rs index dc9ef11..93faaca 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,3 +1,7 @@ +//! ### Prelude +//! +//! all-in-one renamed imports with `use codemp::prelude::*` + pub use crate::{ Error as CodempError, Result as CodempResult,