diff --git a/Cargo.toml b/Cargo.toml index 09adeb1..87b0f18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ name = "codemp" # core tracing = "0.1" # woot -codemp-woot = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/woot.git", features = ["serde"], tag = "v0.1.1", optional = true } +codemp-woot = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/woot.git", features = ["serde"], tag = "v0.1.2", optional = true } # proto uuid = { version = "1.3.1", features = ["v4"], optional = true } tonic = { version = "0.9", features = ["tls", "tls-roots"], optional = true } diff --git a/src/api/change.rs b/src/api/change.rs index 041e628..9fbf0a5 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -3,7 +3,8 @@ //! an editor-friendly representation of a text change in a buffer //! to easily interface with codemp from various editors -use crate::proto::cursor::RowCol; +#[cfg(feature = "woot")] +use crate::woot::{WootResult, woot::Woot, crdt::{TextEditor, CRDT, Op}}; /// an editor-friendly representation of a text change in a buffer /// @@ -59,6 +60,33 @@ impl TextChange { } } + #[cfg(feature = "woot")] + pub fn transform(self, woot: &Woot) -> WootResult> { + let mut out = Vec::new(); + if self.is_empty() { return Ok(out); } // no-op + let view = woot.view(); + let Some(span) = view.get(self.span.clone()) else { + return Err(crate::woot::WootError::OutOfBounds); + }; + let diff = similar::TextDiff::from_chars(span, &self.content); + for (i, diff) in diff.iter_all_changes().enumerate() { + match diff.tag() { + similar::ChangeTag::Equal => {}, + similar::ChangeTag::Delete => match woot.delete_one(self.span.start + i) { + Err(e) => tracing::error!("could not create deletion: {}", e), + Ok(op) => out.push(op), + }, + similar::ChangeTag::Insert => { + match woot.insert(self.span.start + i, diff.value()) { + Ok(mut op) => out.append(&mut op), + Err(e) => tracing::error!("could not create insertion: {}", e), + } + }, + } + } + Ok(out) + } + /// returns true if this TextChange deletes existing text pub fn is_deletion(&self) -> bool { !self.span.is_empty() @@ -84,11 +112,12 @@ impl TextChange { /// convert from byte index to row and column /// txt must be the whole content of the buffer, in order to count lines - pub fn index_to_rowcol(txt: &str, index: usize) -> RowCol { + #[cfg(feature = "transport")] + pub fn index_to_rowcol(txt: &str, index: usize) -> crate::proto::cursor::RowCol { // FIXME might panic, use .get() let row = txt[..index].matches('\n').count() as i32; let col = txt[..index].split('\n').last().unwrap_or("").len() as i32; - RowCol { row, col } + crate::proto::cursor::RowCol { row, col } } } diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index cccf1d9..e98ea7c 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -1,11 +1,10 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use similar::{TextDiff, ChangeTag}; use tokio::sync::{watch, mpsc, oneshot}; use tonic::{async_trait, Streaming}; use uuid::Uuid; -use woot::crdt::{Op, CRDT, TextEditor}; +use woot::crdt::{Op, CRDT}; use woot::woot::Woot; use crate::errors::IgnorableError; @@ -95,53 +94,22 @@ impl ControllerWorker for BufferWorker { // received a text change from editor res = self.operations.recv() => match res { - None => break, - Some(change) => { - if !change.is_empty() { - let view = self.buffer.view(); - match view.get(change.span.clone()) { - None => tracing::error!("received illegal span from client: {:?} but buffer is of len {}", change.span, view.len()), - Some(span) => { - let diff = TextDiff::from_chars(span, &change.content); - - let mut i = 0; - let mut ops = Vec::new(); - for diff in diff.iter_all_changes() { - match diff.tag() { - ChangeTag::Equal => i += 1, - ChangeTag::Delete => match self.buffer.delete(change.span.start + i) { - Ok(op) => ops.push(op), - Err(e) => tracing::error!("could not apply deletion: {}", e), - }, - ChangeTag::Insert => { - for c in diff.value().chars() { - match self.buffer.insert(change.span.start + i, c) { - Ok(op) => { - ops.push(op); - i += 1; - }, - Err(e) => tracing::error!("could not apply insertion: {}", e), - } - } - }, - } - } - - for op in ops { - let operation = Operation { - data: postcard::to_extend(&op, Vec::new()).unwrap(), - }; - - match tx.send(operation).await { - Err(e) => tracing::error!("server refused to broadcast {}: {}", op, e), - Ok(()) => { - self.content.send(self.buffer.view()).unwrap_or_warn("could not send buffer update"); - }, - } - } - }, + None => break tracing::debug!("stopping: editor closed channel"), + Some(change) => match change.transform(&self.buffer) { + Err(e) => break tracing::error!("could not apply operation from client: {}", e), + Ok(ops) => { + for op in ops { + self.buffer.merge(op.clone()); + let operation = Operation { + data: postcard::to_extend(&op, Vec::new()).unwrap(), + }; + if let Err(e) = tx.send(operation).await { + tracing::error!("server refused to broadcast {}: {}", op, e); + } } - } + self.content.send(self.buffer.view()) + .unwrap_or_warn("could not send buffer update"); + }, } },