From a24c1328238d31ae2c5de34ab18dd4a2801adb90 Mon Sep 17 00:00:00 2001 From: adz Date: Mon, 9 Dec 2024 22:42:58 +0100 Subject: [PATCH 1/5] Introduce document module This module holds the automerge `AutoCommit` document now with all the necessary methods around it we need. Additionally this commit prepares the use of a hard-coded byte representation to construct the document. This will allow us to independently create documents across peers as all peers will create the _same_ document schema whenever they do it. More about it here: --- src/document.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 2 files changed, 83 insertions(+) create mode 100644 src/document.rs diff --git a/src/document.rs b/src/document.rs new file mode 100644 index 0000000..0111d6a --- /dev/null +++ b/src/document.rs @@ -0,0 +1,82 @@ +use std::fmt; + +use anyhow::Result; +use automerge::{AutoCommit, AutoSerde, Patch}; + +const DOCUMENT_OBJ_ID: &str = "doc"; + +const DOCUMENT_SCHEMA: [u8] = [1, 2, 3]; + +#[derive(Debug)] +pub struct Document { + doc: RefCell, +} + +impl Document { + pub fn new() -> Self { + let doc = AutoCommit::new(); + doc.put_object(automerge::ROOT, DOCUMENT_OBJ_ID, ObjType::Text) + .expect("inserting text object '{DOCUMENT_OBJ_ID}' 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), + } + } + + pub fn update(&mut self, position: i32, del: i32, text: &str) -> Result<()> { + let mut doc = self.doc.borrow_mut(); + doc.splice_text(&root, position as usize, del as isize, text)?; + // Move the diff pointer forward to current position + doc.update_diff_cursor(); + Ok(()) + } + + pub fn load_incremental(&mut self, bytes: &[u8]) -> Result<()> { + let mut doc = self.doc.borrow_mut(); + doc.load_incremental(&bytes)?; + Ok(()) + } + + pub fn diff_incremental(&mut self) -> Vec { + let mut doc = self.doc.borrow_mut(); + doc.diff_incremental() + } + + pub fn text(&self) -> String { + let doc = self.doc.borrow(); + let obj = doc.get(automerge::ROOT, DOCUMENT_OBJ_ID); + doc.text(&obj) + .expect("text to be given in automerge document") + } + + pub fn save(&mut self) -> Vec { + let mut doc = self.doc.borrow_mut(); + doc.save() + } + + pub fn save_incremental(&mut 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 mut 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; From 760e5fed4339527a2316acd8cadf0455a0e3fc9a Mon Sep 17 00:00:00 2001 From: adz Date: Mon, 9 Dec 2024 23:15:42 +0100 Subject: [PATCH 2/5] Write some doc-strings for document types --- src/document.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/document.rs b/src/document.rs index 0111d6a..efe76a7 100644 --- a/src/document.rs +++ b/src/document.rs @@ -1,12 +1,22 @@ use std::fmt; use anyhow::Result; +use automerge::transaction::Transactable; use automerge::{AutoCommit, AutoSerde, Patch}; -const DOCUMENT_OBJ_ID: &str = "doc"; - +/// 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] = [1, 2, 3]; +/// Identifier in automerge document path where we store the text. +const DOCUMENT_OBJ_ID: &str = "doc"; + #[derive(Debug)] pub struct Document { doc: RefCell, From e4349f80731a97bb050c1b989135fc565ede206b Mon Sep 17 00:00:00 2001 From: adz Date: Tue, 10 Dec 2024 01:16:20 +0100 Subject: [PATCH 3/5] Hard-coded document schema --- src/document.rs | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/document.rs b/src/document.rs index efe76a7..0f3d800 100644 --- a/src/document.rs +++ b/src/document.rs @@ -1,8 +1,9 @@ +use std::cell::RefCell; use std::fmt; use anyhow::Result; use automerge::transaction::Transactable; -use automerge::{AutoCommit, AutoSerde, Patch}; +use automerge::{AutoCommit, AutoSerde, ObjId, ObjType, Patch, ReadDoc}; /// Hard-coded automerge document schema in bytes representation for "Aardvark". /// @@ -12,21 +13,28 @@ use automerge::{AutoCommit, AutoSerde, Patch}; /// /// Read more here: /// -const DOCUMENT_SCHEMA: [u8] = [1, 2, 3]; +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"; -#[derive(Debug)] pub struct Document { doc: RefCell, } impl Document { + #[allow(dead_code)] pub fn new() -> Self { - let doc = AutoCommit::new(); + let mut doc = AutoCommit::new(); doc.put_object(automerge::ROOT, DOCUMENT_OBJ_ID, ObjType::Text) - .expect("inserting text object '{DOCUMENT_OBJ_ID}' at root"); + .expect("inserting text object at root"); Self { doc: RefCell::new(doc), } @@ -39,9 +47,19 @@ impl Document { } } + 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(&mut self, position: i32, del: i32, text: &str) -> Result<()> { + let text_obj = self.text_object(); let mut doc = self.doc.borrow_mut(); - doc.splice_text(&root, position as usize, del as isize, text)?; + 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(()) @@ -59,9 +77,9 @@ impl Document { } pub fn text(&self) -> String { + let text_obj = self.text_object(); let doc = self.doc.borrow(); - let obj = doc.get(automerge::ROOT, DOCUMENT_OBJ_ID); - doc.text(&obj) + doc.text(&text_obj) .expect("text to be given in automerge document") } @@ -84,8 +102,8 @@ impl Default for Document { impl fmt::Debug for Document { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut doc = self.doc.borrow(); - let json = serde_json::to_string_pretty(&AutoSerde::from(doc)) + let doc = self.doc.borrow(); + let json = serde_json::to_string_pretty(&AutoSerde::from(&*doc)) .expect("serialize automerge document to JSON"); write!(f, "{}", json) } From f255e4a80f77a4b82ee718e10f8350fd26f0624e Mon Sep 17 00:00:00 2001 From: adz Date: Tue, 10 Dec 2024 01:42:16 +0100 Subject: [PATCH 4/5] Use Document in application --- src/application.rs | 195 +++++++++++++++------------------------------ src/document.rs | 11 +-- 2 files changed, 70 insertions(+), 136 deletions(-) diff --git a/src/application.rs b/src/application.rs index ab4f6e1..5c9c9af 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 mut rx = application.imp().rx.take().expect("rx should be given at this point"); - 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 window = window.clone(); + let application = application.clone(); - 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); + glib::spawn_future_local(async move { + while let Some(bytes) = rx.recv().await { + let document = &application.imp().document; - // get the latest changes - let patches = doc_local.diff_incremental(); - for patch in patches.iter() { - println!("PATCH RECEIVED: {}", patch.action); + // 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, - marks: _, + .. } => { - w.splice_text_view( + window.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, ""); + window.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: _ } => {} + _ => (), } } - doc_local.text(&root).unwrap() - }; - dbg!(&text); - } - }); + dbg!(document.text()); + } + }); + } window - }) - .clone(); + }); - 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 index 0f3d800..ac46f96 100644 --- a/src/document.rs +++ b/src/document.rs @@ -56,7 +56,7 @@ impl Document { obj_id } - pub fn update(&mut self, position: i32, del: i32, text: &str) -> Result<()> { + 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)?; @@ -65,13 +65,13 @@ impl Document { Ok(()) } - pub fn load_incremental(&mut self, bytes: &[u8]) -> Result<()> { + pub fn load_incremental(&self, bytes: &[u8]) -> Result<()> { let mut doc = self.doc.borrow_mut(); doc.load_incremental(&bytes)?; Ok(()) } - pub fn diff_incremental(&mut self) -> Vec { + pub fn diff_incremental(&self) -> Vec { let mut doc = self.doc.borrow_mut(); doc.diff_incremental() } @@ -83,12 +83,13 @@ impl Document { .expect("text to be given in automerge document") } - pub fn save(&mut self) -> Vec { + #[allow(dead_code)] + pub fn save(&self) -> Vec { let mut doc = self.doc.borrow_mut(); doc.save() } - pub fn save_incremental(&mut self) -> Vec { + pub fn save_incremental(&self) -> Vec { let mut doc = self.doc.borrow_mut(); doc.save_incremental() } From fe482b94f2d4e3154d66fa04f54ee6cf2384e174 Mon Sep 17 00:00:00 2001 From: adz Date: Tue, 10 Dec 2024 01:47:28 +0100 Subject: [PATCH 5/5] Formatting --- src/application.rs | 86 +++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/application.rs b/src/application.rs index 5c9c9af..a4e6f3e 100644 --- a/src/application.rs +++ b/src/application.rs @@ -101,54 +101,54 @@ mod imp { 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 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 { - 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; - } + 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 { + 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, ""); - } - _ => (), + // 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, ""); } + _ => (), } - - dbg!(document.text()); } - }); - } - window - }); + dbg!(document.text()); + } + }); + } + + window + }); { let application = application.clone();