diff --git a/src/application.rs b/src/application.rs index ab4f6e1..a4e6f3e 100644 --- a/src/application.rs +++ b/src/application.rs @@ -22,77 +22,44 @@ use std::cell::{OnceCell, RefCell}; use adw::prelude::*; use adw::subclass::prelude::*; -use automerge::transaction::Transactable; -use automerge::PatchAction; -use automerge::ReadDoc; -use automerge::{AutoCommit, ObjType}; use gettextrs::gettext; use gtk::{gio, glib}; use tokio::sync::{mpsc, oneshot}; use crate::config::VERSION; +use crate::document::Document; use crate::glib::closure_local; use crate::network; use crate::{AardvarkTextBuffer, AardvarkWindow}; mod imp { + use automerge::PatchAction; + use super::*; #[derive(Debug)] pub struct AardvarkApplication { window: OnceCell, - automerge: RefCell, - #[allow(dead_code)] - backend_shutdown_tx: oneshot::Sender<()>, + document: Document, tx: mpsc::Sender>, rx: RefCell>>>, + #[allow(dead_code)] + backend_shutdown: oneshot::Sender<()>, } impl AardvarkApplication { fn update_text(&self, position: i32, del: i32, text: &str) { - let mut doc = self.automerge.borrow_mut(); - - let root = match doc.get(automerge::ROOT, "root").expect("root exists") { - Some(root) => root.1, - None => doc - .put_object(automerge::ROOT, "root", ObjType::Text) - .expect("inserting map at root"), - }; - println!("root = {}", root); - - doc.splice_text(&root, position as usize, del as isize, text) - .unwrap(); - - // move the diff pointer forward to current position - doc.update_diff_cursor(); - - /* - let patches = doc.diff_incremental(); - for patch in patches.iter() { - println!("{}", patch.action); - match &patch.action { - PatchAction::SpliceText { index: _, value: _, marks: _ } => {}, - PatchAction::DeleteSeq { index: _, length: _ } => {}, - PatchAction::PutMap { key: _, value: _, conflict: _ } => {}, - PatchAction::PutSeq { index: _, value: _, conflict: _ } => {}, - PatchAction::Insert { index: _, values: _ } => {}, - PatchAction::Increment { prop: _, value: _ } => {}, - PatchAction::Conflict { prop: _ } => {}, - PatchAction::DeleteMap { key: _ } => {}, - PatchAction::Mark { marks: _ } => {}, - } - } - */ - - { - let bytes = doc.save_incremental(); - let tx = self.tx.clone(); - glib::spawn_future_local(async move { - if let Err(e) = tx.send(bytes).await { - println!("{}", e); - } - }); - } + self.document + .update(position, del, text) + .expect("update automerge document after text update"); + + let bytes = self.document.save_incremental(); + let tx = self.tx.clone(); + glib::spawn_future_local(async move { + tx.send(bytes) + .await + .expect("sending message to networking backend"); + }); } } @@ -103,12 +70,12 @@ mod imp { type ParentType = adw::Application; fn new() -> Self { - let automerge = RefCell::new(AutoCommit::new()); - let (backend_shutdown_tx, tx, rx) = network::run().expect("running p2p backend"); + let document = Document::default(); + let (backend_shutdown, tx, rx) = network::run().expect("running p2p backend"); AardvarkApplication { - automerge, - backend_shutdown_tx, + document, + backend_shutdown, tx, rx: RefCell::new(Some(rx)), window: OnceCell::new(), @@ -126,105 +93,79 @@ mod imp { } impl ApplicationImpl for AardvarkApplication { - // We connect to the activate callback to create a window when the application - // has been launched. Additionally, this callback notifies us when the user - // tries to launch a "second instance" of the application. When they try - // to do that, we'll just present any existing window. + // We connect to the activate callback to create a window when the application has been + // launched. Additionally, this callback notifies us when the user tries to launch a + // "second instance" of the application. When they try to do that, we'll just present any + // existing window. fn activate(&self) { let application = self.obj(); + // Get the current window or create one if necessary - let window = self - .window - .get_or_init(|| { - let window = AardvarkWindow::new(&*application); - let app = application.clone(); - let mut rx = application.imp().rx.take().unwrap(); - let w = window.clone(); + let window = self.window.get_or_init(|| { + let window = AardvarkWindow::new(&*application); + let mut rx = application + .imp() + .rx + .take() + .expect("rx should be given at this point"); + + { + let window = window.clone(); + let application = application.clone(); glib::spawn_future_local(async move { while let Some(bytes) = rx.recv().await { - println!("got {:?}", bytes); - let text = { - let mut doc_local = app.imp().automerge.borrow_mut(); - doc_local.load_incremental(&bytes).unwrap(); - println!("LOCAL:"); - print_document(&*doc_local); - - let root = match doc_local - .get(automerge::ROOT, "root") - .expect("root exists") - { - Some(root) => root.1, - None => doc_local - .put_object(automerge::ROOT, "root", ObjType::Text) - .expect("inserting map at root"), - }; - println!("root = {}", root); - - // get the latest changes - let patches = doc_local.diff_incremental(); - for patch in patches.iter() { - println!("PATCH RECEIVED: {}", patch.action); - match &patch.action { - PatchAction::SpliceText { - index, - value, - marks: _, - } => { - w.splice_text_view( - *index as i32, - 0, - value.make_string().as_str(), - ); - } - PatchAction::DeleteSeq { index, length } => { - w.splice_text_view(*index as i32, *length as i32, ""); - } - PatchAction::PutMap { - key: _, - value: _, - conflict: _, - } => {} - PatchAction::PutSeq { - index: _, - value: _, - conflict: _, - } => {} - PatchAction::Insert { - index: _, - values: _, - } => {} - PatchAction::Increment { prop: _, value: _ } => {} - PatchAction::Conflict { prop: _ } => {} - PatchAction::DeleteMap { key: _ } => {} - PatchAction::Mark { marks: _ } => {} + let document = &application.imp().document; + + // Apply remote changes to our local text CRDT + if let Err(err) = document.load_incremental(&bytes) { + eprintln!( + "failed applying text change from remote peer to automerge document: {err}" + ); + continue; + } + + // Get latest changes and apply them to our local text buffer + for patch in document.diff_incremental() { + match &patch.action { + PatchAction::SpliceText { index, value, .. } => { + window.splice_text_view( + *index as i32, + 0, + value.make_string().as_str(), + ); + } + PatchAction::DeleteSeq { index, length } => { + window.splice_text_view(*index as i32, *length as i32, ""); } + _ => (), } + } - doc_local.text(&root).unwrap() - }; - dbg!(&text); + dbg!(document.text()); } }); + } - window - }) - .clone(); + window + }); - let app = application.clone(); - window.clone().get_text_buffer().connect_closure( - "text-change", - false, - closure_local!(|_buffer: AardvarkTextBuffer, - position: i32, - del: i32, - text: &str| { - app.imp().update_text(position, del, text); - }), - ); + { + let application = application.clone(); + window.get_text_buffer().connect_closure( + "text-change", + false, + closure_local!(|_buffer: AardvarkTextBuffer, + position: i32, + del: i32, + text: &str| { + application.imp().update_text(position, del, text); + }), + ); + } // Ask the window manager/compositor to present the window - window.upcast::().present(); + window.clone().upcast::().present(); } } @@ -272,11 +213,3 @@ impl AardvarkApplication { about.present(Some(&window)); } } - -fn print_document(doc: &R) -where - R: ReadDoc, -{ - let serialized = serde_json::to_string_pretty(&automerge::AutoSerde::from(doc)).unwrap(); - println!("{serialized}"); -} diff --git a/src/document.rs b/src/document.rs new file mode 100644 index 0000000..ac46f96 --- /dev/null +++ b/src/document.rs @@ -0,0 +1,111 @@ +use std::cell::RefCell; +use std::fmt; + +use anyhow::Result; +use automerge::transaction::Transactable; +use automerge::{AutoCommit, AutoSerde, ObjId, ObjType, Patch, ReadDoc}; + +/// Hard-coded automerge document schema in bytes representation for "Aardvark". +/// +/// Creating a local document based on this schema allows peers to independently do so as they'll +/// all have the same schema and object ids in the end. Otherwise peers wouldn't be able to merge +/// their changes into each other's documents as the id's wouldn't match. +/// +/// Read more here: +/// +const DOCUMENT_SCHEMA: [u8; 119] = [ + 133, 111, 74, 131, 14, 200, 8, 95, 0, 109, 1, 16, 163, 64, 79, 49, 42, 30, 77, 109, 146, 45, + 91, 5, 214, 2, 217, 205, 1, 252, 203, 208, 39, 6, 89, 188, 223, 101, 41, 50, 160, 144, 47, 147, + 187, 74, 77, 252, 185, 64, 18, 211, 205, 23, 118, 97, 221, 216, 176, 1, 239, 6, 1, 2, 3, 2, 19, + 2, 35, 2, 64, 2, 86, 2, 7, 21, 5, 33, 2, 35, 2, 52, 1, 66, 2, 86, 2, 128, 1, 2, 127, 0, 127, 1, + 127, 1, 127, 0, 127, 0, 127, 7, 127, 3, 100, 111, 99, 127, 0, 127, 1, 1, 127, 4, 127, 0, 127, + 0, 0, +]; + +/// Identifier in automerge document path where we store the text. +const DOCUMENT_OBJ_ID: &str = "doc"; + +pub struct Document { + doc: RefCell, +} + +impl Document { + #[allow(dead_code)] + pub fn new() -> Self { + let mut doc = AutoCommit::new(); + doc.put_object(automerge::ROOT, DOCUMENT_OBJ_ID, ObjType::Text) + .expect("inserting text object at root"); + Self { + doc: RefCell::new(doc), + } + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + let doc = AutoCommit::load(bytes).expect("load automerge document from bytes"); + Self { + doc: RefCell::new(doc), + } + } + + fn text_object(&self) -> ObjId { + let doc = self.doc.borrow(); + let (_value, obj_id) = doc + .get(automerge::ROOT, DOCUMENT_OBJ_ID) + .unwrap_or_default() + .expect("text object at root"); + obj_id + } + + pub fn update(&self, position: i32, del: i32, text: &str) -> Result<()> { + let text_obj = self.text_object(); + let mut doc = self.doc.borrow_mut(); + doc.splice_text(&text_obj, position as usize, del as isize, text)?; + // Move the diff pointer forward to current position + doc.update_diff_cursor(); + Ok(()) + } + + pub fn load_incremental(&self, bytes: &[u8]) -> Result<()> { + let mut doc = self.doc.borrow_mut(); + doc.load_incremental(&bytes)?; + Ok(()) + } + + pub fn diff_incremental(&self) -> Vec { + let mut doc = self.doc.borrow_mut(); + doc.diff_incremental() + } + + pub fn text(&self) -> String { + let text_obj = self.text_object(); + let doc = self.doc.borrow(); + doc.text(&text_obj) + .expect("text to be given in automerge document") + } + + #[allow(dead_code)] + pub fn save(&self) -> Vec { + let mut doc = self.doc.borrow_mut(); + doc.save() + } + + pub fn save_incremental(&self) -> Vec { + let mut doc = self.doc.borrow_mut(); + doc.save_incremental() + } +} + +impl Default for Document { + fn default() -> Self { + Self::from_bytes(&DOCUMENT_SCHEMA) + } +} + +impl fmt::Debug for Document { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let doc = self.doc.borrow(); + let json = serde_json::to_string_pretty(&AutoSerde::from(&*doc)) + .expect("serialize automerge document to JSON"); + write!(f, "{}", json) + } +} diff --git a/src/main.rs b/src/main.rs index 5497a30..bd5947c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod application; mod config; +mod document; mod network; mod operation; mod textbuffer;