From d745685f5e3a4b8bf74f8d12e97ea41db67ef27d Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 20 Jan 2023 13:38:53 -0600 Subject: [PATCH 01/26] wip --- rust/automerge-wasm/src/lib.rs | 36 +++++++++++++++++++ rust/automerge/src/autocommit.rs | 22 ++++++++++++ rust/automerge/src/lib.rs | 1 + rust/automerge/src/transaction/inner.rs | 19 ++++++++++ .../src/transaction/manual_transaction.rs | 12 +++++++ .../automerge/src/transaction/transactable.rs | 10 ++++++ 6 files changed, 100 insertions(+) diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index d6ccc8c8..8c9f096b 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -31,6 +31,7 @@ use am::ScalarValue; use automerge as am; use automerge::{Change, ObjId, Prop, TextEncoding, Value, ROOT}; use js_sys::{Array, Function, Object, Uint8Array}; +use regex::Regex; use serde::ser::Serialize; use std::borrow::Cow; use std::collections::HashMap; @@ -430,6 +431,41 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: JsValue, + range: JsValue, + name: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result<(), JsValue> { + let (obj, _) = self.import(obj)?; + let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); + let range = range.as_string().ok_or("range must be a string")?; + let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; + let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; + let start_sticky = &cap[1] == "("; + let end_sticky = &cap[4] == ")"; + let name = name + .as_string() + .ok_or("invalid mark name") + .map_err(to_js_err)?; + let value = self + .import_scalar(&value, &datatype.as_string()) + .ok_or_else(|| to_js_err("invalid value"))?; + self.doc + .mark( + &obj, + &(start..end), + am::marks::RangeExpand::new(start_sticky, end_sticky), + &name, + value, + ) + .map_err(to_js_err)?; + Ok(()) + } + #[wasm_bindgen(js_name = get)] pub fn get( &self, diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 2258fa2e..3cd798a5 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -1,6 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; +use crate::marks::RangeExpand; use crate::op_observer::OpObserver; use crate::transaction::{CommitOptions, Transactable}; use crate::{ @@ -527,6 +528,27 @@ impl Transactable for AutoCommitWithObs { self.doc.text_at(obj, heads) } + fn mark, V: Into>( + &mut self, + obj: O, + range: &std::ops::Range, + expand: RangeExpand, + mark: &str, + value: V, + ) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let (current, tx) = self.transaction.as_mut().unwrap(); + tx.mark( + &mut self.doc, + current.observer(), + obj.as_ref(), + range, + expand, + mark, + value, + ) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? diff --git a/rust/automerge/src/lib.rs b/rust/automerge/src/lib.rs index 97ff0650..9dd3276e 100644 --- a/rust/automerge/src/lib.rs +++ b/rust/automerge/src/lib.rs @@ -71,6 +71,7 @@ mod list_range; mod list_range_at; mod map_range; mod map_range_at; +pub mod marks; mod op_observer; mod op_set; mod op_tree; diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index cba4e723..3236a9ec 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -2,6 +2,7 @@ use std::num::NonZeroU64; use crate::automerge::Actor; use crate::exid::ExId; +use crate::marks::RangeExpand; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -650,6 +651,24 @@ impl TransactionInner { Ok(()) } + pub(crate) fn mark>( + &mut self, + _doc: &mut Automerge, + mut _op_observer: Option<&mut Obs>, + _ex_obj: &ExId, + _range: &std::ops::Range, + expand: RangeExpand, + _mark: &str, + _value: V, + ) -> Result<(), AutomergeError> { + match expand { + RangeExpand::Neither => todo!(), + RangeExpand::ExpandLeft => todo!(), + RangeExpand::ExpandRight => todo!(), + RangeExpand::ExpandBoth => todo!(), + } + } + fn finalize_op( &mut self, doc: &mut Automerge, diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 22115aab..62dbab89 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -1,6 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; +use crate::marks::RangeExpand; use crate::{Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ScalarValue, Value, Values}; use crate::{AutomergeError, Keys}; use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; @@ -201,6 +202,17 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { self.do_tx(|tx, doc, obs| tx.splice_text(doc, obs, obj.as_ref(), pos, del, text)) } + fn mark, V: Into>( + &mut self, + obj: O, + range: &std::ops::Range, + expand: RangeExpand, + mark: &str, + value: V, + ) -> Result<(), AutomergeError> { + self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), range, expand, mark, value)) + } + fn keys>(&self, obj: O) -> Keys<'_, '_> { self.doc.keys(obj) } diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index 7f38edbe..e5b55883 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -1,6 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; +use crate::marks::RangeExpand; use crate::{ AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, Prop, ScalarValue, Value, Values, @@ -93,6 +94,15 @@ pub trait Transactable { text: &str, ) -> Result<(), AutomergeError>; + fn mark, V: Into>( + &mut self, + obj: O, + range: &std::ops::Range, + expand: RangeExpand, + mark: &str, + value: V, + ) -> Result<(), AutomergeError>; + /// Get the keys of the given object, it should be a map. fn keys>(&self, obj: O) -> Keys<'_, '_>; From 9bc424d7764878829e714eaac70991c5d649be8e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 8 Feb 2023 10:43:33 -0600 Subject: [PATCH 02/26] first test passing - needs serialization --- rust/automerge-wasm/index.d.ts | 9 + rust/automerge-wasm/src/lib.rs | 242 +++++++++++++++--- rust/automerge-wasm/test/marks.ts | 202 +++++++++++++++ rust/automerge/src/autocommit.rs | 56 +++- rust/automerge/src/automerge.rs | 61 +++++ .../src/legacy/serde_impls/op_type.rs | 2 + rust/automerge/src/marks.rs | 40 +-- rust/automerge/src/query.rs | 20 ++ rust/automerge/src/query/attribute.rs | 128 +++++++++ rust/automerge/src/query/attribute2.rs | 173 +++++++++++++ rust/automerge/src/query/insert.rs | 6 + rust/automerge/src/query/raw_spans.rs | 78 ++++++ rust/automerge/src/query/spans.rs | 112 ++++++++ rust/automerge/src/read.rs | 20 +- .../src/storage/convert/op_as_changeop.rs | 2 + rust/automerge/src/transaction/inner.rs | 86 +++++-- .../src/transaction/manual_transaction.rs | 44 +++- .../automerge/src/transaction/transactable.rs | 11 +- rust/automerge/src/types.rs | 56 +++- rust/automerge/src/visualisation.rs | 2 + 20 files changed, 1245 insertions(+), 105 deletions(-) create mode 100644 rust/automerge-wasm/test/marks.ts create mode 100644 rust/automerge/src/query/attribute.rs create mode 100644 rust/automerge/src/query/attribute2.rs create mode 100644 rust/automerge/src/query/raw_spans.rs create mode 100644 rust/automerge/src/query/spans.rs diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index be12e4c1..da49de42 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -165,6 +165,15 @@ export class Automerge { increment(obj: ObjID, prop: Prop, value: number): void; delete(obj: ObjID, prop: Prop): void; + // marks + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + unmark(obj: ObjID, mark: ObjID): void; + spans(obj: ObjID): any; + raw_spans(obj: ObjID): any; + blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + // returns a single value - if there is a conflict return the winner get(obj: ObjID, prop: Prop, heads?: Heads): Value | undefined; getWithType(obj: ObjID, prop: Prop, heads?: Heads): FullValue | null; diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 7c5d3735..94efadbd 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -432,41 +432,6 @@ impl Automerge { Ok(()) } - pub fn mark( - &mut self, - obj: JsValue, - range: JsValue, - name: JsValue, - value: JsValue, - datatype: JsValue, - ) -> Result<(), JsValue> { - let (obj, _) = self.import(obj)?; - let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); - let range = range.as_string().ok_or("range must be a string")?; - let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; - let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; - let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; - let start_sticky = &cap[1] == "("; - let end_sticky = &cap[4] == ")"; - let name = name - .as_string() - .ok_or("invalid mark name") - .map_err(to_js_err)?; - let value = self - .import_scalar(&value, &datatype.as_string()) - .ok_or_else(|| to_js_err("invalid value"))?; - self.doc - .mark( - &obj, - &(start..end), - am::marks::RangeExpand::new(start_sticky, end_sticky), - &name, - value, - ) - .map_err(to_js_err)?; - Ok(()) - } - #[wasm_bindgen(js_name = get)] pub fn get( &self, @@ -822,6 +787,213 @@ impl Automerge { let hash = self.doc.empty_change(options); JsValue::from_str(&hex::encode(hash)) } + + pub fn mark( + &mut self, + obj: JsValue, + range: JsValue, + name: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result<(), JsValue> { + let (obj, _) = self.import(obj)?; + let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); + let range = range.as_string().ok_or("range must be a string")?; + let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; + let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; + let start_sticky = &cap[1] == "("; + let end_sticky = &cap[4] == ")"; + let name = name + .as_string() + .ok_or("invalid mark name") + .map_err(to_js_err)?; + let value = self + .import_scalar(&value, &datatype.as_string()) + .ok_or_else(|| to_js_err("invalid value"))?; + self.doc + .mark( + &obj, + &am::marks::MarkRange::new(start, end, start_sticky, end_sticky), + &name, + value, + ) + .map_err(to_js_err)?; + Ok(()) + } + + pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { + let (obj, _) = self.import(obj)?; + let (mark, _) = self.import(mark)?; + self.doc.unmark(&obj, &mark).map_err(to_js_err)?; + Ok(()) + } + + pub fn spans(&mut self, obj: JsValue) -> Result { + let (obj, _) = self.import(obj)?; + let text = self.doc.text(&obj)?; + let spans = self.doc.spans(&obj).map_err(to_js_err)?; + let mut last_pos = 0; + let result = Array::new(); + for s in spans { + let marks = Array::new(); + for m in s.marks { + let mark = Array::new(); + mark.push(&m.0.into()); // span name + let (datatype, value) = alloc(&am::Value::Scalar(m.1.clone()), self.text_rep); + mark.push(&datatype.into()); + mark.push(&value); + marks.push(&mark.into()); + } + let text_span = &text.get(last_pos..s.pos); //.slice(last_pos, s.pos); + if let Some(t) = text_span { + if !t.is_empty() { + result.push(&t.to_string().into()); + } + } + result.push(&marks); + last_pos = s.pos; + //let obj = Object::new().into(); + //js_set(&obj, "pos", s.pos as i32)?; + //js_set(&obj, "marks", marks)?; + //result.push(&obj.into()); + } + let text_span = &text.get(last_pos..); + if let Some(t) = text_span { + result.push(&t.to_string().into()); + } + Ok(result.into()) + } + + pub fn raw_spans(&mut self, obj: JsValue) -> Result { + let (obj, _) = self.import(obj)?; + let spans = self.doc.raw_spans(obj).map_err(to_js_err)?; + let result = Array::new(); + for s in spans { + //#[allow(deprecated)] + //result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); + result.push(&serde_wasm_bindgen::to_value(&s)?); + } + Ok(result) + } + + pub fn blame( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + am::log!("doc.blame() is depricated - please use doc.attribute()"); + self.attribute(obj, baseline, change_sets) + } + + pub fn attribute( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let (obj, _) = self.import(obj)?; + let baseline = baseline.dyn_into::()?; + let baseline = get_heads(Some(baseline))?.unwrap(); // we know its an array + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .filter_map(|o| get_heads(o.dyn_into::().ok()).transpose()) + .collect::, _>>()?; + let result = self.doc.attribute(obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|range| { + let r = Object::new(); + js_set(&r, "start", range.start as f64)?; + js_set(&r, "end", range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "pos", d.0 as f64)?; + js_set(&r, "val", &d.1)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } + + pub fn attribute2( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let (obj, _) = self.import(obj)?; + let baseline = baseline.dyn_into::()?; + let baseline = get_heads(Some(baseline))?.unwrap(); + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .filter_map(|o| get_heads(o.dyn_into::().ok()).transpose()) + .collect::, _>>()?; + let result = self.doc.attribute2(obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|a| { + let r = Object::new(); + js_set(&r, "actor", a.actor)?; + js_set(&r, "start", a.range.start as f64)?; + js_set(&r, "end", a.range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "actor", d.actor)?; + js_set(&r, "pos", d.pos as f64)?; + js_set(&r, "val", &d.span)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } } #[wasm_bindgen(js_name = create)] diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts new file mode 100644 index 00000000..54c834fa --- /dev/null +++ b/rust/automerge-wasm/test/marks.ts @@ -0,0 +1,202 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, load, Automerge, encodeChange, decodeChange } from '..' + +describe('Automerge', () => { + describe('marks', () => { + it.skip('should handle marks [..]', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let text = doc.text(list) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); + }) + + it.skip('should handle marks [..] at the beginning of a string', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.insert(list, 0, "A") + doc2.insert(list, 4, "B") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); + }) + + it.skip('should handle marks [..] with splice', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 0, 2, "AAA") + doc2.splice(list, 4, 0, "BBB") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); + }) + + it.skip('should handle marks across multiple forks', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. + + let doc3 = doc.fork() + doc3.insert(list, 0, "AAA") // should not be included in mark. + + doc.merge(doc2) + doc.merge(doc3) + + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); + }) + + + it.only('should handle marks with deleted ends [..]', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.delete(list,5); + doc.delete(list,5); + doc.delete(list,2); + doc.delete(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) + }) + + it.skip('should handle sticky marks (..)', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); + }) + + it.skip('should handle sticky marks with deleted ends (..)', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.delete(list,5); + doc.delete(list,5); + doc.delete(list,2); + doc.delete(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + // make sure save/load can handle marks + + let doc2 = load(doc.save(),true) + spans = doc2.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + + it.skip('should handle overlapping marks', () => { + let doc : Automerge = create(true, "aabbcc") + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc.mark(list, "[0..37]", "bold" , true) + doc.mark(list, "[4..19]", "itallic" , true) + doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + doc.commit("marks"); + let spans = doc.spans(list); + assert.deepStrictEqual(spans, + [ + [ [ 'bold', 'boolean', true ] ], + 'the ', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + 'quick ', + [ + [ 'bold', 'boolean', true ], + [ 'comment', 'str', 'foxes are my favorite animal!' ], + [ 'itallic', 'boolean', true ] + ], + 'fox', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + ' jumps', + [ [ 'bold', 'boolean', true ] ], + ' over the lazy dog', + [], + ] + ) + let text = doc.text(list); + assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); + let raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + doc.unmark(list, "41@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + // mark sure encode decode can handle marks + + doc.unmark(list, "39@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + let all = doc.getChanges([]) + let decoded = all.map((c) => decodeChange(c)) + let encoded = decoded.map((c) => encodeChange(c)) + let doc2 = create(true); + doc2.applyChanges(encoded) + + assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + }) +}) diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 398e78e1..75425beb 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -1,18 +1,19 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::RangeExpand; +use crate::marks::MarkRange; use crate::op_observer::{BranchableObserver, OpObserver}; use crate::sync::SyncDoc; use crate::transaction::{CommitOptions, Transactable}; use crate::{ - sync, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, ReadDoc, - ScalarValue, -}; -use crate::{ + query, transaction::{Observation, Observed, TransactionInner, UnObserved}, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, TextEncoding, Value, Values, }; +use crate::{ + sync, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, ReadDoc, + ScalarValue, +}; /// An automerge document that automatically manages transactions. /// @@ -495,6 +496,32 @@ impl ReadDoc for AutoCommitWithObs { fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&Change> { self.doc.get_change_by_hash(hash) } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn spans>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } } impl Transactable for AutoCommitWithObs { @@ -625,8 +652,7 @@ impl Transactable for AutoCommitWithObs { fn mark, V: Into>( &mut self, obj: O, - range: &std::ops::Range, - expand: RangeExpand, + range: &MarkRange, mark: &str, value: V, ) -> Result<(), AutomergeError> { @@ -637,12 +663,26 @@ impl Transactable for AutoCommitWithObs { current.observer(), obj.as_ref(), range, - expand, mark, value, ) } + fn unmark, M: AsRef>( + &mut self, + obj: O, + mark: M, + ) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let (current, tx) = self.transaction.as_mut().unwrap(); + tx.unmark( + &mut self.doc, + current.observer(), + obj.as_ref(), + mark.as_ref(), + ) + } + fn base_heads(&self) -> Vec { self.doc.get_heads() } diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 128d4418..bef5b5d1 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -966,6 +966,8 @@ impl Automerge { OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), + OpType::MarkBegin(_) => format!("markBegin{}", 0), + OpType::MarkEnd(_) => format!("markEnd{}", 0), }; let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); let succ: Vec<_> = op.succ.into_iter().map(|id| self.to_string(*id)).collect(); @@ -1308,6 +1310,65 @@ impl ReadDoc for Automerge { Ok(buffer) } + fn spans>(&self, obj: O) -> Result>, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?.0; + let mut query = self.ops.search( + &obj, + query::Spans::new(ListEncoding::Text(self.text_encoding)), + ); + query.check_marks(); + Ok(query.spans) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?.0; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); + let mut query = self + .ops + .search(&obj, query::Attribute::new(baseline, change_sets)); + query.finish(); + Ok(query.change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?.0; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); + let mut query = self + .ops + .search(&obj, query::Attribute2::new(baseline, change_sets)); + query.finish(); + Ok(query.change_sets) + } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?.0; + let query = self.ops.search(&obj, query::RawSpans::new()); + let result = query + .spans + .into_iter() + .map(|s| query::SpanInfo { + id: self.id_to_exid(s.id), + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }) + .collect(); + Ok(result) + } + fn get, P: Into>( &self, obj: O, diff --git a/rust/automerge/src/legacy/serde_impls/op_type.rs b/rust/automerge/src/legacy/serde_impls/op_type.rs index b054bad7..1d168248 100644 --- a/rust/automerge/src/legacy/serde_impls/op_type.rs +++ b/rust/automerge/src/legacy/serde_impls/op_type.rs @@ -18,6 +18,8 @@ impl Serialize for OpType { OpType::Delete => RawOpType::Del, OpType::Increment(_) => RawOpType::Inc, OpType::Put(_) => RawOpType::Set, + OpType::MarkBegin(_) => todo!(), + OpType::MarkEnd(_) => todo!(), }; raw_type.serialize(serializer) } diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 082a3504..028c4ac4 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -1,34 +1,18 @@ #[derive(Debug, Clone)] -pub enum RangeExpand { - Neither, - ExpandLeft, - ExpandRight, - ExpandBoth, +pub struct MarkRange { + pub start: usize, + pub end: usize, + pub expand_left: bool, + pub expand_right: bool, } -impl RangeExpand { - pub fn new(left: bool, right: bool) -> Self { - match (left, right) { - (true, true) => Self::ExpandBoth, - (true, false) => Self::ExpandLeft, - (false, true) => Self::ExpandRight, - (false, false) => Self::Neither, - } - } - - pub fn expand_left(&self) -> bool { - match self { - Self::ExpandLeft => true, - Self::ExpandBoth => true, - _ => false, - } - } - - pub fn expand_right(&self) -> bool { - match self { - Self::ExpandRight => true, - Self::ExpandBoth => true, - _ => false, +impl MarkRange { + pub fn new(start: usize, end: usize, expand_left: bool, expand_right: bool) -> Self { + MarkRange { + start, + end, + expand_left, + expand_right, } } } diff --git a/rust/automerge/src/query.rs b/rust/automerge/src/query.rs index 640ecf8d..b9aac2a7 100644 --- a/rust/automerge/src/query.rs +++ b/rust/automerge/src/query.rs @@ -1,12 +1,16 @@ +use crate::exid::ExId; use crate::op_tree::{OpSetMetadata, OpTree, OpTreeNode}; use crate::types::{ Clock, Counter, Key, ListEncoding, Op, OpId, OpType, ScalarValue, TextEncoding, }; use fxhash::FxBuildHasher; +use serde::Serialize; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +mod attribute; +mod attribute2; mod elem_id_pos; mod insert; mod keys; @@ -25,9 +29,13 @@ mod opid; mod opid_vis; mod prop; mod prop_at; +mod raw_spans; mod seek_op; mod seek_op_with_patch; +mod spans; +pub(crate) use attribute::{Attribute, ChangeSet}; +pub(crate) use attribute2::{Attribute2, ChangeSet2}; pub(crate) use elem_id_pos::ElemIdPos; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -46,8 +54,20 @@ pub(crate) use opid::OpIdSearch; pub(crate) use opid_vis::OpIdVisSearch; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; +pub(crate) use raw_spans::RawSpans; pub(crate) use seek_op::SeekOp; pub(crate) use seek_op_with_patch::SeekOpWithPatch; +pub(crate) use spans::{Span, Spans}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct SpanInfo { + pub id: ExId, + pub start: usize, + pub end: usize, + #[serde(rename = "type")] + pub span_type: String, + pub value: ScalarValue, +} // use a struct for the args for clarity as they are passed up the update chain in the optree #[derive(Debug, Clone)] diff --git a/rust/automerge/src/query/attribute.rs b/rust/automerge/src/query/attribute.rs new file mode 100644 index 00000000..7dfea5ac --- /dev/null +++ b/rust/automerge/src/query/attribute.rs @@ -0,0 +1,128 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub(crate) change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet { + clock: Clock, + next_add: Option>, + next_del: Option<(usize, String)>, + pub add: Vec>, + pub del: Vec<(usize, String)>, +} + +impl From for ChangeSet { + fn from(clock: Clock) -> Self { + ChangeSet { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute { + pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(range) = &mut cs.next_add { + range.end += 1; + } else { + cs.next_add = Some(Range { + start: self.seen, + end: self.seen + 1, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + if !self.baseline.covers(&element.id) + || element.succ.iter().any(|id| self.baseline.covers(id)) + { + return; + } + for cs in &mut self.change_sets { + if element.succ.iter().any(|id| cs.clock.covers(id)) { + // was deleted by change set + let s = element.to_str(); + if let Some((_, span)) = &mut cs.next_del { + span.push_str(s); + } else { + cs.next_del = Some((self.seen, s.to_owned())) + } + } + } + } + + pub(crate) fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl<'a> TreeQuery<'a> for Attribute { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/rust/automerge/src/query/attribute2.rs b/rust/automerge/src/query/attribute2.rs new file mode 100644 index 00000000..3bec55a5 --- /dev/null +++ b/rust/automerge/src/query/attribute2.rs @@ -0,0 +1,173 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute2 { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub(crate) change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet2 { + clock: Clock, + next_add: Option, + next_del: Option, + pub add: Vec, + pub del: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Add { + pub actor: usize, + pub range: Range, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Del { + pub pos: usize, + pub actor: usize, + pub span: String, +} + +impl From for ChangeSet2 { + fn from(clock: Clock) -> Self { + ChangeSet2 { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet2 { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute2 { + pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute2 { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(CS2Add { range, actor }) = &mut cs.next_add { + if *actor == element.id.actor() { + range.end += 1; + } else { + cs.cut_add(); + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + if !self.baseline.covers(&element.id) + || element.succ.iter().any(|id| self.baseline.covers(id)) + { + return; + } + for cs in &mut self.change_sets { + let succ: Vec<_> = element + .succ + .iter() + .filter(|id| cs.clock.covers(id)) + .collect(); + // was deleted by change set + if let Some(suc) = succ.get(0) { + let s = element.to_str(); + if let Some(CS2Del { actor, span, .. }) = &mut cs.next_del { + if suc.actor() == *actor { + span.push_str(s); + } else { + cs.cut_del(); + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s.to_owned(), + }) + } + } else { + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s.to_owned(), + }) + } + } + } + } + + pub(crate) fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl<'a> TreeQuery<'a> for Attribute2 { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/rust/automerge/src/query/insert.rs b/rust/automerge/src/query/insert.rs index 0dc0e98d..8e86d619 100644 --- a/rust/automerge/src/query/insert.rs +++ b/rust/automerge/src/query/insert.rs @@ -110,6 +110,12 @@ impl<'a> TreeQuery<'a> for InsertNth { self.last_seen = None; self.last_insert = element.elemid(); } + /*-------------------*/ + if self.valid.is_some() && element.valid_mark_anchor() { + self.last_valid_insert = Some(element.elemid_or_key()); + self.valid = None; + } + /*-------------------*/ if self.last_seen.is_none() && element.visible() { if self.seen >= self.target { return QueryResult::Finish; diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs new file mode 100644 index 00000000..e3b54f46 --- /dev/null +++ b/rust/automerge/src/query/raw_spans.rs @@ -0,0 +1,78 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpans { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + changed: bool, + pub(crate) spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpan { + pub(crate) id: OpId, + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) name: String, + pub(crate) value: ScalarValue, +} + +impl RawSpans { + pub(crate) fn new() -> Self { + RawSpans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + changed: false, + spans: Vec::new(), + } + } +} + +impl<'a> TreeQuery<'a> for RawSpans { + fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(md) = &element.action { + let pos = self + .spans + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.spans.insert( + pos, + RawSpan { + id: element.id, + start: self.seen, + end: 0, + name: md.name.clone(), + value: md.value.clone(), + }, + ); + } + if let OpType::MarkEnd(_) = &element.action { + for s in self.spans.iter_mut() { + if s.id == element.id.prev() { + s.end = self.seen; + break; + } + } + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.seen += 1; + self.last_seen = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs new file mode 100644 index 00000000..31bf2878 --- /dev/null +++ b/rust/automerge/src/query/spans.rs @@ -0,0 +1,112 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, ListEncoding, Op, OpType, ScalarValue}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Spans<'a> { + pos: usize, + seen: usize, + encoding: ListEncoding, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec<&'a Op>, + marks: HashMap, + changed: bool, + pub(crate) spans: Vec>, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Span<'a> { + pub pos: usize, + pub marks: Vec<(String, Cow<'a, ScalarValue>)>, +} + +impl<'a> Spans<'a> { + pub(crate) fn new(encoding: ListEncoding) -> Self { + Spans { + pos: 0, + seen: 0, + encoding, + last_seen: None, + last_insert: None, + seen_at_last_mark: None, + seen_at_this_mark: None, + changed: false, + ops: Vec::new(), + marks: HashMap::new(), + spans: Vec::new(), + } + } + + pub(crate) fn check_marks(&mut self) { + let mut new_marks = HashMap::new(); + for op in &self.ops { + if let OpType::MarkBegin(m) = &op.action { + new_marks.insert(m.name.clone(), &m.value); + } + } + if new_marks != self.marks { + self.changed = true; + self.marks = new_marks; + } + if self.changed + && (self.seen_at_last_mark != self.seen_at_this_mark + || self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none()) + { + self.changed = false; + self.seen_at_last_mark = self.seen_at_this_mark; + let mut marks: Vec<_> = self + .marks + .iter() + .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) + .collect(); + marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); + self.spans.push(Span { + pos: self.seen, + marks, + }); + } + } +} + +impl<'a> TreeQuery<'a> for Spans<'a> { + /* + fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { + unimplemented!() + } + */ + + fn query_element_with_metadata(&mut self, element: &'a Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(_) = &element.action { + let pos = self + .ops + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.ops.insert(pos, element); + } + if let OpType::MarkEnd(_) = &element.action { + self.ops.retain(|op| op.id != element.id.prev()); + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.check_marks(); + let last_width = element.width(self.encoding); + self.seen += last_width; + self.last_seen = element.elemid(); + self.seen_at_this_mark = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 6d479718..624b5973 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -1,7 +1,7 @@ use crate::{ error::AutomergeError, exid::ExId, keys::Keys, keys_at::KeysAt, list_range::ListRange, list_range_at::ListRangeAt, map_range::MapRange, map_range_at::MapRangeAt, parents::Parents, - values::Values, Change, ChangeHash, ObjType, Prop, Value, + query, values::Values, Change, ChangeHash, ObjType, Prop, Value, }; use std::ops::RangeBounds; @@ -196,4 +196,22 @@ pub trait ReadDoc { /// Get a change by its hash. fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&Change>; + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError>; + + fn spans>(&self, obj: O) -> Result>, AutomergeError>; + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; } diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index 00b5e940..b110bb78 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -96,6 +96,8 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { OpType::Make(..) | OpType::Delete => Cow::Owned(ScalarValue::Null), OpType::Increment(i) => Cow::Owned(ScalarValue::Int(*i)), OpType::Put(s) => Cow::Borrowed(s), + OpType::MarkBegin(_) => todo!(), + OpType::MarkEnd(_) => todo!(), } } diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 982517f5..1a478aa8 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use crate::exid::ExId; -use crate::marks::RangeExpand; +use crate::marks::MarkRange; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -650,21 +650,57 @@ impl TransactionInner { } pub(crate) fn mark>( + &mut self, + doc: &mut Automerge, + op_observer: Option<&mut Obs>, + ex_obj: &ExId, + range: &MarkRange, + mark: &str, + value: V, + ) -> Result<(), AutomergeError> { + let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; + if let Some(obs) = op_observer { + self.do_insert( + doc, + Some(obs), + obj, + range.start, + OpType::mark(mark.to_owned(), range.expand_left, value.into()), + )?; + self.do_insert( + doc, + Some(obs), + obj, + range.end, + OpType::MarkEnd(range.expand_right), + )?; + } else { + self.do_insert::( + doc, + None, + obj, + range.start, + OpType::mark(mark.to_owned(), range.expand_left, value.into()), + )?; + self.do_insert::( + doc, + None, + obj, + range.end, + OpType::MarkEnd(range.expand_right), + )?; + } + Ok(()) + } + + pub(crate) fn unmark( &mut self, _doc: &mut Automerge, mut _op_observer: Option<&mut Obs>, _ex_obj: &ExId, - _range: &std::ops::Range, - expand: RangeExpand, - _mark: &str, - _value: V, + _mark: &ExId, ) -> Result<(), AutomergeError> { - match expand { - RangeExpand::Neither => todo!(), - RangeExpand::ExpandLeft => todo!(), - RangeExpand::ExpandRight => todo!(), - RangeExpand::ExpandBoth => todo!(), - } + unimplemented!() } fn finalize_op( @@ -679,23 +715,25 @@ impl TransactionInner { if let Some(op_observer) = op_observer { let ex_obj = doc.ops().id_to_exid(obj.0); if op.insert { - let obj_type = doc.ops().object_type(&obj); - assert!(obj_type.unwrap().is_sequence()); - match (obj_type, prop) { - (Some(ObjType::List), Prop::Seq(index)) => { - let value = (op.value(), doc.ops().id_to_exid(op.id)); - op_observer.insert(doc, ex_obj, index, value) - } - (Some(ObjType::Text), Prop::Seq(index)) => { - // FIXME - if op_observer.text_as_seq() { + if !op.is_mark() { + let obj_type = doc.ops().object_type(&obj); + assert!(obj_type.unwrap().is_sequence()); + match (obj_type, prop) { + (Some(ObjType::List), Prop::Seq(index)) => { let value = (op.value(), doc.ops().id_to_exid(op.id)); op_observer.insert(doc, ex_obj, index, value) - } else { - op_observer.splice_text(doc, ex_obj, index, op.to_str()) } + (Some(ObjType::Text), Prop::Seq(index)) => { + // FIXME + if op_observer.text_as_seq() { + let value = (op.value(), doc.ops().id_to_exid(op.id)); + op_observer.insert(doc, ex_obj, index, value) + } else { + op_observer.splice_text(doc, ex_obj, index, op.to_str()) + } + } + _ => {} } - _ => {} } } else if op.is_delete() { op_observer.delete(doc, ex_obj, prop); diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 8bf312c4..6d418587 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -1,10 +1,11 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::RangeExpand; +use crate::marks::MarkRange; use crate::op_observer::BranchableObserver; use crate::{ - Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, Values, + query, Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, + Values, }; use crate::{AutomergeError, Keys}; use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; @@ -240,6 +241,32 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&crate::Change> { self.doc.get_change_by_hash(hash) } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn spans>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } } impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { @@ -334,12 +361,19 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { fn mark, V: Into>( &mut self, obj: O, - range: &std::ops::Range, - expand: RangeExpand, + range: &MarkRange, mark: &str, value: V, ) -> Result<(), AutomergeError> { - self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), range, expand, mark, value)) + self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), range, mark, value)) + } + + fn unmark, M: AsRef>( + &mut self, + obj: O, + mark: M, + ) -> Result<(), AutomergeError> { + self.do_tx(|tx, doc, obs| tx.unmark(doc, obs, obj.as_ref(), mark.as_ref())) } fn base_heads(&self) -> Vec { diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index b8743951..a2b15f99 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -1,5 +1,5 @@ use crate::exid::ExId; -use crate::marks::RangeExpand; +use crate::marks::MarkRange; use crate::{AutomergeError, ChangeHash, ObjType, Prop, ReadDoc, ScalarValue}; /// A way of mutating a document within a single change. @@ -92,12 +92,17 @@ pub trait Transactable: ReadDoc { fn mark, V: Into>( &mut self, obj: O, - range: &std::ops::Range, - expand: RangeExpand, + range: &MarkRange, mark: &str, value: V, ) -> Result<(), AutomergeError>; + fn unmark, M: AsRef>( + &mut self, + obj: O, + mark: M, + ) -> Result<(), AutomergeError>; + /// The heads this transaction will be based on fn base_heads(&self) -> Vec; } diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 870569e9..1e48a247 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -198,6 +198,25 @@ pub enum OpType { Delete, Increment(i64), Put(ScalarValue), + MarkBegin(MarkData), + MarkEnd(bool), +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: String, + pub value: ScalarValue, + pub expand: bool, +} + +impl Display for MarkData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "name={} value={} expand={}", + self.name, self.value, self.expand + ) + } } impl OpType { @@ -213,9 +232,19 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, + Self::MarkBegin(_) => todo!(), + Self::MarkEnd(_) => todo!(), } } + pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> OpType { + OpType::MarkBegin(MarkData { + name, + value, + expand, + }) + } + pub(crate) fn from_index_and_value( index: u64, value: ScalarValue, @@ -446,6 +475,11 @@ impl OpId { .cmp(&other.0) .then_with(|| actors[self.1 as usize].cmp(&actors[other.1 as usize])) } + + #[inline] + pub(crate) fn prev(&self) -> OpId { + OpId(self.0 - 1, self.1) + } } #[derive(Debug, Clone, Copy, PartialOrd, Eq, PartialEq, Ord, Hash, Default)] @@ -572,13 +606,15 @@ impl Op { pub(crate) fn to_str(&self) -> &str { if let OpType::Put(ScalarValue::Str(s)) = &self.action { s + } else if self.is_mark() { + "" } else { "\u{fffc}" } } pub(crate) fn visible(&self) -> bool { - if self.is_inc() { + if self.is_inc() || self.is_mark() { false } else if self.is_counter() { self.succ.len() <= self.incs() @@ -607,6 +643,18 @@ impl Op { matches!(&self.action, OpType::Put(ScalarValue::Counter(_))) } + pub(crate) fn is_mark(&self) -> bool { + matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) + } + + pub(crate) fn valid_mark_anchor(&self) -> bool { + self.succ.is_empty() + && matches!( + &self.action, + OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) + ) + } + pub(crate) fn is_noop(&self, action: &OpType) -> bool { matches!((&self.action, action), (OpType::Put(n), OpType::Put(m)) if n == m) } @@ -643,6 +691,10 @@ impl Op { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), + OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned( + format!("markBegin[{}]={}", mark.name, mark.value).into(), + )), + OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), _ => panic!("cant convert op into a value - {:?}", self), } } @@ -663,6 +715,8 @@ impl Op { OpType::Make(obj) => format!("make{}", obj), OpType::Increment(val) => format!("inc:{}", val), OpType::Delete => "del".to_string(), + OpType::MarkBegin(_) => "markBegin".to_string(), + OpType::MarkEnd(_) => "markEnd".to_string(), } } } diff --git a/rust/automerge/src/visualisation.rs b/rust/automerge/src/visualisation.rs index 31e9bbdb..677f9db9 100644 --- a/rust/automerge/src/visualisation.rs +++ b/rust/automerge/src/visualisation.rs @@ -249,6 +249,8 @@ impl OpTableRow { crate::OpType::Put(v) => format!("set {}", v), crate::OpType::Make(obj) => format!("make {}", obj), crate::OpType::Increment(v) => format!("inc {}", v), + crate::OpType::MarkBegin(m) => format!("markEnd {}", m), + crate::OpType::MarkEnd(m) => format!("markEnd {}", m), }; let prop = match op.key { crate::types::Key::Map(k) => metadata.props[k].clone(), From 23451765263961d29029bf5fa776384fff0dbee4 Mon Sep 17 00:00:00 2001 From: Alex Good Date: Wed, 8 Feb 2023 17:53:58 +0000 Subject: [PATCH 03/26] DocOp --- .../src/columnar/column_range/opid.rs | 40 ++++++++++- rust/automerge/src/error.rs | 2 + .../src/storage/document/doc_op_columns.rs | 70 ++++++++++++------- rust/automerge/src/types.rs | 16 ++++- 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/rust/automerge/src/columnar/column_range/opid.rs b/rust/automerge/src/columnar/column_range/opid.rs index ae95d758..7077fb38 100644 --- a/rust/automerge/src/columnar/column_range/opid.rs +++ b/rust/automerge/src/columnar/column_range/opid.rs @@ -48,6 +48,16 @@ impl OpIdRange { Self { actor, counter } } + pub(crate) fn encode_optional(opids: I, out: &mut Vec) -> Self + where + O: convert::OpId, + I: Iterator> + Clone, + { + let actor = RleRange::encode(opids.clone().map(|o| o.map(|o| o.actor() as u64)), out); + let counter = DeltaRange::encode(opids.map(|o| o.map(|o| o.counter() as i64)), out); + Self { actor, counter } + } + #[allow(dead_code)] pub(crate) fn splice( &self, @@ -89,6 +99,10 @@ impl<'a> OpIdIter<'a> { pub(crate) fn done(&self) -> bool { self.counter.done() } + + pub(crate) fn actor_range(&self) -> Range { + self.actor.range() + } } impl<'a> OpIdIter<'a> { @@ -134,9 +148,21 @@ pub(crate) struct OpIdEncoder { } impl OpIdEncoder { - pub(crate) fn append>(&mut self, opid: O) { - self.actor.append_value(opid.actor() as u64); - self.counter.append_value(opid.counter() as i64); + pub(crate) fn append>(&mut self, opid: Option) { + match opid { + Some(o) => { + self.actor.append_value(o.actor() as u64); + self.counter.append_value(o.counter() as i64); + }, + None => { + self.append_null() + } + } + } + + pub(crate) fn append_null(&mut self) { + self.actor.append_null(); + self.counter.append_null(); } } @@ -163,6 +189,14 @@ impl OpIdEncoder> { counter: (actor_end..counter_end).into(), } } + + pub(crate) fn actor_range(&self) -> Range { + self.actor.range() + } + + pub(crate) fn counter_range(&self) -> Range { + self.counter.range() + } } #[cfg(test)] diff --git a/rust/automerge/src/error.rs b/rust/automerge/src/error.rs index 57a87167..1790bc3c 100644 --- a/rust/automerge/src/error.rs +++ b/rust/automerge/src/error.rs @@ -97,4 +97,6 @@ pub(crate) enum InvalidOpType { UnknownAction(u64), #[error("non numeric argument for inc op")] NonNumericInc, + #[error("Invalid mark value")] + InvalidMark, } diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 82de17eb..38fee1d6 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -1,5 +1,7 @@ use std::{borrow::Cow, convert::TryFrom}; +use smol_str::SmolStr; + use crate::{ columnar::{ column_range::{ @@ -18,7 +20,7 @@ use crate::{ columns::{compression, ColumnId, ColumnSpec, ColumnType}, Columns, MismatchingColumn, RawColumn, RawColumns, }, - types::{ObjId, OpId, ScalarValue}, + types::{ObjId, OpId, ScalarValue, ElemId}, }; const OBJ_COL_ID: ColumnId = ColumnId::new(0); @@ -34,7 +36,8 @@ const SUCC_COL_ID: ColumnId = ColumnId::new(8); pub(crate) struct DocOp { pub(crate) id: OpId, pub(crate) object: ObjId, - pub(crate) key: Key, + pub(crate) prop: Option, + pub(crate) elem_id: Option, pub(crate) insert: bool, pub(crate) action: usize, pub(crate) value: ScalarValue, @@ -44,7 +47,8 @@ pub(crate) struct DocOp { #[derive(Debug, Clone)] pub(crate) struct DocOpColumns { obj: Option, - key: KeyRange, + prop: RleRange, + elem: OpIdRange, id: OpIdRange, insert: BooleanRange, action: RleRange, @@ -85,7 +89,9 @@ pub(crate) trait AsDocOp<'a> { fn obj(&self) -> convert::ObjId; fn id(&self) -> Self::OpId; - fn key(&self) -> convert::Key<'a, Self::OpId>; + //fn key(&self) -> convert::Key<'a, Self::OpId>; + fn prop(&self) -> Option>; + fn elem(&self) -> Option; fn insert(&self) -> bool; fn action(&self) -> u64; fn val(&self) -> Cow<'a, ScalarValue>; @@ -99,11 +105,7 @@ impl DocOpColumns { O: convert::OpId, C: AsDocOp<'a, OpId = O>, { - if ops.len() > 30000 { - Self::encode_rowwise(ops, out) - } else { - Self::encode_columnwise(ops, out) - } + Self::encode_rowwise(ops, out) } fn encode_columnwise<'a, I, O, C>(ops: I, out: &mut Vec) -> DocOpColumns @@ -113,15 +115,17 @@ impl DocOpColumns { C: AsDocOp<'a, OpId = O>, { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); - let key = KeyRange::encode(ops.clone().map(|o| o.key()), out); let id = OpIdRange::encode(ops.clone().map(|o| o.id()), out); + let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); + let elem = OpIdRange::encode_optional(ops.clone().map(|o| o.elem().map(|o| o)), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let succ = OpIdListRange::encode(ops.map(|o| o.succ()), out); Self { obj, - key, + prop, + elem, id, insert, action, @@ -138,7 +142,8 @@ impl DocOpColumns { C: AsDocOp<'a, OpId = O>, { let mut obj = ObjIdEncoder::new(); - let mut key = KeyEncoder::new(); + let mut prop = RleEncoder::<_, SmolStr>::new(Vec::new()); + let mut elem = OpIdEncoder::new(); let mut id = OpIdEncoder::new(); let mut insert = BooleanEncoder::new(); let mut action = RleEncoder::<_, u64>::from(Vec::new()); @@ -146,17 +151,23 @@ impl DocOpColumns { let mut succ = OpIdListEncoder::new(); for op in ops { obj.append(op.obj()); - key.append(op.key()); - id.append(op.id()); + prop.append(op.prop()); + elem.append(op.elem()); + id.append(Some(op.id())); insert.append(op.insert()); - action.append(Some(op.action())); val.append(&op.val()); succ.append(op.succ()); } let obj = obj.finish(out); - let key = key.finish(out); let id = id.finish(out); + let elem = elem.finish(out); + + let prop_start = out.len(); + let (prop_out, _prop_end) = prop.finish(); + out.extend(prop_out); + let prop = RleRange::from(prop_start..out.len()); + let insert_start = out.len(); let (insert_out, _) = insert.finish(); out.extend(insert_out); @@ -171,8 +182,9 @@ impl DocOpColumns { let succ = succ.finish(out); DocOpColumns { obj, - key, id, + elem, + prop, insert, action, val, @@ -186,7 +198,9 @@ impl DocOpColumns { id: self.id.iter(data), action: self.action.decoder(data), objs: self.obj.as_ref().map(|o| o.iter(data)), - keys: self.key.iter(data), + //keys: self.key.iter(data), + elem: self.elem.iter(data), + prop: self.prop.decoder(data), insert: self.insert.decoder(data), value: self.val.iter(data), succ: self.succ.iter(data), @@ -211,15 +225,15 @@ impl DocOpColumns { ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), - self.key.actor_range().clone().into(), + self.elem.actor_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), - self.key.counter_range().clone().into(), + self.elem.counter_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), - self.key.string_range().clone().into(), + self.prop.into(), ), RawColumn::new( ColumnSpec::new(ID_COL_ID, ColumnType::Actor, false), @@ -273,7 +287,9 @@ pub(crate) struct DocOpColumnIter<'a> { id: OpIdIter<'a>, action: RleDecoder<'a, u64>, objs: Option>, - keys: KeyIter<'a>, + //keys: KeyIter<'a>, + prop: RleDecoder<'a, SmolStr>, + elem: OpIdIter<'a>, insert: BooleanDecoder<'a>, value: ValueIter<'a>, succ: OpIdListIter<'a>, @@ -317,7 +333,8 @@ impl<'a> DocOpColumnIter<'a> { } else { ObjId::root() }; - let key = self.keys.next_in_col("key")?; + let prop = self.prop.maybe_next_in_col("key:prop")?; + let elem_id = self.elem.maybe_next_in_col("key:elem")?.map(|o| o.into()); let value = self.value.next_in_col("value")?; let succ = self.succ.next_in_col("succ")?; let insert = self.insert.next_in_col("insert")?; @@ -326,7 +343,8 @@ impl<'a> DocOpColumnIter<'a> { value, action: action as usize, object: obj, - key, + elem_id, + prop, succ, insert, })) @@ -427,10 +445,10 @@ impl TryFrom for DocOpColumns { obj_actor.unwrap_or_else(|| (0..0).into()), obj_ctr.unwrap_or_else(|| (0..0).into()), ), - key: KeyRange::new( + prop: key_str.unwrap_or_else(|| (0..0).into()), + elem: OpIdRange::new( key_actor.unwrap_or_else(|| (0..0).into()), key_ctr.unwrap_or_else(|| (0..0).into()), - key_str.unwrap_or_else(|| (0..0).into()), ), id: OpIdRange::new( id_actor.unwrap_or_else(|| (0..0).into()), diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 1e48a247..cb5c6564 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -232,8 +232,8 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) => todo!(), - Self::MarkEnd(_) => todo!(), + Self::MarkBegin(_) => 7, + Self::MarkEnd(_) => 8, } } @@ -261,6 +261,18 @@ impl OpType { _ => Err(error::InvalidOpType::NonNumericInc), }, 6 => Ok(Self::Make(ObjType::Table)), + 7 => match value { + let (name, value, expand) = ScalarValue::Bytes(b) => todo!(); + Ok(MarkBegin(MarkData { + name, + value, + expand, + })) + }, + 8 => match value { + ScalarValue::Bool(b) => Ok(Self::MarkEnd(b)), + _ => Err(error::InvalidOpType::InvalidMark), + }, other => Err(error::InvalidOpType::UnknownAction(other)), } } From 9a7dba09a4e3ff508170440332e682399921764c Mon Sep 17 00:00:00 2001 From: Alex Good Date: Wed, 8 Feb 2023 18:20:38 +0000 Subject: [PATCH 04/26] wip --- rust/automerge/src/automerge.rs | 2 +- .../src/columnar/column_range/opid.rs | 12 ---------- rust/automerge/src/query/raw_spans.rs | 2 +- rust/automerge/src/query/spans.rs | 4 ++-- rust/automerge/src/storage/change.rs | 3 ++- .../src/storage/change/change_op_columns.rs | 22 ++++++++---------- .../src/storage/load/reconstruct_document.rs | 23 +++++++++++++------ rust/automerge/src/transaction/inner.rs | 4 ++-- rust/automerge/src/types.rs | 23 ++++++++----------- 9 files changed, 44 insertions(+), 51 deletions(-) diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index bef5b5d1..a8d2b22e 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -721,7 +721,7 @@ impl Automerge { obj, Op { id, - action: OpType::from_index_and_value(c.action, c.val).unwrap(), + action: OpType::from_index_and_value(c.action, c.val, c.insert, None).unwrap(), key, succ: Default::default(), pred, diff --git a/rust/automerge/src/columnar/column_range/opid.rs b/rust/automerge/src/columnar/column_range/opid.rs index 7077fb38..e9a94688 100644 --- a/rust/automerge/src/columnar/column_range/opid.rs +++ b/rust/automerge/src/columnar/column_range/opid.rs @@ -99,10 +99,6 @@ impl<'a> OpIdIter<'a> { pub(crate) fn done(&self) -> bool { self.counter.done() } - - pub(crate) fn actor_range(&self) -> Range { - self.actor.range() - } } impl<'a> OpIdIter<'a> { @@ -189,14 +185,6 @@ impl OpIdEncoder> { counter: (actor_end..counter_end).into(), } } - - pub(crate) fn actor_range(&self) -> Range { - self.actor.range() - } - - pub(crate) fn counter_range(&self) -> Range { - self.counter.range() - } } #[cfg(test)] diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index e3b54f46..f6b225cc 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -50,7 +50,7 @@ impl<'a> TreeQuery<'a> for RawSpans { id: element.id, start: self.seen, end: 0, - name: md.name.clone(), + name: md.name.into(), value: md.value.clone(), }, ); diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs index 31bf2878..9795a711 100644 --- a/rust/automerge/src/query/spans.rs +++ b/rust/automerge/src/query/spans.rs @@ -14,7 +14,7 @@ pub(crate) struct Spans<'a> { seen_at_this_mark: Option, seen_at_last_mark: Option, ops: Vec<&'a Op>, - marks: HashMap, + marks: HashMap, changed: bool, pub(crate) spans: Vec>, } @@ -22,7 +22,7 @@ pub(crate) struct Spans<'a> { #[derive(Debug, Clone, PartialEq)] pub struct Span<'a> { pub pos: usize, - pub marks: Vec<(String, Cow<'a, ScalarValue>)>, + pub marks: Vec<(smol_str::SmolStr, Cow<'a, ScalarValue>)>, } impl<'a> Spans<'a> { diff --git a/rust/automerge/src/storage/change.rs b/rust/automerge/src/storage/change.rs index ff3cc9ab..1f3cdeb5 100644 --- a/rust/automerge/src/storage/change.rs +++ b/rust/automerge/src/storage/change.rs @@ -429,7 +429,8 @@ pub(crate) trait AsChangeOp<'a> { type PredIter: Iterator + ExactSizeIterator; fn obj(&self) -> convert::ObjId; - fn key(&self) -> convert::Key<'a, Self::OpId>; + fn prop(&self) -> Option<&smol_str::SmolStr>; + fn elem(&self) -> Option>; fn insert(&self) -> bool; fn action(&self) -> u64; fn val(&self) -> Cow<'a, ScalarValue>; diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index 7c3a65ec..b435d4bd 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -33,7 +33,8 @@ const PRED_COL_ID: ColumnId = ColumnId::new(7); #[derive(Clone, Debug, PartialEq)] pub(crate) struct ChangeOp { - pub(crate) key: Key, + pub(crate) prop: Option, + pub(crate) elem_id: Option, pub(crate) insert: bool, pub(crate) val: ScalarValue, pub(crate) pred: Vec, @@ -44,11 +45,8 @@ pub(crate) struct ChangeOp { impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { fn from(a: A) -> Self { ChangeOp { - key: match a.key() { - convert::Key::Prop(s) => Key::Prop(s.into_owned()), - convert::Key::Elem(convert::ElemId::Head) => Key::Elem(ElemId::head()), - convert::Key::Elem(convert::ElemId::Op(o)) => Key::Elem(ElemId(o)), - }, + prop: a.prop().cloned(), + elem_id: convert::ElemId(a.elem()), obj: match a.obj() { convert::ObjId::Root => ObjId::root(), convert::ObjId::Op(o) => ObjId(o), @@ -74,12 +72,12 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { } } - fn key(&self) -> convert::Key<'a, Self::OpId> { - match &self.key { - Key::Prop(s) => convert::Key::Prop(std::borrow::Cow::Borrowed(s)), - Key::Elem(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), - Key::Elem(e) => convert::Key::Elem(convert::ElemId::Op(&e.0)), - } + fn prop(&self) -> Option<&smol_str::SmolStr> { + return self.prop + } + + fn elem(&self) -> Option> { + return self.elem_id } fn val(&self) -> std::borrow::Cow<'a, ScalarValue> { diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index 44ace72a..3e7a9f8f 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -4,7 +4,6 @@ use tracing::instrument; use crate::{ change::Change, - columnar::Key as DocOpKey, op_tree::OpSetMetadata, storage::{change::Verified, Change as StoredChange, DocOp, Document}, types::{ChangeHash, ElemId, Key, ObjId, ObjType, Op, OpId, OpIds, OpType}, @@ -29,6 +28,10 @@ pub(crate) enum Error { MissingOps, #[error("succ out of order")] SuccOutOfOrder, + #[error("no key")] + MissingKey, + #[error(transparent)] + InvalidOpType(#[from] crate::error::InvalidOpType), } pub(crate) struct MismatchedHeads { @@ -335,23 +338,29 @@ impl LoadingObject { } fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { - let key = match op.key { - DocOpKey::Prop(s) => Key::Map(m.import_prop(s)), - DocOpKey::Elem(ElemId(op)) => Key::Seq(ElemId(check_opid(m, op)?)), - }; + let key = match (op.prop, op.elem_id) { + (Some(k), None) => Ok(Key::Map(m.import_prop(k))), + (_, Some(elem)) => Ok(Key::Seq(ElemId(check_opid(m, elem.0)?))), + (None, None) => Err(Error::MissingKey), + }?; for opid in &op.succ { if m.actors.safe_get(opid.actor()).is_none() { tracing::error!(?opid, "missing actor"); return Err(Error::MissingActor); } } + let action = OpType::from_index_and_value(op.action as u64, op.value, op.insert, op.prop)?; + let insert = match action { + OpType::MarkBegin(_) | OpType::MarkEnd(_) => true, + _ => op.insert, + }; Ok(Op { id: check_opid(m, op.id)?, - action: parse_optype(op.action, op.value)?, + action, key, succ: m.try_sorted_opids(op.succ).ok_or(Error::SuccOutOfOrder)?, pred: OpIds::empty(), - insert: op.insert, + insert, }) } diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 1a478aa8..c8ae29ff 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -665,7 +665,7 @@ impl TransactionInner { Some(obs), obj, range.start, - OpType::mark(mark.to_owned(), range.expand_left, value.into()), + OpType::mark(mark.into(), range.expand_left, value.into()), )?; self.do_insert( doc, @@ -680,7 +680,7 @@ impl TransactionInner { None, obj, range.start, - OpType::mark(mark.to_owned(), range.expand_left, value.into()), + OpType::mark(mark.into(), range.expand_left, value.into()), )?; self.do_insert::( doc, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index cb5c6564..92b3bdf3 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -204,7 +204,7 @@ pub enum OpType { #[derive(PartialEq, Debug, Clone)] pub struct MarkData { - pub name: String, + pub name: smol_str::SmolStr, pub value: ScalarValue, pub expand: bool, } @@ -232,12 +232,11 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) => 7, - Self::MarkEnd(_) => 8, + Self::MarkBegin(_) | Self::MarkEnd(..) => 7, } } - pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> OpType { + pub(crate) fn mark(name: smol_str::SmolStr, expand: bool, value: ScalarValue) -> OpType { OpType::MarkBegin(MarkData { name, value, @@ -248,6 +247,8 @@ impl OpType { pub(crate) fn from_index_and_value( index: u64, value: ScalarValue, + insert_or_expand: bool, + prop: Option, ) -> Result { match index { 0 => Ok(Self::Make(ObjType::Map)), @@ -261,17 +262,13 @@ impl OpType { _ => Err(error::InvalidOpType::NonNumericInc), }, 6 => Ok(Self::Make(ObjType::Table)), - 7 => match value { - let (name, value, expand) = ScalarValue::Bytes(b) => todo!(); - Ok(MarkBegin(MarkData { + 7 => match prop { + Some(name) => Ok(Self::MarkBegin(MarkData { name, value, - expand, - })) - }, - 8 => match value { - ScalarValue::Bool(b) => Ok(Self::MarkEnd(b)), - _ => Err(error::InvalidOpType::InvalidMark), + expand: insert_or_expand, + })), + None => Ok(Self::MarkEnd(insert_or_expand)), }, other => Err(error::InvalidOpType::UnknownAction(other)), } From 290c9e687246528e6b20fd7eb02fb20fafc1e0f2 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 8 Feb 2023 14:59:41 -0600 Subject: [PATCH 05/26] attempt to finish - two issues outstanding --- rust/automerge/src/automerge.rs | 13 +- rust/automerge/src/change.rs | 54 ++++-- rust/automerge/src/columnar.rs | 2 +- rust/automerge/src/columnar/column_range.rs | 2 +- .../src/columnar/column_range/key.rs | 167 +++++++----------- .../src/columnar/column_range/opid.rs | 4 +- .../src/columnar/encoding/column_decoder.rs | 6 +- rust/automerge/src/storage/change.rs | 2 +- .../src/storage/change/change_actors.rs | 19 +- .../src/storage/change/change_op_columns.rs | 68 ++++--- .../src/storage/convert/op_as_changeop.rs | 24 ++- .../src/storage/convert/op_as_docop.rs | 29 ++- .../src/storage/document/doc_op_columns.rs | 18 +- 13 files changed, 228 insertions(+), 180 deletions(-) diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index a8d2b22e..98ebe7fa 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -5,7 +5,7 @@ use std::num::NonZeroU64; use std::ops::RangeBounds; use crate::change_graph::ChangeGraph; -use crate::columnar::Key as EncodedKey; +//use crate::columnar::Key as EncodedKey; use crate::exid::ExId; use crate::keys::Keys; use crate::op_observer::{BranchableObserver, OpObserver}; @@ -697,10 +697,10 @@ impl Automerge { .enumerate() .map(|(i, c)| { let id = OpId::new(change.start_op().get() + i as u64, actor); - let key = match &c.key { - EncodedKey::Prop(n) => Key::Map(self.ops.m.props.cache(n.to_string())), - EncodedKey::Elem(e) if e.is_head() => Key::Seq(ElemId::head()), - EncodedKey::Elem(ElemId(o)) => { + let key = match (&c.elem_id, &c.prop) { + (None, Some(n)) => Key::Map(self.ops.m.props.cache(n.to_string())), + (Some(e), _) if e.is_head() => Key::Seq(ElemId::head()), + (Some(ElemId(o)), _) => { Key::Seq(ElemId(OpId::new(o.counter(), actors[o.actor()]))) } }; @@ -721,7 +721,8 @@ impl Automerge { obj, Op { id, - action: OpType::from_index_and_value(c.action, c.val, c.insert, None).unwrap(), + action: OpType::from_index_and_value(c.action, c.val, c.insert, c.prop) + .unwrap(), key, succ: Default::default(), pred, diff --git a/rust/automerge/src/change.rs b/rust/automerge/src/change.rs index b5cae7df..ce4dc8a2 100644 --- a/rust/automerge/src/change.rs +++ b/rust/automerge/src/change.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, num::NonZeroU64}; use crate::{ - columnar::Key as StoredKey, + // columnar::Key as StoredKey, storage::{ change::{Unverified, Verified}, parse, Change as StoredChange, ChangeOp, Chunk, Compressed, ReadChangeOpError, @@ -230,18 +230,35 @@ mod convert_expanded { self.pred.iter() } - fn key(&self) -> convert::Key<'a, Self::OpId> { + fn prop(&self) -> Option> { match &self.key { - legacy::Key::Map(s) => convert::Key::Prop(Cow::Borrowed(s)), - legacy::Key::Seq(legacy::ElementId::Head) => { - convert::Key::Elem(convert::ElemId::Head) - } - legacy::Key::Seq(legacy::ElementId::Id(o)) => { - convert::Key::Elem(convert::ElemId::Op(o)) - } + legacy::Key::Map(s) => Some(Cow::Borrowed(s)), + _ => None, } } + fn elem(&self) -> Option> { + match &self.key { + legacy::Key::Seq(legacy::ElementId::Head) => Some(convert::ElemId::Head), + legacy::Key::Seq(legacy::ElementId::Id(o)) => Some(convert::ElemId::Op(o)), + _ => None, + } + } + + /* + fn key(&self) -> convert::Key<'a, Self::OpId> { + match &self.key { + legacy::Key::Map(s) => convert::Key::Prop(Cow::Borrowed(s)), + legacy::Key::Seq(legacy::ElementId::Head) => { + convert::Key::Elem(convert::ElemId::Head) + } + legacy::Key::Seq(legacy::ElementId::Id(o)) => { + convert::Key::Elem(convert::ElemId::Op(o)) + } + } + } + */ + fn obj(&self) -> convert::ObjId { match &self.obj { legacy::ObjectId::Root => convert::ObjId::Root, @@ -278,18 +295,19 @@ impl From<&Change> for crate::ExpandedChange { let operations = c .iter_ops() .map(|o| crate::legacy::Op { - action: crate::types::OpType::from_index_and_value(o.action, o.val).unwrap(), + action: crate::types::OpType::from_index_and_value( + o.action, o.val, o.insert, o.prop, + ) + .unwrap(), insert: o.insert, - key: match o.key { - StoredKey::Elem(e) if e.is_head() => { + key: match (o.elem_id, o.prop) { + (None, Some(p)) => crate::legacy::Key::Map(p), + (Some(e), _) if e.is_head() => { crate::legacy::Key::Seq(crate::legacy::ElementId::Head) } - StoredKey::Elem(ElemId(o)) => { - crate::legacy::Key::Seq(crate::legacy::ElementId::Id( - crate::legacy::OpId::new(o.counter(), actors.get(&o.actor()).unwrap()), - )) - } - StoredKey::Prop(p) => crate::legacy::Key::Map(p), + (Some(ElemId(o)), _) => crate::legacy::Key::Seq(crate::legacy::ElementId::Id( + crate::legacy::OpId::new(o.counter(), actors.get(&o.actor()).unwrap()), + )), }, obj: if o.obj.is_root() { crate::legacy::ObjectId::Root diff --git a/rust/automerge/src/columnar.rs b/rust/automerge/src/columnar.rs index bb727626..30dd0833 100644 --- a/rust/automerge/src/columnar.rs +++ b/rust/automerge/src/columnar.rs @@ -7,7 +7,7 @@ //! `Range` - which have useful instance methods such as `encode()` to create a new range and //! `decoder()` to return an iterator of the correct type. pub(crate) mod column_range; -pub(crate) use column_range::Key; +//pub(crate) use column_range::Key; pub(crate) mod encoding; mod splice_error; diff --git a/rust/automerge/src/columnar/column_range.rs b/rust/automerge/src/columnar/column_range.rs index 5762ed14..6c1525ab 100644 --- a/rust/automerge/src/columnar/column_range.rs +++ b/rust/automerge/src/columnar/column_range.rs @@ -16,6 +16,6 @@ mod value; pub(crate) use value::{ValueEncoder, ValueIter, ValueRange}; pub(crate) mod generic; mod key; -pub(crate) use key::{Key, KeyEncoder, KeyIter, KeyRange}; +pub(crate) use key::{ElemEncoder, ElemIter, ElemRange}; mod obj_id; pub(crate) use obj_id::{ObjIdEncoder, ObjIdIter, ObjIdRange}; diff --git a/rust/automerge/src/columnar/column_range/key.rs b/rust/automerge/src/columnar/column_range/key.rs index 70ea8e1e..12687aba 100644 --- a/rust/automerge/src/columnar/column_range/key.rs +++ b/rust/automerge/src/columnar/column_range/key.rs @@ -13,29 +13,14 @@ use crate::{ }; #[derive(Clone, Debug, PartialEq)] -pub(crate) enum Key { - Prop(smol_str::SmolStr), - Elem(ElemId), -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct KeyRange { +pub(crate) struct ElemRange { actor: RleRange, counter: DeltaRange, - string: RleRange, } -impl KeyRange { - pub(crate) fn new( - actor: RleRange, - counter: DeltaRange, - string: RleRange, - ) -> Self { - Self { - actor, - counter, - string, - } +impl ElemRange { + pub(crate) fn new(actor: RleRange, counter: DeltaRange) -> Self { + Self { actor, counter } } pub(crate) fn actor_range(&self) -> &RleRange { @@ -46,19 +31,36 @@ impl KeyRange { &self.counter } - pub(crate) fn string_range(&self) -> &RleRange { - &self.string - } + // pub(crate) fn string_range(&self) -> &RleRange { + // &self.string + // } - pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> KeyIter<'a> { - KeyIter { + pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> ElemIter<'a> { + ElemIter { actor: self.actor.decoder(data), counter: self.counter.decoder(data), - string: self.string.decoder(data), } } - pub(crate) fn encode<'b, O, I: Iterator> + Clone>( +/* + pub(crate) fn encode<'b, O, I: Iterator> + Clone>( + items: I, + out: &mut Vec, + ) -> Self + where + O: convert::OpId, + { + // SAFETY: The incoming iterator is infallible and there are no existing items + Self { + actor: (0..0).into(), + counter: (0..0).into(), + } + .splice::<_, Infallible, _>(&[], 0..0, items.map(Ok), out) + .unwrap() + } +*/ + + pub(crate) fn maybe_encode<'b, O, I: Iterator>> + Clone>( items: I, out: &mut Vec, ) -> Self @@ -69,7 +71,6 @@ impl KeyRange { Self { actor: (0..0).into(), counter: (0..0).into(), - string: (0..0).into(), } .splice::<_, Infallible, _>(&[], 0..0, items.map(Ok), out) .unwrap() @@ -87,16 +88,16 @@ impl KeyRange { where O: convert::OpId, E: std::error::Error, - I: Iterator, E>> + Clone, + I: Iterator>, E>> + Clone, { let actor = self.actor.splice( data, replace.clone(), replace_with.clone().map(|k| { k.map(|k| match k { - convert::Key::Prop(_) => None, - convert::Key::Elem(convert::ElemId::Head) => None, - convert::Key::Elem(convert::ElemId::Op(o)) => Some(o.actor() as u64), + Some(convert::ElemId::Head) => None, + Some(convert::ElemId::Op(o)) => Some(o.actor() as u64), + None => None, }) }), out, @@ -107,43 +108,26 @@ impl KeyRange { replace.clone(), replace_with.clone().map(|k| { k.map(|k| match k { - convert::Key::Prop(_) => None, - convert::Key::Elem(convert::ElemId::Head) => Some(0), - convert::Key::Elem(convert::ElemId::Op(o)) => Some(o.counter() as i64), + Some(convert::ElemId::Head) => Some(0), + Some(convert::ElemId::Op(o)) => Some(o.counter() as i64), + None => None, }) }), out, )?; - let string = self.string.splice( - data, - replace, - replace_with.map(|k| { - k.map(|k| match k { - convert::Key::Prop(s) => Some(s), - convert::Key::Elem(_) => None, - }) - }), - out, - )?; - - Ok(Self { - actor, - counter, - string, - }) + Ok(Self { actor, counter }) } } #[derive(Clone, Debug)] -pub(crate) struct KeyIter<'a> { +pub(crate) struct ElemIter<'a> { actor: RleDecoder<'a, u64>, counter: DeltaDecoder<'a>, - string: RleDecoder<'a, smol_str::SmolStr>, } -impl<'a> KeyIter<'a> { - fn try_next(&mut self) -> Result, DecodeColumnError> { +impl<'a> ElemIter<'a> { + fn try_next(&mut self) -> Result, DecodeColumnError> { let actor = self .actor .next() @@ -154,63 +138,48 @@ impl<'a> KeyIter<'a> { .next() .transpose() .map_err(|e| DecodeColumnError::decode_raw("counter", e))?; - let string = self - .string - .next() - .transpose() - .map_err(|e| DecodeColumnError::decode_raw("string", e))?; - match (actor, counter, string) { - (Some(Some(_)), Some(Some(_)), Some(Some(_))) => { - Err(DecodeColumnError::invalid_value("key", "too many values")) - } - (Some(None) | None, Some(None) | None, Some(Some(string))) => { - Ok(Some(Key::Prop(string))) - } - (Some(None) | None, Some(Some(0)), Some(None) | None) => { - Ok(Some(Key::Elem(ElemId(OpId::new(0, 0))))) - } - (Some(Some(actor)), Some(Some(ctr)), Some(None) | None) => match ctr.try_into() { - //Ok(ctr) => Some(Ok(Key::Elem(ElemId(OpId(ctr, actor as usize))))), - Ok(ctr) => Ok(Some(Key::Elem(ElemId(OpId::new(ctr, actor as usize))))), + match (actor, counter) { + (Some(None) | None, Some(Some(0))) => Ok(Some(ElemId(OpId::new(0, 0)))), + (Some(Some(actor)), Some(Some(ctr))) => match ctr.try_into() { + Ok(ctr) => Ok(Some(ElemId(OpId::new(ctr, actor as usize)))), Err(_) => Err(DecodeColumnError::invalid_value( "counter", "negative value for counter", )), }, - (None | Some(None), None | Some(None), None | Some(None)) => Ok(None), - (None | Some(None), k, _) => { + (None | Some(None), None | Some(None)) => Ok(None), + (None | Some(None), k) => { tracing::error!(key=?k, "unexpected null actor"); Err(DecodeColumnError::unexpected_null("actor")) } - (_, None | Some(None), _) => Err(DecodeColumnError::unexpected_null("counter")), + (_, None | Some(None)) => Err(DecodeColumnError::unexpected_null("counter")), } } } -impl<'a> Iterator for KeyIter<'a> { - type Item = Result; +impl<'a> Iterator for ElemIter<'a> { + type Item = Result; fn next(&mut self) -> Option { self.try_next().transpose() } } -pub(crate) struct KeyEncoder { +pub(crate) struct ElemEncoder { actor: RleEncoder, counter: DeltaEncoder, - string: RleEncoder, + //string: RleEncoder, } -impl KeyEncoder> { - pub(crate) fn new() -> KeyEncoder> { - KeyEncoder { +impl ElemEncoder> { + pub(crate) fn new() -> ElemEncoder> { + ElemEncoder { actor: RleEncoder::new(Vec::new()), counter: DeltaEncoder::new(Vec::new()), - string: RleEncoder::new(Vec::new()), } } - pub(crate) fn finish(self, out: &mut Vec) -> KeyRange { + pub(crate) fn finish(self, out: &mut Vec) -> ElemRange { let actor_start = out.len(); let (actor, _) = self.actor.finish(); out.extend(actor); @@ -220,39 +189,31 @@ impl KeyEncoder> { out.extend(counter); let counter_end = out.len(); - let (string, _) = self.string.finish(); - out.extend(string); - let string_end = out.len(); - - KeyRange { + ElemRange { actor: (actor_start..actor_end).into(), counter: (actor_end..counter_end).into(), - string: (counter_end..string_end).into(), } } } -impl KeyEncoder { - pub(crate) fn append(&mut self, key: convert::Key<'_, O>) +impl ElemEncoder { + pub(crate) fn append(&mut self, elem: Option>) where O: convert::OpId, { - match key { - convert::Key::Prop(p) => { - self.string.append_value(p.clone()); - self.actor.append_null(); - self.counter.append_null(); - } - convert::Key::Elem(convert::ElemId::Head) => { - self.string.append_null(); + match elem { + Some(convert::ElemId::Head) => { self.actor.append_null(); self.counter.append_value(0); } - convert::Key::Elem(convert::ElemId::Op(o)) => { - self.string.append_null(); + Some(convert::ElemId::Op(o)) => { self.actor.append_value(o.actor() as u64); self.counter.append_value(o.counter() as i64); } + None => { + self.actor.append_null(); + self.counter.append_null(); + } } } } diff --git a/rust/automerge/src/columnar/column_range/opid.rs b/rust/automerge/src/columnar/column_range/opid.rs index e9a94688..a68d024a 100644 --- a/rust/automerge/src/columnar/column_range/opid.rs +++ b/rust/automerge/src/columnar/column_range/opid.rs @@ -149,10 +149,8 @@ impl OpIdEncoder { Some(o) => { self.actor.append_value(o.actor() as u64); self.counter.append_value(o.counter() as i64); - }, - None => { - self.append_null() } + None => self.append_null(), } } diff --git a/rust/automerge/src/columnar/encoding/column_decoder.rs b/rust/automerge/src/columnar/encoding/column_decoder.rs index 8e3237fb..0ad055ee 100644 --- a/rust/automerge/src/columnar/encoding/column_decoder.rs +++ b/rust/automerge/src/columnar/encoding/column_decoder.rs @@ -1,7 +1,7 @@ use crate::{ columnar::{ - column_range::{DepsIter, KeyIter, ObjIdIter, OpIdIter, OpIdListIter, ValueIter}, - encoding, Key, + column_range::{DepsIter, ObjIdIter, OpIdIter, OpIdListIter, ValueIter}, + encoding, }, types::{ObjId, OpId}, ScalarValue, @@ -108,6 +108,7 @@ impl<'a> ColumnDecoder for ValueIter<'a> { } } +/* impl<'a> ColumnDecoder for KeyIter<'a> { type Error = encoding::DecodeColumnError; type Value = Key; @@ -119,6 +120,7 @@ impl<'a> ColumnDecoder for KeyIter<'a> { self.next().transpose().map_err(|e| e.in_column(col_name)) } } +*/ impl<'a> ColumnDecoder for ObjIdIter<'a> { type Value = ObjId; diff --git a/rust/automerge/src/storage/change.rs b/rust/automerge/src/storage/change.rs index 1f3cdeb5..3f19ff38 100644 --- a/rust/automerge/src/storage/change.rs +++ b/rust/automerge/src/storage/change.rs @@ -429,7 +429,7 @@ pub(crate) trait AsChangeOp<'a> { type PredIter: Iterator + ExactSizeIterator; fn obj(&self) -> convert::ObjId; - fn prop(&self) -> Option<&smol_str::SmolStr>; + fn prop(&self) -> Option>; fn elem(&self) -> Option>; fn insert(&self) -> bool; fn action(&self) -> u64; diff --git a/rust/automerge/src/storage/change/change_actors.rs b/rust/automerge/src/storage/change/change_actors.rs index 61f1221d..3f374960 100644 --- a/rust/automerge/src/storage/change/change_actors.rs +++ b/rust/automerge/src/storage/change/change_actors.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use crate::convert; @@ -71,7 +72,7 @@ where let (num_ops, mut other_actors) = ops.clone() .try_fold((0, BTreeSet::new()), |(count, mut acc), op| { - if let convert::Key::Elem(convert::ElemId::Op(o)) = op.key() { + if let Some(convert::ElemId::Op(o)) = op.elem() { if o.actor() != &actor { acc.insert(o.actor()); } @@ -233,8 +234,20 @@ where } } - fn key(&self) -> convert::Key<'aschangeop, Self::OpId> { - self.op.key().map(|o| self.actors.translate_opid(&o)) + /* + fn key(&self) -> convert::Key<'aschangeop, Self::OpId> { + self.op.key().map(|o| self.actors.translate_opid(&o)) + } + */ + + fn prop(&self) -> Option> { + self.op.prop() + } + + fn elem(&self) -> Option> { + self.op + .elem() + .map(|e| e.map(|id| self.actors.translate_opid(&id))) } fn obj(&self) -> convert::ObjId { diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index b435d4bd..61e55f59 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -1,10 +1,11 @@ +use std::borrow::Cow; use std::{convert::TryFrom, ops::Range}; use crate::{ columnar::{ column_range::{ generic::{GenericColumnRange, GroupRange, GroupedColumnRange, SimpleColRange}, - BooleanRange, DeltaRange, Key, KeyEncoder, KeyIter, KeyRange, ObjIdEncoder, ObjIdIter, + BooleanRange, DeltaRange, ElemEncoder, ElemIter, ElemRange, ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdListEncoder, OpIdListIter, OpIdListRange, RleRange, ValueEncoder, ValueIter, ValueRange, }, @@ -45,8 +46,12 @@ pub(crate) struct ChangeOp { impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { fn from(a: A) -> Self { ChangeOp { - prop: a.prop().cloned(), - elem_id: convert::ElemId(a.elem()), + prop: a.prop().map(|s| s.into_owned()), + elem_id: match a.elem() { + Some(convert::ElemId::Head) => Some(ElemId::head()), + Some(convert::ElemId::Op(id)) => Some(ElemId(id)), + None => None, + }, obj: match a.obj() { convert::ObjId::Root => ObjId::root(), convert::ObjId::Op(o) => ObjId(o), @@ -72,12 +77,16 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { } } - fn prop(&self) -> Option<&smol_str::SmolStr> { - return self.prop + fn prop(&self) -> Option> { + return self.prop.as_ref().map(|p| Cow::Borrowed(p)); } fn elem(&self) -> Option> { - return self.elem_id + match &self.elem_id { + Some(e) if e.is_head() => Some(convert::ElemId::Head), + Some(ElemId(o)) => Some(convert::ElemId::Op(o)), + _ => None, + } } fn val(&self) -> std::borrow::Cow<'a, ScalarValue> { @@ -100,7 +109,8 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { #[derive(Clone, Debug, PartialEq)] pub(crate) struct ChangeOpsColumns { obj: Option, - key: KeyRange, + elem: ElemRange, + prop: RleRange, insert: BooleanRange, action: RleRange, val: ValueRange, @@ -112,7 +122,8 @@ impl ChangeOpsColumns { ChangeOpsIter { failed: false, obj: self.obj.as_ref().map(|o| o.iter(data)), - key: self.key.iter(data), + elem: self.elem.iter(data), + prop: self.prop.decoder(data), insert: self.insert.decoder(data), action: self.action.decoder(data), val: self.val.iter(data), @@ -144,14 +155,16 @@ impl ChangeOpsColumns { C: AsChangeOp<'c, OpId = Op> + 'a, { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); - let key = KeyRange::encode(ops.clone().map(|o| o.key()), out); + let elem = ElemRange::maybe_encode(ops.clone().map(|o| o.elem()), out); + let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let pred = OpIdListRange::encode(ops.map(|o| o.pred()), out); Self { obj, - key, + elem, + prop, insert, action, val, @@ -166,21 +179,28 @@ impl ChangeOpsColumns { C: AsChangeOp<'c, OpId = Op> + 'a, { let mut obj = ObjIdEncoder::new(); - let mut key = KeyEncoder::new(); + let mut elem = ElemEncoder::new(); + let mut prop = RleEncoder::<_, smol_str::SmolStr>::from(Vec::new()); let mut insert = BooleanEncoder::new(); let mut action = RleEncoder::<_, u64>::from(Vec::new()); let mut val = ValueEncoder::new(); let mut pred = OpIdListEncoder::new(); for op in ops { obj.append(op.obj()); - key.append(op.key()); + elem.append(op.elem()); + prop.append(op.prop()); insert.append(op.insert()); action.append_value(op.action()); val.append(&op.val()); pred.append(op.pred()); } let obj = obj.finish(out); - let key = key.finish(out); + let elem = elem.finish(out); + + let prop_start = out.len(); + let (prop, _) = prop.finish(); + out.extend(prop); + let prop = RleRange::from(prop_start..out.len()); let insert_start = out.len(); let (insert, _) = insert.finish(); @@ -197,7 +217,8 @@ impl ChangeOpsColumns { Self { obj, - key, + elem, + prop, insert, action, val, @@ -223,15 +244,15 @@ impl ChangeOpsColumns { ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), - self.key.actor_range().clone().into(), + self.elem.actor_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), - self.key.counter_range().clone().into(), + self.elem.counter_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), - self.key.string_range().clone().into(), + self.prop.clone().into(), ), RawColumn::new( ColumnSpec::new(INSERT_COL_ID, ColumnType::Boolean, false), @@ -280,7 +301,8 @@ pub struct ReadChangeOpError(#[from] DecodeColumnError); pub(crate) struct ChangeOpsIter<'a> { failed: bool, obj: Option>, - key: KeyIter<'a>, + prop: RleDecoder<'a, smol_str::SmolStr>, + elem: ElemIter<'a>, insert: BooleanDecoder<'a>, action: RleDecoder<'a, u64>, val: ValueIter<'a>, @@ -301,14 +323,16 @@ impl<'a> ChangeOpsIter<'a> { } else { ObjId::root() }; - let key = self.key.next_in_col("key")?; + let prop = self.prop.maybe_next_in_col("key:prop")?; + let elem_id = self.elem.maybe_next_in_col("key:elem")?; let insert = self.insert.next_in_col("insert")?; let action = self.action.next_in_col("action")?; let val = self.val.next_in_col("value")?; let pred = self.pred.next_in_col("pred")?; Ok(Some(ChangeOp { obj, - key, + prop, + elem_id, insert, action, val, @@ -429,11 +453,11 @@ impl TryFrom for ChangeOpsColumns { obj_actor.unwrap_or_else(|| (0..0).into()), obj_ctr.unwrap_or_else(|| (0..0).into()), ), - key: KeyRange::new( + elem: ElemRange::new( key_actor.unwrap_or_else(|| (0..0).into()), key_ctr.unwrap_or_else(|| (0..0).into()), - key_str.unwrap_or_else(|| (0..0).into()), ), + prop: key_str.unwrap_or_else(|| (0..0).into()), insert: insert.unwrap_or(0..0).into(), action: action.unwrap_or(0..0).into(), val: val.unwrap_or_else(|| ValueRange::new((0..0).into(), (0..0).into())), diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index b110bb78..14c217a7 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -120,11 +120,27 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { } } - fn key(&self) -> convert::Key<'a, Self::OpId> { + fn prop(&self) -> Option> { match &self.op.key { - Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.metadata.props.get(*idx).into())), - Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), - Key::Seq(e) => convert::Key::Elem(convert::ElemId::Op(self.wrap(&e.0))), + Key::Map(idx) => Some(Cow::Owned(self.metadata.props.get(*idx).into())), + _ => None, } } + + fn elem(&self) -> Option> { + match &self.op.key { + Key::Seq(e) if e.is_head() => Some(convert::ElemId::Head), + Key::Seq(e) => Some(convert::ElemId::Op(self.wrap(&e.0))), + _ => None, + } + } + /* + fn key(&self) -> convert::Key<'a, Self::OpId> { + match &self.op.key { + Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.metadata.props.get(*idx).into())), + Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), + Key::Seq(e) => convert::Key::Elem(convert::ElemId::Op(self.wrap(&e.0))), + } + } + */ } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index 8d237354..aa867f11 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -76,16 +76,33 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { } } - fn key(&self) -> convert::Key<'a, Self::OpId> { + fn elem(&self) -> Option> { match self.op.key { - Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.props.get(idx).into())), - Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), - Key::Seq(ElemId(o)) => { - convert::Key::Elem(convert::ElemId::Op(translate(self.actor_lookup, &o))) - } + Key::Map(_) => None, + Key::Seq(e) if e.is_head() => Some(convert::ElemId::Head), + Key::Seq(ElemId(o)) => Some(convert::ElemId::Op(translate(self.actor_lookup, &o))), } } + fn prop(&self) -> Option> { + match self.op.key { + Key::Map(idx) => Some(Cow::Owned(self.props.get(idx).into())), + _ => None, + } + } + + /* + fn key(&self) -> convert::Key<'a, Self::OpId> { + match self.op.key { + Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.props.get(idx).into())), + Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), + Key::Seq(ElemId(o)) => { + convert::Key::Elem(convert::ElemId::Op(translate(self.actor_lookup, &o))) + } + } + } + */ + fn val(&self) -> Cow<'a, crate::ScalarValue> { match &self.op.action { OpType::Put(v) => Cow::Borrowed(v), diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 38fee1d6..7781c3fb 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -6,9 +6,9 @@ use crate::{ columnar::{ column_range::{ generic::{GenericColumnRange, GroupRange, GroupedColumnRange, SimpleColRange}, - BooleanRange, DeltaRange, Key, KeyEncoder, KeyIter, KeyRange, ObjIdEncoder, ObjIdIter, - ObjIdRange, OpIdEncoder, OpIdIter, OpIdListEncoder, OpIdListIter, OpIdListRange, - OpIdRange, RleRange, ValueEncoder, ValueIter, ValueRange, + BooleanRange, DeltaRange, ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdEncoder, OpIdIter, + OpIdListEncoder, OpIdListIter, OpIdListRange, OpIdRange, RleRange, ValueEncoder, + ValueIter, ValueRange, }, encoding::{ BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, RleDecoder, @@ -20,7 +20,7 @@ use crate::{ columns::{compression, ColumnId, ColumnSpec, ColumnType}, Columns, MismatchingColumn, RawColumn, RawColumns, }, - types::{ObjId, OpId, ScalarValue, ElemId}, + types::{ElemId, ObjId, OpId, ScalarValue}, }; const OBJ_COL_ID: ColumnId = ColumnId::new(0); @@ -89,9 +89,8 @@ pub(crate) trait AsDocOp<'a> { fn obj(&self) -> convert::ObjId; fn id(&self) -> Self::OpId; - //fn key(&self) -> convert::Key<'a, Self::OpId>; fn prop(&self) -> Option>; - fn elem(&self) -> Option; + fn elem(&self) -> Option>; fn insert(&self) -> bool; fn action(&self) -> u64; fn val(&self) -> Cow<'a, ScalarValue>; @@ -117,7 +116,7 @@ impl DocOpColumns { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); let id = OpIdRange::encode(ops.clone().map(|o| o.id()), out); let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); - let elem = OpIdRange::encode_optional(ops.clone().map(|o| o.elem().map(|o| o)), out); + let elem = OpIdRange::encode_optional(ops.clone().map(|o| o.elem()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); @@ -287,7 +286,6 @@ pub(crate) struct DocOpColumnIter<'a> { id: OpIdIter<'a>, action: RleDecoder<'a, u64>, objs: Option>, - //keys: KeyIter<'a>, prop: RleDecoder<'a, SmolStr>, elem: OpIdIter<'a>, insert: BooleanDecoder<'a>, @@ -334,7 +332,7 @@ impl<'a> DocOpColumnIter<'a> { ObjId::root() }; let prop = self.prop.maybe_next_in_col("key:prop")?; - let elem_id = self.elem.maybe_next_in_col("key:elem")?.map(|o| o.into()); + let elem_id = self.elem.maybe_next_in_col("key:elem")?; let value = self.value.next_in_col("value")?; let succ = self.succ.next_in_col("succ")?; let insert = self.insert.next_in_col("insert")?; @@ -343,7 +341,7 @@ impl<'a> DocOpColumnIter<'a> { value, action: action as usize, object: obj, - elem_id, + elem_id: elem_id.map(|i| i.into()), prop, succ, insert, From a44ceacb1ca31eda9ac81b5b80a821b0e9bb380e Mon Sep 17 00:00:00 2001 From: Alex Good Date: Thu, 9 Feb 2023 09:50:28 +0000 Subject: [PATCH 06/26] everything compiles --- rust/automerge-wasm/src/lib.rs | 2 +- rust/automerge/src/automerge.rs | 3 +- rust/automerge/src/change.rs | 5 ++- .../src/columnar/column_range/key.rs | 11 ++--- .../src/columnar/column_range/opid.rs | 43 +++++++++++++++++++ .../src/columnar/encoding/properties.rs | 12 +----- rust/automerge/src/query/raw_spans.rs | 2 +- .../src/storage/change/change_op_columns.rs | 10 +++-- .../src/storage/document/doc_op_columns.rs | 7 +-- .../src/storage/load/reconstruct_document.rs | 4 +- 10 files changed, 69 insertions(+), 30 deletions(-) diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 94efadbd..b3568f05 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -839,7 +839,7 @@ impl Automerge { let marks = Array::new(); for m in s.marks { let mark = Array::new(); - mark.push(&m.0.into()); // span name + mark.push(&m.0.to_string().into()); // span name let (datatype, value) = alloc(&am::Value::Scalar(m.1.clone()), self.text_rep); mark.push(&datatype.into()); mark.push(&value); diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 98ebe7fa..88acb832 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -702,7 +702,8 @@ impl Automerge { (Some(e), _) if e.is_head() => Key::Seq(ElemId::head()), (Some(ElemId(o)), _) => { Key::Seq(ElemId(OpId::new(o.counter(), actors[o.actor()]))) - } + }, + (None, None) => unreachable!(), }; let obj = if c.obj.is_root() { ObjId::root() diff --git a/rust/automerge/src/change.rs b/rust/automerge/src/change.rs index ce4dc8a2..b338f6c8 100644 --- a/rust/automerge/src/change.rs +++ b/rust/automerge/src/change.rs @@ -296,18 +296,19 @@ impl From<&Change> for crate::ExpandedChange { .iter_ops() .map(|o| crate::legacy::Op { action: crate::types::OpType::from_index_and_value( - o.action, o.val, o.insert, o.prop, + o.action, o.val, o.insert, o.prop.clone(), ) .unwrap(), insert: o.insert, key: match (o.elem_id, o.prop) { - (None, Some(p)) => crate::legacy::Key::Map(p), + (None, Some(p)) => crate::legacy::Key::Map(p.clone()), (Some(e), _) if e.is_head() => { crate::legacy::Key::Seq(crate::legacy::ElementId::Head) } (Some(ElemId(o)), _) => crate::legacy::Key::Seq(crate::legacy::ElementId::Id( crate::legacy::OpId::new(o.counter(), actors.get(&o.actor()).unwrap()), )), + (None, None) => unreachable!(), }, obj: if o.obj.is_root() { crate::legacy::ObjectId::Root diff --git a/rust/automerge/src/columnar/column_range/key.rs b/rust/automerge/src/columnar/column_range/key.rs index 12687aba..a0b8a623 100644 --- a/rust/automerge/src/columnar/column_range/key.rs +++ b/rust/automerge/src/columnar/column_range/key.rs @@ -127,7 +127,7 @@ pub(crate) struct ElemIter<'a> { } impl<'a> ElemIter<'a> { - fn try_next(&mut self) -> Result, DecodeColumnError> { + fn try_next(&mut self) -> Result>, DecodeColumnError> { let actor = self .actor .next() @@ -139,15 +139,16 @@ impl<'a> ElemIter<'a> { .transpose() .map_err(|e| DecodeColumnError::decode_raw("counter", e))?; match (actor, counter) { - (Some(None) | None, Some(Some(0))) => Ok(Some(ElemId(OpId::new(0, 0)))), + (Some(None) | None, Some(Some(0))) => Ok(Some(Some(ElemId(OpId::new(0, 0))))), (Some(Some(actor)), Some(Some(ctr))) => match ctr.try_into() { - Ok(ctr) => Ok(Some(ElemId(OpId::new(ctr, actor as usize)))), + Ok(ctr) => Ok(Some(Some(ElemId(OpId::new(ctr, actor as usize))))), Err(_) => Err(DecodeColumnError::invalid_value( "counter", "negative value for counter", )), }, - (None | Some(None), None | Some(None)) => Ok(None), + (Some(None), Some(None)) => Ok(Some(None)), + (None, None) => Ok(None), (None | Some(None), k) => { tracing::error!(key=?k, "unexpected null actor"); Err(DecodeColumnError::unexpected_null("actor")) @@ -158,7 +159,7 @@ impl<'a> ElemIter<'a> { } impl<'a> Iterator for ElemIter<'a> { - type Item = Result; + type Item = Result, DecodeColumnError>; fn next(&mut self) -> Option { self.try_next().transpose() diff --git a/rust/automerge/src/columnar/column_range/opid.rs b/rust/automerge/src/columnar/column_range/opid.rs index a68d024a..6dce202b 100644 --- a/rust/automerge/src/columnar/column_range/opid.rs +++ b/rust/automerge/src/columnar/column_range/opid.rs @@ -58,6 +58,32 @@ impl OpIdRange { Self { actor, counter } } + pub(crate) fn encode_elemids(opids: I, out: &mut Vec) -> Self + where + O: convert::OpId, + I: Iterator>> + Clone, + { + let actor = RleRange::encode( + opids.clone().map(|o| { + o.map(|o| match o { + convert::ElemId::Op(o) => o.actor() as u64, + convert::ElemId::Head => 0, + }) + }), + out, + ); + let counter = DeltaRange::encode( + opids.map(|o| { + o.map(|o| match o { + convert::ElemId::Op(o) => o.counter() as i64, + convert::ElemId::Head => 0, + }) + }), + out, + ); + Self { actor, counter } + } + #[allow(dead_code)] pub(crate) fn splice( &self, @@ -154,6 +180,23 @@ impl OpIdEncoder { } } + pub(crate) fn append_elemid>( + &mut self, + elem: Option>, + ) { + match elem { + Some(convert::ElemId::Op(o)) => { + self.actor.append_value(o.actor() as u64); + self.counter.append_value(o.counter() as i64); + } + Some(convert::ElemId::Head) => { + self.actor.append_value(0); + self.counter.append_value(0); + } + None => self.append_null(), + } + } + pub(crate) fn append_null(&mut self) { self.actor.append_null(); self.counter.append_null(); diff --git a/rust/automerge/src/columnar/encoding/properties.rs b/rust/automerge/src/columnar/encoding/properties.rs index a3bf1ed0..2dc5373d 100644 --- a/rust/automerge/src/columnar/encoding/properties.rs +++ b/rust/automerge/src/columnar/encoding/properties.rs @@ -5,10 +5,7 @@ use std::{fmt::Debug, ops::Range}; use proptest::prelude::*; use smol_str::SmolStr; -use crate::{ - columnar::Key, - types::{ElemId, OpId, ScalarValue}, -}; +use crate::types::{ElemId, OpId, ScalarValue}; #[derive(Clone, Debug)] pub(crate) struct SpliceScenario { @@ -146,13 +143,6 @@ pub(crate) fn elemid() -> impl Strategy + Clone { opid().prop_map(ElemId) } -pub(crate) fn key() -> impl Strategy + Clone { - prop_oneof! { - elemid().prop_map(Key::Elem), - any::().prop_map(|s| Key::Prop(s.into())), - } -} - pub(crate) fn encodable_int() -> impl Strategy + Clone { let bounds = i64::MAX / 2; -bounds..bounds diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index f6b225cc..caa80610 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -50,7 +50,7 @@ impl<'a> TreeQuery<'a> for RawSpans { id: element.id, start: self.seen, end: 0, - name: md.name.into(), + name: md.name.clone().into(), value: md.value.clone(), }, ); diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index 61e55f59..20b14279 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -469,20 +469,22 @@ impl TryFrom for ChangeOpsColumns { #[cfg(test)] mod tests { use super::*; - use crate::columnar::encoding::properties::{key, opid, scalar_value}; + use crate::columnar::encoding::properties::{elemid, opid, scalar_value}; use proptest::prelude::*; prop_compose! { fn change_op() - (key in key(), - value in scalar_value(), + (value in scalar_value(), + prop in proptest::option::of(any::().prop_map(|s| s.into())), + elem_id in proptest::option::of(elemid()), pred in proptest::collection::vec(opid(), 0..20), action in 0_u64..6, obj in opid(), insert in any::()) -> ChangeOp { ChangeOp { obj: obj.into(), - key, + prop, + elem_id, val: value, pred, action, diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 7781c3fb..f266630d 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -116,7 +116,7 @@ impl DocOpColumns { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); let id = OpIdRange::encode(ops.clone().map(|o| o.id()), out); let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); - let elem = OpIdRange::encode_optional(ops.clone().map(|o| o.elem()), out); + let elem = OpIdRange::encode_elemids(ops.clone().map(|o| o.elem()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); @@ -151,9 +151,10 @@ impl DocOpColumns { for op in ops { obj.append(op.obj()); prop.append(op.prop()); - elem.append(op.elem()); + elem.append_elemid(op.elem()); id.append(Some(op.id())); insert.append(op.insert()); + action.append(Some(op.action())); val.append(&op.val()); succ.append(op.succ()); } @@ -232,7 +233,7 @@ impl DocOpColumns { ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), - self.prop.into(), + self.prop.clone().into(), ), RawColumn::new( ColumnSpec::new(ID_COL_ID, ColumnType::Actor, false), diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index 3e7a9f8f..42cabc59 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -338,8 +338,8 @@ impl LoadingObject { } fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { - let key = match (op.prop, op.elem_id) { - (Some(k), None) => Ok(Key::Map(m.import_prop(k))), + let key = match (&op.prop, op.elem_id) { + (Some(k), None) => Ok(Key::Map(m.import_prop(k.as_str()))), (_, Some(elem)) => Ok(Key::Seq(ElemId(check_opid(m, elem.0)?))), (None, None) => Err(Error::MissingKey), }?; From f281213a47c6c5c701ee3d1b23a56b8ac5aa8d71 Mon Sep 17 00:00:00 2001 From: Alex Good Date: Thu, 9 Feb 2023 10:55:19 +0000 Subject: [PATCH 07/26] tests passing --- rust/automerge/src/automerge/tests.rs | 2 ++ rust/automerge/src/columnar/column_range/key.rs | 3 ++- .../src/storage/document/doc_op_columns.rs | 14 +++++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rust/automerge/src/automerge/tests.rs b/rust/automerge/src/automerge/tests.rs index 3511c4ed..6e40c9b1 100644 --- a/rust/automerge/src/automerge/tests.rs +++ b/rust/automerge/src/automerge/tests.rs @@ -7,6 +7,8 @@ use crate::transaction::Transactable; use crate::*; use std::convert::TryInto; +use test_log::test; + #[test] fn insert_op() -> Result<(), AutomergeError> { let mut doc = Automerge::new(); diff --git a/rust/automerge/src/columnar/column_range/key.rs b/rust/automerge/src/columnar/column_range/key.rs index a0b8a623..ab9269ad 100644 --- a/rust/automerge/src/columnar/column_range/key.rs +++ b/rust/automerge/src/columnar/column_range/key.rs @@ -149,8 +149,9 @@ impl<'a> ElemIter<'a> { }, (Some(None), Some(None)) => Ok(Some(None)), (None, None) => Ok(None), + (None | Some(None), None | Some(None) ) => Ok(Some(None)), (None | Some(None), k) => { - tracing::error!(key=?k, "unexpected null actor"); + tracing::error!(counter=?k, "unexpected null actor"); Err(DecodeColumnError::unexpected_null("actor")) } (_, None | Some(None)) => Err(DecodeColumnError::unexpected_null("counter")), diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index f266630d..584a46ff 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -8,7 +8,7 @@ use crate::{ generic::{GenericColumnRange, GroupRange, GroupedColumnRange, SimpleColRange}, BooleanRange, DeltaRange, ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdEncoder, OpIdIter, OpIdListEncoder, OpIdListIter, OpIdListRange, OpIdRange, RleRange, ValueEncoder, - ValueIter, ValueRange, + ValueIter, ValueRange, ElemRange, ElemEncoder, ElemIter, }, encoding::{ BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, RleDecoder, @@ -48,7 +48,7 @@ pub(crate) struct DocOp { pub(crate) struct DocOpColumns { obj: Option, prop: RleRange, - elem: OpIdRange, + elem: ElemRange, id: OpIdRange, insert: BooleanRange, action: RleRange, @@ -116,7 +116,7 @@ impl DocOpColumns { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); let id = OpIdRange::encode(ops.clone().map(|o| o.id()), out); let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); - let elem = OpIdRange::encode_elemids(ops.clone().map(|o| o.elem()), out); + let elem = ElemRange::maybe_encode(ops.clone().map(|o| o.elem()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); @@ -142,7 +142,7 @@ impl DocOpColumns { { let mut obj = ObjIdEncoder::new(); let mut prop = RleEncoder::<_, SmolStr>::new(Vec::new()); - let mut elem = OpIdEncoder::new(); + let mut elem = ElemEncoder::new(); let mut id = OpIdEncoder::new(); let mut insert = BooleanEncoder::new(); let mut action = RleEncoder::<_, u64>::from(Vec::new()); @@ -151,7 +151,7 @@ impl DocOpColumns { for op in ops { obj.append(op.obj()); prop.append(op.prop()); - elem.append_elemid(op.elem()); + elem.append(op.elem()); id.append(Some(op.id())); insert.append(op.insert()); action.append(Some(op.action())); @@ -288,7 +288,7 @@ pub(crate) struct DocOpColumnIter<'a> { action: RleDecoder<'a, u64>, objs: Option>, prop: RleDecoder<'a, SmolStr>, - elem: OpIdIter<'a>, + elem: ElemIter<'a>, insert: BooleanDecoder<'a>, value: ValueIter<'a>, succ: OpIdListIter<'a>, @@ -445,7 +445,7 @@ impl TryFrom for DocOpColumns { obj_ctr.unwrap_or_else(|| (0..0).into()), ), prop: key_str.unwrap_or_else(|| (0..0).into()), - elem: OpIdRange::new( + elem: ElemRange::new( key_actor.unwrap_or_else(|| (0..0).into()), key_ctr.unwrap_or_else(|| (0..0).into()), ), From a02f70f2b8cb9dd8615268f2f05ad15f6d2028be Mon Sep 17 00:00:00 2001 From: Alex Good Date: Thu, 9 Feb 2023 15:09:52 +0000 Subject: [PATCH 08/26] Use new columns instead of existing ones The previous approach of using the key and insert columns of existing ops was leading to quite confusing code. There's no real cost to introducing new columns so I've switched the code to do that instead. Introduce an `expand` and a `mark_name` column. `expand` is a boolean column and `mark_name` is a RLE encoded string column. Neither of these columns are encoded if they are empty. Also move the `MarkData::name` property to use strings interned in `OpSetMetadata::props` rather than representing the string directly on the basis that we probably will have a lot of repeated mark names and we do a bunch of equality checks on them while searching so this will probably speed things up a bit. Introduced a new `MaybeBooleanEncoder` (and associated `MaybeBooleanDecoder` and `MaybeBooleanRange`) types to represent a boolean column which is entirely skipped if all it contains are `false` values. This allows us to omit encoding the `expand` column for groups of ops which only ever set it to `false` which in turn makes us backwards compatible when not using marks. --- rust/automerge/src/automerge.rs | 33 ++-- rust/automerge/src/change.rs | 74 ++++---- rust/automerge/src/columnar.rs | 2 +- rust/automerge/src/columnar/column_range.rs | 4 +- .../src/columnar/column_range/boolean.rs | 45 ++++- .../src/columnar/column_range/key.rs | 175 +++++++++++------- .../src/columnar/column_range/opid.rs | 69 +------ rust/automerge/src/columnar/encoding.rs | 4 +- .../src/columnar/encoding/boolean.rs | 66 +++++++ .../src/columnar/encoding/column_decoder.rs | 6 +- .../src/columnar/encoding/properties.rs | 12 +- rust/automerge/src/columnar/encoding/raw.rs | 4 + rust/automerge/src/error.rs | 4 +- rust/automerge/src/legacy/mod.rs | 93 +++++++++- .../src/legacy/serde_impls/op_type.rs | 2 +- rust/automerge/src/op_set.rs | 8 +- rust/automerge/src/op_tree.rs | 5 +- rust/automerge/src/query/raw_spans.rs | 6 +- rust/automerge/src/query/spans.rs | 35 +++- rust/automerge/src/storage/change.rs | 5 +- .../src/storage/change/change_actors.rs | 32 ++-- .../src/storage/change/change_op_columns.rs | 169 ++++++++++------- .../src/storage/convert/op_as_changeop.rs | 43 ++--- .../src/storage/convert/op_as_docop.rs | 49 ++--- .../src/storage/document/doc_op_columns.rs | 138 +++++++++----- .../src/storage/load/reconstruct_document.rs | 54 ++---- rust/automerge/src/transaction/inner.rs | 5 +- rust/automerge/src/types.rs | 61 ++++-- rust/automerge/tests/test.rs | 3 +- 29 files changed, 755 insertions(+), 451 deletions(-) diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 88acb832..d19541be 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -5,7 +5,7 @@ use std::num::NonZeroU64; use std::ops::RangeBounds; use crate::change_graph::ChangeGraph; -//use crate::columnar::Key as EncodedKey; +use crate::columnar::Key as EncodedKey; use crate::exid::ExId; use crate::keys::Keys; use crate::op_observer::{BranchableObserver, OpObserver}; @@ -16,8 +16,8 @@ use crate::transaction::{ self, CommitOptions, Failure, Observed, Success, Transaction, TransactionArgs, UnObserved, }; use crate::types::{ - ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, ObjId, Op, OpId, - OpType, ScalarValue, TextEncoding, Value, + ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, MarkName, ObjId, Op, + OpId, OpType, OpTypeParts, ScalarValue, TextEncoding, Value, }; use crate::{ query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, @@ -697,13 +697,12 @@ impl Automerge { .enumerate() .map(|(i, c)| { let id = OpId::new(change.start_op().get() + i as u64, actor); - let key = match (&c.elem_id, &c.prop) { - (None, Some(n)) => Key::Map(self.ops.m.props.cache(n.to_string())), - (Some(e), _) if e.is_head() => Key::Seq(ElemId::head()), - (Some(ElemId(o)), _) => { + let key = match &c.key { + EncodedKey::Prop(n) => Key::Map(self.ops.m.props.cache(n.to_string())), + EncodedKey::Elem(e) if e.is_head() => Key::Seq(ElemId::head()), + EncodedKey::Elem(ElemId(o)) => { Key::Seq(ElemId(OpId::new(o.counter(), actors[o.actor()]))) - }, - (None, None) => unreachable!(), + } }; let obj = if c.obj.is_root() { ObjId::root() @@ -718,12 +717,20 @@ impl Automerge { .iter() .map(|p| OpId::new(p.counter(), actors[p.actor()])); let pred = self.ops.m.sorted_opids(pred); + let mark_name = c + .mark_name + .map(|m| MarkName::from_prop_index(self.ops.m.props.cache(m.to_string()))); ( obj, Op { id, - action: OpType::from_index_and_value(c.action, c.val, c.insert, c.prop) - .unwrap(), + action: OpType::from_parts(OpTypeParts { + action: c.action, + value: c.val, + expand: c.expand, + mark_name, + }) + .unwrap(), key, succ: Default::default(), pred, @@ -1319,7 +1326,7 @@ impl ReadDoc for Automerge { query::Spans::new(ListEncoding::Text(self.text_encoding)), ); query.check_marks(); - Ok(query.spans) + Ok(query.into_spans(&self.ops.m)) } fn attribute>( @@ -1364,7 +1371,7 @@ impl ReadDoc for Automerge { id: self.id_to_exid(s.id), start: s.start, end: s.end, - span_type: s.name, + span_type: self.ops.m.props.get(s.name.props_index()).to_string(), value: s.value, }) .collect(); diff --git a/rust/automerge/src/change.rs b/rust/automerge/src/change.rs index b338f6c8..a6ab112a 100644 --- a/rust/automerge/src/change.rs +++ b/rust/automerge/src/change.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, num::NonZeroU64}; use crate::{ - // columnar::Key as StoredKey, + columnar::Key as StoredKey, storage::{ change::{Unverified, Verified}, parse, Change as StoredChange, ChangeOp, Chunk, Compressed, ReadChangeOpError, @@ -230,34 +230,17 @@ mod convert_expanded { self.pred.iter() } - fn prop(&self) -> Option> { + fn key(&self) -> convert::Key<'a, Self::OpId> { match &self.key { - legacy::Key::Map(s) => Some(Cow::Borrowed(s)), - _ => None, - } - } - - fn elem(&self) -> Option> { - match &self.key { - legacy::Key::Seq(legacy::ElementId::Head) => Some(convert::ElemId::Head), - legacy::Key::Seq(legacy::ElementId::Id(o)) => Some(convert::ElemId::Op(o)), - _ => None, - } - } - - /* - fn key(&self) -> convert::Key<'a, Self::OpId> { - match &self.key { - legacy::Key::Map(s) => convert::Key::Prop(Cow::Borrowed(s)), - legacy::Key::Seq(legacy::ElementId::Head) => { - convert::Key::Elem(convert::ElemId::Head) - } - legacy::Key::Seq(legacy::ElementId::Id(o)) => { - convert::Key::Elem(convert::ElemId::Op(o)) - } - } + legacy::Key::Map(s) => convert::Key::Prop(Cow::Borrowed(s)), + legacy::Key::Seq(legacy::ElementId::Head) => { + convert::Key::Elem(convert::ElemId::Head) } - */ + legacy::Key::Seq(legacy::ElementId::Id(o)) => { + convert::Key::Elem(convert::ElemId::Op(o)) + } + } + } fn obj(&self) -> convert::ObjId { match &self.obj { @@ -272,6 +255,18 @@ mod convert_expanded { None => Cow::Owned(ScalarValue::Null), } } + + fn expand(&self) -> bool { + self.action.expand() + } + + fn mark_name(&self) -> Option> { + if let legacy::OpType::MarkBegin(legacy::MarkData { name, .. }) = &self.action { + Some(Cow::Borrowed(name)) + } else { + None + } + } } impl<'a> convert::OpId<&'a ActorId> for &'a legacy::OpId { @@ -295,20 +290,23 @@ impl From<&Change> for crate::ExpandedChange { let operations = c .iter_ops() .map(|o| crate::legacy::Op { - action: crate::types::OpType::from_index_and_value( - o.action, o.val, o.insert, o.prop.clone(), - ) - .unwrap(), + action: crate::legacy::OpType::from_parts(crate::legacy::OpTypeParts { + action: o.action, + value: o.val, + expand: o.expand, + mark_name: o.mark_name, + }), insert: o.insert, - key: match (o.elem_id, o.prop) { - (None, Some(p)) => crate::legacy::Key::Map(p.clone()), - (Some(e), _) if e.is_head() => { + key: match o.key { + StoredKey::Elem(e) if e.is_head() => { crate::legacy::Key::Seq(crate::legacy::ElementId::Head) } - (Some(ElemId(o)), _) => crate::legacy::Key::Seq(crate::legacy::ElementId::Id( - crate::legacy::OpId::new(o.counter(), actors.get(&o.actor()).unwrap()), - )), - (None, None) => unreachable!(), + StoredKey::Elem(ElemId(o)) => { + crate::legacy::Key::Seq(crate::legacy::ElementId::Id( + crate::legacy::OpId::new(o.counter(), actors.get(&o.actor()).unwrap()), + )) + } + StoredKey::Prop(p) => crate::legacy::Key::Map(p), }, obj: if o.obj.is_root() { crate::legacy::ObjectId::Root diff --git a/rust/automerge/src/columnar.rs b/rust/automerge/src/columnar.rs index 30dd0833..bb727626 100644 --- a/rust/automerge/src/columnar.rs +++ b/rust/automerge/src/columnar.rs @@ -7,7 +7,7 @@ //! `Range` - which have useful instance methods such as `encode()` to create a new range and //! `decoder()` to return an iterator of the correct type. pub(crate) mod column_range; -//pub(crate) use column_range::Key; +pub(crate) use column_range::Key; pub(crate) mod encoding; mod splice_error; diff --git a/rust/automerge/src/columnar/column_range.rs b/rust/automerge/src/columnar/column_range.rs index 6c1525ab..709cbe9a 100644 --- a/rust/automerge/src/columnar/column_range.rs +++ b/rust/automerge/src/columnar/column_range.rs @@ -3,7 +3,7 @@ pub(crate) use rle::RleRange; mod delta; pub(crate) use delta::DeltaRange; mod boolean; -pub(crate) use boolean::BooleanRange; +pub(crate) use boolean::{BooleanRange, MaybeBooleanRange}; mod raw; pub(crate) use raw::RawRange; mod opid; @@ -16,6 +16,6 @@ mod value; pub(crate) use value::{ValueEncoder, ValueIter, ValueRange}; pub(crate) mod generic; mod key; -pub(crate) use key::{ElemEncoder, ElemIter, ElemRange}; +pub(crate) use key::{Key, KeyEncoder, KeyIter, KeyRange}; mod obj_id; pub(crate) use obj_id::{ObjIdEncoder, ObjIdIter, ObjIdRange}; diff --git a/rust/automerge/src/columnar/column_range/boolean.rs b/rust/automerge/src/columnar/column_range/boolean.rs index 3cefaf0d..b2530462 100644 --- a/rust/automerge/src/columnar/column_range/boolean.rs +++ b/rust/automerge/src/columnar/column_range/boolean.rs @@ -1,6 +1,8 @@ use std::{borrow::Cow, ops::Range}; -use crate::columnar::encoding::{BooleanDecoder, BooleanEncoder}; +use crate::columnar::encoding::{ + BooleanDecoder, BooleanEncoder, MaybeBooleanDecoder, MaybeBooleanEncoder, +}; #[derive(Clone, Debug, PartialEq)] pub(crate) struct BooleanRange(Range); @@ -38,3 +40,44 @@ impl From for Range { r.0 } } + +#[derive(Clone, Debug, PartialEq)] +pub struct MaybeBooleanRange(Range); + +impl MaybeBooleanRange { + pub(crate) fn decoder<'a>(&self, data: &'a [u8]) -> MaybeBooleanDecoder<'a> { + MaybeBooleanDecoder::from(Cow::Borrowed(&data[self.0.clone()])) + } + + pub(crate) fn encode>(items: I, out: &mut Vec) -> Self { + let start = out.len(); + let mut encoder = MaybeBooleanEncoder::from_sink(out); + for i in items { + encoder.append(i); + } + let (_, len) = encoder.finish(); + (start..(start + len)).into() + } + + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl AsRef> for MaybeBooleanRange { + fn as_ref(&self) -> &Range { + &self.0 + } +} + +impl From> for MaybeBooleanRange { + fn from(r: Range) -> MaybeBooleanRange { + MaybeBooleanRange(r) + } +} + +impl From for Range { + fn from(r: MaybeBooleanRange) -> Range { + r.0 + } +} diff --git a/rust/automerge/src/columnar/column_range/key.rs b/rust/automerge/src/columnar/column_range/key.rs index ab9269ad..70ea8e1e 100644 --- a/rust/automerge/src/columnar/column_range/key.rs +++ b/rust/automerge/src/columnar/column_range/key.rs @@ -13,14 +13,29 @@ use crate::{ }; #[derive(Clone, Debug, PartialEq)] -pub(crate) struct ElemRange { - actor: RleRange, - counter: DeltaRange, +pub(crate) enum Key { + Prop(smol_str::SmolStr), + Elem(ElemId), } -impl ElemRange { - pub(crate) fn new(actor: RleRange, counter: DeltaRange) -> Self { - Self { actor, counter } +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct KeyRange { + actor: RleRange, + counter: DeltaRange, + string: RleRange, +} + +impl KeyRange { + pub(crate) fn new( + actor: RleRange, + counter: DeltaRange, + string: RleRange, + ) -> Self { + Self { + actor, + counter, + string, + } } pub(crate) fn actor_range(&self) -> &RleRange { @@ -31,36 +46,19 @@ impl ElemRange { &self.counter } - // pub(crate) fn string_range(&self) -> &RleRange { - // &self.string - // } + pub(crate) fn string_range(&self) -> &RleRange { + &self.string + } - pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> ElemIter<'a> { - ElemIter { + pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> KeyIter<'a> { + KeyIter { actor: self.actor.decoder(data), counter: self.counter.decoder(data), + string: self.string.decoder(data), } } -/* - pub(crate) fn encode<'b, O, I: Iterator> + Clone>( - items: I, - out: &mut Vec, - ) -> Self - where - O: convert::OpId, - { - // SAFETY: The incoming iterator is infallible and there are no existing items - Self { - actor: (0..0).into(), - counter: (0..0).into(), - } - .splice::<_, Infallible, _>(&[], 0..0, items.map(Ok), out) - .unwrap() - } -*/ - - pub(crate) fn maybe_encode<'b, O, I: Iterator>> + Clone>( + pub(crate) fn encode<'b, O, I: Iterator> + Clone>( items: I, out: &mut Vec, ) -> Self @@ -71,6 +69,7 @@ impl ElemRange { Self { actor: (0..0).into(), counter: (0..0).into(), + string: (0..0).into(), } .splice::<_, Infallible, _>(&[], 0..0, items.map(Ok), out) .unwrap() @@ -88,16 +87,16 @@ impl ElemRange { where O: convert::OpId, E: std::error::Error, - I: Iterator>, E>> + Clone, + I: Iterator, E>> + Clone, { let actor = self.actor.splice( data, replace.clone(), replace_with.clone().map(|k| { k.map(|k| match k { - Some(convert::ElemId::Head) => None, - Some(convert::ElemId::Op(o)) => Some(o.actor() as u64), - None => None, + convert::Key::Prop(_) => None, + convert::Key::Elem(convert::ElemId::Head) => None, + convert::Key::Elem(convert::ElemId::Op(o)) => Some(o.actor() as u64), }) }), out, @@ -108,26 +107,43 @@ impl ElemRange { replace.clone(), replace_with.clone().map(|k| { k.map(|k| match k { - Some(convert::ElemId::Head) => Some(0), - Some(convert::ElemId::Op(o)) => Some(o.counter() as i64), - None => None, + convert::Key::Prop(_) => None, + convert::Key::Elem(convert::ElemId::Head) => Some(0), + convert::Key::Elem(convert::ElemId::Op(o)) => Some(o.counter() as i64), }) }), out, )?; - Ok(Self { actor, counter }) + let string = self.string.splice( + data, + replace, + replace_with.map(|k| { + k.map(|k| match k { + convert::Key::Prop(s) => Some(s), + convert::Key::Elem(_) => None, + }) + }), + out, + )?; + + Ok(Self { + actor, + counter, + string, + }) } } #[derive(Clone, Debug)] -pub(crate) struct ElemIter<'a> { +pub(crate) struct KeyIter<'a> { actor: RleDecoder<'a, u64>, counter: DeltaDecoder<'a>, + string: RleDecoder<'a, smol_str::SmolStr>, } -impl<'a> ElemIter<'a> { - fn try_next(&mut self) -> Result>, DecodeColumnError> { +impl<'a> KeyIter<'a> { + fn try_next(&mut self) -> Result, DecodeColumnError> { let actor = self .actor .next() @@ -138,50 +154,63 @@ impl<'a> ElemIter<'a> { .next() .transpose() .map_err(|e| DecodeColumnError::decode_raw("counter", e))?; - match (actor, counter) { - (Some(None) | None, Some(Some(0))) => Ok(Some(Some(ElemId(OpId::new(0, 0))))), - (Some(Some(actor)), Some(Some(ctr))) => match ctr.try_into() { - Ok(ctr) => Ok(Some(Some(ElemId(OpId::new(ctr, actor as usize))))), + let string = self + .string + .next() + .transpose() + .map_err(|e| DecodeColumnError::decode_raw("string", e))?; + match (actor, counter, string) { + (Some(Some(_)), Some(Some(_)), Some(Some(_))) => { + Err(DecodeColumnError::invalid_value("key", "too many values")) + } + (Some(None) | None, Some(None) | None, Some(Some(string))) => { + Ok(Some(Key::Prop(string))) + } + (Some(None) | None, Some(Some(0)), Some(None) | None) => { + Ok(Some(Key::Elem(ElemId(OpId::new(0, 0))))) + } + (Some(Some(actor)), Some(Some(ctr)), Some(None) | None) => match ctr.try_into() { + //Ok(ctr) => Some(Ok(Key::Elem(ElemId(OpId(ctr, actor as usize))))), + Ok(ctr) => Ok(Some(Key::Elem(ElemId(OpId::new(ctr, actor as usize))))), Err(_) => Err(DecodeColumnError::invalid_value( "counter", "negative value for counter", )), }, - (Some(None), Some(None)) => Ok(Some(None)), - (None, None) => Ok(None), - (None | Some(None), None | Some(None) ) => Ok(Some(None)), - (None | Some(None), k) => { - tracing::error!(counter=?k, "unexpected null actor"); + (None | Some(None), None | Some(None), None | Some(None)) => Ok(None), + (None | Some(None), k, _) => { + tracing::error!(key=?k, "unexpected null actor"); Err(DecodeColumnError::unexpected_null("actor")) } - (_, None | Some(None)) => Err(DecodeColumnError::unexpected_null("counter")), + (_, None | Some(None), _) => Err(DecodeColumnError::unexpected_null("counter")), } } } -impl<'a> Iterator for ElemIter<'a> { - type Item = Result, DecodeColumnError>; +impl<'a> Iterator for KeyIter<'a> { + type Item = Result; fn next(&mut self) -> Option { self.try_next().transpose() } } -pub(crate) struct ElemEncoder { +pub(crate) struct KeyEncoder { actor: RleEncoder, counter: DeltaEncoder, - //string: RleEncoder, + string: RleEncoder, } -impl ElemEncoder> { - pub(crate) fn new() -> ElemEncoder> { - ElemEncoder { +impl KeyEncoder> { + pub(crate) fn new() -> KeyEncoder> { + KeyEncoder { actor: RleEncoder::new(Vec::new()), counter: DeltaEncoder::new(Vec::new()), + string: RleEncoder::new(Vec::new()), } } - pub(crate) fn finish(self, out: &mut Vec) -> ElemRange { + pub(crate) fn finish(self, out: &mut Vec) -> KeyRange { let actor_start = out.len(); let (actor, _) = self.actor.finish(); out.extend(actor); @@ -191,31 +220,39 @@ impl ElemEncoder> { out.extend(counter); let counter_end = out.len(); - ElemRange { + let (string, _) = self.string.finish(); + out.extend(string); + let string_end = out.len(); + + KeyRange { actor: (actor_start..actor_end).into(), counter: (actor_end..counter_end).into(), + string: (counter_end..string_end).into(), } } } -impl ElemEncoder { - pub(crate) fn append(&mut self, elem: Option>) +impl KeyEncoder { + pub(crate) fn append(&mut self, key: convert::Key<'_, O>) where O: convert::OpId, { - match elem { - Some(convert::ElemId::Head) => { + match key { + convert::Key::Prop(p) => { + self.string.append_value(p.clone()); + self.actor.append_null(); + self.counter.append_null(); + } + convert::Key::Elem(convert::ElemId::Head) => { + self.string.append_null(); self.actor.append_null(); self.counter.append_value(0); } - Some(convert::ElemId::Op(o)) => { + convert::Key::Elem(convert::ElemId::Op(o)) => { + self.string.append_null(); self.actor.append_value(o.actor() as u64); self.counter.append_value(o.counter() as i64); } - None => { - self.actor.append_null(); - self.counter.append_null(); - } } } } diff --git a/rust/automerge/src/columnar/column_range/opid.rs b/rust/automerge/src/columnar/column_range/opid.rs index 6dce202b..ae95d758 100644 --- a/rust/automerge/src/columnar/column_range/opid.rs +++ b/rust/automerge/src/columnar/column_range/opid.rs @@ -48,42 +48,6 @@ impl OpIdRange { Self { actor, counter } } - pub(crate) fn encode_optional(opids: I, out: &mut Vec) -> Self - where - O: convert::OpId, - I: Iterator> + Clone, - { - let actor = RleRange::encode(opids.clone().map(|o| o.map(|o| o.actor() as u64)), out); - let counter = DeltaRange::encode(opids.map(|o| o.map(|o| o.counter() as i64)), out); - Self { actor, counter } - } - - pub(crate) fn encode_elemids(opids: I, out: &mut Vec) -> Self - where - O: convert::OpId, - I: Iterator>> + Clone, - { - let actor = RleRange::encode( - opids.clone().map(|o| { - o.map(|o| match o { - convert::ElemId::Op(o) => o.actor() as u64, - convert::ElemId::Head => 0, - }) - }), - out, - ); - let counter = DeltaRange::encode( - opids.map(|o| { - o.map(|o| match o { - convert::ElemId::Op(o) => o.counter() as i64, - convert::ElemId::Head => 0, - }) - }), - out, - ); - Self { actor, counter } - } - #[allow(dead_code)] pub(crate) fn splice( &self, @@ -170,36 +134,9 @@ pub(crate) struct OpIdEncoder { } impl OpIdEncoder { - pub(crate) fn append>(&mut self, opid: Option) { - match opid { - Some(o) => { - self.actor.append_value(o.actor() as u64); - self.counter.append_value(o.counter() as i64); - } - None => self.append_null(), - } - } - - pub(crate) fn append_elemid>( - &mut self, - elem: Option>, - ) { - match elem { - Some(convert::ElemId::Op(o)) => { - self.actor.append_value(o.actor() as u64); - self.counter.append_value(o.counter() as i64); - } - Some(convert::ElemId::Head) => { - self.actor.append_value(0); - self.counter.append_value(0); - } - None => self.append_null(), - } - } - - pub(crate) fn append_null(&mut self) { - self.actor.append_null(); - self.counter.append_null(); + pub(crate) fn append>(&mut self, opid: O) { + self.actor.append_value(opid.actor() as u64); + self.counter.append_value(opid.counter() as i64); } } diff --git a/rust/automerge/src/columnar/encoding.rs b/rust/automerge/src/columnar/encoding.rs index bbdb34a8..91c1e6eb 100644 --- a/rust/automerge/src/columnar/encoding.rs +++ b/rust/automerge/src/columnar/encoding.rs @@ -4,7 +4,9 @@ pub(crate) use raw::{RawDecoder, RawEncoder}; mod rle; pub(crate) use rle::{RleDecoder, RleEncoder}; mod boolean; -pub(crate) use boolean::{BooleanDecoder, BooleanEncoder}; +pub(crate) use boolean::{ + BooleanDecoder, BooleanEncoder, MaybeBooleanDecoder, MaybeBooleanEncoder, +}; mod delta; pub(crate) use delta::{DeltaDecoder, DeltaEncoder}; pub(crate) mod leb128; diff --git a/rust/automerge/src/columnar/encoding/boolean.rs b/rust/automerge/src/columnar/encoding/boolean.rs index 26cb1838..00f27c16 100644 --- a/rust/automerge/src/columnar/encoding/boolean.rs +++ b/rust/automerge/src/columnar/encoding/boolean.rs @@ -100,6 +100,72 @@ impl<'a> Iterator for BooleanDecoder<'a> { } } +/// Like a `BooleanEncoder` but if all the values in the column are `false` then will return an +/// empty range rather than a range with `count` false values. +pub(crate) struct MaybeBooleanEncoder { + encoder: BooleanEncoder, + all_false: bool, +} + +impl MaybeBooleanEncoder> { + pub(crate) fn new() -> MaybeBooleanEncoder> { + MaybeBooleanEncoder::from_sink(Vec::new()) + } +} + +impl MaybeBooleanEncoder { + pub(crate) fn from_sink(buf: S) -> MaybeBooleanEncoder { + MaybeBooleanEncoder { + encoder: BooleanEncoder::from_sink(buf), + all_false: true, + } + } + + pub(crate) fn append(&mut self, value: bool) { + if value { + self.all_false = false; + } + self.encoder.append(value); + } + + pub(crate) fn finish(self) -> (S, usize) { + if self.all_false { + (self.encoder.buf, 0) + } else { + self.encoder.finish() + } + } +} + +/// Like a `BooleanDecoder` but if the underlying range is empty then just returns an infinite +/// sequence of `None` +#[derive(Clone, Debug)] +pub(crate) struct MaybeBooleanDecoder<'a>(BooleanDecoder<'a>); + +impl<'a> From> for MaybeBooleanDecoder<'a> { + fn from(bytes: Cow<'a, [u8]>) -> Self { + MaybeBooleanDecoder(BooleanDecoder::from(bytes)) + } +} + +impl<'a> From<&'a [u8]> for MaybeBooleanDecoder<'a> { + fn from(d: &'a [u8]) -> Self { + Cow::Borrowed(d).into() + } +} + +impl<'a> Iterator for MaybeBooleanDecoder<'a> { + type Item = Result, raw::Error>; + + fn next(&mut self) -> Option { + if self.0.decoder.is_empty() { + None + } else { + self.0.next().transpose().map(Some).transpose() + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/automerge/src/columnar/encoding/column_decoder.rs b/rust/automerge/src/columnar/encoding/column_decoder.rs index 0ad055ee..8e3237fb 100644 --- a/rust/automerge/src/columnar/encoding/column_decoder.rs +++ b/rust/automerge/src/columnar/encoding/column_decoder.rs @@ -1,7 +1,7 @@ use crate::{ columnar::{ - column_range::{DepsIter, ObjIdIter, OpIdIter, OpIdListIter, ValueIter}, - encoding, + column_range::{DepsIter, KeyIter, ObjIdIter, OpIdIter, OpIdListIter, ValueIter}, + encoding, Key, }, types::{ObjId, OpId}, ScalarValue, @@ -108,7 +108,6 @@ impl<'a> ColumnDecoder for ValueIter<'a> { } } -/* impl<'a> ColumnDecoder for KeyIter<'a> { type Error = encoding::DecodeColumnError; type Value = Key; @@ -120,7 +119,6 @@ impl<'a> ColumnDecoder for KeyIter<'a> { self.next().transpose().map_err(|e| e.in_column(col_name)) } } -*/ impl<'a> ColumnDecoder for ObjIdIter<'a> { type Value = ObjId; diff --git a/rust/automerge/src/columnar/encoding/properties.rs b/rust/automerge/src/columnar/encoding/properties.rs index 2dc5373d..a3bf1ed0 100644 --- a/rust/automerge/src/columnar/encoding/properties.rs +++ b/rust/automerge/src/columnar/encoding/properties.rs @@ -5,7 +5,10 @@ use std::{fmt::Debug, ops::Range}; use proptest::prelude::*; use smol_str::SmolStr; -use crate::types::{ElemId, OpId, ScalarValue}; +use crate::{ + columnar::Key, + types::{ElemId, OpId, ScalarValue}, +}; #[derive(Clone, Debug)] pub(crate) struct SpliceScenario { @@ -143,6 +146,13 @@ pub(crate) fn elemid() -> impl Strategy + Clone { opid().prop_map(ElemId) } +pub(crate) fn key() -> impl Strategy + Clone { + prop_oneof! { + elemid().prop_map(Key::Elem), + any::().prop_map(|s| Key::Prop(s.into())), + } +} + pub(crate) fn encodable_int() -> impl Strategy + Clone { let bounds = i64::MAX / 2; -bounds..bounds diff --git a/rust/automerge/src/columnar/encoding/raw.rs b/rust/automerge/src/columnar/encoding/raw.rs index b86443e5..5c46ef1e 100644 --- a/rust/automerge/src/columnar/encoding/raw.rs +++ b/rust/automerge/src/columnar/encoding/raw.rs @@ -59,6 +59,10 @@ impl<'a> RawDecoder<'a> { pub(crate) fn done(&self) -> bool { self.offset >= self.data.len() } + + pub(crate) fn is_empty(&self) -> bool { + self.data.is_empty() + } } impl<'a> From<&'a [u8]> for RawDecoder<'a> { diff --git a/rust/automerge/src/error.rs b/rust/automerge/src/error.rs index 1790bc3c..5a9616e4 100644 --- a/rust/automerge/src/error.rs +++ b/rust/automerge/src/error.rs @@ -97,6 +97,6 @@ pub(crate) enum InvalidOpType { UnknownAction(u64), #[error("non numeric argument for inc op")] NonNumericInc, - #[error("Invalid mark value")] - InvalidMark, + #[error("MarkBegin operation with no name")] + MarkBeginWithoutName, } diff --git a/rust/automerge/src/legacy/mod.rs b/rust/automerge/src/legacy/mod.rs index 6e6acec5..ab835834 100644 --- a/rust/automerge/src/legacy/mod.rs +++ b/rust/automerge/src/legacy/mod.rs @@ -3,7 +3,7 @@ mod utility_impls; use std::num::NonZeroU64; -pub(crate) use crate::types::{ActorId, ChangeHash, ObjType, OpType, ScalarValue}; +pub(crate) use crate::types::{ActorId, ChangeHash, ObjType, ScalarValue}; pub(crate) use crate::value::DataType; use serde::{Deserialize, Serialize}; @@ -204,6 +204,97 @@ where } } +pub(crate) struct OpTypeParts { + pub(crate) action: u64, + pub(crate) value: ScalarValue, + pub(crate) expand: bool, + pub(crate) mark_name: Option, +} + +// Like `types::OpType` except using a String for mark names +#[derive(PartialEq, Debug, Clone)] +pub enum OpType { + Make(ObjType), + Delete, + Increment(i64), + Put(ScalarValue), + MarkBegin(MarkData), + MarkEnd(bool), +} + +impl OpType { + /// Create a new legacy OpType + /// + /// This is really only meant to be used to convert from a crate::Change to a + /// crate::legacy::Change, so the arguments should all have been validated. Consequently it + /// does not return an error and instead panics on the following conditions + /// + /// # Panics + /// + /// * If The action index is unrecognized + /// * If the action index indicates that the value should be numeric but the value is not a + /// number + pub(crate) fn from_parts( + OpTypeParts { + action, + value, + expand, + mark_name, + }: OpTypeParts, + ) -> Self { + match action { + 0 => Self::Make(ObjType::Map), + 1 => Self::Put(value), + 2 => Self::Make(ObjType::List), + 3 => Self::Delete, + 4 => Self::Make(ObjType::Text), + 5 => match value { + ScalarValue::Int(i) => Self::Increment(i), + ScalarValue::Uint(i) => Self::Increment(i as i64), + _ => panic!("non numeric value for integer action"), + }, + 6 => Self::Make(ObjType::Table), + 7 => match mark_name { + Some(name) => Self::MarkBegin(MarkData { + name, + value, + expand, + }), + None => Self::MarkEnd(expand), + }, + other => panic!("unknown action type {}", other), + } + } + + pub(crate) fn action_index(&self) -> u64 { + match self { + Self::Make(ObjType::Map) => 0, + Self::Put(_) => 1, + Self::Make(ObjType::List) => 2, + Self::Delete => 3, + Self::Make(ObjType::Text) => 4, + Self::Increment(_) => 5, + Self::Make(ObjType::Table) => 6, + Self::MarkBegin(_) => 7, + Self::MarkEnd(..) => 8, + } + } + + pub(crate) fn expand(&self) -> bool { + matches!( + self, + Self::MarkBegin(MarkData { expand: true, .. }) | Self::MarkEnd(true) + ) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: smol_str::SmolStr, + pub value: ScalarValue, + pub expand: bool, +} + #[derive(PartialEq, Debug, Clone)] pub struct Op { pub action: OpType, diff --git a/rust/automerge/src/legacy/serde_impls/op_type.rs b/rust/automerge/src/legacy/serde_impls/op_type.rs index 1d168248..9dba8701 100644 --- a/rust/automerge/src/legacy/serde_impls/op_type.rs +++ b/rust/automerge/src/legacy/serde_impls/op_type.rs @@ -1,7 +1,7 @@ use serde::{Serialize, Serializer}; use super::op::RawOpType; -use crate::{ObjType, OpType}; +use crate::{legacy::OpType, ObjType}; impl Serialize for OpType { fn serialize(&self, serializer: S) -> Result diff --git a/rust/automerge/src/op_set.rs b/rust/automerge/src/op_set.rs index aab8ce74..988afb19 100644 --- a/rust/automerge/src/op_set.rs +++ b/rust/automerge/src/op_set.rs @@ -4,7 +4,9 @@ use crate::indexed_cache::IndexedCache; use crate::op_tree::{self, OpTree}; use crate::parents::Parents; use crate::query::{self, OpIdVisSearch, TreeQuery}; -use crate::types::{self, ActorId, Key, ListEncoding, ObjId, Op, OpId, OpIds, OpType, Prop}; +use crate::types::{ + self, ActorId, Key, ListEncoding, MarkName, ObjId, Op, OpId, OpIds, OpType, Prop, +}; use crate::ObjType; use fxhash::FxBuildHasher; use std::borrow::Borrow; @@ -396,6 +398,10 @@ impl OpSetMetadata { pub(crate) fn import_prop>(&mut self, key: S) -> usize { self.props.cache(key.borrow().to_string()) } + + pub(crate) fn import_markname>(&mut self, name: S) -> MarkName { + MarkName::from_prop_index(self.props.cache(name.borrow().to_string())) + } } pub(crate) struct Parent { diff --git a/rust/automerge/src/op_tree.rs b/rust/automerge/src/op_tree.rs index 7de00dc3..60bd2a6e 100644 --- a/rust/automerge/src/op_tree.rs +++ b/rust/automerge/src/op_tree.rs @@ -319,8 +319,7 @@ struct CounterData { #[cfg(test)] mod tests { - use crate::legacy as amp; - use crate::types::{Op, OpId}; + use crate::types::{Op, OpId, OpType}; use super::*; @@ -328,7 +327,7 @@ mod tests { let zero = OpId::new(0, 0); Op { id: zero, - action: amp::OpType::Put(0.into()), + action: OpType::Put(0.into()), key: zero.into(), succ: Default::default(), pred: Default::default(), diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index caa80610..9f97028a 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -1,5 +1,5 @@ use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; +use crate::types::{ElemId, MarkName, Op, OpId, OpType, ScalarValue}; use std::fmt::Debug; #[derive(Debug, Clone, PartialEq)] @@ -17,7 +17,7 @@ pub(crate) struct RawSpan { pub(crate) id: OpId, pub(crate) start: usize, pub(crate) end: usize, - pub(crate) name: String, + pub(crate) name: MarkName, pub(crate) value: ScalarValue, } @@ -50,7 +50,7 @@ impl<'a> TreeQuery<'a> for RawSpans { id: element.id, start: self.seen, end: 0, - name: md.name.clone().into(), + name: md.name, value: md.value.clone(), }, ); diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs index 9795a711..5538d29d 100644 --- a/rust/automerge/src/query/spans.rs +++ b/rust/automerge/src/query/spans.rs @@ -1,5 +1,5 @@ use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, ListEncoding, Op, OpType, ScalarValue}; +use crate::types::{ElemId, ListEncoding, MarkName, Op, OpType, ScalarValue}; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Debug; @@ -14,9 +14,9 @@ pub(crate) struct Spans<'a> { seen_at_this_mark: Option, seen_at_last_mark: Option, ops: Vec<&'a Op>, - marks: HashMap, + marks: HashMap, changed: bool, - pub(crate) spans: Vec>, + spans: Vec>, } #[derive(Debug, Clone, PartialEq)] @@ -25,6 +25,25 @@ pub struct Span<'a> { pub marks: Vec<(smol_str::SmolStr, Cow<'a, ScalarValue>)>, } +#[derive(Debug, Clone, PartialEq)] +struct InternalSpan<'a> { + pos: usize, + marks: Vec<(MarkName, Cow<'a, ScalarValue>)>, +} + +impl<'a> InternalSpan<'a> { + fn into_span(self, m: &OpSetMetadata) -> Span<'a> { + Span { + pos: self.pos, + marks: self + .marks + .into_iter() + .map(|(name, value)| (m.props.get(name.props_index()).into(), value)) + .collect(), + } + } +} + impl<'a> Spans<'a> { pub(crate) fn new(encoding: ListEncoding) -> Self { Spans { @@ -42,11 +61,15 @@ impl<'a> Spans<'a> { } } + pub(crate) fn into_spans(self, m: &OpSetMetadata) -> Vec> { + self.spans.into_iter().map(|s| s.into_span(m)).collect() + } + pub(crate) fn check_marks(&mut self) { let mut new_marks = HashMap::new(); for op in &self.ops { if let OpType::MarkBegin(m) = &op.action { - new_marks.insert(m.name.clone(), &m.value); + new_marks.insert(m.name, &m.value); } } if new_marks != self.marks { @@ -62,10 +85,10 @@ impl<'a> Spans<'a> { let mut marks: Vec<_> = self .marks .iter() - .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) + .map(|(key, val)| (*key, Cow::Borrowed(*val))) .collect(); marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); - self.spans.push(Span { + self.spans.push(InternalSpan { pos: self.seen, marks, }); diff --git a/rust/automerge/src/storage/change.rs b/rust/automerge/src/storage/change.rs index 3f19ff38..13198015 100644 --- a/rust/automerge/src/storage/change.rs +++ b/rust/automerge/src/storage/change.rs @@ -429,12 +429,13 @@ pub(crate) trait AsChangeOp<'a> { type PredIter: Iterator + ExactSizeIterator; fn obj(&self) -> convert::ObjId; - fn prop(&self) -> Option>; - fn elem(&self) -> Option>; + fn key(&self) -> convert::Key<'a, Self::OpId>; fn insert(&self) -> bool; fn action(&self) -> u64; fn val(&self) -> Cow<'a, ScalarValue>; fn pred(&self) -> Self::PredIter; + fn expand(&self) -> bool; + fn mark_name(&self) -> Option>; } impl ChangeBuilder, Set, Set, Set> { diff --git a/rust/automerge/src/storage/change/change_actors.rs b/rust/automerge/src/storage/change/change_actors.rs index 3f374960..951201dd 100644 --- a/rust/automerge/src/storage/change/change_actors.rs +++ b/rust/automerge/src/storage/change/change_actors.rs @@ -1,5 +1,7 @@ -use std::borrow::Cow; -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, +}; use crate::convert; @@ -72,7 +74,7 @@ where let (num_ops, mut other_actors) = ops.clone() .try_fold((0, BTreeSet::new()), |(count, mut acc), op| { - if let Some(convert::ElemId::Op(o)) = op.elem() { + if let convert::Key::Elem(convert::ElemId::Op(o)) = op.key() { if o.actor() != &actor { acc.insert(o.actor()); } @@ -234,20 +236,8 @@ where } } - /* - fn key(&self) -> convert::Key<'aschangeop, Self::OpId> { - self.op.key().map(|o| self.actors.translate_opid(&o)) - } - */ - - fn prop(&self) -> Option> { - self.op.prop() - } - - fn elem(&self) -> Option> { - self.op - .elem() - .map(|e| e.map(|id| self.actors.translate_opid(&id))) + fn key(&self) -> convert::Key<'aschangeop, Self::OpId> { + self.op.key().map(|o| self.actors.translate_opid(&o)) } fn obj(&self) -> convert::ObjId { @@ -257,6 +247,14 @@ where fn val(&self) -> std::borrow::Cow<'aschangeop, crate::ScalarValue> { self.op.val() } + + fn expand(&self) -> bool { + self.op.expand() + } + + fn mark_name(&self) -> Option> { + self.op.mark_name() + } } pub(crate) struct WithChangeActorsPredIter<'actors, 'aschangeop, A, I, O, C, P> { diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index 20b14279..ab15142c 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -1,17 +1,16 @@ -use std::borrow::Cow; -use std::{convert::TryFrom, ops::Range}; +use std::{borrow::Cow, convert::TryFrom, ops::Range}; use crate::{ columnar::{ column_range::{ generic::{GenericColumnRange, GroupRange, GroupedColumnRange, SimpleColRange}, - BooleanRange, DeltaRange, ElemEncoder, ElemIter, ElemRange, ObjIdEncoder, ObjIdIter, - ObjIdRange, OpIdListEncoder, OpIdListIter, OpIdListRange, RleRange, ValueEncoder, - ValueIter, ValueRange, + BooleanRange, DeltaRange, Key, KeyEncoder, KeyIter, KeyRange, MaybeBooleanRange, + ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdListEncoder, OpIdListIter, OpIdListRange, + RleRange, ValueEncoder, ValueIter, ValueRange, }, encoding::{ - BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, RleDecoder, - RleEncoder, + BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, MaybeBooleanDecoder, + MaybeBooleanEncoder, RleDecoder, RleEncoder, }, }, convert, @@ -31,26 +30,28 @@ const INSERT_COL_ID: ColumnId = ColumnId::new(3); const ACTION_COL_ID: ColumnId = ColumnId::new(4); const VAL_COL_ID: ColumnId = ColumnId::new(5); const PRED_COL_ID: ColumnId = ColumnId::new(7); +const EXPAND_COL_ID: ColumnId = ColumnId::new(8); +const MARK_NAME_COL_ID: ColumnId = ColumnId::new(9); #[derive(Clone, Debug, PartialEq)] pub(crate) struct ChangeOp { - pub(crate) prop: Option, - pub(crate) elem_id: Option, + pub(crate) key: Key, pub(crate) insert: bool, pub(crate) val: ScalarValue, pub(crate) pred: Vec, pub(crate) action: u64, pub(crate) obj: ObjId, + pub(crate) expand: bool, + pub(crate) mark_name: Option, } impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { fn from(a: A) -> Self { ChangeOp { - prop: a.prop().map(|s| s.into_owned()), - elem_id: match a.elem() { - Some(convert::ElemId::Head) => Some(ElemId::head()), - Some(convert::ElemId::Op(id)) => Some(ElemId(id)), - None => None, + key: match a.key() { + convert::Key::Prop(s) => Key::Prop(s.into_owned()), + convert::Key::Elem(convert::ElemId::Head) => Key::Elem(ElemId::head()), + convert::Key::Elem(convert::ElemId::Op(o)) => Key::Elem(ElemId(o)), }, obj: match a.obj() { convert::ObjId::Root => ObjId::root(), @@ -60,6 +61,8 @@ impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { pred: a.pred().collect(), insert: a.insert(), action: a.action(), + expand: a.expand(), + mark_name: a.mark_name().map(|n| n.into_owned()), } } } @@ -77,15 +80,11 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { } } - fn prop(&self) -> Option> { - return self.prop.as_ref().map(|p| Cow::Borrowed(p)); - } - - fn elem(&self) -> Option> { - match &self.elem_id { - Some(e) if e.is_head() => Some(convert::ElemId::Head), - Some(ElemId(o)) => Some(convert::ElemId::Op(o)), - _ => None, + fn key(&self) -> convert::Key<'a, Self::OpId> { + match &self.key { + Key::Prop(s) => convert::Key::Prop(std::borrow::Cow::Borrowed(s)), + Key::Elem(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), + Key::Elem(e) => convert::Key::Elem(convert::ElemId::Op(&e.0)), } } @@ -104,17 +103,26 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { fn action(&self) -> u64 { self.action } + + fn expand(&self) -> bool { + self.expand + } + + fn mark_name(&self) -> Option> { + self.mark_name.as_ref().map(Cow::Borrowed) + } } #[derive(Clone, Debug, PartialEq)] pub(crate) struct ChangeOpsColumns { obj: Option, - elem: ElemRange, - prop: RleRange, + key: KeyRange, insert: BooleanRange, action: RleRange, val: ValueRange, pred: OpIdListRange, + expand: MaybeBooleanRange, + mark_name: RleRange, } impl ChangeOpsColumns { @@ -122,12 +130,13 @@ impl ChangeOpsColumns { ChangeOpsIter { failed: false, obj: self.obj.as_ref().map(|o| o.iter(data)), - elem: self.elem.iter(data), - prop: self.prop.decoder(data), + key: self.key.iter(data), insert: self.insert.decoder(data), action: self.action.decoder(data), val: self.val.iter(data), pred: self.pred.iter(data), + expand: self.expand.decoder(data), + mark_name: self.mark_name.decoder(data), } } @@ -154,21 +163,25 @@ impl ChangeOpsColumns { Op: convert::OpId + 'a, C: AsChangeOp<'c, OpId = Op> + 'a, { + tracing::trace!(expands = ?ops.clone().map(|op| op.expand()).collect::>(), "encoding change ops"); let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); - let elem = ElemRange::maybe_encode(ops.clone().map(|o| o.elem()), out); - let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); + let key = KeyRange::encode(ops.clone().map(|o| o.key()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); - let pred = OpIdListRange::encode(ops.map(|o| o.pred()), out); + let pred = OpIdListRange::encode(ops.clone().map(|o| o.pred()), out); + let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); + let mark_name = + RleRange::encode::, _>(ops.map(|o| o.mark_name()), out); Self { obj, - elem, - prop, + key, insert, action, val, pred, + expand, + mark_name, } } @@ -179,28 +192,26 @@ impl ChangeOpsColumns { C: AsChangeOp<'c, OpId = Op> + 'a, { let mut obj = ObjIdEncoder::new(); - let mut elem = ElemEncoder::new(); - let mut prop = RleEncoder::<_, smol_str::SmolStr>::from(Vec::new()); + let mut key = KeyEncoder::new(); let mut insert = BooleanEncoder::new(); let mut action = RleEncoder::<_, u64>::from(Vec::new()); let mut val = ValueEncoder::new(); let mut pred = OpIdListEncoder::new(); + let mut expand = MaybeBooleanEncoder::new(); + let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { + tracing::trace!(expand=?op.expand(), "expand"); obj.append(op.obj()); - elem.append(op.elem()); - prop.append(op.prop()); + key.append(op.key()); insert.append(op.insert()); action.append_value(op.action()); val.append(&op.val()); pred.append(op.pred()); + expand.append(op.expand()); + mark_name.append(op.mark_name()); } let obj = obj.finish(out); - let elem = elem.finish(out); - - let prop_start = out.len(); - let (prop, _) = prop.finish(); - out.extend(prop); - let prop = RleRange::from(prop_start..out.len()); + let key = key.finish(out); let insert_start = out.len(); let (insert, _) = insert.finish(); @@ -215,14 +226,25 @@ impl ChangeOpsColumns { let val = val.finish(out); let pred = pred.finish(out); + let expand_start = out.len(); + let (expand, _) = expand.finish(); + out.extend(expand); + let expand = MaybeBooleanRange::from(expand_start..out.len()); + + let mark_name_start = out.len(); + let (mark_name, _) = mark_name.finish(); + out.extend(mark_name); + let mark_name = RleRange::from(mark_name_start..out.len()); + Self { obj, - elem, - prop, + key, insert, action, val, pred, + expand, + mark_name, } } @@ -244,15 +266,15 @@ impl ChangeOpsColumns { ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), - self.elem.actor_range().clone().into(), + self.key.actor_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), - self.elem.counter_range().clone().into(), + self.key.counter_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), - self.prop.clone().into(), + self.key.string_range().clone().into(), ), RawColumn::new( ColumnSpec::new(INSERT_COL_ID, ColumnType::Boolean, false), @@ -289,6 +311,18 @@ impl ChangeOpsColumns { ), ]); } + if !self.expand.is_empty() { + cols.push(RawColumn::new( + ColumnSpec::new(EXPAND_COL_ID, ColumnType::Boolean, false), + self.expand.clone().into(), + )); + } + if !self.mark_name.is_empty() { + cols.push(RawColumn::new( + ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), + self.mark_name.clone().into(), + )); + } cols.into_iter().collect() } } @@ -301,12 +335,13 @@ pub struct ReadChangeOpError(#[from] DecodeColumnError); pub(crate) struct ChangeOpsIter<'a> { failed: bool, obj: Option>, - prop: RleDecoder<'a, smol_str::SmolStr>, - elem: ElemIter<'a>, + key: KeyIter<'a>, insert: BooleanDecoder<'a>, action: RleDecoder<'a, u64>, val: ValueIter<'a>, pred: OpIdListIter<'a>, + expand: MaybeBooleanDecoder<'a>, + mark_name: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> ChangeOpsIter<'a> { @@ -323,20 +358,22 @@ impl<'a> ChangeOpsIter<'a> { } else { ObjId::root() }; - let prop = self.prop.maybe_next_in_col("key:prop")?; - let elem_id = self.elem.maybe_next_in_col("key:elem")?; + let key = self.key.next_in_col("key")?; let insert = self.insert.next_in_col("insert")?; let action = self.action.next_in_col("action")?; let val = self.val.next_in_col("value")?; let pred = self.pred.next_in_col("pred")?; + let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); + let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; Ok(Some(ChangeOp { obj, - prop, - elem_id, + key, insert, action, val, pred, + expand, + mark_name, })) } } @@ -383,6 +420,8 @@ impl TryFrom for ChangeOpsColumns { let mut pred_group: Option> = None; let mut pred_actor: Option> = None; let mut pred_ctr: Option = None; + let mut expand: Option = None; + let mut mark_name: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -437,6 +476,8 @@ impl TryFrom for ChangeOpsColumns { } _ => return Err(ParseChangeColumnsError::MismatchingColumn { index }), }, + (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), (other_type, other_col) => { tracing::warn!(typ=?other_type, id=?other_col, "unknown column"); other.append(col); @@ -453,15 +494,17 @@ impl TryFrom for ChangeOpsColumns { obj_actor.unwrap_or_else(|| (0..0).into()), obj_ctr.unwrap_or_else(|| (0..0).into()), ), - elem: ElemRange::new( + key: KeyRange::new( key_actor.unwrap_or_else(|| (0..0).into()), key_ctr.unwrap_or_else(|| (0..0).into()), + key_str.unwrap_or_else(|| (0..0).into()), ), - prop: key_str.unwrap_or_else(|| (0..0).into()), insert: insert.unwrap_or(0..0).into(), action: action.unwrap_or(0..0).into(), val: val.unwrap_or_else(|| ValueRange::new((0..0).into(), (0..0).into())), pred, + expand: expand.unwrap_or_else(|| (0..0).into()), + mark_name: mark_name.unwrap_or_else(|| (0..0).into()), }) } } @@ -469,26 +512,28 @@ impl TryFrom for ChangeOpsColumns { #[cfg(test)] mod tests { use super::*; - use crate::columnar::encoding::properties::{elemid, opid, scalar_value}; + use crate::columnar::encoding::properties::{key, opid, scalar_value}; use proptest::prelude::*; prop_compose! { fn change_op() - (value in scalar_value(), - prop in proptest::option::of(any::().prop_map(|s| s.into())), - elem_id in proptest::option::of(elemid()), + (key in key(), + value in scalar_value(), pred in proptest::collection::vec(opid(), 0..20), action in 0_u64..6, obj in opid(), - insert in any::()) -> ChangeOp { + insert in any::(), + mark_name in proptest::option::of(any::().prop_map(|s| s.into())), + expand in any::()) -> ChangeOp { ChangeOp { obj: obj.into(), - prop, - elem_id, + key, val: value, pred, action, insert, + expand, + mark_name, } } } diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index 14c217a7..66c4cd69 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -5,7 +5,7 @@ use crate::{ convert, op_set::OpSetMetadata, storage::AsChangeOp, - types::{ActorId, Key, ObjId, Op, OpId, OpType, ScalarValue}, + types::{ActorId, Key, MarkData, ObjId, Op, OpId, OpType, ScalarValue}, }; /// Wrap an op in an implementation of `AsChangeOp` which represents actor IDs using a reference to @@ -93,11 +93,12 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { fn val(&self) -> Cow<'a, ScalarValue> { match &self.op.action { - OpType::Make(..) | OpType::Delete => Cow::Owned(ScalarValue::Null), + OpType::Make(..) | OpType::Delete | OpType::MarkEnd(..) => { + Cow::Owned(ScalarValue::Null) + } OpType::Increment(i) => Cow::Owned(ScalarValue::Int(*i)), OpType::Put(s) => Cow::Borrowed(s), - OpType::MarkBegin(_) => todo!(), - OpType::MarkEnd(_) => todo!(), + OpType::MarkBegin(MarkData { value, .. }) => Cow::Borrowed(value), } } @@ -120,27 +121,27 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { } } - fn prop(&self) -> Option> { + fn key(&self) -> convert::Key<'a, Self::OpId> { match &self.op.key { - Key::Map(idx) => Some(Cow::Owned(self.metadata.props.get(*idx).into())), - _ => None, + Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.metadata.props.get(*idx).into())), + Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), + Key::Seq(e) => convert::Key::Elem(convert::ElemId::Op(self.wrap(&e.0))), } } - fn elem(&self) -> Option> { - match &self.op.key { - Key::Seq(e) if e.is_head() => Some(convert::ElemId::Head), - Key::Seq(e) => Some(convert::ElemId::Op(self.wrap(&e.0))), - _ => None, + fn expand(&self) -> bool { + matches!( + self.op.action, + OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(true) + ) + } + + fn mark_name(&self) -> Option> { + if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { + let name = self.metadata.props.get(name.props_index()); + Some(Cow::Owned(name.into())) + } else { + None } } - /* - fn key(&self) -> convert::Key<'a, Self::OpId> { - match &self.op.key { - Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.metadata.props.get(*idx).into())), - Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), - Key::Seq(e) => convert::Key::Elem(convert::ElemId::Op(self.wrap(&e.0))), - } - } - */ } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index aa867f11..9aedeff3 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -4,7 +4,7 @@ use crate::{ convert, indexed_cache::IndexedCache, storage::AsDocOp, - types::{ElemId, Key, ObjId, Op, OpId, OpType, ScalarValue}, + types::{ElemId, Key, MarkData, ObjId, Op, OpId, OpType, ScalarValue}, }; /// Create an [`AsDocOp`] implementation for a [`crate::types::Op`] @@ -76,32 +76,15 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { } } - fn elem(&self) -> Option> { + fn key(&self) -> convert::Key<'a, Self::OpId> { match self.op.key { - Key::Map(_) => None, - Key::Seq(e) if e.is_head() => Some(convert::ElemId::Head), - Key::Seq(ElemId(o)) => Some(convert::ElemId::Op(translate(self.actor_lookup, &o))), - } - } - - fn prop(&self) -> Option> { - match self.op.key { - Key::Map(idx) => Some(Cow::Owned(self.props.get(idx).into())), - _ => None, - } - } - - /* - fn key(&self) -> convert::Key<'a, Self::OpId> { - match self.op.key { - Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.props.get(idx).into())), - Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), - Key::Seq(ElemId(o)) => { - convert::Key::Elem(convert::ElemId::Op(translate(self.actor_lookup, &o))) - } + Key::Map(idx) => convert::Key::Prop(Cow::Owned(self.props.get(idx).into())), + Key::Seq(e) if e.is_head() => convert::Key::Elem(convert::ElemId::Head), + Key::Seq(ElemId(o)) => { + convert::Key::Elem(convert::ElemId::Op(translate(self.actor_lookup, &o))) } } - */ + } fn val(&self) -> Cow<'a, crate::ScalarValue> { match &self.op.action { @@ -126,6 +109,24 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { fn action(&self) -> u64 { self.op.action.action_index() } + + fn expand(&self) -> bool { + if let OpType::MarkBegin(MarkData { expand, .. }) | OpType::MarkEnd(expand) = + &self.op.action + { + *expand + } else { + false + } + } + + fn mark_name(&self) -> Option> { + if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { + Some(Cow::Owned(self.props.get(name.props_index()).into())) + } else { + None + } + } } pub(crate) struct OpAsDocOpSuccIter<'a> { diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 584a46ff..7248b1b3 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -1,18 +1,16 @@ use std::{borrow::Cow, convert::TryFrom}; -use smol_str::SmolStr; - use crate::{ columnar::{ column_range::{ generic::{GenericColumnRange, GroupRange, GroupedColumnRange, SimpleColRange}, - BooleanRange, DeltaRange, ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdEncoder, OpIdIter, - OpIdListEncoder, OpIdListIter, OpIdListRange, OpIdRange, RleRange, ValueEncoder, - ValueIter, ValueRange, ElemRange, ElemEncoder, ElemIter, + BooleanRange, DeltaRange, Key, KeyEncoder, KeyIter, KeyRange, MaybeBooleanRange, + ObjIdEncoder, ObjIdIter, ObjIdRange, OpIdEncoder, OpIdIter, OpIdListEncoder, + OpIdListIter, OpIdListRange, OpIdRange, RleRange, ValueEncoder, ValueIter, ValueRange, }, encoding::{ - BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, RleDecoder, - RleEncoder, + BooleanDecoder, BooleanEncoder, ColumnDecoder, DecodeColumnError, MaybeBooleanDecoder, + MaybeBooleanEncoder, RleDecoder, RleEncoder, }, }, convert, @@ -20,7 +18,7 @@ use crate::{ columns::{compression, ColumnId, ColumnSpec, ColumnType}, Columns, MismatchingColumn, RawColumn, RawColumns, }, - types::{ElemId, ObjId, OpId, ScalarValue}, + types::{ObjId, OpId, ScalarValue}, }; const OBJ_COL_ID: ColumnId = ColumnId::new(0); @@ -30,25 +28,27 @@ const INSERT_COL_ID: ColumnId = ColumnId::new(3); const ACTION_COL_ID: ColumnId = ColumnId::new(4); const VAL_COL_ID: ColumnId = ColumnId::new(5); const SUCC_COL_ID: ColumnId = ColumnId::new(8); +const EXPAND_COL_ID: ColumnId = ColumnId::new(9); +const MARK_NAME_COL_ID: ColumnId = ColumnId::new(10); /// The form operations take in the compressed document format. #[derive(Debug)] pub(crate) struct DocOp { pub(crate) id: OpId, pub(crate) object: ObjId, - pub(crate) prop: Option, - pub(crate) elem_id: Option, + pub(crate) key: Key, pub(crate) insert: bool, - pub(crate) action: usize, + pub(crate) action: u64, pub(crate) value: ScalarValue, pub(crate) succ: Vec, + pub(crate) expand: bool, + pub(crate) mark_name: Option, } #[derive(Debug, Clone)] pub(crate) struct DocOpColumns { obj: Option, - prop: RleRange, - elem: ElemRange, + key: KeyRange, id: OpIdRange, insert: BooleanRange, action: RleRange, @@ -56,6 +56,8 @@ pub(crate) struct DocOpColumns { succ: OpIdListRange, #[allow(dead_code)] other: Columns, + expand: MaybeBooleanRange, + mark_name: RleRange, } struct DocId { @@ -89,12 +91,13 @@ pub(crate) trait AsDocOp<'a> { fn obj(&self) -> convert::ObjId; fn id(&self) -> Self::OpId; - fn prop(&self) -> Option>; - fn elem(&self) -> Option>; + fn key(&self) -> convert::Key<'a, Self::OpId>; fn insert(&self) -> bool; fn action(&self) -> u64; fn val(&self) -> Cow<'a, ScalarValue>; fn succ(&self) -> Self::SuccIter; + fn expand(&self) -> bool; + fn mark_name(&self) -> Option>; } impl DocOpColumns { @@ -104,7 +107,11 @@ impl DocOpColumns { O: convert::OpId, C: AsDocOp<'a, OpId = O>, { - Self::encode_rowwise(ops, out) + if ops.len() > 30000 { + Self::encode_rowwise(ops, out) + } else { + Self::encode_columnwise(ops, out) + } } fn encode_columnwise<'a, I, O, C>(ops: I, out: &mut Vec) -> DocOpColumns @@ -114,22 +121,24 @@ impl DocOpColumns { C: AsDocOp<'a, OpId = O>, { let obj = ObjIdRange::encode(ops.clone().map(|o| o.obj()), out); + let key = KeyRange::encode(ops.clone().map(|o| o.key()), out); let id = OpIdRange::encode(ops.clone().map(|o| o.id()), out); - let prop = RleRange::encode(ops.clone().map(|o| o.prop()), out); - let elem = ElemRange::maybe_encode(ops.clone().map(|o| o.elem()), out); let insert = BooleanRange::encode(ops.clone().map(|o| o.insert()), out); let action = RleRange::encode(ops.clone().map(|o| Some(o.action())), out); let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); - let succ = OpIdListRange::encode(ops.map(|o| o.succ()), out); + let succ = OpIdListRange::encode(ops.clone().map(|o| o.succ()), out); + let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); + let mark_name = RleRange::encode(ops.map(|o| o.mark_name()), out); Self { obj, - prop, - elem, + key, id, insert, action, val, succ, + expand, + mark_name, other: Columns::empty(), } } @@ -141,33 +150,29 @@ impl DocOpColumns { C: AsDocOp<'a, OpId = O>, { let mut obj = ObjIdEncoder::new(); - let mut prop = RleEncoder::<_, SmolStr>::new(Vec::new()); - let mut elem = ElemEncoder::new(); + let mut key = KeyEncoder::new(); let mut id = OpIdEncoder::new(); let mut insert = BooleanEncoder::new(); let mut action = RleEncoder::<_, u64>::from(Vec::new()); let mut val = ValueEncoder::new(); let mut succ = OpIdListEncoder::new(); + let mut expand = MaybeBooleanEncoder::new(); + let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { obj.append(op.obj()); - prop.append(op.prop()); - elem.append(op.elem()); - id.append(Some(op.id())); + key.append(op.key()); + id.append(op.id()); insert.append(op.insert()); action.append(Some(op.action())); val.append(&op.val()); succ.append(op.succ()); + expand.append(op.expand()); + mark_name.append(op.mark_name()); } let obj = obj.finish(out); + let key = key.finish(out); let id = id.finish(out); - let elem = elem.finish(out); - - let prop_start = out.len(); - let (prop_out, _prop_end) = prop.finish(); - out.extend(prop_out); - let prop = RleRange::from(prop_start..out.len()); - let insert_start = out.len(); let (insert_out, _) = insert.finish(); out.extend(insert_out); @@ -180,15 +185,27 @@ impl DocOpColumns { let val = val.finish(out); let succ = succ.finish(out); + + let expand_start = out.len(); + let (expand_out, _) = expand.finish(); + out.extend(expand_out); + let expand = MaybeBooleanRange::from(expand_start..out.len()); + + let mark_name_start = out.len(); + let (mark_name_out, _) = mark_name.finish(); + out.extend(mark_name_out); + let mark_name = RleRange::from(mark_name_start..out.len()); + DocOpColumns { obj, + key, id, - elem, - prop, insert, action, val, succ, + expand, + mark_name, other: Columns::empty(), } } @@ -198,12 +215,12 @@ impl DocOpColumns { id: self.id.iter(data), action: self.action.decoder(data), objs: self.obj.as_ref().map(|o| o.iter(data)), - //keys: self.key.iter(data), - elem: self.elem.iter(data), - prop: self.prop.decoder(data), + keys: self.key.iter(data), insert: self.insert.decoder(data), value: self.val.iter(data), succ: self.succ.iter(data), + expand: self.expand.decoder(data), + mark_name: self.mark_name.decoder(data), } } @@ -225,15 +242,15 @@ impl DocOpColumns { ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), - self.elem.actor_range().clone().into(), + self.key.actor_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), - self.elem.counter_range().clone().into(), + self.key.counter_range().clone().into(), ), RawColumn::new( ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), - self.prop.clone().into(), + self.key.string_range().clone().into(), ), RawColumn::new( ColumnSpec::new(ID_COL_ID, ColumnType::Actor, false), @@ -278,6 +295,18 @@ impl DocOpColumns { ), ]); } + if !self.expand.is_empty() { + cols.push(RawColumn::new( + ColumnSpec::new(EXPAND_COL_ID, ColumnType::Boolean, false), + self.expand.clone().into(), + )); + } + if !self.mark_name.is_empty() { + cols.push(RawColumn::new( + ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), + self.mark_name.clone().into(), + )); + } cols.into_iter().collect() } } @@ -287,11 +316,12 @@ pub(crate) struct DocOpColumnIter<'a> { id: OpIdIter<'a>, action: RleDecoder<'a, u64>, objs: Option>, - prop: RleDecoder<'a, SmolStr>, - elem: ElemIter<'a>, + keys: KeyIter<'a>, insert: BooleanDecoder<'a>, value: ValueIter<'a>, succ: OpIdListIter<'a>, + expand: MaybeBooleanDecoder<'a>, + mark_name: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> DocOpColumnIter<'a> { @@ -332,20 +362,22 @@ impl<'a> DocOpColumnIter<'a> { } else { ObjId::root() }; - let prop = self.prop.maybe_next_in_col("key:prop")?; - let elem_id = self.elem.maybe_next_in_col("key:elem")?; + let key = self.keys.next_in_col("key")?; let value = self.value.next_in_col("value")?; let succ = self.succ.next_in_col("succ")?; let insert = self.insert.next_in_col("insert")?; + let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); + let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; Ok(Some(DocOp { id, value, - action: action as usize, + action, object: obj, - elem_id: elem_id.map(|i| i.into()), - prop, + key, succ, insert, + expand, + mark_name, })) } } @@ -380,6 +412,8 @@ impl TryFrom for DocOpColumns { let mut succ_group: Option> = None; let mut succ_actor: Option> = None; let mut succ_ctr: Option = None; + let mut expand: Option = None; + let mut mark_name: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -433,6 +467,8 @@ impl TryFrom for DocOpColumns { } _ => return Err(Error::MismatchingColumn { index }), }, + (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), (other_col, other_type) => { tracing::warn!(id=?other_col, typ=?other_type, "unknown column type"); other.append(col) @@ -444,10 +480,10 @@ impl TryFrom for DocOpColumns { obj_actor.unwrap_or_else(|| (0..0).into()), obj_ctr.unwrap_or_else(|| (0..0).into()), ), - prop: key_str.unwrap_or_else(|| (0..0).into()), - elem: ElemRange::new( + key: KeyRange::new( key_actor.unwrap_or_else(|| (0..0).into()), key_ctr.unwrap_or_else(|| (0..0).into()), + key_str.unwrap_or_else(|| (0..0).into()), ), id: OpIdRange::new( id_actor.unwrap_or_else(|| (0..0).into()), @@ -461,6 +497,8 @@ impl TryFrom for DocOpColumns { succ_actor.unwrap_or_else(|| (0..0).into()), succ_ctr.unwrap_or_else(|| (0..0).into()), ), + expand: expand.unwrap_or_else(|| (0..0).into()), + mark_name: mark_name.unwrap_or_else(|| (0..0).into()), other, }) } diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index 42cabc59..194f32eb 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -4,9 +4,10 @@ use tracing::instrument; use crate::{ change::Change, + columnar::Key as DocOpKey, op_tree::OpSetMetadata, storage::{change::Verified, Change as StoredChange, DocOp, Document}, - types::{ChangeHash, ElemId, Key, ObjId, ObjType, Op, OpId, OpIds, OpType}, + types::{ChangeHash, ElemId, Key, ObjId, ObjType, Op, OpId, OpIds, OpType, OpTypeParts}, ScalarValue, }; @@ -16,8 +17,6 @@ pub(crate) enum Error { OpsOutOfOrder, #[error("error reading operation: {0:?}")] ReadOp(Box), - #[error("an operation contained an invalid action")] - InvalidAction, #[error("an operation referenced a missing actor id")] MissingActor, #[error("invalid changes: {0}")] @@ -28,10 +27,8 @@ pub(crate) enum Error { MissingOps, #[error("succ out of order")] SuccOutOfOrder, - #[error("no key")] - MissingKey, #[error(transparent)] - InvalidOpType(#[from] crate::error::InvalidOpType), + InvalidOp(#[from] crate::error::InvalidOpType), } pub(crate) struct MismatchedHeads { @@ -338,29 +335,30 @@ impl LoadingObject { } fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { - let key = match (&op.prop, op.elem_id) { - (Some(k), None) => Ok(Key::Map(m.import_prop(k.as_str()))), - (_, Some(elem)) => Ok(Key::Seq(ElemId(check_opid(m, elem.0)?))), - (None, None) => Err(Error::MissingKey), - }?; + let key = match op.key { + DocOpKey::Prop(s) => Key::Map(m.import_prop(s)), + DocOpKey::Elem(ElemId(op)) => Key::Seq(ElemId(check_opid(m, op)?)), + }; for opid in &op.succ { if m.actors.safe_get(opid.actor()).is_none() { tracing::error!(?opid, "missing actor"); return Err(Error::MissingActor); } } - let action = OpType::from_index_and_value(op.action as u64, op.value, op.insert, op.prop)?; - let insert = match action { - OpType::MarkBegin(_) | OpType::MarkEnd(_) => true, - _ => op.insert, - }; + let mark_name = op.mark_name.map(|n| m.import_markname(n)); + let action = OpType::from_parts(OpTypeParts { + action: op.action, + value: op.value, + expand: op.expand, + mark_name, + })?; Ok(Op { id: check_opid(m, op.id)?, action, key, succ: m.try_sorted_opids(op.succ).ok_or(Error::SuccOutOfOrder)?, pred: OpIds::empty(), - insert, + insert: op.insert, }) } @@ -376,25 +374,3 @@ fn check_opid(m: &OpSetMetadata, opid: OpId) -> Result { } } } - -fn parse_optype(action_index: usize, value: ScalarValue) -> Result { - match action_index { - 0 => Ok(OpType::Make(ObjType::Map)), - 1 => Ok(OpType::Put(value)), - 2 => Ok(OpType::Make(ObjType::List)), - 3 => Ok(OpType::Delete), - 4 => Ok(OpType::Make(ObjType::Text)), - 5 => match value { - ScalarValue::Int(i) => Ok(OpType::Increment(i)), - _ => { - tracing::error!(?value, "invalid value for counter op"); - Err(Error::InvalidAction) - } - }, - 6 => Ok(OpType::Make(ObjType::Table)), - other => { - tracing::error!(action = other, "unknown action type"); - Err(Error::InvalidAction) - } - } -} diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index c8ae29ff..4011f2af 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -659,13 +659,14 @@ impl TransactionInner { value: V, ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; + let mark = doc.ops_mut().m.import_markname(mark); if let Some(obs) = op_observer { self.do_insert( doc, Some(obs), obj, range.start, - OpType::mark(mark.into(), range.expand_left, value.into()), + OpType::mark(mark, range.expand_left, value.into()), )?; self.do_insert( doc, @@ -680,7 +681,7 @@ impl TransactionInner { None, obj, range.start, - OpType::mark(mark.into(), range.expand_left, value.into()), + OpType::mark(mark, range.expand_left, value.into()), )?; self.do_insert::( doc, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 92b3bdf3..59ba5ceb 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -204,21 +204,24 @@ pub enum OpType { #[derive(PartialEq, Debug, Clone)] pub struct MarkData { - pub name: smol_str::SmolStr, + pub name: MarkName, pub value: ScalarValue, pub expand: bool, } impl Display for MarkData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "name={} value={} expand={}", - self.name, self.value, self.expand - ) + write!(f, "value={} expand={}", self.value, self.value) } } +pub(crate) struct OpTypeParts { + pub(crate) action: u64, + pub(crate) value: ScalarValue, + pub(crate) expand: bool, + pub(crate) mark_name: Option, +} + impl OpType { /// The index into the action array as specified in [1] /// @@ -232,11 +235,12 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) | Self::MarkEnd(..) => 7, + Self::MarkBegin(_) => 7, + Self::MarkEnd(..) => 8, } } - pub(crate) fn mark(name: smol_str::SmolStr, expand: bool, value: ScalarValue) -> OpType { + pub(crate) fn mark(name: MarkName, expand: bool, value: ScalarValue) -> OpType { OpType::MarkBegin(MarkData { name, value, @@ -244,13 +248,15 @@ impl OpType { }) } - pub(crate) fn from_index_and_value( - index: u64, - value: ScalarValue, - insert_or_expand: bool, - prop: Option, + pub(crate) fn from_parts( + OpTypeParts { + action, + value, + expand, + mark_name, + }: OpTypeParts, ) -> Result { - match index { + match action { 0 => Ok(Self::Make(ObjType::Map)), 1 => Ok(Self::Put(value)), 2 => Ok(Self::Make(ObjType::List)), @@ -262,14 +268,15 @@ impl OpType { _ => Err(error::InvalidOpType::NonNumericInc), }, 6 => Ok(Self::Make(ObjType::Table)), - 7 => match prop { + 7 => match mark_name { Some(name) => Ok(Self::MarkBegin(MarkData { name, value, - expand: insert_or_expand, + expand, })), - None => Ok(Self::MarkEnd(insert_or_expand)), + None => Err(error::InvalidOpType::MarkBeginWithoutName), }, + 8 => Ok(Self::MarkEnd(expand)), other => Err(error::InvalidOpType::UnknownAction(other)), } } @@ -421,6 +428,20 @@ pub(crate) enum Key { Seq(ElemId), } +// Index of a mark name string in the OpSetMetadata::props IndexedCache +#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)] +pub struct MarkName(usize); + +impl MarkName { + pub(crate) fn props_index(&self) -> usize { + self.0 + } + + pub(crate) fn from_prop_index(index: usize) -> Self { + Self(index) + } +} + /// A property of an object /// /// This is either a string representing a property in a map, or an integer @@ -700,9 +721,9 @@ impl Op { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), - OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned( - format!("markBegin[{}]={}", mark.name, mark.value).into(), - )), + OpType::MarkBegin(mark) => { + Value::Scalar(Cow::Owned(format!("markBegin={}", mark.value).into())) + } OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), _ => panic!("cant convert op into a value - {:?}", self), } diff --git a/rust/automerge/tests/test.rs b/rust/automerge/tests/test.rs index ca6c64c0..abc5cf56 100644 --- a/rust/automerge/tests/test.rs +++ b/rust/automerge/tests/test.rs @@ -6,7 +6,7 @@ use automerge::{ use std::fs; // set up logging for all the tests -//use test_log::test; +use test_log::test; #[allow(unused_imports)] use automerge_test::{ @@ -1283,6 +1283,7 @@ fn test_change_encoding_expanded_change_round_trip() { let change = automerge::Change::try_from(&change_bytes[..]).unwrap(); assert_eq!(change.raw_bytes(), change_bytes); let expanded = automerge::ExpandedChange::from(&change); + println!("{:?}", expanded); let unexpanded: automerge::Change = expanded.try_into().unwrap(); assert_eq!(unexpanded.raw_bytes(), change_bytes); } From a006a32e3f3e0160fbfcea525d709a78caa32bfd Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Sun, 12 Feb 2023 15:38:30 -0600 Subject: [PATCH 09/26] get tests passing + unmark --- rust/automerge-wasm/index.d.ts | 15 ++++ rust/automerge-wasm/src/interop.rs | 9 +++ rust/automerge-wasm/src/lib.rs | 6 +- rust/automerge-wasm/test/marks.ts | 24 +++--- rust/automerge/src/autocommit.rs | 5 ++ rust/automerge/src/automerge.rs | 57 +++++++++----- rust/automerge/src/error.rs | 2 - rust/automerge/src/legacy/mod.rs | 4 +- rust/automerge/src/legacy/serde_impls/op.rs | 76 ++++++++++++++----- .../src/legacy/serde_impls/op_type.rs | 4 +- .../src/storage/convert/op_as_docop.rs | 1 + rust/automerge/src/storage/parse.rs | 1 + rust/automerge/src/transaction/inner.rs | 46 +++++++++-- rust/automerge/src/types.rs | 11 ++- 14 files changed, 193 insertions(+), 68 deletions(-) diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index da49de42..c6b0ace4 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -245,3 +245,18 @@ export class SyncState { sentHashes: Heads; readonly sharedHeads: Heads; } + +export type ChangeSetDeletion = { + pos: number; + val: string; +} + +export type ChangeSetAddition = { + start: number; + end: number; +}; + +export type ChangeSet = { + add: ChangeSetAddition[]; + del: ChangeSetDeletion[]; +}; diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index 1546ff10..74adab0c 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -1001,6 +1001,15 @@ impl Automerge { } } + pub(crate) fn import_obj(&self, id: JsValue) -> Result { + if let Some(s) = id.as_string() { + // only valid formats is 123@aabbcc + self.doc.import_obj(&s).map_err(error::ImportObj::BadImport) + } else { + Err(error::ImportObj::NotString) + } + } + fn import_path<'a, I: Iterator>( &self, mut obj: ObjId, diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index b3568f05..16a135db 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -824,7 +824,7 @@ impl Automerge { pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { let (obj, _) = self.import(obj)?; - let (mark, _) = self.import(mark)?; + let mark = self.import_obj(mark)?; self.doc.unmark(&obj, &mark).map_err(to_js_err)?; Ok(()) } @@ -860,7 +860,9 @@ impl Automerge { } let text_span = &text.get(last_pos..); if let Some(t) = text_span { - result.push(&t.to_string().into()); + if !t.is_empty() { + result.push(&t.to_string().into()); + } } Ok(result.into()) } diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 54c834fa..f0e19d98 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -6,7 +6,7 @@ import { create, load, Automerge, encodeChange, decodeChange } from '..' describe('Automerge', () => { describe('marks', () => { - it.skip('should handle marks [..]', () => { + it('should handle marks [..]', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -20,7 +20,7 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); }) - it.skip('should handle marks [..] at the beginning of a string', () => { + it('should handle marks [..] at the beginning of a string', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -36,7 +36,7 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); }) - it.skip('should handle marks [..] with splice', () => { + it('should handle marks [..] with splice', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -52,7 +52,7 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); }) - it.skip('should handle marks across multiple forks', () => { + it('should handle marks across multiple forks', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -74,7 +74,7 @@ describe('Automerge', () => { }) - it.only('should handle marks with deleted ends [..]', () => { + it('should handle marks with deleted ends [..]', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") @@ -94,7 +94,7 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) }) - it.skip('should handle sticky marks (..)', () => { + it('should handle sticky marks (..)', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -107,7 +107,7 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); }) - it.skip('should handle sticky marks with deleted ends (..)', () => { + it('should handle sticky marks with deleted ends (..)', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") @@ -127,7 +127,9 @@ describe('Automerge', () => { // make sure save/load can handle marks - let doc2 = load(doc.save(),true) + let saved = doc.save() + let doc2 = load(saved,true) + //let doc2 = load(doc.save(),true) spans = doc2.spans(list); assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) @@ -135,7 +137,7 @@ describe('Automerge', () => { assert.deepStrictEqual(doc.save(), doc2.save()) }) - it.skip('should handle overlapping marks', () => { + it('should handle overlapping marks', () => { let doc : Automerge = create(true, "aabbcc") let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") @@ -152,8 +154,8 @@ describe('Automerge', () => { 'quick ', [ [ 'bold', 'boolean', true ], + [ 'itallic', 'boolean', true ], [ 'comment', 'str', 'foxes are my favorite animal!' ], - [ 'itallic', 'boolean', true ] ], 'fox', [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], @@ -191,7 +193,9 @@ describe('Automerge', () => { let all = doc.getChanges([]) let decoded = all.map((c) => decodeChange(c)) + let util = require('util'); let encoded = decoded.map((c) => encodeChange(c)) + let decoded2 = encoded.map((c) => decodeChange(c)) let doc2 = create(true); doc2.applyChanges(encoded) diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 75425beb..4e1afc9f 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -282,6 +282,11 @@ impl AutoCommitWithObs { self.doc.import(s) } + #[doc(hidden)] + pub fn import_obj(&self, s: &str) -> Result { + self.doc.import_obj(s) + } + #[doc(hidden)] pub fn dump(&mut self) { self.ensure_transaction_closed(); diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index d19541be..8c8178ac 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -16,8 +16,8 @@ use crate::transaction::{ self, CommitOptions, Failure, Observed, Success, Transaction, TransactionArgs, UnObserved, }; use crate::types::{ - ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, MarkName, ObjId, Op, - OpId, OpType, OpTypeParts, ScalarValue, TextEncoding, Value, + ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, MarkData, MarkName, + ObjId, Op, OpId, OpType, OpTypeParts, ScalarValue, TextEncoding, Value, }; use crate::{ query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, @@ -400,11 +400,23 @@ impl Automerge { pub(crate) fn exid_to_obj(&self, id: &ExId) -> Result<(ObjId, ObjType), AutomergeError> { match id { ExId::Root => Ok((ObjId::root(), ObjType::Map)), + ExId::Id(..) => { + let obj = ObjId(self.exid_to_opid(id)?); + if let Some(obj_type) = self.ops.object_type(&obj) { + Ok((obj, obj_type)) + } else { + Err(AutomergeError::NotAnObject) + } + } + } + } + + pub(crate) fn exid_to_opid(&self, id: &ExId) -> Result { + match id { + ExId::Root => Err(AutomergeError::Fail), ExId::Id(ctr, actor, idx) => { - // do a direct get here b/c this could be foriegn and not be within the array - // bounds - let obj = if self.ops.m.actors.cache.get(*idx) == Some(actor) { - ObjId(OpId::new(*ctr, *idx)) + if self.ops.m.actors.cache.get(*idx) == Some(actor) { + Ok(OpId::new(*ctr, *idx)) } else { // FIXME - make a real error let idx = self @@ -413,12 +425,7 @@ impl Automerge { .actors .lookup(actor) .ok_or(AutomergeError::Fail)?; - ObjId(OpId::new(*ctr, idx)) - }; - if let Some(obj_type) = self.ops.object_type(&obj) { - Ok((obj, obj_type)) - } else { - Err(AutomergeError::NotAnObject) + Ok(OpId::new(*ctr, idx)) } } } @@ -921,8 +928,21 @@ impl Automerge { #[doc(hidden)] pub fn import(&self, s: &str) -> Result<(ExId, ObjType), AutomergeError> { - if s == "_root" { + let obj = self.import_obj(s)?; + if obj == ExId::Root { Ok((ExId::Root, ObjType::Map)) + } else { + let obj_type = self + .object_type(&obj) + .map_err(|_| AutomergeError::InvalidObjId(s.to_owned()))?; + Ok((obj, obj_type)) + } + } + + #[doc(hidden)] + pub fn import_obj(&self, s: &str) -> Result { + if s == "_root" { + Ok(ExId::Root) } else { let n = s .find('@') @@ -938,10 +958,7 @@ impl Automerge { .lookup(&actor) .ok_or_else(|| AutomergeError::InvalidObjId(s.to_owned()))?; let obj = ExId::Id(counter, self.ops.m.actors.cache[actor].clone(), actor); - let obj_type = self - .object_type(&obj) - .map_err(|_| AutomergeError::InvalidObjId(s.to_owned()))?; - Ok((obj, obj_type)) + Ok(obj) } } @@ -975,8 +992,10 @@ impl Automerge { OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), - OpType::MarkBegin(_) => format!("markBegin{}", 0), - OpType::MarkEnd(_) => format!("markEnd{}", 0), + OpType::MarkBegin(MarkData { name, value, .. }) => { + format!("mark({},{})", self.ops.m.props[name.props_index()], value) + } + OpType::MarkEnd(_) => "/mark".to_string(), }; let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); let succ: Vec<_> = op.succ.into_iter().map(|id| self.to_string(*id)).collect(); diff --git a/rust/automerge/src/error.rs b/rust/automerge/src/error.rs index 5a9616e4..57a87167 100644 --- a/rust/automerge/src/error.rs +++ b/rust/automerge/src/error.rs @@ -97,6 +97,4 @@ pub(crate) enum InvalidOpType { UnknownAction(u64), #[error("non numeric argument for inc op")] NonNumericInc, - #[error("MarkBegin operation with no name")] - MarkBeginWithoutName, } diff --git a/rust/automerge/src/legacy/mod.rs b/rust/automerge/src/legacy/mod.rs index ab835834..d4fb1fd0 100644 --- a/rust/automerge/src/legacy/mod.rs +++ b/rust/automerge/src/legacy/mod.rs @@ -275,8 +275,7 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) => 7, - Self::MarkEnd(..) => 8, + Self::MarkBegin(_) | Self::MarkEnd(_) => 7, } } @@ -308,6 +307,7 @@ impl Op { pub fn primitive_value(&self) -> Option { match &self.action { OpType::Put(v) => Some(v.clone()), + OpType::MarkBegin(MarkData { value, .. }) => Some(value.clone()), OpType::Increment(i) => Some(ScalarValue::Int(*i)), _ => None, } diff --git a/rust/automerge/src/legacy/serde_impls/op.rs b/rust/automerge/src/legacy/serde_impls/op.rs index a3719fd6..d42879cd 100644 --- a/rust/automerge/src/legacy/serde_impls/op.rs +++ b/rust/automerge/src/legacy/serde_impls/op.rs @@ -5,7 +5,9 @@ use serde::{ }; use super::read_field; -use crate::legacy::{DataType, Key, ObjType, ObjectId, Op, OpId, OpType, ScalarValue, SortedVec}; +use crate::legacy::{ + DataType, Key, MarkData, ObjType, ObjectId, Op, OpId, OpType, ScalarValue, SortedVec, +}; impl Serialize for Op { fn serialize(&self, serializer: S) -> Result @@ -50,6 +52,16 @@ impl Serialize for Op { OpType::Increment(n) => op.serialize_field("value", &n)?, OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?, OpType::Put(value) => op.serialize_field("value", &value)?, + OpType::MarkBegin(MarkData { + name, + value, + expand, + }) => { + op.serialize_field("name", &name)?; + op.serialize_field("value", &value)?; + op.serialize_field("expand", &expand)? + } + OpType::MarkEnd(expand) => op.serialize_field("expand", &expand)?, _ => {} } op.serialize_field("pred", &self.pred)?; @@ -71,6 +83,8 @@ pub(crate) enum RawOpType { Del, Inc, Set, + MarkBegin, + MarkEnd, } impl Serialize for RawOpType { @@ -86,6 +100,8 @@ impl Serialize for RawOpType { RawOpType::Del => "del", RawOpType::Inc => "inc", RawOpType::Set => "set", + RawOpType::MarkBegin => "markBegin", + RawOpType::MarkEnd => "markEnd", }; serializer.serialize_str(s) } @@ -104,8 +120,8 @@ impl<'de> Deserialize<'de> for RawOpType { "del", "inc", "set", - "mark", - "unmark", + "markBegin", + "markEnd", ]; // TODO: Probably more efficient to deserialize to a `&str` let raw_type = String::deserialize(deserializer)?; @@ -117,6 +133,8 @@ impl<'de> Deserialize<'de> for RawOpType { "del" => Ok(RawOpType::Del), "inc" => Ok(RawOpType::Inc), "set" => Ok(RawOpType::Set), + "markBegin" => Ok(RawOpType::MarkBegin), + "markEnd" => Ok(RawOpType::MarkEnd), other => Err(Error::unknown_variant(other, VARIANTS)), } } @@ -189,24 +207,7 @@ impl<'de> Deserialize<'de> for Op { RawOpType::MakeList => OpType::Make(ObjType::List), RawOpType::MakeText => OpType::Make(ObjType::Text), RawOpType::Del => OpType::Delete, - RawOpType::Set => { - let value = if let Some(datatype) = datatype { - let raw_value = value - .ok_or_else(|| Error::missing_field("value"))? - .unwrap_or(ScalarValue::Null); - raw_value.as_datatype(datatype).map_err(|e| { - Error::invalid_value( - Unexpected::Other(e.unexpected.as_str()), - &e.expected.as_str(), - ) - })? - } else { - value - .ok_or_else(|| Error::missing_field("value"))? - .unwrap_or(ScalarValue::Null) - }; - OpType::Put(value) - } + RawOpType::Set => OpType::Put(unwrap_value(value, datatype)?), RawOpType::Inc => match value.flatten() { Some(ScalarValue::Int(n)) => Ok(OpType::Increment(n)), Some(ScalarValue::Uint(n)) => Ok(OpType::Increment(n as i64)), @@ -230,6 +231,18 @@ impl<'de> Deserialize<'de> for Op { } None => Err(Error::missing_field("value")), }?, + RawOpType::MarkBegin => { + let name = name.ok_or_else(|| Error::missing_field("name"))?; + let name = smol_str::SmolStr::new(name); + let expand = expand.unwrap_or(false); + let value = unwrap_value(value, datatype)?; + OpType::MarkBegin(MarkData { + name, + value, + expand, + }) + } + RawOpType::MarkEnd => OpType::MarkEnd(expand.unwrap_or(false)), }; Ok(Op { action, @@ -244,6 +257,27 @@ impl<'de> Deserialize<'de> for Op { } } +fn unwrap_value( + value: Option>, + datatype: Option, +) -> Result { + if let Some(datatype) = datatype { + let raw_value = value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null); + raw_value.as_datatype(datatype).map_err(|e| { + Error::invalid_value( + Unexpected::Other(e.unexpected.as_str()), + &e.expected.as_str(), + ) + }) + } else { + Ok(value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null)) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/rust/automerge/src/legacy/serde_impls/op_type.rs b/rust/automerge/src/legacy/serde_impls/op_type.rs index 9dba8701..32825119 100644 --- a/rust/automerge/src/legacy/serde_impls/op_type.rs +++ b/rust/automerge/src/legacy/serde_impls/op_type.rs @@ -18,8 +18,8 @@ impl Serialize for OpType { OpType::Delete => RawOpType::Del, OpType::Increment(_) => RawOpType::Inc, OpType::Put(_) => RawOpType::Set, - OpType::MarkBegin(_) => todo!(), - OpType::MarkEnd(_) => todo!(), + OpType::MarkBegin(_) => RawOpType::MarkBegin, + OpType::MarkEnd(_) => RawOpType::MarkEnd, }; raw_type.serialize(serializer) } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index 9aedeff3..e3b4d918 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -90,6 +90,7 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { match &self.op.action { OpType::Put(v) => Cow::Borrowed(v), OpType::Increment(i) => Cow::Owned(ScalarValue::Int(*i)), + OpType::MarkBegin(MarkData { value, .. }) => Cow::Borrowed(value), _ => Cow::Owned(ScalarValue::Null), } } diff --git a/rust/automerge/src/storage/parse.rs b/rust/automerge/src/storage/parse.rs index 54668da4..1f55a8ad 100644 --- a/rust/automerge/src/storage/parse.rs +++ b/rust/automerge/src/storage/parse.rs @@ -308,6 +308,7 @@ impl<'a> Input<'a> { } /// The bytes behind this input - including bytes which have been consumed + // #[allow(clippy::misnamed_getters)] pub(crate) fn bytes(&self) -> &'a [u8] { self.original } diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 4011f2af..7288aa47 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -694,14 +694,48 @@ impl TransactionInner { Ok(()) } - pub(crate) fn unmark( + pub(crate) fn unmark, M: AsRef, Obs: OpObserver>( &mut self, - _doc: &mut Automerge, - mut _op_observer: Option<&mut Obs>, - _ex_obj: &ExId, - _mark: &ExId, + doc: &mut Automerge, + _op_observer: Option<&mut Obs>, + obj: O, + mark: M, ) -> Result<(), AutomergeError> { - unimplemented!() + let (obj, _) = doc.exid_to_obj(obj.as_ref())?; + let markid = doc.exid_to_opid(mark.as_ref())?; + let ops = doc.ops_mut(); + let op1 = Op { + id: self.next_id(), + action: OpType::Delete, + key: markid.into(), + succ: Default::default(), + pred: ops.m.sorted_opids(vec![markid].into_iter()), + insert: false, + }; + let q1 = ops.search(&obj, query::SeekOp::new(&op1)); + ops.add_succ(&obj, &q1.succ, &op1); + //for i in q1.succ { + // ops.replace(&obj, i, |old_op| old_op.add_succ(&op1)); + //} + self.operations.push((obj, op1)); + + let markid = markid.next(); + let op2 = Op { + id: self.next_id(), + action: OpType::Delete, + key: markid.into(), + succ: Default::default(), + pred: ops.m.sorted_opids(vec![markid].into_iter()), + insert: false, + }; + let q2 = ops.search(&obj, query::SeekOp::new(&op2)); + + ops.add_succ(&obj, &q2.succ, &op2); + //for i in q2.succ { + // ops.replace(&obj, i, |old_op| old_op.add_succ(&op2)); + //} + self.operations.push((obj, op2)); + Ok(()) } fn finalize_op( diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 59ba5ceb..d93392a1 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -235,8 +235,7 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) => 7, - Self::MarkEnd(..) => 8, + Self::MarkBegin(_) | Self::MarkEnd(_) => 7, } } @@ -274,9 +273,8 @@ impl OpType { value, expand, })), - None => Err(error::InvalidOpType::MarkBeginWithoutName), + None => Ok(Self::MarkEnd(expand)), }, - 8 => Ok(Self::MarkEnd(expand)), other => Err(error::InvalidOpType::UnknownAction(other)), } } @@ -510,6 +508,11 @@ impl OpId { pub(crate) fn prev(&self) -> OpId { OpId(self.0 - 1, self.1) } + + #[inline] + pub(crate) fn next(&self) -> OpId { + OpId(self.0 + 1, self.1) + } } #[derive(Debug, Clone, Copy, PartialOrd, Eq, PartialEq, Ord, Hash, Default)] From 02e8ae2c703484d720cf5089bcc961d6f9484d1c Mon Sep 17 00:00:00 2001 From: Alex Good Date: Thu, 9 Feb 2023 11:06:08 +0000 Subject: [PATCH 10/26] Remove nightly from CI --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2d469d5..bfa31bd5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -137,8 +137,6 @@ jobs: matrix: toolchain: - 1.66.0 - - nightly - continue-on-error: ${{ matrix.toolchain == 'nightly' }} steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 From e2bb0eb6b9f43837f6c4613589a8eb60a3f2093b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Jan 2023 14:51:02 -0700 Subject: [PATCH 11/26] Use our leb128 parser for values This ensures that values in automerge documents are encoded correctly, and that no extra data is smuggled in any LEB fields. --- .../src/columnar/column_range/value.rs | 62 +++++++++--------- rust/automerge/src/columnar/encoding.rs | 2 + ...counter_value_has_incorrect_meta.automerge | Bin 0 -> 63 bytes .../fixtures/counter_value_is_ok.automerge | Bin 0 -> 63 bytes .../counter_value_is_overlong.automerge | Bin 0 -> 63 bytes rust/automerge/tests/test.rs | 14 ++++ 6 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 rust/automerge/tests/fixtures/counter_value_has_incorrect_meta.automerge create mode 100644 rust/automerge/tests/fixtures/counter_value_is_ok.automerge create mode 100644 rust/automerge/tests/fixtures/counter_value_is_overlong.automerge diff --git a/rust/automerge/src/columnar/column_range/value.rs b/rust/automerge/src/columnar/column_range/value.rs index 43f63437..03a5aa60 100644 --- a/rust/automerge/src/columnar/column_range/value.rs +++ b/rust/automerge/src/columnar/column_range/value.rs @@ -4,10 +4,15 @@ use crate::{ columnar::{ encoding::{ leb128::{lebsize, ulebsize}, - raw, DecodeColumnError, RawBytes, RawDecoder, RawEncoder, RleDecoder, RleEncoder, Sink, + raw, DecodeColumnError, DecodeError, RawBytes, RawDecoder, RawEncoder, RleDecoder, + RleEncoder, Sink, }, SpliceError, }, + storage::parse::{ + leb128::{leb128_i64, leb128_u64}, + Input, ParseResult, + }, ScalarValue, }; @@ -217,18 +222,8 @@ impl<'a> Iterator for ValueIter<'a> { ValueType::Null => Some(Ok(ScalarValue::Null)), ValueType::True => Some(Ok(ScalarValue::Boolean(true))), ValueType::False => Some(Ok(ScalarValue::Boolean(false))), - ValueType::Uleb => self.parse_raw(val_meta, |mut bytes| { - let val = leb128::read::unsigned(&mut bytes).map_err(|e| { - DecodeColumnError::invalid_value("value", e.to_string()) - })?; - Ok(ScalarValue::Uint(val)) - }), - ValueType::Leb => self.parse_raw(val_meta, |mut bytes| { - let val = leb128::read::signed(&mut bytes).map_err(|e| { - DecodeColumnError::invalid_value("value", e.to_string()) - })?; - Ok(ScalarValue::Int(val)) - }), + ValueType::Uleb => self.parse_input(val_meta, leb128_u64), + ValueType::Leb => self.parse_input(val_meta, leb128_i64), ValueType::String => self.parse_raw(val_meta, |bytes| { let val = std::str::from_utf8(bytes) .map_err(|e| DecodeColumnError::invalid_value("value", e.to_string()))? @@ -250,17 +245,11 @@ impl<'a> Iterator for ValueIter<'a> { let val = f64::from_le_bytes(raw); Ok(ScalarValue::F64(val)) }), - ValueType::Counter => self.parse_raw(val_meta, |mut bytes| { - let val = leb128::read::signed(&mut bytes).map_err(|e| { - DecodeColumnError::invalid_value("value", e.to_string()) - })?; - Ok(ScalarValue::Counter(val.into())) + ValueType::Counter => self.parse_input(val_meta, |input| { + leb128_i64(input).map(|(i, n)| (i, ScalarValue::Counter(n.into()))) }), - ValueType::Timestamp => self.parse_raw(val_meta, |mut bytes| { - let val = leb128::read::signed(&mut bytes).map_err(|e| { - DecodeColumnError::invalid_value("value", e.to_string()) - })?; - Ok(ScalarValue::Timestamp(val)) + ValueType::Timestamp => self.parse_input(val_meta, |input| { + leb128_i64(input).map(|(i, n)| (i, ScalarValue::Timestamp(n))) }), ValueType::Unknown(code) => self.parse_raw(val_meta, |bytes| { Ok(ScalarValue::Unknown { @@ -284,8 +273,8 @@ impl<'a> Iterator for ValueIter<'a> { } impl<'a> ValueIter<'a> { - fn parse_raw Result>( - &mut self, + fn parse_raw<'b, R, F: Fn(&'b [u8]) -> Result>( + &'b mut self, meta: ValueMeta, f: F, ) -> Option> { @@ -298,11 +287,24 @@ impl<'a> ValueIter<'a> { } Ok(bytes) => bytes, }; - let val = match f(raw) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - Some(Ok(val)) + Some(f(raw)) + } + + fn parse_input<'b, R, F: Fn(Input<'b>) -> ParseResult<'b, R, DecodeError>>( + &'b mut self, + meta: ValueMeta, + f: F, + ) -> Option> + where + R: Into, + { + self.parse_raw(meta, |raw| match f(Input::new(raw)) { + Err(e) => Err(DecodeColumnError::invalid_value("value", e.to_string())), + Ok((i, _)) if !i.is_empty() => { + Err(DecodeColumnError::invalid_value("value", "extra bytes")) + } + Ok((_, v)) => Ok(v.into()), + }) } pub(crate) fn done(&self) -> bool { diff --git a/rust/automerge/src/columnar/encoding.rs b/rust/automerge/src/columnar/encoding.rs index 91c1e6eb..e15600d7 100644 --- a/rust/automerge/src/columnar/encoding.rs +++ b/rust/automerge/src/columnar/encoding.rs @@ -48,6 +48,8 @@ pub(crate) enum DecodeError { FromInt(#[from] std::num::TryFromIntError), #[error("bad leb128")] BadLeb(#[from] ::leb128::read::Error), + #[error(transparent)] + BadLeb128(#[from] crate::storage::parse::leb128::Error), #[error("attempted to allocate {attempted} which is larger than the maximum of {maximum}")] OverlargeAllocation { attempted: usize, maximum: usize }, #[error("invalid string encoding")] diff --git a/rust/automerge/tests/fixtures/counter_value_has_incorrect_meta.automerge b/rust/automerge/tests/fixtures/counter_value_has_incorrect_meta.automerge new file mode 100644 index 0000000000000000000000000000000000000000..2290b446ca661f302f6591c522a6653ba0be54a6 GIT binary patch literal 63 zcmZq8_iDCFPJPB`${^6qmb+L*-z{NbN`A*m!H-iI8Mkb^bm5T!0|T2Vvk9XUQy5b? TQvp*wVH2@I&u}A*O5KaD{l&S)MXnSh`0lxRq(Bd!v00tEUGyy^a VRsvT7Z~}h;VF7;ue<;uoe*j$F7aafq literal 0 HcmV?d00001 diff --git a/rust/automerge/tests/fixtures/counter_value_is_overlong.automerge b/rust/automerge/tests/fixtures/counter_value_is_overlong.automerge new file mode 100644 index 0000000000000000000000000000000000000000..831346f7f4109e2f292e502e13b326ca2485b351 GIT binary patch literal 63 zcmZq8_iD~Rd#9GsltG}IEqAeszFWe=l>CmBf*+?aGH%&+>B1ue1_m}!W)nsyrZA>( TrUIsV#ze+?#(Iql_4Nz@=B*VY literal 0 HcmV?d00001 diff --git a/rust/automerge/tests/test.rs b/rust/automerge/tests/test.rs index abc5cf56..c77af9f5 100644 --- a/rust/automerge/tests/test.rs +++ b/rust/automerge/tests/test.rs @@ -1413,6 +1413,20 @@ fn fuzz_crashers() { } } +fn fixture(name: &str) -> Vec { + fs::read("./tests/fixtures/".to_owned() + name).unwrap() +} + +#[test] +fn overlong_leb() { + // the value metadata says "2", but the LEB is only 1-byte long and there's an extra 0 + assert!(Automerge::load(&fixture("counter_value_has_incorrect_meta.automerge")).is_err()); + // the LEB is overlong (using 2 bytes where one would have sufficed) + assert!(Automerge::load(&fixture("counter_value_is_overlong.automerge")).is_err()); + // the LEB is correct + assert!(Automerge::load(&fixture("counter_value_is_ok.automerge")).is_ok()); +} + #[test] fn negative_64() { let mut doc = Automerge::new(); From a9612371e01b6e3cd4cb13bfcd4b9f25b63958ab Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 13 Feb 2023 21:17:27 -0600 Subject: [PATCH 12/26] rework how skip works to push the logic into node --- javascript/test/basic_test.ts | 16 +++++ rust/automerge/src/op_tree/node.rs | 68 +++++++++++-------- rust/automerge/src/query/prop.rs | 47 ++----------- rust/automerge/src/query/seek_op.rs | 39 ++--------- .../automerge/src/query/seek_op_with_patch.rs | 38 +---------- 5 files changed, 67 insertions(+), 141 deletions(-) diff --git a/javascript/test/basic_test.ts b/javascript/test/basic_test.ts index 5aa1ac34..0e30dc7c 100644 --- a/javascript/test/basic_test.ts +++ b/javascript/test/basic_test.ts @@ -58,6 +58,22 @@ describe("Automerge", () => { }) }) + it("should be able to insert and delete a large number of properties", () => { + let doc = Automerge.init() + + doc = Automerge.change(doc, doc => { + doc['k1'] = true; + }); + + for (let idx = 1; idx <= 200; idx++) { + doc = Automerge.change(doc, doc => { + delete doc['k' + idx]; + doc['k' + (idx + 1)] = true; + assert(Object.keys(doc).length == 1) + }); + } + }) + it("can detect an automerge doc with isAutomerge()", () => { const doc1 = Automerge.from({ sub: { object: true } }) assert(Automerge.isAutomerge(doc1)) diff --git a/rust/automerge/src/op_tree/node.rs b/rust/automerge/src/op_tree/node.rs index ea7fbf48..8f2de662 100644 --- a/rust/automerge/src/op_tree/node.rs +++ b/rust/automerge/src/op_tree/node.rs @@ -27,50 +27,67 @@ impl OpTreeNode { } } + fn search_element<'a, 'b: 'a, Q>( + &'b self, + query: &mut Q, + m: &OpSetMetadata, + ops: &'a [Op], + index: usize, + ) -> bool + where + Q: TreeQuery<'a>, + { + if let Some(e) = self.elements.get(index) { + if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish { + return true; + } + } + false + } + pub(crate) fn search<'a, 'b: 'a, Q>( &'b self, query: &mut Q, m: &OpSetMetadata, ops: &'a [Op], - skip: Option, + mut skip: Option, ) -> bool where Q: TreeQuery<'a>, { if self.is_leaf() { - let skip = skip.unwrap_or(0); - for e in self.elements.iter().skip(skip) { + for e in self.elements.iter().skip(skip.unwrap_or(0)) { if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish { return true; } } false } else { - let mut skip = skip.unwrap_or(0); for (child_index, child) in self.children.iter().enumerate() { - match skip.cmp(&child.len()) { - Ordering::Greater => { - // not in this child at all - // take off the number of elements in the child as well as the next element - skip -= child.len() + 1; + match skip { + Some(n) if n > child.len() => { + skip = Some(n - child.len() - 1); } - Ordering::Equal => { - // just try the element - skip -= child.len(); - if let Some(e) = self.elements.get(child_index) { - if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish - { - return true; - } + Some(n) if n == child.len() => { + skip = None; + if self.search_element(query, m, ops, child_index) { + return true; } } - Ordering::Less => { + Some(n) => { + if child.search(query, m, ops, Some(n)) { + return true; + } + skip = Some(0); // important to not be None so we never call query_node again + if self.search_element(query, m, ops, child_index) { + return true; + } + } + None => { // descend and try find it match query.query_node_with_metadata(child, m, ops) { QueryResult::Descend => { - // search in the child node, passing in the number of items left to - // skip - if child.search(query, m, ops, Some(skip)) { + if child.search(query, m, ops, None) { return true; } } @@ -78,14 +95,9 @@ impl OpTreeNode { QueryResult::Next => (), QueryResult::Skip(_) => panic!("had skip from non-root node"), } - if let Some(e) = self.elements.get(child_index) { - if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish - { - return true; - } + if self.search_element(query, m, ops, child_index) { + return true; } - // reset the skip to zero so we continue iterating normally - skip = 0; } } } diff --git a/rust/automerge/src/query/prop.rs b/rust/automerge/src/query/prop.rs index f6062ec6..d2a11361 100644 --- a/rust/automerge/src/query/prop.rs +++ b/rust/automerge/src/query/prop.rs @@ -1,6 +1,6 @@ use crate::op_tree::{OpSetMetadata, OpTreeNode}; use crate::query::{binary_search_by, QueryResult, TreeQuery}; -use crate::types::{Key, ListEncoding, Op}; +use crate::types::{Key, Op}; use std::fmt::Debug; #[derive(Debug, Clone, PartialEq)] @@ -9,15 +9,6 @@ pub(crate) struct Prop<'a> { pub(crate) ops: Vec<&'a Op>, pub(crate) ops_pos: Vec, pub(crate) pos: usize, - start: Option, -} - -#[derive(Debug, Clone, PartialEq)] -struct Start { - /// The index to start searching for in the optree - idx: usize, - /// The total length of the optree - optree_len: usize, } impl<'a> Prop<'a> { @@ -27,7 +18,6 @@ impl<'a> Prop<'a> { ops: vec![], ops_pos: vec![], pos: 0, - start: None, } } } @@ -39,38 +29,9 @@ impl<'a> TreeQuery<'a> for Prop<'a> { m: &OpSetMetadata, ops: &[Op], ) -> QueryResult { - if let Some(Start { - idx: start, - optree_len, - }) = self.start - { - if self.pos + child.len() >= start { - // skip empty nodes - if child.index.visible_len(ListEncoding::default()) == 0 { - if self.pos + child.len() >= optree_len { - self.pos = optree_len; - QueryResult::Finish - } else { - self.pos += child.len(); - QueryResult::Next - } - } else { - QueryResult::Descend - } - } else { - self.pos += child.len(); - QueryResult::Next - } - } else { - // in the root node find the first op position for the key - let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.key)); - self.start = Some(Start { - idx: start, - optree_len: child.len(), - }); - self.pos = start; - QueryResult::Skip(start) - } + let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.key)); + self.pos = start; + QueryResult::Skip(start) } fn query_element(&mut self, op: &'a Op) -> QueryResult { diff --git a/rust/automerge/src/query/seek_op.rs b/rust/automerge/src/query/seek_op.rs index 22d1f58d..2ed875d2 100644 --- a/rust/automerge/src/query/seek_op.rs +++ b/rust/automerge/src/query/seek_op.rs @@ -1,6 +1,6 @@ use crate::op_tree::{OpSetMetadata, OpTreeNode}; use crate::query::{binary_search_by, QueryResult, TreeQuery}; -use crate::types::{Key, ListEncoding, Op, HEAD}; +use crate::types::{Key, Op, HEAD}; use std::cmp::Ordering; use std::fmt::Debug; @@ -14,8 +14,6 @@ pub(crate) struct SeekOp<'a> { pub(crate) succ: Vec, /// whether a position has been found found: bool, - /// The found start position of the key if there is one yet (for map objects). - start: Option, } impl<'a> SeekOp<'a> { @@ -25,7 +23,6 @@ impl<'a> SeekOp<'a> { succ: vec![], pos: 0, found: false, - start: None, } } @@ -72,37 +69,9 @@ impl<'a> TreeQuery<'a> for SeekOp<'a> { } } Key::Map(_) => { - if let Some(start) = self.start { - if self.pos + child.len() >= start { - // skip empty nodes - if child.index.visible_len(ListEncoding::List) == 0 { - let child_contains_key = - child.elements.iter().any(|e| ops[*e].key == self.op.key); - if !child_contains_key { - // If we are in a node which has no visible ops, but none of the - // elements of the node match the key of the op, then we must have - // finished processing and so we can just return. - // See https://github.com/automerge/automerge-rs/pull/480 - QueryResult::Finish - } else { - // Otherwise, we need to proceed to the next node - self.pos += child.len(); - QueryResult::Next - } - } else { - QueryResult::Descend - } - } else { - self.pos += child.len(); - QueryResult::Next - } - } else { - // in the root node find the first op position for the key - let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.op.key)); - self.start = Some(start); - self.pos = start; - QueryResult::Skip(start) - } + let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.op.key)); + self.pos = start; + QueryResult::Skip(start) } } } diff --git a/rust/automerge/src/query/seek_op_with_patch.rs b/rust/automerge/src/query/seek_op_with_patch.rs index 7cacb032..cd30f5bb 100644 --- a/rust/automerge/src/query/seek_op_with_patch.rs +++ b/rust/automerge/src/query/seek_op_with_patch.rs @@ -16,8 +16,6 @@ pub(crate) struct SeekOpWithPatch<'a> { last_seen: Option, pub(crate) values: Vec<&'a Op>, pub(crate) had_value_before: bool, - /// The found start position of the key if there is one yet (for map objects). - start: Option, } impl<'a> SeekOpWithPatch<'a> { @@ -33,7 +31,6 @@ impl<'a> SeekOpWithPatch<'a> { last_seen: None, values: vec![], had_value_before: false, - start: None, } } @@ -132,38 +129,9 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> { // Updating a map: operations appear in sorted order by key Key::Map(_) => { - if let Some(start) = self.start { - if self.pos + child.len() >= start { - // skip empty nodes - if child.index.visible_len(self.encoding) == 0 { - let child_contains_key = - child.elements.iter().any(|e| ops[*e].key == self.op.key); - if !child_contains_key { - // If we are in a node which has no visible ops, but none of the - // elements of the node match the key of the op, then we must have - // finished processing and so we can just return. - // See https://github.com/automerge/automerge-rs/pull/480 - QueryResult::Finish - } else { - self.pos += child.len(); - QueryResult::Next - } - } else { - QueryResult::Descend - } - } else { - self.pos += child.len(); - QueryResult::Next - } - } else { - // in the root node find the first op position for the key - // Search for the place where we need to insert the new operation. First find the - // first op with a key >= the key we're updating - let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.op.key)); - self.start = Some(start); - self.pos = start; - QueryResult::Skip(start) - } + let start = binary_search_by(child, ops, |op| m.key_cmp(&op.key, &self.op.key)); + self.pos = start; + QueryResult::Skip(start) } } } From c6a32d83681748a06ffc4267e895256438a43a5c Mon Sep 17 00:00:00 2001 From: Alex Good Date: Tue, 14 Feb 2023 16:24:25 +0000 Subject: [PATCH 13/26] Correct logic when skip = B and fix formatting A few tests were failing which exposed the fact that if skip is `B` (the out factor of the OpTree) then we set `skip = None` and this causes us to attempt to return `Skip` in a non root node. I ported the failing test from JS to Rust and fixed the problem. I also fixed the formatting issues. --- javascript/test/basic_test.ts | 10 +++---- rust/automerge-wasm/test/test.ts | 2 +- rust/automerge/src/op_tree/node.rs | 4 +-- rust/automerge/src/sync.rs | 45 ++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/javascript/test/basic_test.ts b/javascript/test/basic_test.ts index 0e30dc7c..e34484c4 100644 --- a/javascript/test/basic_test.ts +++ b/javascript/test/basic_test.ts @@ -62,15 +62,15 @@ describe("Automerge", () => { let doc = Automerge.init() doc = Automerge.change(doc, doc => { - doc['k1'] = true; - }); + doc["k1"] = true + }) for (let idx = 1; idx <= 200; idx++) { doc = Automerge.change(doc, doc => { - delete doc['k' + idx]; - doc['k' + (idx + 1)] = true; + delete doc["k" + idx] + doc["k" + (idx + 1)] = true assert(Object.keys(doc).length == 1) - }); + }) } }) diff --git a/rust/automerge-wasm/test/test.ts b/rust/automerge-wasm/test/test.ts index 56aaae74..bb4f71e3 100644 --- a/rust/automerge-wasm/test/test.ts +++ b/rust/automerge-wasm/test/test.ts @@ -1447,7 +1447,7 @@ describe('Automerge', () => { sync(n1, n2, s1, s2) // Having n3's last change concurrent to the last sync heads forces us into the slower code path - const change3 = n2.getLastLocalChange() + const change3 = n3.getLastLocalChange() if (change3 === null) throw new RangeError("no local change") n2.applyChanges([change3]) n1.put("_root", "n1", "final"); n1.commit("", 0) diff --git a/rust/automerge/src/op_tree/node.rs b/rust/automerge/src/op_tree/node.rs index 8f2de662..ed1b7646 100644 --- a/rust/automerge/src/op_tree/node.rs +++ b/rust/automerge/src/op_tree/node.rs @@ -69,7 +69,7 @@ impl OpTreeNode { skip = Some(n - child.len() - 1); } Some(n) if n == child.len() => { - skip = None; + skip = Some(0); // important to not be None so we never call query_node again if self.search_element(query, m, ops, child_index) { return true; } @@ -78,7 +78,7 @@ impl OpTreeNode { if child.search(query, m, ops, Some(n)) { return true; } - skip = Some(0); // important to not be None so we never call query_node again + skip = Some(0); // important to not be None so we never call query_node again if self.search_element(query, m, ops, child_index) { return true; } diff --git a/rust/automerge/src/sync.rs b/rust/automerge/src/sync.rs index d3b6b3fa..d6dc2580 100644 --- a/rust/automerge/src/sync.rs +++ b/rust/automerge/src/sync.rs @@ -887,6 +887,51 @@ mod tests { assert_eq!(doc2.get_heads(), all_heads); } + #[test] + fn should_handle_lots_of_branching_and_merging() { + let mut doc1 = crate::AutoCommit::new().with_actor(ActorId::try_from("01234567").unwrap()); + let mut doc2 = crate::AutoCommit::new().with_actor(ActorId::try_from("89abcdef").unwrap()); + let mut doc3 = crate::AutoCommit::new().with_actor(ActorId::try_from("fedcba98").unwrap()); + let mut s1 = State::new(); + let mut s2 = State::new(); + + doc1.put(crate::ROOT, "x", 0).unwrap(); + let change1 = doc1.get_last_local_change().unwrap().clone(); + + doc2.apply_changes([change1.clone()]).unwrap(); + doc3.apply_changes([change1]).unwrap(); + + doc3.put(crate::ROOT, "x", 1).unwrap(); + + //// - n1c1 <------ n1c2 <------ n1c3 <-- etc. <-- n1c20 <------ n1c21 + //// / \/ \/ \/ + //// / /\ /\ /\ + //// c0 <---- n2c1 <------ n2c2 <------ n2c3 <-- etc. <-- n2c20 <------ n2c21 + //// \ / + //// ---------------------------------------------- n3c1 <----- + for i in 1..20 { + doc1.put(crate::ROOT, "n1", i).unwrap(); + doc2.put(crate::ROOT, "n2", i).unwrap(); + let change1 = doc1.get_last_local_change().unwrap().clone(); + let change2 = doc2.get_last_local_change().unwrap().clone(); + doc1.apply_changes([change2.clone()]).unwrap(); + doc2.apply_changes([change1]).unwrap(); + } + + sync(&mut doc1, &mut doc2, &mut s1, &mut s2); + + //// Having n3's last change concurrent to the last sync heads forces us into the slower code path + let change3 = doc3.get_last_local_change().unwrap().clone(); + doc2.apply_changes([change3]).unwrap(); + + doc1.put(crate::ROOT, "n1", "final").unwrap(); + doc2.put(crate::ROOT, "n1", "final").unwrap(); + + sync(&mut doc1, &mut doc2, &mut s1, &mut s2); + + assert_eq!(doc1.get_heads(), doc2.get_heads()); + } + fn sync( a: &mut crate::AutoCommit, b: &mut crate::AutoCommit, From e1d81e01fc74883488fbf1806aa09f335e1d62d4 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 16 Feb 2023 19:35:39 -0600 Subject: [PATCH 14/26] mark patch callbacks and js mark api --- javascript/src/stable.ts | 26 +++ javascript/src/unstable.ts | 23 +++ javascript/test/marks.ts | 33 ++++ rust/automerge-wasm/src/interop.rs | 4 + rust/automerge-wasm/src/lib.rs | 7 +- rust/automerge-wasm/src/observer.rs | 46 +++++- rust/automerge-wasm/test/marks.ts | 91 ++++++++++ rust/automerge/src/autocommit.rs | 19 +-- rust/automerge/src/automerge.rs | 10 +- rust/automerge/src/marks.rs | 36 +++- rust/automerge/src/op_observer.rs | 7 + rust/automerge/src/op_observer/compose.rs | 12 ++ rust/automerge/src/query.rs | 2 + rust/automerge/src/query/seek_mark.rs | 156 ++++++++++++++++++ rust/automerge/src/transaction/inner.rs | 31 ++-- .../src/transaction/manual_transaction.rs | 12 +- .../automerge/src/transaction/transactable.rs | 10 +- 17 files changed, 461 insertions(+), 64 deletions(-) create mode 100644 javascript/test/marks.ts create mode 100644 rust/automerge/src/query/seek_mark.rs diff --git a/javascript/src/stable.ts b/javascript/src/stable.ts index 74410346..19f5f9d5 100644 --- a/javascript/src/stable.ts +++ b/javascript/src/stable.ts @@ -517,6 +517,32 @@ export function loadIncremental( return progressDocument(doc, heads, opts.patchCallback || state.patchCallback) } +/** + * Create binary save data to be appended to a save file or fed into {@link loadIncremental} + * + * @typeParam T - The type of the value which is contained in the document. + * Note that no validation is done to make sure this type is in + * fact the type of the contained value so be a bit careful + * + * This function is useful for incrementally saving state. The data can be appended to a + * automerge save file, or passed to a document replicating its state. + * + */ +export function saveIncremental( + doc: Doc, +): Uint8Array { + const state = _state(doc) + if (state.heads) { + throw new RangeError( + "Attempting to change an out of date document - set at: " + _trace(doc) + ) + } + if (_is_proxy(doc)) { + throw new RangeError("Calls to Automerge.change cannot be nested") + } + return state.handle.saveIncremental() +} + /** * Export the contents of a document to a compressed format * diff --git a/javascript/src/unstable.ts b/javascript/src/unstable.ts index 7c73afb9..38127632 100644 --- a/javascript/src/unstable.ts +++ b/javascript/src/unstable.ts @@ -233,6 +233,29 @@ export function splice( } } +export function mark( + doc: Doc, + prop: stable.Prop, + markName: string, + range: string, + value: string | boolean | number | Uint8Array | null +) { + if (!_is_proxy(doc)) { + throw new RangeError("object cannot be modified outside of a change block") + } + const state = _state(doc, false) + const objectId = _obj(doc) + if (!objectId) { + throw new RangeError("invalid object for mark") + } + const obj = `${objectId}/${prop}` + try { + return state.handle.mark(obj, range, markName, value) + } catch (e) { + throw new RangeError(`Cannot mark: ${e}`) + } +} + /** * Get the conflicts associated with a property * diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts new file mode 100644 index 00000000..5f4b7f47 --- /dev/null +++ b/javascript/test/marks.ts @@ -0,0 +1,33 @@ +import * as assert from "assert" +import { unstable as Automerge } from "../src" +import * as WASM from "@automerge/automerge-wasm" + +describe("Automerge", () => { + describe("marks", () => { + it("should allow marks that can be seen in patches", () => { + let callbacks = []; + let doc1 = Automerge.init({ + patchCallback: (patches, before, after) => + callbacks.push(patches) + }) + doc1 = Automerge.change(doc1, (d) => { + d.x = "the quick fox jumps over the lazy dog" + }) + doc1 = Automerge.change(doc1, (d) => { + Automerge.mark(d, "x", "font-weight", "[5..10]", "bold"); + }) + assert.deepStrictEqual(callbacks[1], [ + { + action: 'mark', + path: [ 'x' ], + marks: [ + { "name": "font-weight", + "range": "5..10", + "value": "bold" + } + ] + } + ]) + }) + }) +}) diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index 74adab0c..0c5fe346 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -819,6 +819,7 @@ impl Automerge { } } } + Patch::Mark { .. } => Ok(result.into()), } } @@ -878,6 +879,7 @@ impl Automerge { //Patch::SpliceText { .. } => Err(to_js_err("cannot Splice into map")), Patch::SpliceText { .. } => Err(error::ApplyPatch::SpliceTextInMap), Patch::PutSeq { .. } => Err(error::ApplyPatch::PutIdxInMap), + Patch::Mark { .. } => Err(error::ApplyPatch::MarkInMap), } } @@ -1415,6 +1417,8 @@ pub(crate) mod error { SpliceTextInMap, #[error("cannot put a seq index in a map")] PutIdxInMap, + #[error("cannot mark a span in a map")] + MarkInMap, #[error(transparent)] GetProp(#[from] GetProp), #[error(transparent)] diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 16a135db..cf3f1852 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -799,7 +799,7 @@ impl Automerge { let (obj, _) = self.import(obj)?; let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); let range = range.as_string().ok_or("range must be a string")?; - let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let cap = re.captures_iter(&range).next().ok_or(format!("(range={}) range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal",range))?; let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; let start_sticky = &cap[1] == "("; @@ -814,9 +814,7 @@ impl Automerge { self.doc .mark( &obj, - &am::marks::MarkRange::new(start, end, start_sticky, end_sticky), - &name, - value, + am::marks::Mark::new(name, value, start, end, start_sticky, end_sticky), ) .map_err(to_js_err)?; Ok(()) @@ -885,7 +883,6 @@ impl Automerge { baseline: JsValue, change_sets: JsValue, ) -> Result { - am::log!("doc.blame() is depricated - please use doc.attribute()"); self.attribute(obj, baseline, change_sets) } diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 2351c762..740ffa2b 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -6,7 +6,7 @@ use crate::{ interop::{self, alloc, js_set}, TextRepresentation, }; -use automerge::{ObjId, OpObserver, Prop, ReadDoc, ScalarValue, Value}; +use automerge::{marks::Mark, ObjId, OpObserver, Prop, ReadDoc, ScalarValue, Value}; use js_sys::{Array, Object}; use wasm_bindgen::prelude::*; @@ -97,6 +97,11 @@ pub(crate) enum Patch { index: usize, length: usize, }, + Mark { + obj: ObjId, + path: Vec<(ObjId, Prop)>, + marks: Vec, + }, } impl OpObserver for Observer { @@ -345,6 +350,17 @@ impl OpObserver for Observer { } } + fn mark>(&mut self, doc: &R, obj: ObjId, mark: M) { + if self.enabled { + if let Some(path) = self.get_path(doc, &obj) { + let marks : Vec<_> = mark.collect(); + if !marks.is_empty() { + self.patches.push(Patch::Mark { path, obj, marks }); + } + } + } + } + fn text_as_seq(&self) -> bool { self.text_rep == TextRepresentation::Array } @@ -380,6 +396,14 @@ fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array { result } +fn export_just_path(path: &[(ObjId, Prop)]) -> Array { + let result = Array::new(); + for p in path { + result.push(&prop_to_js(&p.1)); + } + result +} + impl Patch { pub(crate) fn path(&self) -> &[(ObjId, Prop)] { match &self { @@ -390,6 +414,7 @@ impl Patch { Self::SpliceText { path, .. } => path.as_slice(), Self::DeleteMap { path, .. } => path.as_slice(), Self::DeleteSeq { path, .. } => path.as_slice(), + Self::Mark { path, .. } => path.as_slice(), } } @@ -402,6 +427,7 @@ impl Patch { Self::SpliceText { obj, .. } => obj, Self::DeleteMap { obj, .. } => obj, Self::DeleteSeq { obj, .. } => obj, + Self::Mark { obj, .. } => obj, } } } @@ -513,6 +539,24 @@ impl TryFrom for JsValue { } Ok(result.into()) } + Patch::Mark { path, marks, .. } => { + js_set(&result, "action", "mark")?; + js_set(&result, "path", export_just_path(path.as_slice()))?; + let marks_array = Array::new(); + for m in marks.iter() { + let mark = Object::new(); + js_set(&mark, "name", &m.name)?; + js_set( + &mark, + "value", + &alloc(&m.value.clone().into(), TextRepresentation::String).1, + )?; + js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; + marks_array.push(&mark); + } + js_set(&result, "marks", marks_array)?; + Ok(result.into()) + } } } } diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index f0e19d98..78711a1e 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -4,6 +4,8 @@ import assert from 'assert' //@ts-ignore import { create, load, Automerge, encodeChange, decodeChange } from '..' +let util = require('util') + describe('Automerge', () => { describe('marks', () => { it('should handle marks [..]', () => { @@ -202,5 +204,94 @@ describe('Automerge', () => { assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) assert.deepStrictEqual(doc.save(), doc2.save()) }) + + it('generates patches for marks made locally', () => { + let doc : Automerge = create(true, "aabbcc") + doc.enablePatches(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + let h1 = doc.getHeads() + doc.mark(list, "[0..37]", "bold" , true) + doc.mark(list, "[4..19]", "itallic" , true) + doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + doc.commit("marks"); + let h2 = doc.getHeads() + let x = doc.attribute2(list, [], [h2]); + let patches = doc.popPatches(); + let util = require('util') + assert.deepEqual(patches, [ + { action: 'put', path: [ 'list' ], value: '' }, + { + action: 'splice', + path: [ 'list', 0 ], + value: 'the quick fox jumps over the lazy dog' + }, + { + action: 'mark', + path: [ 'list' ], + marks: [ { name: 'bold', value: true, range: '0..37' } ] + }, + { + action: 'mark', + path: [ 'list' ], + marks: [ { name: 'itallic', value: true, range: '4..19' } ] + }, + { + action: 'mark', + path: [ 'list' ], + marks: [ + { + name: 'comment', + value: 'foxes are my favorite animal!', + range: '10..13' + } + ] + } + ]); + }) + it('marks should create patches that respect marks that supersede it', () => { + + let doc1 : Automerge = create(true, "aabbcc") + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + + let doc2 = load(doc1.save(),true); + + let doc3 = load(doc1.save(),true); + doc3.enablePatches(true) + + doc1.put("/","foo", "bar"); // make a change to our op counter is higher than doc2 + doc1.mark(list, "[0..5]", "x", "a") + doc1.mark(list, "[8..11]", "x", "b") + + doc2.mark(list, "[4..13]", "x", "c"); + + doc3.merge(doc1) + doc3.merge(doc2) + + let patches = doc3.popPatches(); + + assert.deepEqual(patches, [ + { action: 'put', path: [ 'foo' ], value: 'bar' }, + { + action: 'mark', + path: [ 'list' ], + marks: [ { name: 'x', value: 'a', range: '0..5' } ] + }, + { + action: 'mark', + path: [ 'list' ], + marks: [ { name: 'x', value: 'b', range: '8..11' } ] + }, + { + action: 'mark', + path: [ 'list' ], + marks: [ + { name: 'x', value: 'c', range: '5..8' }, + { name: 'x', value: 'c', range: '11..13' }, + ] + }, + ]); + }) }) }) diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 4e1afc9f..2c5f8d62 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -1,7 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::MarkRange; +use crate::marks::Mark; use crate::op_observer::{BranchableObserver, OpObserver}; use crate::sync::SyncDoc; use crate::transaction::{CommitOptions, Transactable}; @@ -654,23 +654,10 @@ impl Transactable for AutoCommitWithObs { ) } - fn mark, V: Into>( - &mut self, - obj: O, - range: &MarkRange, - mark: &str, - value: V, - ) -> Result<(), AutomergeError> { + fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError> { self.ensure_transaction_open(); let (current, tx) = self.transaction.as_mut().unwrap(); - tx.mark( - &mut self.doc, - current.observer(), - obj.as_ref(), - range, - mark, - value, - ) + tx.mark(&mut self.doc, current.observer(), obj.as_ref(), mark) } fn unmark, M: AsRef>( diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 8c8178ac..3ab88c90 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -1074,7 +1074,14 @@ impl Automerge { }; if op.insert { - if obj_type == Some(ObjType::Text) { + if op.is_mark() { + if let OpType::MarkEnd(_) = op.action { + let q = self + .ops + .search(obj, query::SeekMark::new(op.id.prev(), pos, encoding)); + observer.mark(self, ex_obj, q.marks.into_iter()); + } + } else if obj_type == Some(ObjType::Text) { observer.splice_text(self, ex_obj, seen, op.to_str()); } else { let value = (op.value(), self.ops.id_to_exid(op.id)); @@ -1361,6 +1368,7 @@ impl ReadDoc for Automerge { .ops .search(&obj, query::Attribute::new(baseline, change_sets)); query.finish(); + log!("ATTRIBUTE query={:?}", query); Ok(query.change_sets) } diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 028c4ac4..a0c910a8 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -1,14 +1,40 @@ -#[derive(Debug, Clone)] -pub struct MarkRange { +use crate::value::ScalarValue; + +#[derive(Debug, Clone, PartialEq)] +pub struct Mark { pub start: usize, pub end: usize, pub expand_left: bool, pub expand_right: bool, + pub name: String, + pub value: ScalarValue, } -impl MarkRange { - pub fn new(start: usize, end: usize, expand_left: bool, expand_right: bool) -> Self { - MarkRange { +impl Default for Mark { + fn default() -> Self { + Mark { + name: "".into(), + value: ScalarValue::Null, + start: 0, + end: 0, + expand_left: false, + expand_right: false, + } + } +} + +impl Mark { + pub fn new>( + name: String, + value: V, + start: usize, + end: usize, + expand_left: bool, + expand_right: bool, + ) -> Self { + Mark { + name, + value: value.into(), start, end, expand_left, diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 5b33c21f..614e6bf2 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -1,4 +1,5 @@ use crate::exid::ExId; +use crate::marks::Mark; use crate::Prop; use crate::ReadDoc; use crate::Value; @@ -111,6 +112,8 @@ pub trait OpObserver { /// - `num`: the number of sequential elements deleted fn delete_seq(&mut self, doc: &R, objid: ExId, index: usize, num: usize); + fn mark>(&mut self, doc: &R, objid: ExId, mark: M); + /// Whether to call sequence methods or `splice_text` when encountering changes in text /// /// Returns `false` by default @@ -180,6 +183,8 @@ impl OpObserver for () { ) { } + fn mark>(&mut self, _doc: &R, _objid: ExId, _mark: M) {} + fn delete_map(&mut self, _doc: &R, _objid: ExId, _key: &str) {} fn delete_seq(&mut self, _doc: &R, _objid: ExId, _index: usize, _num: usize) {} @@ -282,6 +287,8 @@ impl OpObserver for VecOpObserver { } } + fn mark>(&mut self, _doc: &R, _objid: ExId, _mark: M) {} + fn delete_map(&mut self, doc: &R, obj: ExId, key: &str) { if let Ok(p) = doc.parents(&obj) { self.patches.push(Patch::Delete { diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index 92fe3b1e..d3a033f0 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -84,6 +84,18 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, self.obs2.increment(doc, objid, prop, tagged_value); } + fn mark>( + &mut self, + doc: &R, + objid: crate::ObjId, + mark: M, + ) { + let marks: Vec<_> = mark.collect(); + self.obs1 + .mark(doc, objid.clone(), marks.clone().into_iter()); + self.obs2.mark(doc, objid, marks.into_iter()); + } + fn delete_map(&mut self, doc: &R, objid: crate::ObjId, key: &str) { self.obs1.delete_map(doc, objid.clone(), key); self.obs2.delete_map(doc, objid, key); diff --git a/rust/automerge/src/query.rs b/rust/automerge/src/query.rs index b9aac2a7..36d926cc 100644 --- a/rust/automerge/src/query.rs +++ b/rust/automerge/src/query.rs @@ -30,6 +30,7 @@ mod opid_vis; mod prop; mod prop_at; mod raw_spans; +mod seek_mark; mod seek_op; mod seek_op_with_patch; mod spans; @@ -55,6 +56,7 @@ pub(crate) use opid_vis::OpIdVisSearch; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; pub(crate) use raw_spans::RawSpans; +pub(crate) use seek_mark::SeekMark; pub(crate) use seek_op::SeekOp; pub(crate) use seek_op_with_patch::SeekOpWithPatch; pub(crate) use spans::{Span, Spans}; diff --git a/rust/automerge/src/query/seek_mark.rs b/rust/automerge/src/query/seek_mark.rs new file mode 100644 index 00000000..a2aa6da8 --- /dev/null +++ b/rust/automerge/src/query/seek_mark.rs @@ -0,0 +1,156 @@ +use crate::marks::Mark; +use crate::op_tree::OpSetMetadata; +use crate::query::{QueryResult, TreeQuery}; +use crate::types::{Key, ListEncoding, MarkName, Op, OpId, OpType}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct SeekMark { + /// the mark we are looking for + id: OpId, + end: usize, + encoding: ListEncoding, + found: bool, + mark_name: MarkName, + next_mark: Mark, + pos: usize, + seen: usize, + last_seen: Option, + super_marks: HashMap, + pub(crate) marks: Vec, +} + +impl SeekMark { + pub(crate) fn new(id: OpId, end: usize, encoding: ListEncoding) -> Self { + SeekMark { + id, + encoding, + end, + found: false, + next_mark: Default::default(), + mark_name: MarkName::from_prop_index(0), + pos: 0, + seen: 0, + last_seen: None, + super_marks: Default::default(), + marks: Default::default(), + } + } + + fn count_visible(&mut self, e: &Op) { + if e.insert { + self.last_seen = None + } + if e.visible() && self.last_seen.is_none() { + self.seen += e.width(self.encoding); + self.last_seen = Some(e.elemid_or_key()) + } + } +} + +impl TreeQuery<'_> for SeekMark { + // this is missing an index - active marks + /* + fn query_node_with_metadata( + &mut self, + child: &OpTreeNode, + _m: &OpSetMetadata, + ops: &[Op], + ) -> QueryResult { + if self.found { + log!("node found decend"); + QueryResult::Descend + } else if child.index.ops.contains(&self.id) { + log!("node contains decend"); + QueryResult::Descend + } else { + self.pos += child.len(); + + let mut num_vis = child.index.visible_len(self.encoding); + if num_vis > 0 { + if let Some(last_seen) = self.last_seen { + if child.index.has_visible(&last_seen) { + num_vis -= 1; + } + } + self.seen += num_vis; + self.last_seen = Some(ops[child.last()].elemid_or_key()); + } + log!("node next"); + QueryResult::Next + } + } + */ + + fn query_element_with_metadata(&mut self, op: &Op, m: &OpSetMetadata) -> QueryResult { + match &op.action { + OpType::MarkBegin(mark) if op.id == self.id => { + if !op.succ.is_empty() { + return QueryResult::Finish; + } + self.found = true; + self.mark_name = mark.name; + // retain the name and the value + self.next_mark.name = m.props.get(mark.name.props_index()).clone(); + self.next_mark.value = mark.value.clone(); + // change id to the end id + self.id = self.id.next(); + // begin a new mark if nothing supersedes us + if self.super_marks.is_empty() { + self.next_mark.start = self.seen; + } + // remove all marks that dont match + self.super_marks.retain(|_, v| v == &mark.name); + } + OpType::MarkBegin(mark) => { + if m.lamport_cmp(op.id, self.id) == Ordering::Greater { + if self.found { + // gather marks of the same type that supersede us + if mark.name == self.mark_name { + self.super_marks.insert(op.id.next(), mark.name); + if self.super_marks.len() == 1 { + // complete a mark + self.next_mark.end = self.seen; + self.marks.push(self.next_mark.clone()); + } + } + } else { + // gather all marks until we know what our mark's name is + self.super_marks.insert(op.id.next(), mark.name); + } + } + } + OpType::MarkEnd(_) if self.end == self.pos => { + if self.super_marks.is_empty() { + // complete a mark + self.next_mark.end = self.seen; + self.marks.push(self.next_mark.clone()); + } + return QueryResult::Finish; + } + OpType::MarkEnd(_) if self.super_marks.contains_key(&op.id) => { + self.super_marks.remove(&op.id); + if self.found && self.super_marks.len() == 0 { + // begin a new mark + self.next_mark.start = self.seen; + } + } + _ => {} + } + // the end op hasn't been inserted yet so we need to work off the position + if self.end == self.pos { + if self.super_marks.is_empty() { + // complete a mark + self.next_mark.end = self.seen; + self.marks.push(self.next_mark.clone()); + } + return QueryResult::Finish; + } + + self.pos += 1; + self.count_visible(&op); + QueryResult::Next + } +} diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 7288aa47..206f5fe8 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use crate::exid::ExId; -use crate::marks::MarkRange; +use crate::marks::Mark; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -649,47 +649,40 @@ impl TransactionInner { Ok(()) } - pub(crate) fn mark>( + pub(crate) fn mark( &mut self, doc: &mut Automerge, op_observer: Option<&mut Obs>, ex_obj: &ExId, - range: &MarkRange, - mark: &str, - value: V, + mark: Mark, ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; - let mark = doc.ops_mut().m.import_markname(mark); + let mark_name = doc.ops_mut().m.import_markname(mark.name.as_ref()); if let Some(obs) = op_observer { self.do_insert( doc, Some(obs), obj, - range.start, - OpType::mark(mark, range.expand_left, value.into()), + mark.start, + OpType::mark(mark_name, mark.expand_left, mark.value.clone()), )?; self.do_insert( doc, Some(obs), obj, - range.end, - OpType::MarkEnd(range.expand_right), + mark.end, + OpType::MarkEnd(mark.expand_right), )?; + obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) } else { self.do_insert::( doc, None, obj, - range.start, - OpType::mark(mark, range.expand_left, value.into()), - )?; - self.do_insert::( - doc, - None, - obj, - range.end, - OpType::MarkEnd(range.expand_right), + mark.start, + OpType::mark(mark_name, mark.expand_left, mark.value), )?; + self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(mark.expand_right))?; } Ok(()) } diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 6d418587..a870cd04 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -1,7 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::MarkRange; +use crate::marks::Mark; use crate::op_observer::BranchableObserver; use crate::{ query, Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, @@ -358,14 +358,8 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { self.do_tx(|tx, doc, obs| tx.splice_text(doc, obs, obj.as_ref(), pos, del, text)) } - fn mark, V: Into>( - &mut self, - obj: O, - range: &MarkRange, - mark: &str, - value: V, - ) -> Result<(), AutomergeError> { - self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), range, mark, value)) + fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError> { + self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), mark)) } fn unmark, M: AsRef>( diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index a2b15f99..6f7a3d06 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -1,5 +1,5 @@ use crate::exid::ExId; -use crate::marks::MarkRange; +use crate::marks::Mark; use crate::{AutomergeError, ChangeHash, ObjType, Prop, ReadDoc, ScalarValue}; /// A way of mutating a document within a single change. @@ -89,13 +89,7 @@ pub trait Transactable: ReadDoc { text: &str, ) -> Result<(), AutomergeError>; - fn mark, V: Into>( - &mut self, - obj: O, - range: &MarkRange, - mark: &str, - value: V, - ) -> Result<(), AutomergeError>; + fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError>; fn unmark, M: AsRef>( &mut self, From d7f93c5aca7188bb869a1c38a7ab77df0d8c1e35 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 23 Feb 2023 17:17:18 -0600 Subject: [PATCH 15/26] get mark patches working on load --- javascript/src/stable.ts | 6 +- javascript/src/unstable.ts | 6 +- javascript/test/marks.ts | 41 ++- rust/automerge-wasm/index.d.ts | 15 +- rust/automerge-wasm/package.json | 1 + rust/automerge-wasm/src/observer.rs | 19 +- rust/automerge-wasm/test/marks.ts | 296 ++++++++++++++++-- rust/automerge/src/automerge.rs | 15 +- rust/automerge/src/automerge/current_state.rs | 88 +++++- rust/automerge/src/marks.rs | 162 +++++++++- rust/automerge/src/op_set.rs | 8 +- rust/automerge/src/query/raw_spans.rs | 6 +- rust/automerge/src/query/seek_mark.rs | 53 +--- rust/automerge/src/query/spans.rs | 35 +-- .../src/storage/convert/op_as_changeop.rs | 3 +- .../src/storage/convert/op_as_docop.rs | 2 +- .../src/storage/load/reconstruct_document.rs | 3 +- rust/automerge/src/transaction/inner.rs | 2 +- rust/automerge/src/types.rs | 42 +-- 19 files changed, 604 insertions(+), 199 deletions(-) diff --git a/javascript/src/stable.ts b/javascript/src/stable.ts index 19f5f9d5..c2577d09 100644 --- a/javascript/src/stable.ts +++ b/javascript/src/stable.ts @@ -518,7 +518,7 @@ export function loadIncremental( } /** - * Create binary save data to be appended to a save file or fed into {@link loadIncremental} + * Create binary save data to be appended to a save file or fed into {@link loadIncremental} * * @typeParam T - The type of the value which is contained in the document. * Note that no validation is done to make sure this type is in @@ -528,9 +528,7 @@ export function loadIncremental( * automerge save file, or passed to a document replicating its state. * */ -export function saveIncremental( - doc: Doc, -): Uint8Array { +export function saveIncremental(doc: Doc): Uint8Array { const state = _state(doc) if (state.heads) { throw new RangeError( diff --git a/javascript/src/unstable.ts b/javascript/src/unstable.ts index 38127632..2f17f327 100644 --- a/javascript/src/unstable.ts +++ b/javascript/src/unstable.ts @@ -197,7 +197,11 @@ export function load( ): Doc { const opts = importOpts(_opts) opts.enableTextV2 = true - return stable.load(data, opts) + if (opts.patchCallback) { + return stable.loadIncremental(stable.init(opts), data) + } else { + return stable.load(data, opts) + } } function importOpts( diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts index 5f4b7f47..e427c4e0 100644 --- a/javascript/test/marks.ts +++ b/javascript/test/marks.ts @@ -4,30 +4,37 @@ import * as WASM from "@automerge/automerge-wasm" describe("Automerge", () => { describe("marks", () => { - it("should allow marks that can be seen in patches", () => { - let callbacks = []; + it.only("should allow marks that can be seen in patches", () => { + let callbacks = [] let doc1 = Automerge.init({ - patchCallback: (patches, before, after) => - callbacks.push(patches) - }) - doc1 = Automerge.change(doc1, (d) => { + patchCallback: (patches, before, after) => callbacks.push(patches), + }) + doc1 = Automerge.change(doc1, d => { d.x = "the quick fox jumps over the lazy dog" }) - doc1 = Automerge.change(doc1, (d) => { - Automerge.mark(d, "x", "font-weight", "[5..10]", "bold"); + doc1 = Automerge.change(doc1, d => { + Automerge.mark(d, "x", "font-weight", "[5..10]", "bold") }) assert.deepStrictEqual(callbacks[1], [ { - action: 'mark', - path: [ 'x' ], - marks: [ - { "name": "font-weight", - "range": "5..10", - "value": "bold" - } - ] - } + action: "mark", + path: ["x"], + marks: [{ name: "font-weight", range: "5..10", value: "bold" }], + }, ]) + + callbacks = [] + + let doc2 = Automerge.init({ + patchCallback: (patches, before, after) => callbacks.push(patches), + }) + doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1)) + + assert.deepStrictEqual(callbacks[0][2], { + action: "mark", + path: ["x"], + marks: [{ name: "font-weight", range: "5..10", value: "bold" }], + }) }) }) }) diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index c6b0ace4..493f996e 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -94,7 +94,7 @@ export type Op = { pred: string[], } -export type Patch = PutPatch | DelPatch | SpliceTextPatch | IncPatch | InsertPatch; +export type Patch = PutPatch | DelPatch | SpliceTextPatch | IncPatch | InsertPatch | MarkPatch; export type PutPatch = { action: 'put' @@ -103,6 +103,12 @@ export type PutPatch = { conflict: boolean } +export type MarkPatch = { + action: 'mark' + path: Prop[], + marks: Mark[] +} + export type IncPatch = { action: 'inc' path: Prop[], @@ -127,6 +133,13 @@ export type InsertPatch = { values: Value[], } +export type Mark = { + name: string, + value: Value, + range: string, +} + + export function encodeChange(change: ChangeToEncode): Change; export function create(text_v2: boolean, actor?: Actor): Automerge; export function load(data: Uint8Array, text_v2: boolean, actor?: Actor): Automerge; diff --git a/rust/automerge-wasm/package.json b/rust/automerge-wasm/package.json index 57354ce1..aec0ee82 100644 --- a/rust/automerge-wasm/package.json +++ b/rust/automerge-wasm/package.json @@ -30,6 +30,7 @@ "scripts": { "lint": "eslint test/*.ts index.d.ts", "debug": "cross-env PROFILE=dev TARGET_DIR=debug yarn buildall", + "dev": "cross-env PROFILE=dev TARGET_DIR=debug FEATURES='' TARGET=nodejs yarn target", "build": "cross-env PROFILE=dev TARGET_DIR=debug FEATURES='' yarn buildall", "release": "cross-env PROFILE=release TARGET_DIR=release yarn buildall", "buildall": "cross-env TARGET=nodejs yarn target && cross-env TARGET=bundler yarn target && cross-env TARGET=deno yarn target", diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 740ffa2b..939f3b3f 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -352,10 +352,23 @@ impl OpObserver for Observer { fn mark>(&mut self, doc: &R, obj: ObjId, mark: M) { if self.enabled { + if let Some(Patch::Mark { + obj: tail_obj, + marks, + .. + }) = self.patches.last_mut() + { + if tail_obj == &obj { + for m in mark { + marks.push(m) + } + return; + } + } if let Some(path) = self.get_path(doc, &obj) { - let marks : Vec<_> = mark.collect(); + let marks: Vec<_> = mark.collect(); if !marks.is_empty() { - self.patches.push(Patch::Mark { path, obj, marks }); + self.patches.push(Patch::Mark { path, obj, marks }); } } } @@ -545,7 +558,7 @@ impl TryFrom for JsValue { let marks_array = Array::new(); for m in marks.iter() { let mark = Object::new(); - js_set(&mark, "name", &m.name)?; + js_set(&mark, "name", m.name.as_str())?; js_set( &mark, "value", diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 78711a1e..67bc4f0a 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -75,7 +75,6 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); }) - it('should handle marks with deleted ends [..]', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") @@ -131,7 +130,6 @@ describe('Automerge', () => { let saved = doc.save() let doc2 = load(saved,true) - //let doc2 = load(doc.save(),true) spans = doc2.spans(list); assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) @@ -156,8 +154,8 @@ describe('Automerge', () => { 'quick ', [ [ 'bold', 'boolean', true ], - [ 'itallic', 'boolean', true ], [ 'comment', 'str', 'foxes are my favorite animal!' ], + [ 'itallic', 'boolean', true ], ], 'fox', [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], @@ -222,33 +220,20 @@ describe('Automerge', () => { assert.deepEqual(patches, [ { action: 'put', path: [ 'list' ], value: '' }, { - action: 'splice', - path: [ 'list', 0 ], + action: 'splice', path: [ 'list', 0 ], value: 'the quick fox jumps over the lazy dog' }, { - action: 'mark', - path: [ 'list' ], - marks: [ { name: 'bold', value: true, range: '0..37' } ] - }, - { - action: 'mark', - path: [ 'list' ], - marks: [ { name: 'itallic', value: true, range: '4..19' } ] - }, - { - action: 'mark', - path: [ 'list' ], + action: 'mark', path: [ 'list' ], marks: [ - { - name: 'comment', - value: 'foxes are my favorite animal!', - range: '10..13' - } + { name: 'bold', value: true, range: '0..37' }, + { name: 'itallic', value: true, range: '4..19' }, + { name: 'comment', value: 'foxes are my favorite animal!', range: '10..13' } ] } ]); }) + it('marks should create patches that respect marks that supersede it', () => { let doc1 : Automerge = create(true, "aabbcc") @@ -273,20 +258,12 @@ describe('Automerge', () => { assert.deepEqual(patches, [ { action: 'put', path: [ 'foo' ], value: 'bar' }, - { - action: 'mark', - path: [ 'list' ], - marks: [ { name: 'x', value: 'a', range: '0..5' } ] - }, - { - action: 'mark', - path: [ 'list' ], - marks: [ { name: 'x', value: 'b', range: '8..11' } ] - }, { action: 'mark', path: [ 'list' ], marks: [ + { name: 'x', value: 'a', range: '0..5' }, + { name: 'x', value: 'b', range: '8..11' }, { name: 'x', value: 'c', range: '5..8' }, { name: 'x', value: 'c', range: '11..13' }, ] @@ -294,4 +271,259 @@ describe('Automerge', () => { ]); }) }) + describe('loading marks', () => { + it('a mark will appear on load', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc1.mark(list, "[5..10]", "xxx", "aaa") + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [{ + action: 'mark', path: [ 'list' ], marks: [ { name: 'xxx', value: 'aaa', range: '5..10' }], + }]); + + let doc2 : Automerge = create(true); + doc2.enablePatches(true) + doc2.loadIncremental(doc1.save()) + + let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [{ + action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', range: '5..10'}], + }]); + }) + + it('a overlapping marks will coalesse on load', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc1.mark(list, "[5..15]", "xxx", "aaa") + doc1.mark(list, "[10..20]", "xxx", "aaa") + doc1.mark(list, "[15..25]", "xxx", "aaa") + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..15' }, + { name: 'xxx', value: 'aaa', range: '10..20' }, + { name: 'xxx', value: 'aaa', range: '15..25' }, + ] }, + ]); + + let doc2 : Automerge = create(true); + doc2.enablePatches(true) + doc2.loadIncremental(doc1.save()) + + let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', range: '5..25'}] }, + ]); + }) + + it('coalesse handles different values', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc1.mark(list, "[5..15]", "xxx", "aaa") + doc1.mark(list, "[10..20]", "xxx", "bbb") + doc1.mark(list, "[15..25]", "xxx", "aaa") + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..15' }, + { name: 'xxx', value: 'bbb', range: '10..20' }, + { name: 'xxx', value: 'aaa', range: '15..25' }, + ]} + ]); + + let doc2 : Automerge = create(true); + doc2.enablePatches(true) + doc2.loadIncremental(doc1.save()) + + let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: ['list'], marks: [ + { name: 'xxx', value: 'aaa', range: '5..10'}, + { name: 'xxx', value: 'bbb', range: '10..15'}, + { name: 'xxx', value: 'aaa', range: '15..25'}, + ]}, + ]); + }) + + it('wont coalesse handles different names', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc1.mark(list, "[5..15]", "xxx", "aaa") + doc1.mark(list, "[10..20]", "yyy", "aaa") + doc1.mark(list, "[15..25]", "zzz", "aaa") + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..15' }, + { name: 'yyy', value: 'aaa', range: '10..20' }, + { name: 'zzz', value: 'aaa', range: '15..25' }, + ]} + ]); + + let doc2 : Automerge = create(true); + doc2.enablePatches(true) + doc2.loadIncremental(doc1.save()) + + let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..15' }, + { name: 'yyy', value: 'aaa', range: '10..20' }, + { name: 'zzz', value: 'aaa', range: '15..25' }, + ]} + ]); + }) + + it('coalesse handles async merge', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + + let doc2 = doc1.fork() + + doc1.put("/", "key1", "value"); // incrementing op counter so we win vs doc2 + doc1.put("/", "key2", "value"); // incrementing op counter so we win vs doc2 + doc1.mark(list, "[10..20]", "xxx", "aaa") + doc1.mark(list, "[15..25]", "xxx", "aaa") + + doc2.mark(list, "[5..30]" , "xxx", "bbb") + + doc1.merge(doc2) + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '10..20' }, + { name: 'xxx', value: 'aaa', range: '15..25' }, + { name: 'xxx', value: 'bbb', range: '5..10' }, + { name: 'xxx', value: 'bbb', range: '25..30' }, + ] + }, + ]); + + let doc3 : Automerge = create(true); + doc3.enablePatches(true) + doc3.loadIncremental(doc1.save()) + + let patches2 = doc3.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'bbb', range: '5..10' }, + { name: 'xxx', value: 'aaa', range: '10..25' }, + { name: 'xxx', value: 'bbb', range: '25..30' }, + ]} + ]); + }) + + it('does not show marks hidden in merge', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + + let doc2 = doc1.fork() + + doc1.put("/", "key1", "value"); // incrementing op counter so we win vs doc2 + doc1.put("/", "key2", "value"); // incrementing op counter so we win vs doc2 + doc1.mark(list, "[10..20]", "xxx", "aaa") + doc1.mark(list, "[15..25]", "xxx", "aaa") + + doc2.mark(list, "[11..24]" , "xxx", "bbb") + + doc1.merge(doc2) + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '10..20' }, + { name: 'xxx', value: 'aaa', range: '15..25' }, + ] + }, + ]); + + let doc3 : Automerge = create(true); + doc3.enablePatches(true) + doc3.loadIncremental(doc1.save()) + + let patches2 = doc3.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '10..25' }, + ]} + ]); + }) + + it('coalesse disconnected marks with async merge', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + + let doc2 = doc1.fork() + + doc1.put("/", "key1", "value"); // incrementing op counter so we win vs doc2 + doc1.put("/", "key2", "value"); // incrementing op counter so we win vs doc2 + doc1.mark(list, "[5..11]", "xxx", "aaa") + doc1.mark(list, "[19..25]", "xxx", "aaa") + + doc2.mark(list, "[10..20]" , "xxx", "aaa") + + doc1.merge(doc2) + + let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches1, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..11' }, + { name: 'xxx', value: 'aaa', range: '19..25' }, + { name: 'xxx', value: 'aaa', range: '11..19' }, + ] + }, + ]); + + let doc3 : Automerge = create(true); + doc3.enablePatches(true) + doc3.loadIncremental(doc1.save()) + + let patches2 = doc3.popPatches().filter((p:any) => p.action == "mark") + + assert.deepEqual(patches2, [ + { action: 'mark', path: [ 'list' ], marks: [ + { name: 'xxx', value: 'aaa', range: '5..25' }, + ]} + ]); + }) + }) }) diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 3ab88c90..b6150979 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -16,8 +16,8 @@ use crate::transaction::{ self, CommitOptions, Failure, Observed, Success, Transaction, TransactionArgs, UnObserved, }; use crate::types::{ - ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, MarkData, MarkName, - ObjId, Op, OpId, OpType, OpTypeParts, ScalarValue, TextEncoding, Value, + ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ListEncoding, MarkData, ObjId, Op, + OpId, OpType, OpTypeParts, ScalarValue, TextEncoding, Value, }; use crate::{ query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, @@ -724,9 +724,6 @@ impl Automerge { .iter() .map(|p| OpId::new(p.counter(), actors[p.actor()])); let pred = self.ops.m.sorted_opids(pred); - let mark_name = c - .mark_name - .map(|m| MarkName::from_prop_index(self.ops.m.props.cache(m.to_string()))); ( obj, Op { @@ -735,7 +732,7 @@ impl Automerge { action: c.action, value: c.val, expand: c.expand, - mark_name, + mark_name: c.mark_name, }) .unwrap(), key, @@ -993,7 +990,7 @@ impl Automerge { OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), OpType::MarkBegin(MarkData { name, value, .. }) => { - format!("mark({},{})", self.ops.m.props[name.props_index()], value) + format!("mark({},{})", name, value) } OpType::MarkEnd(_) => "/mark".to_string(), }; @@ -1352,7 +1349,7 @@ impl ReadDoc for Automerge { query::Spans::new(ListEncoding::Text(self.text_encoding)), ); query.check_marks(); - Ok(query.into_spans(&self.ops.m)) + Ok(query.spans) } fn attribute>( @@ -1398,7 +1395,7 @@ impl ReadDoc for Automerge { id: self.id_to_exid(s.id), start: s.start, end: s.end, - span_type: self.ops.m.props.get(s.name.props_index()).to_string(), + span_type: s.name.to_string(), value: s.value, }) .collect(); diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index 1c1bceed..e7d11fb7 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::HashSet, iter::Peekable}; use itertools::Itertools; use crate::{ - types::{ElemId, Key, ListEncoding, ObjId, Op, OpId}, + types::{ElemId, Key, ListEncoding, MarkData, MarkStateMachine, ObjId, Op, OpId}, ObjType, OpObserver, OpType, ScalarValue, Value, }; @@ -32,13 +32,21 @@ pub(super) fn observe_current_state(doc: &crate::Automerge, obser if !visible_objs.contains(obj) { continue; } + + // group by key + // op.insert id=(1@aaa) HEAD value=1 + // op.put id=2@aaa (1@aaa) value=3 + // op.insert id=(1@bbb) 1@aaa value=2 + let ops_by_key = ops.group_by(|o| o.key); let actions = ops_by_key .into_iter() .flat_map(|(key, key_ops)| key_actions(key, key_ops)); + let mut mark_state_machine = MarkStateMachine::new(); if typ == ObjType::Text && !observer.text_as_seq() { track_new_objs_and_notify( &mut visible_objs, + &mut mark_state_machine, doc, obj, typ, @@ -48,6 +56,7 @@ pub(super) fn observe_current_state(doc: &crate::Automerge, obser } else if typ == ObjType::List { track_new_objs_and_notify( &mut visible_objs, + &mut mark_state_machine, doc, obj, typ, @@ -55,13 +64,33 @@ pub(super) fn observe_current_state(doc: &crate::Automerge, obser list_actions(actions), ) } else { - track_new_objs_and_notify(&mut visible_objs, doc, obj, typ, observer, actions) + track_new_objs_and_notify( + &mut visible_objs, + &mut mark_state_machine, + doc, + obj, + typ, + observer, + actions, + ) + } + if !mark_state_machine.spans.is_empty() { + let encoding = match typ { + ObjType::Text => ListEncoding::Text(doc.text_encoding()), + _ => ListEncoding::List, + }; + let marks = mark_state_machine + .spans + .into_iter() + .map(|span| span.into_mark(obj, doc, encoding)); + observer.mark(doc, doc.id_to_exid(obj.0), marks); } } } fn track_new_objs_and_notify, O: OpObserver>( visible_objs: &mut HashSet, + mark_state_machine: &mut MarkStateMachine, doc: &crate::Automerge, obj: &ObjId, typ: ObjType, @@ -73,7 +102,7 @@ fn track_new_objs_and_notify, O: OpObserver>( if let Some(obj) = action.made_object() { visible_objs.insert(obj); } - action.notify_observer(doc, &exid, obj, typ, observer); + action.notify_observer(doc, mark_state_machine, &exid, obj, typ, observer); } } @@ -82,6 +111,7 @@ trait Action { fn notify_observer( self, doc: &crate::Automerge, + mark_state_machine: &mut MarkStateMachine, exid: &crate::ObjId, obj: &ObjId, typ: ObjType, @@ -92,7 +122,7 @@ trait Action { fn made_object(&self) -> Option; } -fn key_actions<'a, I: Iterator>( +fn key_actions<'a, 'b, I: Iterator>( key: Key, key_ops: I, ) -> impl Iterator> { @@ -104,12 +134,14 @@ fn key_actions<'a, I: Iterator>( conflicted: bool, }, Insert(Value<'a>, OpId), + MarkBegin(OpId, &'a MarkData), + MarkEnd(OpId), } - let current_ops = key_ops - .filter(|o| o.visible()) - .filter_map(|o| match o.action { + key_ops + .filter(|o| o.visible_or_mark()) + .filter_map(|o| match &o.action { OpType::Make(obj_type) => { - let value = Value::Object(obj_type); + let value = Value::Object(*obj_type); if o.insert { Some(CurrentOp::Insert(value, o.id)) } else { @@ -120,7 +152,7 @@ fn key_actions<'a, I: Iterator>( }) } } - OpType::Put(ref value) => { + OpType::Put(value) => { let value = Value::Scalar(Cow::Borrowed(value)); if o.insert { Some(CurrentOp::Insert(value, o.id)) @@ -132,9 +164,10 @@ fn key_actions<'a, I: Iterator>( }) } } + OpType::MarkBegin(m) => Some(CurrentOp::MarkBegin(o.id, m)), + OpType::MarkEnd(_) => Some(CurrentOp::MarkEnd(o.id)), _ => None, - }); - current_ops + }) .coalesce(|previous, current| match (previous, current) { (CurrentOp::Put { .. }, CurrentOp::Put { value, id, .. }) => Ok(CurrentOp::Put { value, @@ -157,6 +190,8 @@ fn key_actions<'a, I: Iterator>( elem_id: ElemId(id), tagged_value: (val, id), }, + CurrentOp::MarkBegin(id, data) => SimpleAction::MarkBegin { id, data }, + CurrentOp::MarkEnd(id) => SimpleAction::MarkEnd { id }, }) } @@ -171,12 +206,20 @@ enum SimpleAction<'a> { elem_id: ElemId, tagged_value: (Value<'a>, OpId), }, + MarkBegin { + id: OpId, + data: &'a MarkData, + }, + MarkEnd { + id: OpId, + }, } impl<'a> Action for SimpleAction<'a> { fn notify_observer( self, doc: &crate::Automerge, + mark_state_machine: &mut MarkStateMachine, exid: &crate::ObjId, obj: &ObjId, typ: ObjType, @@ -208,6 +251,12 @@ impl<'a> Action for SimpleAction<'a> { let tagged_value = (value, doc.id_to_exid(opid)); observer.insert(doc, doc.id_to_exid(obj.0), index, tagged_value); } + Self::MarkBegin { id, data } => { + mark_state_machine.mark_begin(id, data, doc); + } + Self::MarkEnd { id } => { + mark_state_machine.mark_end(id, doc); + } } } @@ -236,13 +285,16 @@ impl<'a> Action for TextAction<'a> { fn notify_observer( self, doc: &crate::Automerge, + mark_state_machine: &mut MarkStateMachine, exid: &crate::ObjId, obj: &ObjId, typ: ObjType, observer: &mut O, ) { match self { - Self::Action(action) => action.notify_observer(doc, exid, obj, typ, observer), + Self::Action(action) => { + action.notify_observer(doc, mark_state_machine, exid, obj, typ, observer) + } Self::Splice { start, chars } => { let index = doc .ops() @@ -340,7 +392,9 @@ impl<'a, I: Iterator>> Iterator for TextActions<'a, I> { mod tests { use std::borrow::Cow; - use crate::{transaction::Transactable, ObjType, OpObserver, Prop, ReadDoc, Value}; + use crate::{ + marks::Mark, transaction::Transactable, ObjType, OpObserver, Prop, ReadDoc, Value, + }; // Observer ops often carry a "tagged value", which is a value and the OpID of the op which // created that value. For a lot of values (i.e. any scalar value) we don't care about the @@ -566,6 +620,14 @@ mod tests { fn text_as_seq(&self) -> bool { self.text_as_seq } + + fn mark>( + &mut self, + _doc: &R, + _objid: crate::ObjId, + _mark: M, + ) { + } } #[test] diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index a0c910a8..c34abb37 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -1,4 +1,11 @@ +use smol_str::SmolStr; +use std::fmt; +use std::fmt::Display; + +use crate::types::ListEncoding; +use crate::types::{ObjId, OpId}; use crate::value::ScalarValue; +use crate::Automerge; #[derive(Debug, Clone, PartialEq)] pub struct Mark { @@ -6,7 +13,7 @@ pub struct Mark { pub end: usize, pub expand_left: bool, pub expand_right: bool, - pub name: String, + pub name: smol_str::SmolStr, pub value: ScalarValue, } @@ -33,7 +40,7 @@ impl Mark { expand_right: bool, ) -> Self { Mark { - name, + name: name.into(), value: value.into(), start, end, @@ -42,3 +49,154 @@ impl Mark { } } } + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Span { + pub(crate) start: OpId, + pub(crate) end: OpId, + pub(crate) data: MarkData, +} + +impl Span { + fn new(start: OpId, end: OpId, data: &MarkData) -> Self { + Self { + start, + end, + data: data.clone(), + } + } + + pub(crate) fn into_mark(self, obj: &ObjId, doc: &Automerge, encoding: ListEncoding) -> Mark { + let start_index = doc + .ops() + .search( + obj, + crate::query::ElemIdPos::new(self.start.into(), encoding), + ) + .index() + .unwrap(); + let end_index = doc + .ops() + .search(obj, crate::query::ElemIdPos::new(self.end.into(), encoding)) + .index() + .unwrap(); + Mark::new( + self.data.name.into(), + self.data.value, + start_index, + end_index, + false, + false, + ) + } +} + +pub(crate) struct MarkStateMachine { + state: Vec<(OpId, Span)>, + pub(crate) spans: Vec, +} + +impl MarkStateMachine { + pub(crate) fn new() -> Self { + MarkStateMachine { + state: Default::default(), + spans: Default::default(), + } + } + + pub(crate) fn mark_begin(&mut self, id: OpId, data: &MarkData, doc: &Automerge) { + let mut start = id; + let end = id; + + let index = self.find(id, doc).err(); + if index.is_none() { + return; + } + let index = index.unwrap(); + + if let Some(above) = Self::mark_above(&self.state, index, data) { + if above.data.value == data.value { + start = above.start; + } + } else if let Some(below) = Self::mark_below(&mut self.state, index, data) { + if below.data.value == data.value { + start = below.start; + } else { + self.spans.push(Span::new(below.start, id, &below.data)); + } + } + + let entry = (id, Span::new(start, end, data)); + self.state.insert(index, entry); + } + + pub(crate) fn mark_end(&mut self, id: OpId, doc: &Automerge) { + let index = self.find(id.prev(), doc).ok(); + if index.is_none() { + return; + } + let index = index.unwrap(); + + let mut span = self.state.remove(index).1; + span.end = id; + + if Self::mark_above(&self.state, index, &span.data).is_none() { + match Self::mark_below(&mut self.state, index, &span.data) { + Some(below) if below.data.value == span.data.value => {} + Some(below) => { + below.start = id; + self.spans.push(span); + } + None => { + self.spans.push(span); + } + } + } + } + + fn find(&self, target: OpId, doc: &Automerge) -> Result { + let metadata = &doc.ops().m; + self.state + .binary_search_by(|probe| metadata.lamport_cmp(probe.0, target)) + } + + fn mark_above<'a>( + state: &'a [(OpId, Span)], + index: usize, + data: &MarkData, + ) -> Option<&'a Span> { + Some( + &state[index..] + .iter() + .find(|(_, span)| span.data.name == data.name)? + .1, + ) + } + + fn mark_below<'a>( + state: &'a mut [(OpId, Span)], + index: usize, + data: &MarkData, + ) -> Option<&'a mut Span> { + Some( + &mut state[0..index] + .iter_mut() + .filter(|(_, span)| span.data.name == data.name) + .last()? + .1, + ) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: SmolStr, + pub value: ScalarValue, + pub expand: bool, +} + +impl Display for MarkData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "value={} expand={}", self.value, self.value) + } +} diff --git a/rust/automerge/src/op_set.rs b/rust/automerge/src/op_set.rs index 988afb19..aab8ce74 100644 --- a/rust/automerge/src/op_set.rs +++ b/rust/automerge/src/op_set.rs @@ -4,9 +4,7 @@ use crate::indexed_cache::IndexedCache; use crate::op_tree::{self, OpTree}; use crate::parents::Parents; use crate::query::{self, OpIdVisSearch, TreeQuery}; -use crate::types::{ - self, ActorId, Key, ListEncoding, MarkName, ObjId, Op, OpId, OpIds, OpType, Prop, -}; +use crate::types::{self, ActorId, Key, ListEncoding, ObjId, Op, OpId, OpIds, OpType, Prop}; use crate::ObjType; use fxhash::FxBuildHasher; use std::borrow::Borrow; @@ -398,10 +396,6 @@ impl OpSetMetadata { pub(crate) fn import_prop>(&mut self, key: S) -> usize { self.props.cache(key.borrow().to_string()) } - - pub(crate) fn import_markname>(&mut self, name: S) -> MarkName { - MarkName::from_prop_index(self.props.cache(name.borrow().to_string())) - } } pub(crate) struct Parent { diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index 9f97028a..03e25909 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -1,5 +1,5 @@ use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, MarkName, Op, OpId, OpType, ScalarValue}; +use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; use std::fmt::Debug; #[derive(Debug, Clone, PartialEq)] @@ -17,7 +17,7 @@ pub(crate) struct RawSpan { pub(crate) id: OpId, pub(crate) start: usize, pub(crate) end: usize, - pub(crate) name: MarkName, + pub(crate) name: smol_str::SmolStr, pub(crate) value: ScalarValue, } @@ -50,7 +50,7 @@ impl<'a> TreeQuery<'a> for RawSpans { id: element.id, start: self.seen, end: 0, - name: md.name, + name: md.name.clone(), value: md.value.clone(), }, ); diff --git a/rust/automerge/src/query/seek_mark.rs b/rust/automerge/src/query/seek_mark.rs index a2aa6da8..ccea64c0 100644 --- a/rust/automerge/src/query/seek_mark.rs +++ b/rust/automerge/src/query/seek_mark.rs @@ -1,7 +1,7 @@ use crate::marks::Mark; use crate::op_tree::OpSetMetadata; use crate::query::{QueryResult, TreeQuery}; -use crate::types::{Key, ListEncoding, MarkName, Op, OpId, OpType}; +use crate::types::{Key, ListEncoding, Op, OpId, OpType}; use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::Debug; @@ -13,12 +13,12 @@ pub(crate) struct SeekMark { end: usize, encoding: ListEncoding, found: bool, - mark_name: MarkName, + mark_name: smol_str::SmolStr, next_mark: Mark, pos: usize, seen: usize, last_seen: Option, - super_marks: HashMap, + super_marks: HashMap, pub(crate) marks: Vec, } @@ -30,7 +30,7 @@ impl SeekMark { end, found: false, next_mark: Default::default(), - mark_name: MarkName::from_prop_index(0), + mark_name: "".into(), pos: 0, seen: 0, last_seen: None, @@ -51,39 +51,6 @@ impl SeekMark { } impl TreeQuery<'_> for SeekMark { - // this is missing an index - active marks - /* - fn query_node_with_metadata( - &mut self, - child: &OpTreeNode, - _m: &OpSetMetadata, - ops: &[Op], - ) -> QueryResult { - if self.found { - log!("node found decend"); - QueryResult::Descend - } else if child.index.ops.contains(&self.id) { - log!("node contains decend"); - QueryResult::Descend - } else { - self.pos += child.len(); - - let mut num_vis = child.index.visible_len(self.encoding); - if num_vis > 0 { - if let Some(last_seen) = self.last_seen { - if child.index.has_visible(&last_seen) { - num_vis -= 1; - } - } - self.seen += num_vis; - self.last_seen = Some(ops[child.last()].elemid_or_key()); - } - log!("node next"); - QueryResult::Next - } - } - */ - fn query_element_with_metadata(&mut self, op: &Op, m: &OpSetMetadata) -> QueryResult { match &op.action { OpType::MarkBegin(mark) if op.id == self.id => { @@ -91,9 +58,9 @@ impl TreeQuery<'_> for SeekMark { return QueryResult::Finish; } self.found = true; - self.mark_name = mark.name; + self.mark_name = mark.name.clone(); // retain the name and the value - self.next_mark.name = m.props.get(mark.name.props_index()).clone(); + self.next_mark.name = mark.name.clone(); self.next_mark.value = mark.value.clone(); // change id to the end id self.id = self.id.next(); @@ -109,7 +76,7 @@ impl TreeQuery<'_> for SeekMark { if self.found { // gather marks of the same type that supersede us if mark.name == self.mark_name { - self.super_marks.insert(op.id.next(), mark.name); + self.super_marks.insert(op.id.next(), mark.name.clone()); if self.super_marks.len() == 1 { // complete a mark self.next_mark.end = self.seen; @@ -118,7 +85,7 @@ impl TreeQuery<'_> for SeekMark { } } else { // gather all marks until we know what our mark's name is - self.super_marks.insert(op.id.next(), mark.name); + self.super_marks.insert(op.id.next(), mark.name.clone()); } } } @@ -132,7 +99,7 @@ impl TreeQuery<'_> for SeekMark { } OpType::MarkEnd(_) if self.super_marks.contains_key(&op.id) => { self.super_marks.remove(&op.id); - if self.found && self.super_marks.len() == 0 { + if self.found && self.super_marks.is_empty() { // begin a new mark self.next_mark.start = self.seen; } @@ -150,7 +117,7 @@ impl TreeQuery<'_> for SeekMark { } self.pos += 1; - self.count_visible(&op); + self.count_visible(op); QueryResult::Next } } diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs index 5538d29d..9795a711 100644 --- a/rust/automerge/src/query/spans.rs +++ b/rust/automerge/src/query/spans.rs @@ -1,5 +1,5 @@ use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, ListEncoding, MarkName, Op, OpType, ScalarValue}; +use crate::types::{ElemId, ListEncoding, Op, OpType, ScalarValue}; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Debug; @@ -14,9 +14,9 @@ pub(crate) struct Spans<'a> { seen_at_this_mark: Option, seen_at_last_mark: Option, ops: Vec<&'a Op>, - marks: HashMap, + marks: HashMap, changed: bool, - spans: Vec>, + pub(crate) spans: Vec>, } #[derive(Debug, Clone, PartialEq)] @@ -25,25 +25,6 @@ pub struct Span<'a> { pub marks: Vec<(smol_str::SmolStr, Cow<'a, ScalarValue>)>, } -#[derive(Debug, Clone, PartialEq)] -struct InternalSpan<'a> { - pos: usize, - marks: Vec<(MarkName, Cow<'a, ScalarValue>)>, -} - -impl<'a> InternalSpan<'a> { - fn into_span(self, m: &OpSetMetadata) -> Span<'a> { - Span { - pos: self.pos, - marks: self - .marks - .into_iter() - .map(|(name, value)| (m.props.get(name.props_index()).into(), value)) - .collect(), - } - } -} - impl<'a> Spans<'a> { pub(crate) fn new(encoding: ListEncoding) -> Self { Spans { @@ -61,15 +42,11 @@ impl<'a> Spans<'a> { } } - pub(crate) fn into_spans(self, m: &OpSetMetadata) -> Vec> { - self.spans.into_iter().map(|s| s.into_span(m)).collect() - } - pub(crate) fn check_marks(&mut self) { let mut new_marks = HashMap::new(); for op in &self.ops { if let OpType::MarkBegin(m) = &op.action { - new_marks.insert(m.name, &m.value); + new_marks.insert(m.name.clone(), &m.value); } } if new_marks != self.marks { @@ -85,10 +62,10 @@ impl<'a> Spans<'a> { let mut marks: Vec<_> = self .marks .iter() - .map(|(key, val)| (*key, Cow::Borrowed(*val))) + .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) .collect(); marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); - self.spans.push(InternalSpan { + self.spans.push(Span { pos: self.seen, marks, }); diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index 66c4cd69..dcea5927 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -138,8 +138,7 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { fn mark_name(&self) -> Option> { if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { - let name = self.metadata.props.get(name.props_index()); - Some(Cow::Owned(name.into())) + Some(Cow::Owned(name.clone())) } else { None } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index e3b4d918..2dfdb383 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -123,7 +123,7 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { fn mark_name(&self) -> Option> { if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { - Some(Cow::Owned(self.props.get(name.props_index()).into())) + Some(Cow::Owned(name.clone())) } else { None } diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index 194f32eb..b33beed1 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -345,12 +345,11 @@ fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { return Err(Error::MissingActor); } } - let mark_name = op.mark_name.map(|n| m.import_markname(n)); let action = OpType::from_parts(OpTypeParts { action: op.action, value: op.value, expand: op.expand, - mark_name, + mark_name: op.mark_name, })?; Ok(Op { id: check_opid(m, op.id)?, diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 206f5fe8..a98b2d1a 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -657,7 +657,7 @@ impl TransactionInner { mark: Mark, ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; - let mark_name = doc.ops_mut().m.import_markname(mark.name.as_ref()); + let mark_name = mark.name.clone(); if let Some(obs) = op_observer { self.do_insert( doc, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index d93392a1..c9f14d76 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -14,6 +14,7 @@ mod opids; pub(crate) use opids::OpIds; pub(crate) use crate::clock::Clock; +pub(crate) use crate::marks::{MarkData, MarkStateMachine}; pub(crate) use crate::value::{Counter, ScalarValue, Value}; pub(crate) const HEAD: ElemId = ElemId(OpId(0, 0)); @@ -202,24 +203,11 @@ pub enum OpType { MarkEnd(bool), } -#[derive(PartialEq, Debug, Clone)] -pub struct MarkData { - pub name: MarkName, - pub value: ScalarValue, - pub expand: bool, -} - -impl Display for MarkData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "value={} expand={}", self.value, self.value) - } -} - pub(crate) struct OpTypeParts { pub(crate) action: u64, pub(crate) value: ScalarValue, pub(crate) expand: bool, - pub(crate) mark_name: Option, + pub(crate) mark_name: Option, } impl OpType { @@ -239,7 +227,7 @@ impl OpType { } } - pub(crate) fn mark(name: MarkName, expand: bool, value: ScalarValue) -> OpType { + pub(crate) fn mark(name: smol_str::SmolStr, expand: bool, value: ScalarValue) -> OpType { OpType::MarkBegin(MarkData { name, value, @@ -426,20 +414,6 @@ pub(crate) enum Key { Seq(ElemId), } -// Index of a mark name string in the OpSetMetadata::props IndexedCache -#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)] -pub struct MarkName(usize); - -impl MarkName { - pub(crate) fn props_index(&self) -> usize { - self.0 - } - - pub(crate) fn from_prop_index(index: usize) -> Self { - Self(index) - } -} - /// A property of an object /// /// This is either a string representing a property in a map, or an integer @@ -656,6 +630,16 @@ impl Op { } } + pub(crate) fn visible_or_mark(&self) -> bool { + if self.is_inc() { + false + } else if self.is_counter() { + self.succ.len() <= self.incs() + } else { + self.succ.is_empty() + } + } + pub(crate) fn incs(&self) -> usize { if let OpType::Put(ScalarValue::Counter(Counter { increments, .. })) = &self.action { *increments From 2c6e54390bcc796fadb254216b78c344ed4e89b1 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 24 Feb 2023 00:04:01 -0600 Subject: [PATCH 16/26] rewrote observe_current_state - added get_marks() api call - cleaned up js interface --- javascript/test/marks.ts | 2 +- rust/automerge-wasm/index.d.ts | 11 +- rust/automerge-wasm/src/lib.rs | 19 +- rust/automerge-wasm/test/marks.ts | 116 ++--- rust/automerge/src/autocommit.rs | 4 + rust/automerge/src/automerge.rs | 29 ++ rust/automerge/src/automerge/current_state.rs | 492 ++++++------------ rust/automerge/src/marks.rs | 143 ++--- rust/automerge/src/op_set.rs | 4 + rust/automerge/src/read.rs | 7 +- .../src/transaction/manual_transaction.rs | 4 + rust/automerge/src/types.rs | 9 +- 12 files changed, 337 insertions(+), 503 deletions(-) diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts index e427c4e0..ed6e4543 100644 --- a/javascript/test/marks.ts +++ b/javascript/test/marks.ts @@ -4,7 +4,7 @@ import * as WASM from "@automerge/automerge-wasm" describe("Automerge", () => { describe("marks", () => { - it.only("should allow marks that can be seen in patches", () => { + it("should allow marks that can be seen in patches", () => { let callbacks = [] let doc1 = Automerge.init({ patchCallback: (patches, before, after) => callbacks.push(patches), diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index 493f996e..a01400eb 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -139,6 +139,13 @@ export type Mark = { range: string, } +export type RawMark = { + id: ObjID, + type: string, + value: Value, + start: number, + end: number, +} export function encodeChange(change: ChangeToEncode): Change; export function create(text_v2: boolean, actor?: Actor): Automerge; @@ -181,8 +188,8 @@ export class Automerge { // marks mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; unmark(obj: ObjID, mark: ObjID): void; - spans(obj: ObjID): any; - raw_spans(obj: ObjID): any; + marks(obj: ObjID): Mark[]; + rawMarks(obj: ObjID): RawMark[]; blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index cf3f1852..50bd2a3b 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -827,6 +827,22 @@ impl Automerge { Ok(()) } + pub fn marks(&mut self, obj: JsValue) -> Result { + let (obj, _) = self.import(obj)?; + let marks = self.doc.get_marks(obj).map_err(to_js_err)?; + let result = Array::new(); + for m in marks { + let mark = Object::new(); + let (_datatype, value) = alloc(&m.value.into(), self.text_rep); + js_set(&mark, "name", m.name.as_str())?; + js_set(&mark, "value", value)?; + //js_set(&mark, "datatype",datatype)?; + js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; + result.push(&mark.into()); + } + Ok(result.into()) + } + pub fn spans(&mut self, obj: JsValue) -> Result { let (obj, _) = self.import(obj)?; let text = self.doc.text(&obj)?; @@ -865,7 +881,8 @@ impl Automerge { Ok(result.into()) } - pub fn raw_spans(&mut self, obj: JsValue) -> Result { + #[wasm_bindgen(js_name = rawMarks)] + pub fn raw_marks(&mut self, obj: JsValue) -> Result { let (obj, _) = self.import(obj)?; let spans = self.doc.raw_spans(obj).map_err(to_js_err)?; let result = Array::new(); diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 67bc4f0a..55128a27 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -14,12 +14,12 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) let text = doc.text(list) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "4..7" }]) }) it('should handle marks [..] at the beginning of a string', () => { @@ -27,15 +27,15 @@ describe('Automerge', () => { let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) let doc2 = doc.fork() doc2.insert(list, 0, "A") doc2.insert(list, 4, "B") doc.merge(doc2) - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "1..4" }]) }) it('should handle marks [..] with splice', () => { @@ -43,15 +43,15 @@ describe('Automerge', () => { let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) let doc2 = doc.fork() doc2.splice(list, 0, 2, "AAA") doc2.splice(list, 4, 0, "BBB") doc.merge(doc2) - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..4" }]) }) it('should handle marks across multiple forks', () => { @@ -59,8 +59,8 @@ describe('Automerge', () => { let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) let doc2 = doc.fork() doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. @@ -71,8 +71,8 @@ describe('Automerge', () => { doc.merge(doc2) doc.merge(doc3) - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) }) it('should handle marks with deleted ends [..]', () => { @@ -81,18 +81,18 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..3" }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..4" }]) }) it('should handle sticky marks (..)', () => { @@ -100,12 +100,12 @@ describe('Automerge', () => { let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..8" }]) }) it('should handle sticky marks with deleted ends (..)', () => { @@ -113,25 +113,25 @@ describe('Automerge', () => { let list = doc.putObject("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) - let spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..3" }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") - spans = doc.spans(list); - assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..5" }]) // make sure save/load can handle marks let saved = doc.save() let doc2 = load(saved,true) - spans = doc2.spans(list); - assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + marks = doc2.marks(list); + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..5" }]) assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) assert.deepStrictEqual(doc.save(), doc2.save()) @@ -145,30 +145,16 @@ describe('Automerge', () => { doc.mark(list, "[4..19]", "itallic" , true) doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") doc.commit("marks"); - let spans = doc.spans(list); - assert.deepStrictEqual(spans, - [ - [ [ 'bold', 'boolean', true ] ], - 'the ', - [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], - 'quick ', - [ - [ 'bold', 'boolean', true ], - [ 'comment', 'str', 'foxes are my favorite animal!' ], - [ 'itallic', 'boolean', true ], - ], - 'fox', - [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], - ' jumps', - [ [ 'bold', 'boolean', true ] ], - ' over the lazy dog', - [], - ] - ) + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [ + { name: 'comment', range: '10..13', value: 'foxes are my favorite animal!' }, + { name: 'itallic', range: '4..19', value: true }, + { name: 'bold', range: '0..37', value: true } + ]) let text = doc.text(list); assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); - let raw_spans = doc.raw_spans(list); - assert.deepStrictEqual(raw_spans, + let rawMarks = doc.rawMarks(list); + assert.deepStrictEqual(rawMarks, [ { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, @@ -176,8 +162,8 @@ describe('Automerge', () => { ]); doc.unmark(list, "41@aabbcc") - raw_spans = doc.raw_spans(list); - assert.deepStrictEqual(raw_spans, + rawMarks = doc.rawMarks(list); + assert.deepStrictEqual(rawMarks, [ { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } @@ -185,8 +171,8 @@ describe('Automerge', () => { // mark sure encode decode can handle marks doc.unmark(list, "39@aabbcc") - raw_spans = doc.raw_spans(list); - assert.deepStrictEqual(raw_spans, + rawMarks = doc.rawMarks(list); + assert.deepStrictEqual(rawMarks, [ { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } ]); @@ -199,7 +185,7 @@ describe('Automerge', () => { let doc2 = create(true); doc2.applyChanges(encoded) - assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.marks(list) , doc2.marks(list)) assert.deepStrictEqual(doc.save(), doc2.save()) }) @@ -434,13 +420,15 @@ describe('Automerge', () => { let patches2 = doc3.popPatches().filter((p:any) => p.action == "mark") - assert.deepEqual(patches2, [ - { action: 'mark', path: [ 'list' ], marks: [ + let marks = doc3.marks(list) + + assert.deepEqual(marks, [ { name: 'xxx', value: 'bbb', range: '5..10' }, { name: 'xxx', value: 'aaa', range: '10..25' }, { name: 'xxx', value: 'bbb', range: '25..30' }, - ]} ]); + + assert.deepEqual(patches2, [{ action: 'mark', path: [ 'list' ], marks }]); }) it('does not show marks hidden in merge', () => { diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 2c5f8d62..dcbf99fa 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -448,6 +448,10 @@ impl ReadDoc for AutoCommitWithObs { self.doc.object_type(obj) } + fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + self.doc.get_marks(obj) + } + fn text>(&self, obj: O) -> Result { self.doc.text(obj) } diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index b6150979..9cb258c5 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -4,10 +4,13 @@ use std::fmt::Debug; use std::num::NonZeroU64; use std::ops::RangeBounds; +use itertools::Itertools; + use crate::change_graph::ChangeGraph; use crate::columnar::Key as EncodedKey; use crate::exid::ExId; use crate::keys::Keys; +use crate::marks::{Mark, MarkStateMachine}; use crate::op_observer::{BranchableObserver, OpObserver}; use crate::op_set::OpSet; use crate::parents::Parents; @@ -1342,6 +1345,32 @@ impl ReadDoc for Automerge { Ok(buffer) } + fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + let (obj, obj_type) = self.exid_to_obj(obj.as_ref())?; + let encoding = ListEncoding::new(obj_type, self.text_encoding); + let ops_by_key = self.ops().iter_ops(&obj).group_by(|o| o.elemid_or_key()); + let mut pos = 0; + let mut marks = MarkStateMachine::default(); + + Ok(ops_by_key + .into_iter() + .filter_map(|(_key, key_ops)| { + key_ops + .filter(|o| o.visible_or_mark()) + .last() + .and_then(|o| match &o.action { + OpType::Make(_) | OpType::Put(_) => { + pos += o.width(encoding); + None + } + OpType::MarkBegin(data) => marks.mark_begin(o.id, pos, data, self), + OpType::MarkEnd(_) => marks.mark_end(o.id, pos, self), + OpType::Increment(_) | OpType::Delete => None, + }) + }) + .collect()) + } + fn spans>(&self, obj: O) -> Result>, AutomergeError> { let obj = self.exid_to_obj(obj.as_ref())?.0; let mut query = self.ops.search( diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index e7d11fb7..73e24bb9 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -1,12 +1,27 @@ -use std::{borrow::Cow, collections::HashSet, iter::Peekable}; +use std::borrow::Cow; use itertools::Itertools; use crate::{ - types::{ElemId, Key, ListEncoding, MarkData, MarkStateMachine, ObjId, Op, OpId}, - ObjType, OpObserver, OpType, ScalarValue, Value, + marks::{Mark, MarkStateMachine}, + types::{Key, ListEncoding, ObjId, Op, OpId, Prop}, + Automerge, ObjType, OpObserver, OpType, Value, }; +#[derive(Debug, Default)] +struct TextState { + text: String, + len: usize, + marks: MarkStateMachine, + finished: Vec, +} + +struct Put<'a> { + value: Value<'a>, + key: Key, + id: OpId, +} + /// Traverse the "current" state of the document, notifying `observer` /// /// The "current" state of the document is the set of visible operations. This function will @@ -17,7 +32,8 @@ use crate::{ /// /// Due to only notifying of visible operations the observer will only be called with `put`, /// `insert`, and `splice`, operations. -pub(super) fn observe_current_state(doc: &crate::Automerge, observer: &mut O) { + +pub(crate) fn observe_current_state(doc: &Automerge, observer: &mut O) { // The OpSet already exposes operations in the order they appear in the document. // `OpSet::iter_objs` iterates over the objects in causal order, this means that parent objects // will always appear before their children. Furthermore, the operations within each object are @@ -26,366 +42,164 @@ pub(super) fn observe_current_state(doc: &crate::Automerge, obser // Effectively then we iterate over each object, then we group the operations in the object by // key and for each key find the visible operations for that key. Then we notify the observer // for each of those visible operations. - let mut visible_objs = HashSet::new(); - visible_objs.insert(ObjId::root()); for (obj, typ, ops) in doc.ops().iter_objs() { - if !visible_objs.contains(obj) { - continue; - } - - // group by key - // op.insert id=(1@aaa) HEAD value=1 - // op.put id=2@aaa (1@aaa) value=3 - // op.insert id=(1@bbb) 1@aaa value=2 - - let ops_by_key = ops.group_by(|o| o.key); - let actions = ops_by_key - .into_iter() - .flat_map(|(key, key_ops)| key_actions(key, key_ops)); - let mut mark_state_machine = MarkStateMachine::new(); if typ == ObjType::Text && !observer.text_as_seq() { - track_new_objs_and_notify( - &mut visible_objs, - &mut mark_state_machine, - doc, - obj, - typ, - observer, - text_actions(actions), - ) - } else if typ == ObjType::List { - track_new_objs_and_notify( - &mut visible_objs, - &mut mark_state_machine, - doc, - obj, - typ, - observer, - list_actions(actions), - ) + observe_text(doc, observer, obj, ops) + } else if typ.is_sequence() { + observe_list(doc, observer, obj, ops); } else { - track_new_objs_and_notify( - &mut visible_objs, - &mut mark_state_machine, - doc, - obj, - typ, - observer, - actions, - ) - } - if !mark_state_machine.spans.is_empty() { - let encoding = match typ { - ObjType::Text => ListEncoding::Text(doc.text_encoding()), - _ => ListEncoding::List, - }; - let marks = mark_state_machine - .spans - .into_iter() - .map(|span| span.into_mark(obj, doc, encoding)); - observer.mark(doc, doc.id_to_exid(obj.0), marks); + observe_map(doc, observer, obj, ops); } } } -fn track_new_objs_and_notify, O: OpObserver>( - visible_objs: &mut HashSet, - mark_state_machine: &mut MarkStateMachine, - doc: &crate::Automerge, - obj: &ObjId, - typ: ObjType, +fn observe_text<'a, I: Iterator, O: OpObserver>( + doc: &'a Automerge, observer: &mut O, - actions: I, + obj: &ObjId, + ops: I, ) { let exid = doc.id_to_exid(obj.0); - for action in actions { - if let Some(obj) = action.made_object() { - visible_objs.insert(obj); - } - action.notify_observer(doc, mark_state_machine, &exid, obj, typ, observer); - } + let ops_by_key = ops.group_by(|o| o.elemid_or_key()); + let encoding = ListEncoding::Text(doc.text_encoding()); + let state = TextState::default(); + let state = ops_by_key + .into_iter() + .fold(state, |mut state, (_key, key_ops)| { + if let Some(o) = key_ops.filter(|o| o.visible_or_mark()).last() { + match &o.action { + OpType::Make(_) | OpType::Put(_) => { + state.text.push_str(o.to_str()); + state.len += o.width(encoding); + } + OpType::MarkBegin(data) => { + if let Some(mark) = state.marks.mark_begin(o.id, state.len, data, doc) { + state.finished.push(mark); + } + } + OpType::MarkEnd(_) => { + if let Some(mark) = state.marks.mark_end(o.id, state.len, doc) { + state.finished.push(mark); + } + } + OpType::Increment(_) | OpType::Delete => {} + } + } + state + }); + observer.splice_text(doc, exid.clone(), 0, state.text.as_str()); + observer.mark(doc, exid, state.finished.into_iter()); } -trait Action { - /// Notify an observer of whatever this action does - fn notify_observer( - self, - doc: &crate::Automerge, - mark_state_machine: &mut MarkStateMachine, - exid: &crate::ObjId, - obj: &ObjId, - typ: ObjType, - observer: &mut O, - ); - - /// If this action created an object, return the ID of that object - fn made_object(&self) -> Option; +fn observe_list<'a, I: Iterator, O: OpObserver>( + doc: &'a Automerge, + observer: &mut O, + obj: &ObjId, + ops: I, +) { + let exid = doc.id_to_exid(obj.0); + let mut marks = MarkStateMachine::default(); + let ops_by_key = ops.group_by(|o| o.elemid_or_key()); + let mut len = 0; + let mut finished = Vec::new(); + ops_by_key + .into_iter() + .filter_map(|(_key, key_ops)| { + key_ops + .filter(|o| o.visible_or_mark()) + .filter_map(|o| match &o.action { + OpType::Make(obj_type) => Some((Value::Object(*obj_type), o.id)), + OpType::Put(value) => Some((Value::Scalar(Cow::Borrowed(value)), o.id)), + OpType::MarkBegin(data) => { + if let Some(mark) = marks.mark_begin(o.id, len, data, doc) { + // side effect + finished.push(mark) + } + None + } + OpType::MarkEnd(_) => { + if let Some(mark) = marks.mark_end(o.id, len, doc) { + // side effect + finished.push(mark) + } + None + } + _ => None, + }) + .enumerate() + .last() + .map(|value| { + let pos = len; + len += 1; // increment - side effect + (pos, value) + }) + }) + .for_each(|(index, (val_enum, (value, opid)))| { + let tagged_value = (value, doc.id_to_exid(opid)); + if val_enum == 0 { + observer.insert(doc, exid.clone(), index, tagged_value); + } else { + observer.insert(doc, exid.clone(), index, tagged_value.clone()); + observer.put(doc, exid.clone(), Prop::Seq(index), tagged_value, true); + // cant insert with conflict flag - fixme + } + }); + observer.mark(doc, exid, finished.into_iter()); } -fn key_actions<'a, 'b, I: Iterator>( - key: Key, - key_ops: I, -) -> impl Iterator> { - #[derive(Clone)] - enum CurrentOp<'a> { - Put { - value: Value<'a>, - id: OpId, - conflicted: bool, - }, - Insert(Value<'a>, OpId), - MarkBegin(OpId, &'a MarkData), - MarkEnd(OpId), - } +fn observe_map_key<'a, I: Iterator>( + (key, key_ops): (Key, I), +) -> Option<(usize, Put<'a>)> { key_ops - .filter(|o| o.visible_or_mark()) + .filter(|o| o.visible()) .filter_map(|o| match &o.action { OpType::Make(obj_type) => { let value = Value::Object(*obj_type); - if o.insert { - Some(CurrentOp::Insert(value, o.id)) - } else { - Some(CurrentOp::Put { - value, - id: o.id, - conflicted: false, - }) - } + Some(Put { + value, + key, + id: o.id, + }) } OpType::Put(value) => { let value = Value::Scalar(Cow::Borrowed(value)); - if o.insert { - Some(CurrentOp::Insert(value, o.id)) - } else { - Some(CurrentOp::Put { - value, - id: o.id, - conflicted: false, - }) - } + Some(Put { + value, + key, + id: o.id, + }) } - OpType::MarkBegin(m) => Some(CurrentOp::MarkBegin(o.id, m)), - OpType::MarkEnd(_) => Some(CurrentOp::MarkEnd(o.id)), _ => None, }) - .coalesce(|previous, current| match (previous, current) { - (CurrentOp::Put { .. }, CurrentOp::Put { value, id, .. }) => Ok(CurrentOp::Put { - value, - id, - conflicted: true, - }), - (previous, current) => Err((previous, current)), + .enumerate() + .last() +} + +fn observe_map<'a, I: Iterator, O: OpObserver>( + doc: &'a Automerge, + observer: &mut O, + obj: &ObjId, + ops: I, +) { + let exid = doc.id_to_exid(obj.0); + let ops_by_key = ops.group_by(|o| o.key); + ops_by_key + .into_iter() + .filter_map(observe_map_key) + .filter_map(|(i, put)| { + let tagged_value = (put.value, doc.id_to_exid(put.id)); + let prop = doc + .ops() + .m + .props + .safe_get(put.key.prop_index()?) + .map(|s| Prop::Map(s.to_string()))?; + let conflict = i > 0; + Some((tagged_value, prop, conflict)) }) - .map(move |op| match op { - CurrentOp::Put { - value, - id, - conflicted, - } => SimpleAction::Put { - prop: key, - tagged_value: (value, id), - conflict: conflicted, - }, - CurrentOp::Insert(val, id) => SimpleAction::Insert { - elem_id: ElemId(id), - tagged_value: (val, id), - }, - CurrentOp::MarkBegin(id, data) => SimpleAction::MarkBegin { id, data }, - CurrentOp::MarkEnd(id) => SimpleAction::MarkEnd { id }, - }) -} - -/// Either a "put" or "insert" action. i.e. not splicing for text values -enum SimpleAction<'a> { - Put { - prop: Key, - tagged_value: (Value<'a>, OpId), - conflict: bool, - }, - Insert { - elem_id: ElemId, - tagged_value: (Value<'a>, OpId), - }, - MarkBegin { - id: OpId, - data: &'a MarkData, - }, - MarkEnd { - id: OpId, - }, -} - -impl<'a> Action for SimpleAction<'a> { - fn notify_observer( - self, - doc: &crate::Automerge, - mark_state_machine: &mut MarkStateMachine, - exid: &crate::ObjId, - obj: &ObjId, - typ: ObjType, - observer: &mut O, - ) { - let encoding = match typ { - ObjType::Text => ListEncoding::Text(doc.text_encoding()), - _ => ListEncoding::List, - }; - match self { - Self::Put { - prop, - tagged_value, - conflict, - } => { - let tagged_value = (tagged_value.0, doc.id_to_exid(tagged_value.1)); - let prop = doc.ops().export_key(*obj, prop, encoding).unwrap(); - observer.put(doc, exid.clone(), prop, tagged_value, conflict); - } - Self::Insert { - elem_id, - tagged_value: (value, opid), - } => { - let index = doc - .ops() - .search(obj, crate::query::ElemIdPos::new(elem_id, encoding)) - .index() - .unwrap(); - let tagged_value = (value, doc.id_to_exid(opid)); - observer.insert(doc, doc.id_to_exid(obj.0), index, tagged_value); - } - Self::MarkBegin { id, data } => { - mark_state_machine.mark_begin(id, data, doc); - } - Self::MarkEnd { id } => { - mark_state_machine.mark_end(id, doc); - } - } - } - - fn made_object(&self) -> Option { - match self { - Self::Put { - tagged_value: (Value::Object(_), id), - .. - } => Some((*id).into()), - Self::Insert { - tagged_value: (Value::Object(_), id), - .. - } => Some((*id).into()), - _ => None, - } - } -} - -/// An `Action` which splices for text values -enum TextAction<'a> { - Action(SimpleAction<'a>), - Splice { start: ElemId, chars: String }, -} - -impl<'a> Action for TextAction<'a> { - fn notify_observer( - self, - doc: &crate::Automerge, - mark_state_machine: &mut MarkStateMachine, - exid: &crate::ObjId, - obj: &ObjId, - typ: ObjType, - observer: &mut O, - ) { - match self { - Self::Action(action) => { - action.notify_observer(doc, mark_state_machine, exid, obj, typ, observer) - } - Self::Splice { start, chars } => { - let index = doc - .ops() - .search( - obj, - crate::query::ElemIdPos::new( - start, - ListEncoding::Text(doc.text_encoding()), - ), - ) - .index() - .unwrap(); - observer.splice_text(doc, doc.id_to_exid(obj.0), index, chars.as_str()); - } - } - } - - fn made_object(&self) -> Option { - match self { - Self::Action(action) => action.made_object(), - _ => None, - } - } -} - -fn list_actions<'a, I: Iterator>>( - actions: I, -) -> impl Iterator> { - actions.map(|a| match a { - SimpleAction::Put { - prop: Key::Seq(elem_id), - tagged_value, - .. - } => SimpleAction::Insert { - elem_id, - tagged_value, - }, - a => a, - }) -} - -/// Condense consecutive `SimpleAction::Insert` actions into one `TextAction::Splice` -fn text_actions<'a, I>(actions: I) -> impl Iterator> -where - I: Iterator>, -{ - TextActions { - ops: actions.peekable(), - } -} - -struct TextActions<'a, I: Iterator>> { - ops: Peekable, -} - -impl<'a, I: Iterator>> Iterator for TextActions<'a, I> { - type Item = TextAction<'a>; - - fn next(&mut self) -> Option { - if let Some(SimpleAction::Insert { .. }) = self.ops.peek() { - let (start, value) = match self.ops.next() { - Some(SimpleAction::Insert { - tagged_value: (value, opid), - .. - }) => (opid, value), - _ => unreachable!(), - }; - let mut chars = match value { - Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => s.to_string(), - _ => "\u{fffc}".to_string(), - }; - while let Some(SimpleAction::Insert { .. }) = self.ops.peek() { - if let Some(SimpleAction::Insert { - tagged_value: (value, _), - .. - }) = self.ops.next() - { - match value { - Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => chars.push_str(s), - _ => chars.push('\u{fffc}'), - } - } - } - Some(TextAction::Splice { - start: ElemId(start), - chars, - }) - } else { - self.ops.next().map(TextAction::Action) - } - } + .for_each(|(tagged_value, prop, conflict)| { + observer.put(doc, exid.clone(), prop, tagged_value, conflict); + }); } #[cfg(test)] diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index c34abb37..6ffc64b5 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -2,8 +2,7 @@ use smol_str::SmolStr; use std::fmt; use std::fmt::Display; -use crate::types::ListEncoding; -use crate::types::{ObjId, OpId}; +use crate::types::OpId; use crate::value::ScalarValue; use crate::Automerge; @@ -48,110 +47,77 @@ impl Mark { expand_right, } } -} -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Span { - pub(crate) start: OpId, - pub(crate) end: OpId, - pub(crate) data: MarkData, -} - -impl Span { - fn new(start: OpId, end: OpId, data: &MarkData) -> Self { - Self { + pub(crate) fn from_data(start: usize, end: usize, data: &MarkData) -> Self { + Mark { + name: data.name.clone(), + value: data.value.clone(), start, end, - data: data.clone(), + expand_left: false, + expand_right: false, } } - - pub(crate) fn into_mark(self, obj: &ObjId, doc: &Automerge, encoding: ListEncoding) -> Mark { - let start_index = doc - .ops() - .search( - obj, - crate::query::ElemIdPos::new(self.start.into(), encoding), - ) - .index() - .unwrap(); - let end_index = doc - .ops() - .search(obj, crate::query::ElemIdPos::new(self.end.into(), encoding)) - .index() - .unwrap(); - Mark::new( - self.data.name.into(), - self.data.value, - start_index, - end_index, - false, - false, - ) - } } +#[derive(Debug, Clone, PartialEq, Default)] pub(crate) struct MarkStateMachine { - state: Vec<(OpId, Span)>, - pub(crate) spans: Vec, + state: Vec<(OpId, Mark)>, } impl MarkStateMachine { - pub(crate) fn new() -> Self { - MarkStateMachine { - state: Default::default(), - spans: Default::default(), - } - } + pub(crate) fn mark_begin( + &mut self, + id: OpId, + pos: usize, + data: &MarkData, + doc: &Automerge, + ) -> Option { + let mut result = None; + let index = self.find(id, doc).err()?; - pub(crate) fn mark_begin(&mut self, id: OpId, data: &MarkData, doc: &Automerge) { - let mut start = id; - let end = id; + let mut mark = Mark::from_data(pos, pos, data); - let index = self.find(id, doc).err(); - if index.is_none() { - return; - } - let index = index.unwrap(); - - if let Some(above) = Self::mark_above(&self.state, index, data) { - if above.data.value == data.value { - start = above.start; + if let Some(above) = Self::mark_above(&self.state, index, &mark) { + if above.value == mark.value { + mark.start = above.start; } - } else if let Some(below) = Self::mark_below(&mut self.state, index, data) { - if below.data.value == data.value { - start = below.start; + } else if let Some(below) = Self::mark_below(&mut self.state, index, &mark) { + if below.value == mark.value { + mark.start = below.start; } else { - self.spans.push(Span::new(below.start, id, &below.data)); + let mut m = below.clone(); + m.end = pos; + result = Some(m); } } - let entry = (id, Span::new(start, end, data)); - self.state.insert(index, entry); + self.state.insert(index, (id, mark)); + + result } - pub(crate) fn mark_end(&mut self, id: OpId, doc: &Automerge) { - let index = self.find(id.prev(), doc).ok(); - if index.is_none() { - return; - } - let index = index.unwrap(); + pub(crate) fn mark_end(&mut self, id: OpId, pos: usize, doc: &Automerge) -> Option { + let mut result = None; + let index = self.find(id.prev(), doc).ok()?; - let mut span = self.state.remove(index).1; - span.end = id; + let mut mark = self.state.remove(index).1; + mark.end = pos; - if Self::mark_above(&self.state, index, &span.data).is_none() { - match Self::mark_below(&mut self.state, index, &span.data) { - Some(below) if below.data.value == span.data.value => {} + if Self::mark_above(&self.state, index, &mark).is_none() { + match Self::mark_below(&mut self.state, index, &mark) { + Some(below) if below.value == mark.value => {} Some(below) => { - below.start = id; - self.spans.push(span); + below.start = pos; + result = Some(mark.clone()); } None => { - self.spans.push(span); + result = Some(mark.clone()); } } } + + result } fn find(&self, target: OpId, doc: &Automerge) -> Result { @@ -160,28 +126,19 @@ impl MarkStateMachine { .binary_search_by(|probe| metadata.lamport_cmp(probe.0, target)) } - fn mark_above<'a>( - state: &'a [(OpId, Span)], - index: usize, - data: &MarkData, - ) -> Option<&'a Span> { - Some( - &state[index..] - .iter() - .find(|(_, span)| span.data.name == data.name)? - .1, - ) + fn mark_above<'a>(state: &'a [(OpId, Mark)], index: usize, mark: &Mark) -> Option<&'a Mark> { + Some(&state[index..].iter().find(|(_, m)| m.name == mark.name)?.1) } fn mark_below<'a>( - state: &'a mut [(OpId, Span)], + state: &'a mut [(OpId, Mark)], index: usize, - data: &MarkData, - ) -> Option<&'a mut Span> { + mark: &Mark, + ) -> Option<&'a mut Mark> { Some( &mut state[0..index] .iter_mut() - .filter(|(_, span)| span.data.name == data.name) + .filter(|(_, m)| m.name == mark.name) .last()? .1, ) diff --git a/rust/automerge/src/op_set.rs b/rust/automerge/src/op_set.rs index aab8ce74..cd697abd 100644 --- a/rust/automerge/src/op_set.rs +++ b/rust/automerge/src/op_set.rs @@ -78,6 +78,10 @@ impl OpSetInternal { } } + pub(crate) fn iter_ops(&self, obj: &ObjId) -> impl Iterator { + self.trees.get(obj).map(|o| o.iter()).into_iter().flatten() + } + pub(crate) fn parents(&self, obj: ObjId) -> Parents<'_> { Parents { obj, ops: self } } diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 624b5973..95c2bda2 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -1,7 +1,7 @@ use crate::{ error::AutomergeError, exid::ExId, keys::Keys, keys_at::KeysAt, list_range::ListRange, - list_range_at::ListRangeAt, map_range::MapRange, map_range_at::MapRangeAt, parents::Parents, - query, values::Values, Change, ChangeHash, ObjType, Prop, Value, + list_range_at::ListRangeAt, map_range::MapRange, map_range_at::MapRangeAt, marks::Mark, + parents::Parents, query, values::Values, Change, ChangeHash, ObjType, Prop, Value, }; use std::ops::RangeBounds; @@ -129,6 +129,9 @@ pub trait ReadDoc { /// Get the type of this object, if it is an object. fn object_type>(&self, obj: O) -> Result; + /// Get all marks on a current sequence + fn get_marks>(&self, obj: O) -> Result, AutomergeError>; + /// Get the string represented by the given text object. fn text>(&self, obj: O) -> Result; diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index a870cd04..56c5a03f 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -192,6 +192,10 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { self.doc.text_at(obj, heads) } + fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + self.doc.get_marks(obj) + } + fn get, P: Into>( &self, obj: O, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index c9f14d76..3bcb247f 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -14,7 +14,7 @@ mod opids; pub(crate) use opids::OpIds; pub(crate) use crate::clock::Clock; -pub(crate) use crate::marks::{MarkData, MarkStateMachine}; +pub(crate) use crate::marks::MarkData; pub(crate) use crate::value::{Counter, ScalarValue, Value}; pub(crate) const HEAD: ElemId = ElemId(OpId(0, 0)); @@ -445,6 +445,13 @@ impl Display for Prop { } impl Key { + pub(crate) fn prop_index(&self) -> Option { + match self { + Key::Map(n) => Some(*n), + Key::Seq(_) => None, + } + } + pub(crate) fn elemid(&self) -> Option { match self { Key::Map(_) => None, From 9a6840392e1481b36cd185bc9c6a0ebb0f13ed77 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 24 Feb 2023 12:26:11 -0600 Subject: [PATCH 17/26] let insert take a conflict flag --- rust/automerge-wasm/src/observer.rs | 2 ++ rust/automerge/src/automerge.rs | 4 ++-- rust/automerge/src/automerge/current_state.rs | 10 +++------- rust/automerge/src/op_observer.rs | 6 ++++++ rust/automerge/src/op_observer/compose.rs | 5 +++-- rust/automerge/src/transaction/inner.rs | 6 +++--- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 939f3b3f..5f6d4dd4 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -111,6 +111,7 @@ impl OpObserver for Observer { obj: ObjId, index: usize, tagged_value: (Value<'_>, ObjId), + _conflict: bool, ) { if self.enabled { let value = (tagged_value.0.to_owned(), tagged_value.1); @@ -153,6 +154,7 @@ impl OpObserver for Observer { Value::Scalar(Cow::Owned(ScalarValue::Str(c.to_string().into()))), ObjId::Root, // We hope this is okay ), + false, ); } return; diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 532e02d3..5430263f 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -1084,7 +1084,7 @@ impl Automerge { observer.splice_text(self, ex_obj, seen, op.to_str()); } else { let value = (op.value(), self.ops.id_to_exid(op.id)); - observer.insert(self, ex_obj, seen, value); + observer.insert(self, ex_obj, seen, value, false); } } else if op.is_delete() { if let Some(winner) = &values.last() { @@ -1116,7 +1116,7 @@ impl Automerge { .unwrap_or(false); let value = (op.value(), self.ops.id_to_exid(op.id)); if op.is_list_op() && !had_value_before { - observer.insert(self, ex_obj, seen, value); + observer.insert(self, ex_obj, seen, value, false); } else if just_conflict { observer.flag_conflict(self, ex_obj, key); } else { diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index 73e24bb9..f4acd014 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -136,13 +136,8 @@ fn observe_list<'a, I: Iterator, O: OpObserver>( }) .for_each(|(index, (val_enum, (value, opid)))| { let tagged_value = (value, doc.id_to_exid(opid)); - if val_enum == 0 { - observer.insert(doc, exid.clone(), index, tagged_value); - } else { - observer.insert(doc, exid.clone(), index, tagged_value.clone()); - observer.put(doc, exid.clone(), Prop::Seq(index), tagged_value, true); - // cant insert with conflict flag - fixme - } + let conflict = val_enum > 0; + observer.insert(doc, exid.clone(), index, tagged_value, conflict); }); observer.mark(doc, exid, finished.into_iter()); } @@ -358,6 +353,7 @@ mod tests { objid: crate::ObjId, index: usize, tagged_value: (crate::Value<'_>, crate::ObjId), + _conflict: bool, ) { self.ops.push(ObserverCall::Insert { obj: objid, diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 614e6bf2..984ad2c6 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -22,6 +22,7 @@ pub trait OpObserver { objid: ExId, index: usize, tagged_value: (Value<'_>, ExId), + conflict: bool, ); /// Some text has been spliced into a text object @@ -149,6 +150,7 @@ impl OpObserver for () { _objid: ExId, _index: usize, _tagged_value: (Value<'_>, ExId), + _conflict: bool, ) { } @@ -216,6 +218,7 @@ impl OpObserver for VecOpObserver { obj: ExId, index: usize, (value, id): (Value<'_>, ExId), + conflict: bool, ) { if let Ok(p) = doc.parents(&obj) { self.patches.push(Patch::Insert { @@ -223,6 +226,7 @@ impl OpObserver for VecOpObserver { path: p.path(), index, value: (value.into_owned(), id), + conflict, }); } } @@ -361,6 +365,8 @@ pub enum Patch { index: usize, /// The value that was inserted, and the id of the operation that inserted it there. value: (Value<'static>, ExId), + /// the inserted value has a conflict - only possible to be true on document load + conflict: bool, }, /// Splicing a text object Splice { diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index d3a033f0..cbfb2253 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -19,10 +19,11 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, objid: crate::ObjId, index: usize, tagged_value: (crate::Value<'_>, crate::ObjId), + conflict: bool, ) { self.obs1 - .insert(doc, objid.clone(), index, tagged_value.clone()); - self.obs2.insert(doc, objid, index, tagged_value); + .insert(doc, objid.clone(), index, tagged_value.clone(), conflict); + self.obs2.insert(doc, objid, index, tagged_value, conflict); } fn splice_text( diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index a98b2d1a..a1f70443 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -639,7 +639,7 @@ impl TransactionInner { for (offset, v) in values.iter().enumerate() { let op = &self.operations[start + offset].1; let value = (v.clone().into(), doc.ops().id_to_exid(op.id)); - obs.insert(doc, ex_obj.clone(), index + offset, value) + obs.insert(doc, ex_obj.clone(), index + offset, value, false) } } } @@ -749,13 +749,13 @@ impl TransactionInner { match (obj_type, prop) { (Some(ObjType::List), Prop::Seq(index)) => { let value = (op.value(), doc.ops().id_to_exid(op.id)); - op_observer.insert(doc, ex_obj, index, value) + op_observer.insert(doc, ex_obj, index, value, false) } (Some(ObjType::Text), Prop::Seq(index)) => { // FIXME if op_observer.text_as_seq() { let value = (op.value(), doc.ops().id_to_exid(op.id)); - op_observer.insert(doc, ex_obj, index, value) + op_observer.insert(doc, ex_obj, index, value, false) } else { op_observer.splice_text(doc, ex_obj, index, op.to_str()) } From 3beccfb5ee45574dfabdaecc3c4b8c89cf539fcd Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 24 Feb 2023 13:38:10 -0600 Subject: [PATCH 18/26] track mark data by reference --- rust/automerge-wasm/src/lib.rs | 7 +- rust/automerge-wasm/src/observer.rs | 12 +-- rust/automerge/src/autocommit.rs | 17 ++- rust/automerge/src/automerge.rs | 6 +- rust/automerge/src/automerge/current_state.rs | 12 +-- rust/automerge/src/marks.rs | 102 +++++++++--------- rust/automerge/src/op_observer.rs | 23 +++- rust/automerge/src/op_observer/compose.rs | 4 +- rust/automerge/src/query/raw_spans.rs | 2 +- rust/automerge/src/query/seek_mark.rs | 55 +++++----- rust/automerge/src/query/spans.rs | 4 +- rust/automerge/src/read.rs | 2 +- .../src/storage/convert/op_as_changeop.rs | 6 +- .../src/storage/convert/op_as_docop.rs | 9 +- rust/automerge/src/transaction/inner.rs | 21 ++-- .../src/transaction/manual_transaction.rs | 11 +- .../automerge/src/transaction/transactable.rs | 7 +- rust/automerge/src/types.rs | 24 ++--- rust/automerge/src/value.rs | 6 ++ 19 files changed, 184 insertions(+), 146 deletions(-) diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 50bd2a3b..993c446a 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -814,7 +814,8 @@ impl Automerge { self.doc .mark( &obj, - am::marks::Mark::new(name, value, start, end, start_sticky, end_sticky), + am::marks::Mark::new(name, value, start, end), + (start_sticky, end_sticky), ) .map_err(to_js_err)?; Ok(()) @@ -833,8 +834,8 @@ impl Automerge { let result = Array::new(); for m in marks { let mark = Object::new(); - let (_datatype, value) = alloc(&m.value.into(), self.text_rep); - js_set(&mark, "name", m.name.as_str())?; + let (_datatype, value) = alloc(&m.value().clone().into(), self.text_rep); + js_set(&mark, "name", m.name())?; js_set(&mark, "value", value)?; //js_set(&mark, "datatype",datatype)?; js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 5f6d4dd4..393328c1 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -100,7 +100,7 @@ pub(crate) enum Patch { Mark { obj: ObjId, path: Vec<(ObjId, Prop)>, - marks: Vec, + marks: Vec>, }, } @@ -352,7 +352,7 @@ impl OpObserver for Observer { } } - fn mark>(&mut self, doc: &R, obj: ObjId, mark: M) { + fn mark<'a, R: ReadDoc, M: Iterator>>(&mut self, doc: &'a R, obj: ObjId, mark: M) { if self.enabled { if let Some(Patch::Mark { obj: tail_obj, @@ -362,13 +362,13 @@ impl OpObserver for Observer { { if tail_obj == &obj { for m in mark { - marks.push(m) + marks.push(m.into_owned()) } return; } } if let Some(path) = self.get_path(doc, &obj) { - let marks: Vec<_> = mark.collect(); + let marks: Vec<_> = mark.map(|m| m.into_owned()).collect(); if !marks.is_empty() { self.patches.push(Patch::Mark { path, obj, marks }); } @@ -560,11 +560,11 @@ impl TryFrom for JsValue { let marks_array = Array::new(); for m in marks.iter() { let mark = Object::new(); - js_set(&mark, "name", m.name.as_str())?; + js_set(&mark, "name", m.name())?; js_set( &mark, "value", - &alloc(&m.value.clone().into(), TextRepresentation::String).1, + &alloc(&m.value().into(), TextRepresentation::String).1, )?; js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; marks_array.push(&mark); diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index dcbf99fa..c8ced527 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -448,7 +448,7 @@ impl ReadDoc for AutoCommitWithObs { self.doc.object_type(obj) } - fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { self.doc.get_marks(obj) } @@ -658,10 +658,21 @@ impl Transactable for AutoCommitWithObs { ) } - fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError> { + fn mark>( + &mut self, + obj: O, + mark: Mark<'_>, + expand: (bool, bool), + ) -> Result<(), AutomergeError> { self.ensure_transaction_open(); let (current, tx) = self.transaction.as_mut().unwrap(); - tx.mark(&mut self.doc, current.observer(), obj.as_ref(), mark) + tx.mark( + &mut self.doc, + current.observer(), + obj.as_ref(), + mark, + expand, + ) } fn unmark, M: AsRef>( diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 5430263f..58cf64a4 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -991,7 +991,7 @@ impl Automerge { OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), - OpType::MarkBegin(MarkData { name, value, .. }) => { + OpType::MarkBegin(_, MarkData { name, value }) => { format!("mark({},{})", name, value) } OpType::MarkEnd(_) => "/mark".to_string(), @@ -1344,7 +1344,7 @@ impl ReadDoc for Automerge { Ok(buffer) } - fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { let (obj, obj_type) = self.exid_to_obj(obj.as_ref())?; let encoding = ListEncoding::new(obj_type, self.text_encoding); let ops_by_key = self.ops().iter_ops(&obj).group_by(|o| o.elemid_or_key()); @@ -1362,7 +1362,7 @@ impl ReadDoc for Automerge { pos += o.width(encoding); None } - OpType::MarkBegin(data) => marks.mark_begin(o.id, pos, data, self), + OpType::MarkBegin(_, data) => marks.mark_begin(o.id, pos, data, self), OpType::MarkEnd(_) => marks.mark_end(o.id, pos, self), OpType::Increment(_) | OpType::Delete => None, }) diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index f4acd014..ec8226ef 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -9,11 +9,11 @@ use crate::{ }; #[derive(Debug, Default)] -struct TextState { +struct TextState<'a> { text: String, len: usize, - marks: MarkStateMachine, - finished: Vec, + marks: MarkStateMachine<'a>, + finished: Vec>, } struct Put<'a> { @@ -72,7 +72,7 @@ fn observe_text<'a, I: Iterator, O: OpObserver>( state.text.push_str(o.to_str()); state.len += o.width(encoding); } - OpType::MarkBegin(data) => { + OpType::MarkBegin(_, data) => { if let Some(mark) = state.marks.mark_begin(o.id, state.len, data, doc) { state.finished.push(mark); } @@ -110,7 +110,7 @@ fn observe_list<'a, I: Iterator, O: OpObserver>( .filter_map(|o| match &o.action { OpType::Make(obj_type) => Some((Value::Object(*obj_type), o.id)), OpType::Put(value) => Some((Value::Scalar(Cow::Borrowed(value)), o.id)), - OpType::MarkBegin(data) => { + OpType::MarkBegin(_, data) => { if let Some(mark) = marks.mark_begin(o.id, len, data, doc) { // side effect finished.push(mark) @@ -431,7 +431,7 @@ mod tests { self.text_as_seq } - fn mark>( + fn mark<'a, R: ReadDoc, M: Iterator>>( &mut self, _doc: &R, _objid: crate::ObjId, diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 6ffc64b5..6e3297be 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -5,85 +5,81 @@ use std::fmt::Display; use crate::types::OpId; use crate::value::ScalarValue; use crate::Automerge; +use std::borrow::Cow; #[derive(Debug, Clone, PartialEq)] -pub struct Mark { +pub struct Mark<'a> { pub start: usize, pub end: usize, - pub expand_left: bool, - pub expand_right: bool, - pub name: smol_str::SmolStr, - pub value: ScalarValue, + pub(crate) data: Cow<'a, MarkData>, } -impl Default for Mark { - fn default() -> Self { - Mark { - name: "".into(), - value: ScalarValue::Null, - start: 0, - end: 0, - expand_left: false, - expand_right: false, - } - } -} - -impl Mark { +impl<'a> Mark<'a> { pub fn new>( name: String, value: V, start: usize, end: usize, - expand_left: bool, - expand_right: bool, - ) -> Self { + ) -> Mark<'static> { Mark { - name: name.into(), - value: value.into(), + data: Cow::Owned(MarkData { + name: name.into(), + value: value.into(), + }), start, end, - expand_left, - expand_right, } } - pub(crate) fn from_data(start: usize, end: usize, data: &MarkData) -> Self { + pub(crate) fn from_data(start: usize, end: usize, data: &MarkData) -> Mark<'_> { Mark { - name: data.name.clone(), - value: data.value.clone(), + data: Cow::Borrowed(data), start, end, - expand_left: false, - expand_right: false, } } + + pub fn into_owned(self) -> Mark<'static> { + Mark { + data: Cow::Owned(self.data.into_owned()), + start: self.start, + end: self.end, + } + } + + pub fn name(&self) -> &str { + self.data.name.as_str() + } + + pub fn value(&self) -> &ScalarValue { + &self.data.value + } } #[derive(Debug, Clone, PartialEq, Default)] -pub(crate) struct MarkStateMachine { - state: Vec<(OpId, Mark)>, +pub(crate) struct MarkStateMachine<'a> { + state: Vec<(OpId, Mark<'a>)>, } -impl MarkStateMachine { +impl<'a> MarkStateMachine<'a> { pub(crate) fn mark_begin( &mut self, id: OpId, pos: usize, - data: &MarkData, + data: &'a MarkData, doc: &Automerge, - ) -> Option { + ) -> Option> { let mut result = None; let index = self.find(id, doc).err()?; let mut mark = Mark::from_data(pos, pos, data); if let Some(above) = Self::mark_above(&self.state, index, &mark) { - if above.value == mark.value { + if above.value() == mark.value() { mark.start = above.start; } } else if let Some(below) = Self::mark_below(&mut self.state, index, &mark) { - if below.value == mark.value { + if below.value() == mark.value() { mark.start = below.start; } else { let mut m = below.clone(); @@ -97,7 +93,7 @@ impl MarkStateMachine { result } - pub(crate) fn mark_end(&mut self, id: OpId, pos: usize, doc: &Automerge) -> Option { + pub(crate) fn mark_end(&mut self, id: OpId, pos: usize, doc: &Automerge) -> Option> { let mut result = None; let index = self.find(id.prev(), doc).ok()?; @@ -106,7 +102,7 @@ impl MarkStateMachine { if Self::mark_above(&self.state, index, &mark).is_none() { match Self::mark_below(&mut self.state, index, &mark) { - Some(below) if below.value == mark.value => {} + Some(below) if below.value() == mark.value() => {} Some(below) => { below.start = pos; result = Some(mark.clone()); @@ -126,19 +122,28 @@ impl MarkStateMachine { .binary_search_by(|probe| metadata.lamport_cmp(probe.0, target)) } - fn mark_above<'a>(state: &'a [(OpId, Mark)], index: usize, mark: &Mark) -> Option<&'a Mark> { - Some(&state[index..].iter().find(|(_, m)| m.name == mark.name)?.1) + fn mark_above<'b>( + state: &'b [(OpId, Mark<'a>)], + index: usize, + mark: &Mark<'a>, + ) -> Option<&'b Mark<'a>> { + Some( + &state[index..] + .iter() + .find(|(_, m)| m.name() == mark.name())? + .1, + ) } - fn mark_below<'a>( - state: &'a mut [(OpId, Mark)], + fn mark_below<'b>( + state: &'b mut [(OpId, Mark<'a>)], index: usize, - mark: &Mark, - ) -> Option<&'a mut Mark> { + mark: &Mark<'a>, + ) -> Option<&'b mut Mark<'a>> { Some( &mut state[0..index] .iter_mut() - .filter(|(_, m)| m.name == mark.name) + .filter(|(_, m)| m.data.name == mark.data.name) .last()? .1, ) @@ -149,11 +154,10 @@ impl MarkStateMachine { pub struct MarkData { pub name: SmolStr, pub value: ScalarValue, - pub expand: bool, } impl Display for MarkData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "value={} expand={}", self.value, self.value) + write!(f, "name={} value={}", self.name, self.value) } } diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 984ad2c6..2294e417 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -113,7 +113,12 @@ pub trait OpObserver { /// - `num`: the number of sequential elements deleted fn delete_seq(&mut self, doc: &R, objid: ExId, index: usize, num: usize); - fn mark>(&mut self, doc: &R, objid: ExId, mark: M); + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + objid: ExId, + mark: M, + ); /// Whether to call sequence methods or `splice_text` when encountering changes in text /// @@ -185,7 +190,13 @@ impl OpObserver for () { ) { } - fn mark>(&mut self, _doc: &R, _objid: ExId, _mark: M) {} + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + _doc: &'a R, + _objid: ExId, + _mark: M, + ) { + } fn delete_map(&mut self, _doc: &R, _objid: ExId, _key: &str) {} @@ -291,7 +302,13 @@ impl OpObserver for VecOpObserver { } } - fn mark>(&mut self, _doc: &R, _objid: ExId, _mark: M) {} + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + _doc: &'a R, + _objid: ExId, + _mark: M, + ) { + } fn delete_map(&mut self, doc: &R, obj: ExId, key: &str) { if let Ok(p) = doc.parents(&obj) { diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index cbfb2253..64b5cbbc 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -85,9 +85,9 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, self.obs2.increment(doc, objid, prop, tagged_value); } - fn mark>( + fn mark<'b, R: crate::ReadDoc, M: Iterator>>( &mut self, - doc: &R, + doc: &'b R, objid: crate::ObjId, mark: M, ) { diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index 03e25909..5a388c67 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -39,7 +39,7 @@ impl<'a> TreeQuery<'a> for RawSpans { // find location to insert // mark or set if element.succ.is_empty() { - if let OpType::MarkBegin(md) = &element.action { + if let OpType::MarkBegin(_, md) = &element.action { let pos = self .spans .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) diff --git a/rust/automerge/src/query/seek_mark.rs b/rust/automerge/src/query/seek_mark.rs index ccea64c0..c5834ab8 100644 --- a/rust/automerge/src/query/seek_mark.rs +++ b/rust/automerge/src/query/seek_mark.rs @@ -7,29 +7,29 @@ use std::collections::HashMap; use std::fmt::Debug; #[derive(Debug, Clone, PartialEq)] -pub(crate) struct SeekMark { +pub(crate) struct SeekMark<'a> { /// the mark we are looking for id: OpId, end: usize, encoding: ListEncoding, found: bool, mark_name: smol_str::SmolStr, - next_mark: Mark, + next_mark: Option>, pos: usize, seen: usize, last_seen: Option, super_marks: HashMap, - pub(crate) marks: Vec, + pub(crate) marks: Vec>, } -impl SeekMark { +impl<'a> SeekMark<'a> { pub(crate) fn new(id: OpId, end: usize, encoding: ListEncoding) -> Self { SeekMark { id, encoding, end, found: false, - next_mark: Default::default(), + next_mark: None, mark_name: "".into(), pos: 0, seen: 0, @@ -50,37 +50,32 @@ impl SeekMark { } } -impl TreeQuery<'_> for SeekMark { - fn query_element_with_metadata(&mut self, op: &Op, m: &OpSetMetadata) -> QueryResult { +impl<'a> TreeQuery<'a> for SeekMark<'a> { + fn query_element_with_metadata(&mut self, op: &'a Op, m: &OpSetMetadata) -> QueryResult { match &op.action { - OpType::MarkBegin(mark) if op.id == self.id => { + OpType::MarkBegin(_, data) if op.id == self.id => { if !op.succ.is_empty() { return QueryResult::Finish; } self.found = true; - self.mark_name = mark.name.clone(); + self.mark_name = data.name.clone(); // retain the name and the value - self.next_mark.name = mark.name.clone(); - self.next_mark.value = mark.value.clone(); + self.next_mark = Some(Mark::from_data(self.seen, self.seen, data)); // change id to the end id self.id = self.id.next(); - // begin a new mark if nothing supersedes us - if self.super_marks.is_empty() { - self.next_mark.start = self.seen; - } // remove all marks that dont match - self.super_marks.retain(|_, v| v == &mark.name); + self.super_marks.retain(|_, v| v == &data.name); } - OpType::MarkBegin(mark) => { + OpType::MarkBegin(_, mark) => { if m.lamport_cmp(op.id, self.id) == Ordering::Greater { - if self.found { + if let Some(next_mark) = &mut self.next_mark { // gather marks of the same type that supersede us if mark.name == self.mark_name { self.super_marks.insert(op.id.next(), mark.name.clone()); if self.super_marks.len() == 1 { // complete a mark - self.next_mark.end = self.seen; - self.marks.push(self.next_mark.clone()); + next_mark.end = self.seen; + self.marks.push(next_mark.clone()); } } } else { @@ -92,16 +87,20 @@ impl TreeQuery<'_> for SeekMark { OpType::MarkEnd(_) if self.end == self.pos => { if self.super_marks.is_empty() { // complete a mark - self.next_mark.end = self.seen; - self.marks.push(self.next_mark.clone()); + if let Some(next_mark) = &mut self.next_mark { + next_mark.end = self.seen; + self.marks.push(next_mark.clone()); + } } return QueryResult::Finish; } OpType::MarkEnd(_) if self.super_marks.contains_key(&op.id) => { self.super_marks.remove(&op.id); - if self.found && self.super_marks.is_empty() { - // begin a new mark - self.next_mark.start = self.seen; + if let Some(next_mark) = &mut self.next_mark { + if self.super_marks.is_empty() { + // begin a new mark + next_mark.start = self.seen; + } } } _ => {} @@ -110,8 +109,10 @@ impl TreeQuery<'_> for SeekMark { if self.end == self.pos { if self.super_marks.is_empty() { // complete a mark - self.next_mark.end = self.seen; - self.marks.push(self.next_mark.clone()); + if let Some(next_mark) = &mut self.next_mark { + next_mark.end = self.seen; + self.marks.push(next_mark.clone()); + } } return QueryResult::Finish; } diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs index 9795a711..4aeea0df 100644 --- a/rust/automerge/src/query/spans.rs +++ b/rust/automerge/src/query/spans.rs @@ -45,7 +45,7 @@ impl<'a> Spans<'a> { pub(crate) fn check_marks(&mut self) { let mut new_marks = HashMap::new(); for op in &self.ops { - if let OpType::MarkBegin(m) = &op.action { + if let OpType::MarkBegin(_, m) = &op.action { new_marks.insert(m.name.clone(), &m.value); } } @@ -84,7 +84,7 @@ impl<'a> TreeQuery<'a> for Spans<'a> { // find location to insert // mark or set if element.succ.is_empty() { - if let OpType::MarkBegin(_) = &element.action { + if let OpType::MarkBegin(_, _) = &element.action { let pos = self .ops .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 95c2bda2..3596e033 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -130,7 +130,7 @@ pub trait ReadDoc { fn object_type>(&self, obj: O) -> Result; /// Get all marks on a current sequence - fn get_marks>(&self, obj: O) -> Result, AutomergeError>; + fn get_marks>(&self, obj: O) -> Result>, AutomergeError>; /// Get the string represented by the given text object. fn text>(&self, obj: O) -> Result; diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index dcea5927..c08fa063 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -98,7 +98,7 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { } OpType::Increment(i) => Cow::Owned(ScalarValue::Int(*i)), OpType::Put(s) => Cow::Borrowed(s), - OpType::MarkBegin(MarkData { value, .. }) => Cow::Borrowed(value), + OpType::MarkBegin(_, MarkData { value, .. }) => Cow::Borrowed(value), } } @@ -132,12 +132,12 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { fn expand(&self) -> bool { matches!( self.op.action, - OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(true) + OpType::MarkBegin(true, _) | OpType::MarkEnd(true) ) } fn mark_name(&self) -> Option> { - if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { + if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { Some(Cow::Owned(name.clone())) } else { None diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index 2dfdb383..ecff7a9c 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -90,7 +90,7 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { match &self.op.action { OpType::Put(v) => Cow::Borrowed(v), OpType::Increment(i) => Cow::Owned(ScalarValue::Int(*i)), - OpType::MarkBegin(MarkData { value, .. }) => Cow::Borrowed(value), + OpType::MarkBegin(_, MarkData { value, .. }) => Cow::Borrowed(value), _ => Cow::Owned(ScalarValue::Null), } } @@ -112,17 +112,16 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { } fn expand(&self) -> bool { - if let OpType::MarkBegin(MarkData { expand, .. }) | OpType::MarkEnd(expand) = - &self.op.action - { + if let OpType::MarkBegin(expand, _) | OpType::MarkEnd(expand) = &self.op.action { *expand } else { false } } + // FIXME fn mark_name(&self) -> Option> { - if let OpType::MarkBegin(MarkData { name, .. }) = &self.op.action { + if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { Some(Cow::Owned(name.clone())) } else { None diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index a1f70443..33eddc57 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -654,25 +654,22 @@ impl TransactionInner { doc: &mut Automerge, op_observer: Option<&mut Obs>, ex_obj: &ExId, - mark: Mark, + mark: Mark<'_>, + (expand_left, expand_right): (bool, bool), ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; - let mark_name = mark.name.clone(); + let mark_name = mark.name().into(); + let mark_value = mark.value().clone(); + // FIXME if let Some(obs) = op_observer { self.do_insert( doc, Some(obs), obj, mark.start, - OpType::mark(mark_name, mark.expand_left, mark.value.clone()), - )?; - self.do_insert( - doc, - Some(obs), - obj, - mark.end, - OpType::MarkEnd(mark.expand_right), + OpType::mark(mark_name, expand_left, mark_value), )?; + self.do_insert(doc, Some(obs), obj, mark.end, OpType::MarkEnd(expand_right))?; obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) } else { self.do_insert::( @@ -680,9 +677,9 @@ impl TransactionInner { None, obj, mark.start, - OpType::mark(mark_name, mark.expand_left, mark.value), + OpType::mark(mark_name, expand_left, mark_value), )?; - self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(mark.expand_right))?; + self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(expand_right))?; } Ok(()) } diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 56c5a03f..9a8a659e 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -192,7 +192,7 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { self.doc.text_at(obj, heads) } - fn get_marks>(&self, obj: O) -> Result, AutomergeError> { + fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { self.doc.get_marks(obj) } @@ -362,8 +362,13 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { self.do_tx(|tx, doc, obs| tx.splice_text(doc, obs, obj.as_ref(), pos, del, text)) } - fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError> { - self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), mark)) + fn mark>( + &mut self, + obj: O, + mark: Mark<'_>, + expand: (bool, bool), + ) -> Result<(), AutomergeError> { + self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), mark, expand)) } fn unmark, M: AsRef>( diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index 6f7a3d06..d8401891 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -89,7 +89,12 @@ pub trait Transactable: ReadDoc { text: &str, ) -> Result<(), AutomergeError>; - fn mark>(&mut self, obj: O, mark: Mark) -> Result<(), AutomergeError>; + fn mark>( + &mut self, + obj: O, + mark: Mark<'_>, + expand: (bool, bool), + ) -> Result<(), AutomergeError>; fn unmark, M: AsRef>( &mut self, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 3bcb247f..34bd8fb6 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -199,7 +199,7 @@ pub enum OpType { Delete, Increment(i64), Put(ScalarValue), - MarkBegin(MarkData), + MarkBegin(bool, MarkData), MarkEnd(bool), } @@ -223,16 +223,12 @@ impl OpType { Self::Make(ObjType::Text) => 4, Self::Increment(_) => 5, Self::Make(ObjType::Table) => 6, - Self::MarkBegin(_) | Self::MarkEnd(_) => 7, + Self::MarkBegin(_, _) | Self::MarkEnd(_) => 7, } } pub(crate) fn mark(name: smol_str::SmolStr, expand: bool, value: ScalarValue) -> OpType { - OpType::MarkBegin(MarkData { - name, - value, - expand, - }) + OpType::MarkBegin(expand, MarkData { name, value }) } pub(crate) fn from_parts( @@ -256,11 +252,7 @@ impl OpType { }, 6 => Ok(Self::Make(ObjType::Table)), 7 => match mark_name { - Some(name) => Ok(Self::MarkBegin(MarkData { - name, - value, - expand, - })), + Some(name) => Ok(Self::MarkBegin(expand, MarkData { name, value })), None => Ok(Self::MarkEnd(expand)), }, other => Err(error::InvalidOpType::UnknownAction(other)), @@ -668,14 +660,14 @@ impl Op { } pub(crate) fn is_mark(&self) -> bool { - matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) + matches!(&self.action, OpType::MarkBegin(_, _) | OpType::MarkEnd(_)) } pub(crate) fn valid_mark_anchor(&self) -> bool { self.succ.is_empty() && matches!( &self.action, - OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) + OpType::MarkBegin(true, _) | OpType::MarkEnd(false) ) } @@ -715,7 +707,7 @@ impl Op { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), - OpType::MarkBegin(mark) => { + OpType::MarkBegin(_, mark) => { Value::Scalar(Cow::Owned(format!("markBegin={}", mark.value).into())) } OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), @@ -739,7 +731,7 @@ impl Op { OpType::Make(obj) => format!("make{}", obj), OpType::Increment(val) => format!("inc:{}", val), OpType::Delete => "del".to_string(), - OpType::MarkBegin(_) => "markBegin".to_string(), + OpType::MarkBegin(_, _) => "markBegin".to_string(), OpType::MarkEnd(_) => "markEnd".to_string(), } } diff --git a/rust/automerge/src/value.rs b/rust/automerge/src/value.rs index be128787..22a24c2b 100644 --- a/rust/automerge/src/value.rs +++ b/rust/automerge/src/value.rs @@ -341,6 +341,12 @@ impl<'a> From for Value<'a> { } } +impl<'a> From<&'a ScalarValue> for Value<'a> { + fn from(v: &'a ScalarValue) -> Self { + Value::Scalar(Cow::Borrowed(v)) + } +} + #[derive(Deserialize, Serialize, PartialEq, Debug, Clone, Copy)] pub(crate) enum DataType { #[serde(rename = "counter")] From 01c721e640eb61207499b65a9b138fec7ed357ac Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 24 Feb 2023 13:54:41 -0600 Subject: [PATCH 19/26] remove a fixme --- rust/automerge-wasm/src/observer.rs | 7 ++++++- rust/automerge/src/marks.rs | 10 +++++----- rust/automerge/src/transaction/inner.rs | 8 ++------ rust/automerge/src/types.rs | 4 ---- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 393328c1..c3f1f80e 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -352,7 +352,12 @@ impl OpObserver for Observer { } } - fn mark<'a, R: ReadDoc, M: Iterator>>(&mut self, doc: &'a R, obj: ObjId, mark: M) { + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + obj: ObjId, + mark: M, + ) { if self.enabled { if let Some(Patch::Mark { obj: tail_obj, diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 6e3297be..00b851c1 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -40,11 +40,11 @@ impl<'a> Mark<'a> { } pub fn into_owned(self) -> Mark<'static> { - Mark { - data: Cow::Owned(self.data.into_owned()), - start: self.start, - end: self.end, - } + Mark { + data: Cow::Owned(self.data.into_owned()), + start: self.start, + end: self.end, + } } pub fn name(&self) -> &str { diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 33eddc57..046a21bb 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -658,16 +658,13 @@ impl TransactionInner { (expand_left, expand_right): (bool, bool), ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; - let mark_name = mark.name().into(); - let mark_value = mark.value().clone(); - // FIXME if let Some(obs) = op_observer { self.do_insert( doc, Some(obs), obj, mark.start, - OpType::mark(mark_name, expand_left, mark_value), + OpType::MarkBegin(expand_left, mark.data.clone().into_owned()), )?; self.do_insert(doc, Some(obs), obj, mark.end, OpType::MarkEnd(expand_right))?; obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) @@ -677,7 +674,7 @@ impl TransactionInner { None, obj, mark.start, - OpType::mark(mark_name, expand_left, mark_value), + OpType::MarkBegin(expand_left, mark.data.into_owned()), )?; self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(expand_right))?; } @@ -749,7 +746,6 @@ impl TransactionInner { op_observer.insert(doc, ex_obj, index, value, false) } (Some(ObjType::Text), Prop::Seq(index)) => { - // FIXME if op_observer.text_as_seq() { let value = (op.value(), doc.ops().id_to_exid(op.id)); op_observer.insert(doc, ex_obj, index, value, false) diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 34bd8fb6..e8951325 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -227,10 +227,6 @@ impl OpType { } } - pub(crate) fn mark(name: smol_str::SmolStr, expand: bool, value: ScalarValue) -> OpType { - OpType::MarkBegin(expand, MarkData { name, value }) - } - pub(crate) fn from_parts( OpTypeParts { action, From af9b006bb092adf069f6857d449c3aef6c8394e4 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 24 Feb 2023 16:50:19 -0600 Subject: [PATCH 20/26] marks name->key, range->start,end, patch callback propogates expceitons and has heads --- javascript/src/types.ts | 8 +- javascript/src/unstable.ts | 4 +- javascript/test/legacy_tests.ts | 22 ++- javascript/test/marks.ts | 4 +- rust/automerge-wasm/index.d.ts | 18 ++- rust/automerge-wasm/package.json | 2 +- rust/automerge-wasm/src/interop.rs | 17 ++- rust/automerge-wasm/src/lib.rs | 41 +++--- rust/automerge-wasm/src/observer.rs | 21 ++- rust/automerge-wasm/test/marks.ts | 130 +++++++++--------- rust/automerge-wasm/test/test.ts | 34 +++++ rust/automerge/src/automerge.rs | 20 +-- rust/automerge/src/change.rs | 4 +- rust/automerge/src/legacy/mod.rs | 6 +- rust/automerge/src/marks.rs | 16 +-- rust/automerge/src/query.rs | 3 +- rust/automerge/src/query/raw_spans.rs | 4 +- rust/automerge/src/query/seek_mark.rs | 14 +- rust/automerge/src/query/spans.rs | 2 +- rust/automerge/src/storage/change.rs | 2 +- .../src/storage/change/change_actors.rs | 4 +- .../src/storage/change/change_op_columns.rs | 52 +++---- .../src/storage/convert/op_as_changeop.rs | 6 +- .../src/storage/convert/op_as_docop.rs | 7 +- .../src/storage/document/doc_op_columns.rs | 42 +++--- .../src/storage/load/reconstruct_document.rs | 2 +- rust/automerge/src/types.rs | 8 +- rust/automerge/src/visualisation.rs | 2 +- 28 files changed, 273 insertions(+), 222 deletions(-) diff --git a/javascript/src/types.ts b/javascript/src/types.ts index beb5cf70..e7b7fd46 100644 --- a/javascript/src/types.ts +++ b/javascript/src/types.ts @@ -4,7 +4,7 @@ export { Counter } from "./counter" export { Int, Uint, Float64 } from "./numbers" import { Counter } from "./counter" -import type { Patch } from "@automerge/automerge-wasm" +import type { Patch, PatchInfo } from "@automerge/automerge-wasm" export type { Patch } from "@automerge/automerge-wasm" export type AutomergeValue = @@ -36,11 +36,9 @@ export type Doc = { readonly [P in keyof T]: T[P] } * Callback which is called by various methods in this library to notify the * user of what changes have been made. * @param patch - A description of the changes made - * @param before - The document before the change was made - * @param after - The document after the change was made + * @param info - An object that has the "before" and "after" document state, and the "from" and "to" heads */ export type PatchCallback = ( patches: Array, - before: Doc, - after: Doc + info: PatchInfo ) => void diff --git a/javascript/src/unstable.ts b/javascript/src/unstable.ts index 2f17f327..fcb7a6e0 100644 --- a/javascript/src/unstable.ts +++ b/javascript/src/unstable.ts @@ -240,7 +240,7 @@ export function splice( export function mark( doc: Doc, prop: stable.Prop, - markName: string, + key: string, range: string, value: string | boolean | number | Uint8Array | null ) { @@ -254,7 +254,7 @@ export function mark( } const obj = `${objectId}/${prop}` try { - return state.handle.mark(obj, range, markName, value) + return state.handle.mark(obj, range, key, value) } catch (e) { throw new RangeError(`Cannot mark: ${e}`) } diff --git a/javascript/test/legacy_tests.ts b/javascript/test/legacy_tests.ts index 8c2e552e..11c34490 100644 --- a/javascript/test/legacy_tests.ts +++ b/javascript/test/legacy_tests.ts @@ -340,8 +340,7 @@ describe("Automerge", () => { const s2 = Automerge.change( s1, { - patchCallback: (patches, before, after) => - callbacks.push({ patches, before, after }), + patchCallback: (patches, info) => callbacks.push({ patches, info }), }, doc => { doc.birds = ["Goldfinch"] @@ -363,8 +362,8 @@ describe("Automerge", () => { path: ["birds", 0, 0], value: "Goldfinch", }) - assert.strictEqual(callbacks[0].before, s1) - assert.strictEqual(callbacks[0].after, s2) + assert.strictEqual(callbacks[0].info.before, s1) + assert.strictEqual(callbacks[0].info.after, s2) }) it("should call a patchCallback set up on document initialisation", () => { @@ -374,8 +373,7 @@ describe("Automerge", () => { after: Automerge.Doc }> = [] s1 = Automerge.init({ - patchCallback: (patches, before, after) => - callbacks.push({ patches, before, after }), + patchCallback: (patches, info) => callbacks.push({ patches, info }), }) const s2 = Automerge.change(s1, doc => (doc.bird = "Goldfinch")) assert.strictEqual(callbacks.length, 1) @@ -389,8 +387,8 @@ describe("Automerge", () => { path: ["bird", 0], value: "Goldfinch", }) - assert.strictEqual(callbacks[0].before, s1) - assert.strictEqual(callbacks[0].after, s2) + assert.strictEqual(callbacks[0].info.before, s1) + assert.strictEqual(callbacks[0].info.after, s2) }) }) @@ -1812,8 +1810,8 @@ describe("Automerge", () => { before, Automerge.getAllChanges(s1), { - patchCallback(patch, before, after) { - callbacks.push({ patch, before, after }) + patchCallback(patch, info) { + callbacks.push({ patch, info }) }, } ) @@ -1833,8 +1831,8 @@ describe("Automerge", () => { path: ["birds", 0, 0], value: "Goldfinch", }) - assert.strictEqual(callbacks[0].before, before) - assert.strictEqual(callbacks[0].after, after) + assert.strictEqual(callbacks[0].info.before, before) + assert.strictEqual(callbacks[0].info.after, after) }) it("should merge multiple applied changes into one patch", () => { diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts index ed6e4543..1010101a 100644 --- a/javascript/test/marks.ts +++ b/javascript/test/marks.ts @@ -19,7 +19,7 @@ describe("Automerge", () => { { action: "mark", path: ["x"], - marks: [{ name: "font-weight", range: "5..10", value: "bold" }], + marks: [{ key: "font-weight", start: 5, end: 10, value: "bold" }], }, ]) @@ -33,7 +33,7 @@ describe("Automerge", () => { assert.deepStrictEqual(callbacks[0][2], { action: "mark", path: ["x"], - marks: [{ name: "font-weight", range: "5..10", value: "bold" }], + marks: [{ key: "font-weight", start: 5, end: 10, value: "bold" }], }) }) }) diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index a01400eb..2e804b3c 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -134,14 +134,15 @@ export type InsertPatch = { } export type Mark = { - name: string, + key: string, value: Value, - range: string, + start: number, + end: number, } export type RawMark = { id: ObjID, - type: string, + key: string, value: Value, start: number, end: number, @@ -186,7 +187,7 @@ export class Automerge { delete(obj: ObjID, prop: Prop): void; // marks - mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + mark(obj: ObjID, key: string, range: string, value: Value, datatype?: Datatype): void; unmark(obj: ObjID, mark: ObjID): void; marks(obj: ObjID): Mark[]; rawMarks(obj: ObjID): RawMark[]; @@ -246,7 +247,14 @@ export class Automerge { dump(): void; // experimental api can go here - applyPatches(obj: Doc, meta?: unknown, callback?: (patch: Array, before: Doc, after: Doc) => void): Doc; + applyPatches(obj: Doc, meta?: unknown, callback?: (patch: Array, info: PatchInfo) => void): Doc; +} + +export interface PatchInfo { + before: T, + after: T, + from: Heads, + to: Heads, } export interface JsSyncState { diff --git a/rust/automerge-wasm/package.json b/rust/automerge-wasm/package.json index 4c691511..8f79cad2 100644 --- a/rust/automerge-wasm/package.json +++ b/rust/automerge-wasm/package.json @@ -37,7 +37,7 @@ "target": "rimraf ./$TARGET && yarn compile && yarn bindgen && yarn opt", "compile": "cargo build --target wasm32-unknown-unknown --profile $PROFILE", "bindgen": "wasm-bindgen --no-typescript --weak-refs --target $TARGET --out-dir $TARGET ../target/wasm32-unknown-unknown/$TARGET_DIR/automerge_wasm.wasm", - "opt": "wasm-opt -O4 $TARGET/automerge_wasm_bg.wasm -o $TARGET/automerge_wasm_bg.wasm", + "opt": "echo wasm-opt -O4 $TARGET/automerge_wasm_bg.wasm -o $TARGET/automerge_wasm_bg.wasm", "test": "ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, "devDependencies": { diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index 0c5fe346..a206e5f6 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -28,6 +28,12 @@ impl From for JsValue { } } +impl From for Array { + fn from(ar: AR) -> Self { + ar.0 + } +} + impl From for JsValue { fn from(js: JS) -> Self { js.0 @@ -334,11 +340,20 @@ impl TryFrom for am::sync::Message { } } +impl From> for AR { + fn from(values: Vec) -> Self { + AR(values + .iter() + .map(|h| JsValue::from_str(&h.to_string())) + .collect()) + } +} + impl From<&[ChangeHash]> for AR { fn from(value: &[ChangeHash]) -> Self { AR(value .iter() - .map(|h| JsValue::from_str(&hex::encode(h.0))) + .map(|h| JsValue::from_str(&h.to_string())) .collect()) } } diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 993c446a..8da55e11 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -546,7 +546,8 @@ impl Automerge { let enable = enable .as_bool() .ok_or_else(|| to_js_err("must pass a bool to enablePatches"))?; - let old_enabled = self.doc.observer().enable(enable); + let heads = self.doc.get_heads(); + let old_enabled = self.doc.observer().enable(enable, heads); self.doc.observer().set_text_rep(self.text_rep); Ok(old_enabled.into()) } @@ -572,11 +573,12 @@ impl Automerge { object: JsValue, meta: JsValue, callback: JsValue, - ) -> Result { + ) -> Result { let mut object = object .dyn_into::() .map_err(|_| error::ApplyPatch::NotObjectd)?; - let patches = self.doc.observer().take_patches(); + let end_heads = self.doc.get_heads(); + let (patches, begin_heads) = self.doc.observer().take_patches(end_heads.clone()); let callback = callback.dyn_into::().ok(); // even if there are no patches we may need to update the meta object @@ -595,19 +597,23 @@ impl Automerge { object = self.apply_patch(object, p, 0, &meta, &mut exposed)?; } + self.finalize_exposed(&object, exposed, &meta)?; + if let Some(c) = &callback { if !patches.is_empty() { let patches: Array = patches .into_iter() .map(JsValue::try_from) .collect::>()?; - c.call3(&JsValue::undefined(), &patches.into(), &before, &object) - .map_err(error::ApplyPatch::PatchCallback)?; + let info = Object::new(); + js_set(&info, "before", &before)?; + js_set(&info, "after", &object)?; + js_set(&info, "from", AR::from(begin_heads))?; + js_set(&info, "to", AR::from(end_heads))?; + c.call2(&JsValue::undefined(), &patches.into(), &info)?; } } - self.finalize_exposed(&object, exposed, &meta)?; - Ok(object.into()) } @@ -617,7 +623,8 @@ impl Automerge { // committed. // If we pop the patches then we won't be able to revert them. - let patches = self.doc.observer().take_patches(); + let heads = self.doc.get_heads(); + let (patches, _heads) = self.doc.observer().take_patches(heads); let result = Array::new(); for p in patches { result.push(&p.try_into()?); @@ -703,17 +710,12 @@ impl Automerge { #[wasm_bindgen(js_name = getHeads)] pub fn get_heads(&mut self) -> Array { let heads = self.doc.get_heads(); - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(h.0))) - .collect(); - heads + AR::from(heads).into() } #[wasm_bindgen(js_name = getActorId)] pub fn get_actor_id(&self) -> String { - let actor = self.doc.get_actor(); - actor.to_string() + self.doc.get_actor().to_string() } #[wasm_bindgen(js_name = getLastLocalChange)] @@ -776,7 +778,8 @@ impl Automerge { ) -> Result { let (obj, obj_type) = self.import(obj).unwrap_or((ROOT, am::ObjType::Map)); let heads = get_heads(heads)?; - let _patches = self.doc.observer().take_patches(); // throw away patches + let current_heads = self.doc.get_heads(); + let _patches = self.doc.observer().take_patches(current_heads); // throw away patches Ok(self.export_object(&obj, obj_type.into(), heads.as_ref(), &meta)?) } @@ -835,10 +838,10 @@ impl Automerge { for m in marks { let mark = Object::new(); let (_datatype, value) = alloc(&m.value().clone().into(), self.text_rep); - js_set(&mark, "name", m.name())?; + js_set(&mark, "key", m.key())?; js_set(&mark, "value", value)?; - //js_set(&mark, "datatype",datatype)?; - js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; + js_set(&mark, "start", m.start as i32)?; + js_set(&mark, "end", m.end as i32)?; result.push(&mark.into()); } Ok(result.into()) diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index c3f1f80e..27c701c2 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -2,6 +2,8 @@ use std::borrow::Cow; +use automerge::ChangeHash; + use crate::{ interop::{self, alloc, js_set}, TextRepresentation, @@ -15,17 +17,22 @@ use crate::sequence_tree::SequenceTree; #[derive(Debug, Clone, Default)] pub(crate) struct Observer { enabled: bool, + last_heads: Option>, patches: Vec, text_rep: TextRepresentation, } impl Observer { - pub(crate) fn take_patches(&mut self) -> Vec { - std::mem::take(&mut self.patches) + pub(crate) fn take_patches(&mut self, heads: Vec) -> (Vec, Vec) { + let old_heads = self.last_heads.replace(heads).unwrap_or_default(); + let patches = std::mem::take(&mut self.patches); + (patches, old_heads) } - pub(crate) fn enable(&mut self, enable: bool) -> bool { + + pub(crate) fn enable(&mut self, enable: bool, heads: Vec) -> bool { if self.enabled && !enable { - self.patches.truncate(0) + self.patches.truncate(0); + self.last_heads = Some(heads); } let old_enabled = self.enabled; self.enabled = enable; @@ -394,6 +401,7 @@ impl automerge::op_observer::BranchableObserver for Observer { fn branch(&self) -> Self { Observer { patches: vec![], + last_heads: None, enabled: self.enabled, text_rep: self.text_rep, } @@ -565,13 +573,14 @@ impl TryFrom for JsValue { let marks_array = Array::new(); for m in marks.iter() { let mark = Object::new(); - js_set(&mark, "name", m.name())?; + js_set(&mark, "key", m.key())?; js_set( &mark, "value", &alloc(&m.value().into(), TextRepresentation::String).1, )?; - js_set(&mark, "range", format!("{}..{}", m.start, m.end))?; + js_set(&mark, "start", m.start as i32)?; + js_set(&mark, "end", m.end as i32)?; marks_array.push(&mark); } js_set(&result, "marks", marks_array)?; diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 55128a27..310783bf 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -15,11 +15,11 @@ describe('Automerge', () => { doc.mark(list, "[3..6]", "bold" , true) let text = doc.text(list) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "4..7" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 4, end: 7 }]) }) it('should handle marks [..] at the beginning of a string', () => { @@ -28,14 +28,14 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.insert(list, 0, "A") doc2.insert(list, 4, "B") doc.merge(doc2) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "1..4" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 1, end: 4 }]) }) it('should handle marks [..] with splice', () => { @@ -44,14 +44,14 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.splice(list, 0, 2, "AAA") doc2.splice(list, 4, 0, "BBB") doc.merge(doc2) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..4" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 4 }]) }) it('should handle marks across multiple forks', () => { @@ -60,7 +60,7 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "0..3" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. @@ -72,7 +72,7 @@ describe('Automerge', () => { doc.merge(doc3) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) }) it('should handle marks with deleted ends [..]', () => { @@ -82,17 +82,17 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..3" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 3 }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..4" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 4 }]) }) it('should handle sticky marks (..)', () => { @@ -101,11 +101,11 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..8" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 8 }]) }) it('should handle sticky marks with deleted ends (..)', () => { @@ -114,24 +114,24 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "3..6" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..3" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 3 }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..5" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 5 }]) // make sure save/load can handle marks let saved = doc.save() let doc2 = load(saved,true) marks = doc2.marks(list); - assert.deepStrictEqual(marks, [{ name: 'bold', value: true, range: "2..5" }]) + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 5 }]) assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) assert.deepStrictEqual(doc.save(), doc2.save()) @@ -147,26 +147,26 @@ describe('Automerge', () => { doc.commit("marks"); let marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { name: 'comment', range: '10..13', value: 'foxes are my favorite animal!' }, - { name: 'itallic', range: '4..19', value: true }, - { name: 'bold', range: '0..37', value: true } + { key: 'comment', start: 10, end: 13, value: 'foxes are my favorite animal!' }, + { key: 'itallic', start: 4, end: 19, value: true }, + { key: 'bold', start: 0, end: 37, value: true } ]) let text = doc.text(list); assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); let rawMarks = doc.rawMarks(list); assert.deepStrictEqual(rawMarks, [ - { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, - { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, - { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + { id: "39@aabbcc", start: 0, end: 37, key: 'bold', value: true }, + { id: "41@aabbcc", start: 4, end: 19, key: 'itallic', value: true }, + { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } ]); doc.unmark(list, "41@aabbcc") rawMarks = doc.rawMarks(list); assert.deepStrictEqual(rawMarks, [ - { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, - { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + { id: "39@aabbcc", start: 0, end: 37, key: 'bold', value: true }, + { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } ]); // mark sure encode decode can handle marks @@ -174,7 +174,7 @@ describe('Automerge', () => { rawMarks = doc.rawMarks(list); assert.deepStrictEqual(rawMarks, [ - { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } ]); let all = doc.getChanges([]) @@ -212,9 +212,9 @@ describe('Automerge', () => { { action: 'mark', path: [ 'list' ], marks: [ - { name: 'bold', value: true, range: '0..37' }, - { name: 'itallic', value: true, range: '4..19' }, - { name: 'comment', value: 'foxes are my favorite animal!', range: '10..13' } + { key: 'bold', value: true, start: 0, end: 37 }, + { key: 'itallic', value: true, start: 4, end: 19 }, + { key: 'comment', value: 'foxes are my favorite animal!', start: 10, end: 13 } ] } ]); @@ -248,10 +248,10 @@ describe('Automerge', () => { action: 'mark', path: [ 'list' ], marks: [ - { name: 'x', value: 'a', range: '0..5' }, - { name: 'x', value: 'b', range: '8..11' }, - { name: 'x', value: 'c', range: '5..8' }, - { name: 'x', value: 'c', range: '11..13' }, + { key: 'x', value: 'a', start: 0, end: 5 }, + { key: 'x', value: 'b', start: 8, end: 11 }, + { key: 'x', value: 'c', start: 5, end: 8 }, + { key: 'x', value: 'c', start: 11, end: 13 }, ] }, ]); @@ -269,7 +269,7 @@ describe('Automerge', () => { let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches1, [{ - action: 'mark', path: [ 'list' ], marks: [ { name: 'xxx', value: 'aaa', range: '5..10' }], + action: 'mark', path: [ 'list' ], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 10 }], }]); let doc2 : Automerge = create(true); @@ -279,7 +279,7 @@ describe('Automerge', () => { let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches2, [{ - action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', range: '5..10'}], + action: 'mark', path: ['list'], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 10}], }]); }) @@ -297,9 +297,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..15' }, - { name: 'xxx', value: 'aaa', range: '10..20' }, - { name: 'xxx', value: 'aaa', range: '15..25' }, + { key: 'xxx', value: 'aaa', start: 5, end: 15 }, + { key: 'xxx', value: 'aaa', start: 10, end: 20 }, + { key: 'xxx', value: 'aaa', start: 15, end: 25 }, ] }, ]); @@ -310,7 +310,7 @@ describe('Automerge', () => { let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches2, [ - { action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', range: '5..25'}] }, + { action: 'mark', path: ['list'], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 25}] }, ]); }) @@ -328,9 +328,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..15' }, - { name: 'xxx', value: 'bbb', range: '10..20' }, - { name: 'xxx', value: 'aaa', range: '15..25' }, + { key: 'xxx', value: 'aaa', start: 5, end: 15 }, + { key: 'xxx', value: 'bbb', start: 10, end: 20 }, + { key: 'xxx', value: 'aaa', start: 15, end: 25 }, ]} ]); @@ -342,9 +342,9 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: ['list'], marks: [ - { name: 'xxx', value: 'aaa', range: '5..10'}, - { name: 'xxx', value: 'bbb', range: '10..15'}, - { name: 'xxx', value: 'aaa', range: '15..25'}, + { key: 'xxx', value: 'aaa', start: 5, end: 10 }, + { key: 'xxx', value: 'bbb', start: 10, end: 15 }, + { key: 'xxx', value: 'aaa', start: 15, end: 25 }, ]}, ]); }) @@ -363,9 +363,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..15' }, - { name: 'yyy', value: 'aaa', range: '10..20' }, - { name: 'zzz', value: 'aaa', range: '15..25' }, + { key: 'xxx', value: 'aaa', start: 5, end:15 }, + { key: 'yyy', value: 'aaa', start: 10, end: 20 }, + { key: 'zzz', value: 'aaa', start: 15, end: 25 }, ]} ]); @@ -377,9 +377,9 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..15' }, - { name: 'yyy', value: 'aaa', range: '10..20' }, - { name: 'zzz', value: 'aaa', range: '15..25' }, + { key: 'xxx', value: 'aaa', start: 5, end: 15 }, + { key: 'yyy', value: 'aaa', start: 10, end: 20 }, + { key: 'zzz', value: 'aaa', start: 15, end: 25 }, ]} ]); }) @@ -406,10 +406,10 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '10..20' }, - { name: 'xxx', value: 'aaa', range: '15..25' }, - { name: 'xxx', value: 'bbb', range: '5..10' }, - { name: 'xxx', value: 'bbb', range: '25..30' }, + { key: 'xxx', value: 'aaa', start: 10, end: 20 }, + { key: 'xxx', value: 'aaa', start: 15, end: 25 }, + { key: 'xxx', value: 'bbb', start: 5, end: 10 }, + { key: 'xxx', value: 'bbb', start: 25, end: 30 }, ] }, ]); @@ -423,9 +423,9 @@ describe('Automerge', () => { let marks = doc3.marks(list) assert.deepEqual(marks, [ - { name: 'xxx', value: 'bbb', range: '5..10' }, - { name: 'xxx', value: 'aaa', range: '10..25' }, - { name: 'xxx', value: 'bbb', range: '25..30' }, + { key: 'xxx', value: 'bbb', start: 5, end: 10 }, + { key: 'xxx', value: 'aaa', start: 10, end: 25 }, + { key: 'xxx', value: 'bbb', start: 25, end: 30 }, ]); assert.deepEqual(patches2, [{ action: 'mark', path: [ 'list' ], marks }]); @@ -453,8 +453,8 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '10..20' }, - { name: 'xxx', value: 'aaa', range: '15..25' }, + { key: 'xxx', value: 'aaa', start: 10, end: 20 }, + { key: 'xxx', value: 'aaa', start: 15, end: 25 }, ] }, ]); @@ -467,7 +467,7 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '10..25' }, + { key: 'xxx', value: 'aaa', start: 10, end: 25 }, ]} ]); }) @@ -494,9 +494,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..11' }, - { name: 'xxx', value: 'aaa', range: '19..25' }, - { name: 'xxx', value: 'aaa', range: '11..19' }, + { key: 'xxx', value: 'aaa', start: 5, end: 11 }, + { key: 'xxx', value: 'aaa', start: 19, end: 25 }, + { key: 'xxx', value: 'aaa', start: 11, end: 19 }, ] }, ]); @@ -509,7 +509,7 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { name: 'xxx', value: 'aaa', range: '5..25' }, + { key: 'xxx', value: 'aaa', start: 5, end: 25 }, ]} ]); }) diff --git a/rust/automerge-wasm/test/test.ts b/rust/automerge-wasm/test/test.ts index bb4f71e3..3e57d92c 100644 --- a/rust/automerge-wasm/test/test.ts +++ b/rust/automerge-wasm/test/test.ts @@ -1941,6 +1941,40 @@ describe('Automerge', () => { assert.deepEqual(mat.text, "ab011ij") }) + it('propogates exceptions thrown in patch callback', () => { + const doc = create(true) + doc.enablePatches(true) + let mat : any = doc.materialize("/") + doc.putObject("/", "text", "abcdefghij") + assert.throws(() => { + doc.applyPatches(mat, {}, (patches, info) => { + throw new RangeError("hello world") + }) + }, /RangeError: hello world/) + }) + + it('patch callback has correct patch info', () => { + const doc = create(true) + let mat : any = doc.materialize("/") + doc.putObject("/", "text", "abcdefghij") + + let before = doc.materialize("/") + let from = doc.getHeads() + + doc.enablePatches(true) + doc.splice("/text", 2, 2, "00") + + let after = doc.materialize("/") + let to = doc.getHeads() + + doc.applyPatches(mat, {}, (patches, info) => { + assert.deepEqual(info.before, before); + assert.deepEqual(info.after, after); + assert.deepEqual(info.from, from); + assert.deepEqual(info.to, to); + }) + }) + it('can handle utf16 text', () => { const doc = create(true) doc.enablePatches(true) diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index 58cf64a4..f431fc16 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -26,7 +26,6 @@ use crate::{ query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Prop, ReadDoc, Values, }; -use serde::Serialize; mod current_state; @@ -735,7 +734,7 @@ impl Automerge { action: c.action, value: c.val, expand: c.expand, - mark_name: c.mark_name, + mark_key: c.mark_key, }) .unwrap(), key, @@ -991,8 +990,8 @@ impl Automerge { OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), - OpType::MarkBegin(_, MarkData { name, value }) => { - format!("mark({},{})", name, value) + OpType::MarkBegin(_, MarkData { key, value }) => { + format!("mark({},{})", key, value) } OpType::MarkEnd(_) => "/mark".to_string(), }; @@ -1423,7 +1422,7 @@ impl ReadDoc for Automerge { id: self.id_to_exid(s.id), start: s.start, end: s.end, - span_type: s.name.to_string(), + key: s.key.to_string(), value: s.value, }) .collect(); @@ -1560,14 +1559,3 @@ impl Default for Automerge { Self::new() } } - -#[derive(Serialize, Debug, Clone, PartialEq)] -pub(crate) struct SpanInfo { - pub(crate) id: ExId, - pub(crate) time: i64, - pub(crate) start: usize, - pub(crate) end: usize, - #[serde(rename = "type")] - pub(crate) span_type: String, - pub(crate) value: ScalarValue, -} diff --git a/rust/automerge/src/change.rs b/rust/automerge/src/change.rs index a6ab112a..657ce574 100644 --- a/rust/automerge/src/change.rs +++ b/rust/automerge/src/change.rs @@ -260,7 +260,7 @@ mod convert_expanded { self.action.expand() } - fn mark_name(&self) -> Option> { + fn mark_key(&self) -> Option> { if let legacy::OpType::MarkBegin(legacy::MarkData { name, .. }) = &self.action { Some(Cow::Borrowed(name)) } else { @@ -294,7 +294,7 @@ impl From<&Change> for crate::ExpandedChange { action: o.action, value: o.val, expand: o.expand, - mark_name: o.mark_name, + mark_key: o.mark_key, }), insert: o.insert, key: match o.key { diff --git a/rust/automerge/src/legacy/mod.rs b/rust/automerge/src/legacy/mod.rs index d4fb1fd0..4c731513 100644 --- a/rust/automerge/src/legacy/mod.rs +++ b/rust/automerge/src/legacy/mod.rs @@ -208,7 +208,7 @@ pub(crate) struct OpTypeParts { pub(crate) action: u64, pub(crate) value: ScalarValue, pub(crate) expand: bool, - pub(crate) mark_name: Option, + pub(crate) mark_key: Option, } // Like `types::OpType` except using a String for mark names @@ -239,7 +239,7 @@ impl OpType { action, value, expand, - mark_name, + mark_key, }: OpTypeParts, ) -> Self { match action { @@ -254,7 +254,7 @@ impl OpType { _ => panic!("non numeric value for integer action"), }, 6 => Self::Make(ObjType::Table), - 7 => match mark_name { + 7 => match mark_key { Some(name) => Self::MarkBegin(MarkData { name, value, diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 00b851c1..3dada62d 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -16,14 +16,14 @@ pub struct Mark<'a> { impl<'a> Mark<'a> { pub fn new>( - name: String, + key: String, value: V, start: usize, end: usize, ) -> Mark<'static> { Mark { data: Cow::Owned(MarkData { - name: name.into(), + key: key.into(), value: value.into(), }), start, @@ -47,8 +47,8 @@ impl<'a> Mark<'a> { } } - pub fn name(&self) -> &str { - self.data.name.as_str() + pub fn key(&self) -> &str { + self.data.key.as_str() } pub fn value(&self) -> &ScalarValue { @@ -130,7 +130,7 @@ impl<'a> MarkStateMachine<'a> { Some( &state[index..] .iter() - .find(|(_, m)| m.name() == mark.name())? + .find(|(_, m)| m.key() == mark.key())? .1, ) } @@ -143,7 +143,7 @@ impl<'a> MarkStateMachine<'a> { Some( &mut state[0..index] .iter_mut() - .filter(|(_, m)| m.data.name == mark.data.name) + .filter(|(_, m)| m.data.key == mark.data.key) .last()? .1, ) @@ -152,12 +152,12 @@ impl<'a> MarkStateMachine<'a> { #[derive(PartialEq, Debug, Clone)] pub struct MarkData { - pub name: SmolStr, + pub key: SmolStr, pub value: ScalarValue, } impl Display for MarkData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "name={} value={}", self.name, self.value) + write!(f, "key={} value={}", self.key, self.value) } } diff --git a/rust/automerge/src/query.rs b/rust/automerge/src/query.rs index 36d926cc..0d331b8b 100644 --- a/rust/automerge/src/query.rs +++ b/rust/automerge/src/query.rs @@ -66,8 +66,7 @@ pub struct SpanInfo { pub id: ExId, pub start: usize, pub end: usize, - #[serde(rename = "type")] - pub span_type: String, + pub key: String, pub value: ScalarValue, } diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs index 5a388c67..90535bb1 100644 --- a/rust/automerge/src/query/raw_spans.rs +++ b/rust/automerge/src/query/raw_spans.rs @@ -17,7 +17,7 @@ pub(crate) struct RawSpan { pub(crate) id: OpId, pub(crate) start: usize, pub(crate) end: usize, - pub(crate) name: smol_str::SmolStr, + pub(crate) key: smol_str::SmolStr, pub(crate) value: ScalarValue, } @@ -50,7 +50,7 @@ impl<'a> TreeQuery<'a> for RawSpans { id: element.id, start: self.seen, end: 0, - name: md.name.clone(), + key: md.key.clone(), value: md.value.clone(), }, ); diff --git a/rust/automerge/src/query/seek_mark.rs b/rust/automerge/src/query/seek_mark.rs index c5834ab8..6706167c 100644 --- a/rust/automerge/src/query/seek_mark.rs +++ b/rust/automerge/src/query/seek_mark.rs @@ -13,7 +13,7 @@ pub(crate) struct SeekMark<'a> { end: usize, encoding: ListEncoding, found: bool, - mark_name: smol_str::SmolStr, + mark_key: smol_str::SmolStr, next_mark: Option>, pos: usize, seen: usize, @@ -30,7 +30,7 @@ impl<'a> SeekMark<'a> { end, found: false, next_mark: None, - mark_name: "".into(), + mark_key: "".into(), pos: 0, seen: 0, last_seen: None, @@ -58,20 +58,20 @@ impl<'a> TreeQuery<'a> for SeekMark<'a> { return QueryResult::Finish; } self.found = true; - self.mark_name = data.name.clone(); + self.mark_key = data.key.clone(); // retain the name and the value self.next_mark = Some(Mark::from_data(self.seen, self.seen, data)); // change id to the end id self.id = self.id.next(); // remove all marks that dont match - self.super_marks.retain(|_, v| v == &data.name); + self.super_marks.retain(|_, v| v == &data.key); } OpType::MarkBegin(_, mark) => { if m.lamport_cmp(op.id, self.id) == Ordering::Greater { if let Some(next_mark) = &mut self.next_mark { // gather marks of the same type that supersede us - if mark.name == self.mark_name { - self.super_marks.insert(op.id.next(), mark.name.clone()); + if mark.key == self.mark_key { + self.super_marks.insert(op.id.next(), mark.key.clone()); if self.super_marks.len() == 1 { // complete a mark next_mark.end = self.seen; @@ -80,7 +80,7 @@ impl<'a> TreeQuery<'a> for SeekMark<'a> { } } else { // gather all marks until we know what our mark's name is - self.super_marks.insert(op.id.next(), mark.name.clone()); + self.super_marks.insert(op.id.next(), mark.key.clone()); } } } diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs index 4aeea0df..8fb0cae8 100644 --- a/rust/automerge/src/query/spans.rs +++ b/rust/automerge/src/query/spans.rs @@ -46,7 +46,7 @@ impl<'a> Spans<'a> { let mut new_marks = HashMap::new(); for op in &self.ops { if let OpType::MarkBegin(_, m) = &op.action { - new_marks.insert(m.name.clone(), &m.value); + new_marks.insert(m.key.clone(), &m.value); } } if new_marks != self.marks { diff --git a/rust/automerge/src/storage/change.rs b/rust/automerge/src/storage/change.rs index 13198015..04b930d6 100644 --- a/rust/automerge/src/storage/change.rs +++ b/rust/automerge/src/storage/change.rs @@ -435,7 +435,7 @@ pub(crate) trait AsChangeOp<'a> { fn val(&self) -> Cow<'a, ScalarValue>; fn pred(&self) -> Self::PredIter; fn expand(&self) -> bool; - fn mark_name(&self) -> Option>; + fn mark_key(&self) -> Option>; } impl ChangeBuilder, Set, Set, Set> { diff --git a/rust/automerge/src/storage/change/change_actors.rs b/rust/automerge/src/storage/change/change_actors.rs index 951201dd..afd3204c 100644 --- a/rust/automerge/src/storage/change/change_actors.rs +++ b/rust/automerge/src/storage/change/change_actors.rs @@ -252,8 +252,8 @@ where self.op.expand() } - fn mark_name(&self) -> Option> { - self.op.mark_name() + fn mark_key(&self) -> Option> { + self.op.mark_key() } } diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index ab15142c..c654ad69 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -42,7 +42,7 @@ pub(crate) struct ChangeOp { pub(crate) action: u64, pub(crate) obj: ObjId, pub(crate) expand: bool, - pub(crate) mark_name: Option, + pub(crate) mark_key: Option, } impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { @@ -62,7 +62,7 @@ impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { insert: a.insert(), action: a.action(), expand: a.expand(), - mark_name: a.mark_name().map(|n| n.into_owned()), + mark_key: a.mark_key().map(|n| n.into_owned()), } } } @@ -108,8 +108,8 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { self.expand } - fn mark_name(&self) -> Option> { - self.mark_name.as_ref().map(Cow::Borrowed) + fn mark_key(&self) -> Option> { + self.mark_key.as_ref().map(Cow::Borrowed) } } @@ -122,7 +122,7 @@ pub(crate) struct ChangeOpsColumns { val: ValueRange, pred: OpIdListRange, expand: MaybeBooleanRange, - mark_name: RleRange, + mark_key: RleRange, } impl ChangeOpsColumns { @@ -136,7 +136,7 @@ impl ChangeOpsColumns { val: self.val.iter(data), pred: self.pred.iter(data), expand: self.expand.decoder(data), - mark_name: self.mark_name.decoder(data), + mark_key: self.mark_key.decoder(data), } } @@ -171,8 +171,8 @@ impl ChangeOpsColumns { let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let pred = OpIdListRange::encode(ops.clone().map(|o| o.pred()), out); let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); - let mark_name = - RleRange::encode::, _>(ops.map(|o| o.mark_name()), out); + let mark_key = + RleRange::encode::, _>(ops.map(|o| o.mark_key()), out); Self { obj, key, @@ -181,7 +181,7 @@ impl ChangeOpsColumns { val, pred, expand, - mark_name, + mark_key, } } @@ -198,7 +198,7 @@ impl ChangeOpsColumns { let mut val = ValueEncoder::new(); let mut pred = OpIdListEncoder::new(); let mut expand = MaybeBooleanEncoder::new(); - let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); + let mut mark_key = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { tracing::trace!(expand=?op.expand(), "expand"); obj.append(op.obj()); @@ -208,7 +208,7 @@ impl ChangeOpsColumns { val.append(&op.val()); pred.append(op.pred()); expand.append(op.expand()); - mark_name.append(op.mark_name()); + mark_key.append(op.mark_key()); } let obj = obj.finish(out); let key = key.finish(out); @@ -231,10 +231,10 @@ impl ChangeOpsColumns { out.extend(expand); let expand = MaybeBooleanRange::from(expand_start..out.len()); - let mark_name_start = out.len(); - let (mark_name, _) = mark_name.finish(); - out.extend(mark_name); - let mark_name = RleRange::from(mark_name_start..out.len()); + let mark_key_start = out.len(); + let (mark_key, _) = mark_key.finish(); + out.extend(mark_key); + let mark_key = RleRange::from(mark_key_start..out.len()); Self { obj, @@ -244,7 +244,7 @@ impl ChangeOpsColumns { val, pred, expand, - mark_name, + mark_key, } } @@ -317,10 +317,10 @@ impl ChangeOpsColumns { self.expand.clone().into(), )); } - if !self.mark_name.is_empty() { + if !self.mark_key.is_empty() { cols.push(RawColumn::new( ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), - self.mark_name.clone().into(), + self.mark_key.clone().into(), )); } cols.into_iter().collect() @@ -341,7 +341,7 @@ pub(crate) struct ChangeOpsIter<'a> { val: ValueIter<'a>, pred: OpIdListIter<'a>, expand: MaybeBooleanDecoder<'a>, - mark_name: RleDecoder<'a, smol_str::SmolStr>, + mark_key: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> ChangeOpsIter<'a> { @@ -364,7 +364,7 @@ impl<'a> ChangeOpsIter<'a> { let val = self.val.next_in_col("value")?; let pred = self.pred.next_in_col("pred")?; let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); - let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; + let mark_key = self.mark_key.maybe_next_in_col("mark_key")?; Ok(Some(ChangeOp { obj, key, @@ -373,7 +373,7 @@ impl<'a> ChangeOpsIter<'a> { val, pred, expand, - mark_name, + mark_key, })) } } @@ -421,7 +421,7 @@ impl TryFrom for ChangeOpsColumns { let mut pred_actor: Option> = None; let mut pred_ctr: Option = None; let mut expand: Option = None; - let mut mark_name: Option> = None; + let mut mark_key: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -477,7 +477,7 @@ impl TryFrom for ChangeOpsColumns { _ => return Err(ParseChangeColumnsError::MismatchingColumn { index }), }, (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), - (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_key = Some(col.range().into()), (other_type, other_col) => { tracing::warn!(typ=?other_type, id=?other_col, "unknown column"); other.append(col); @@ -504,7 +504,7 @@ impl TryFrom for ChangeOpsColumns { val: val.unwrap_or_else(|| ValueRange::new((0..0).into(), (0..0).into())), pred, expand: expand.unwrap_or_else(|| (0..0).into()), - mark_name: mark_name.unwrap_or_else(|| (0..0).into()), + mark_key: mark_key.unwrap_or_else(|| (0..0).into()), }) } } @@ -523,7 +523,7 @@ mod tests { action in 0_u64..6, obj in opid(), insert in any::(), - mark_name in proptest::option::of(any::().prop_map(|s| s.into())), + mark_key in proptest::option::of(any::().prop_map(|s| s.into())), expand in any::()) -> ChangeOp { ChangeOp { obj: obj.into(), @@ -533,7 +533,7 @@ mod tests { action, insert, expand, - mark_name, + mark_key, } } } diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index c08fa063..150c782e 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -136,9 +136,9 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { ) } - fn mark_name(&self) -> Option> { - if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { - Some(Cow::Owned(name.clone())) + fn mark_key(&self) -> Option> { + if let OpType::MarkBegin(_, MarkData { key, .. }) = &self.op.action { + Some(Cow::Owned(key.clone())) } else { None } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index ecff7a9c..cb6b7b90 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -119,10 +119,9 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { } } - // FIXME - fn mark_name(&self) -> Option> { - if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { - Some(Cow::Owned(name.clone())) + fn mark_key(&self) -> Option> { + if let OpType::MarkBegin(_, MarkData { key, .. }) = &self.op.action { + Some(Cow::Owned(key.clone())) } else { None } diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 7248b1b3..1ce16f85 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -42,7 +42,7 @@ pub(crate) struct DocOp { pub(crate) value: ScalarValue, pub(crate) succ: Vec, pub(crate) expand: bool, - pub(crate) mark_name: Option, + pub(crate) mark_key: Option, } #[derive(Debug, Clone)] @@ -57,7 +57,7 @@ pub(crate) struct DocOpColumns { #[allow(dead_code)] other: Columns, expand: MaybeBooleanRange, - mark_name: RleRange, + mark_key: RleRange, } struct DocId { @@ -97,7 +97,7 @@ pub(crate) trait AsDocOp<'a> { fn val(&self) -> Cow<'a, ScalarValue>; fn succ(&self) -> Self::SuccIter; fn expand(&self) -> bool; - fn mark_name(&self) -> Option>; + fn mark_key(&self) -> Option>; } impl DocOpColumns { @@ -128,7 +128,7 @@ impl DocOpColumns { let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let succ = OpIdListRange::encode(ops.clone().map(|o| o.succ()), out); let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); - let mark_name = RleRange::encode(ops.map(|o| o.mark_name()), out); + let mark_key = RleRange::encode(ops.map(|o| o.mark_key()), out); Self { obj, key, @@ -138,7 +138,7 @@ impl DocOpColumns { val, succ, expand, - mark_name, + mark_key, other: Columns::empty(), } } @@ -157,7 +157,7 @@ impl DocOpColumns { let mut val = ValueEncoder::new(); let mut succ = OpIdListEncoder::new(); let mut expand = MaybeBooleanEncoder::new(); - let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); + let mut mark_key = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { obj.append(op.obj()); key.append(op.key()); @@ -167,7 +167,7 @@ impl DocOpColumns { val.append(&op.val()); succ.append(op.succ()); expand.append(op.expand()); - mark_name.append(op.mark_name()); + mark_key.append(op.mark_key()); } let obj = obj.finish(out); let key = key.finish(out); @@ -191,10 +191,10 @@ impl DocOpColumns { out.extend(expand_out); let expand = MaybeBooleanRange::from(expand_start..out.len()); - let mark_name_start = out.len(); - let (mark_name_out, _) = mark_name.finish(); - out.extend(mark_name_out); - let mark_name = RleRange::from(mark_name_start..out.len()); + let mark_key_start = out.len(); + let (mark_key_out, _) = mark_key.finish(); + out.extend(mark_key_out); + let mark_key = RleRange::from(mark_key_start..out.len()); DocOpColumns { obj, @@ -205,7 +205,7 @@ impl DocOpColumns { val, succ, expand, - mark_name, + mark_key, other: Columns::empty(), } } @@ -220,7 +220,7 @@ impl DocOpColumns { value: self.val.iter(data), succ: self.succ.iter(data), expand: self.expand.decoder(data), - mark_name: self.mark_name.decoder(data), + mark_key: self.mark_key.decoder(data), } } @@ -301,10 +301,10 @@ impl DocOpColumns { self.expand.clone().into(), )); } - if !self.mark_name.is_empty() { + if !self.mark_key.is_empty() { cols.push(RawColumn::new( ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), - self.mark_name.clone().into(), + self.mark_key.clone().into(), )); } cols.into_iter().collect() @@ -321,7 +321,7 @@ pub(crate) struct DocOpColumnIter<'a> { value: ValueIter<'a>, succ: OpIdListIter<'a>, expand: MaybeBooleanDecoder<'a>, - mark_name: RleDecoder<'a, smol_str::SmolStr>, + mark_key: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> DocOpColumnIter<'a> { @@ -367,7 +367,7 @@ impl<'a> DocOpColumnIter<'a> { let succ = self.succ.next_in_col("succ")?; let insert = self.insert.next_in_col("insert")?; let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); - let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; + let mark_key = self.mark_key.maybe_next_in_col("mark_key")?; Ok(Some(DocOp { id, value, @@ -377,7 +377,7 @@ impl<'a> DocOpColumnIter<'a> { succ, insert, expand, - mark_name, + mark_key, })) } } @@ -413,7 +413,7 @@ impl TryFrom for DocOpColumns { let mut succ_actor: Option> = None; let mut succ_ctr: Option = None; let mut expand: Option = None; - let mut mark_name: Option> = None; + let mut mark_key: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -468,7 +468,7 @@ impl TryFrom for DocOpColumns { _ => return Err(Error::MismatchingColumn { index }), }, (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), - (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_key = Some(col.range().into()), (other_col, other_type) => { tracing::warn!(id=?other_col, typ=?other_type, "unknown column type"); other.append(col) @@ -498,7 +498,7 @@ impl TryFrom for DocOpColumns { succ_ctr.unwrap_or_else(|| (0..0).into()), ), expand: expand.unwrap_or_else(|| (0..0).into()), - mark_name: mark_name.unwrap_or_else(|| (0..0).into()), + mark_key: mark_key.unwrap_or_else(|| (0..0).into()), other, }) } diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index b33beed1..c9921d44 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -349,7 +349,7 @@ fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { action: op.action, value: op.value, expand: op.expand, - mark_name: op.mark_name, + mark_key: op.mark_key, })?; Ok(Op { id: check_opid(m, op.id)?, diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index e8951325..25dd9ade 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -207,7 +207,7 @@ pub(crate) struct OpTypeParts { pub(crate) action: u64, pub(crate) value: ScalarValue, pub(crate) expand: bool, - pub(crate) mark_name: Option, + pub(crate) mark_key: Option, } impl OpType { @@ -232,7 +232,7 @@ impl OpType { action, value, expand, - mark_name, + mark_key, }: OpTypeParts, ) -> Result { match action { @@ -247,8 +247,8 @@ impl OpType { _ => Err(error::InvalidOpType::NonNumericInc), }, 6 => Ok(Self::Make(ObjType::Table)), - 7 => match mark_name { - Some(name) => Ok(Self::MarkBegin(expand, MarkData { name, value })), + 7 => match mark_key { + Some(key) => Ok(Self::MarkBegin(expand, MarkData { key, value })), None => Ok(Self::MarkEnd(expand)), }, other => Err(error::InvalidOpType::UnknownAction(other)), diff --git a/rust/automerge/src/visualisation.rs b/rust/automerge/src/visualisation.rs index 677f9db9..4e639141 100644 --- a/rust/automerge/src/visualisation.rs +++ b/rust/automerge/src/visualisation.rs @@ -249,7 +249,7 @@ impl OpTableRow { crate::OpType::Put(v) => format!("set {}", v), crate::OpType::Make(obj) => format!("make {}", obj), crate::OpType::Increment(v) => format!("inc {}", v), - crate::OpType::MarkBegin(m) => format!("markEnd {}", m), + crate::OpType::MarkBegin(_, m) => format!("markEnd {}", m), crate::OpType::MarkEnd(m) => format!("markEnd {}", m), }; let prop = match op.key { From 1a539a3d791662783b1da60f0cd74867009f9797 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 27 Feb 2023 12:33:55 -0600 Subject: [PATCH 21/26] add unmark / remove spans/raw_spans/attr --- rust/automerge-wasm/index.d.ts | 6 +- rust/automerge-wasm/package.json | 4 +- rust/automerge-wasm/src/interop.rs | 13 +- rust/automerge-wasm/src/lib.rs | 182 ++---------------- rust/automerge-wasm/src/observer.rs | 44 +++++ rust/automerge-wasm/test/marks.ts | 81 +++++--- rust/automerge/src/autocommit.rs | 37 +--- rust/automerge/src/automerge.rs | 60 ------ rust/automerge/src/automerge/current_state.rs | 11 ++ rust/automerge/src/marks.rs | 12 +- rust/automerge/src/op_observer.rs | 29 +++ rust/automerge/src/op_observer/compose.rs | 12 ++ rust/automerge/src/query.rs | 19 -- rust/automerge/src/query/attribute.rs | 128 ------------ rust/automerge/src/query/attribute2.rs | 173 ----------------- rust/automerge/src/query/raw_spans.rs | 78 -------- rust/automerge/src/query/spans.rs | 112 ----------- rust/automerge/src/read.rs | 20 +- rust/automerge/src/transaction/inner.rs | 73 ++----- .../src/transaction/manual_transaction.rs | 36 +--- .../automerge/src/transaction/transactable.rs | 6 +- 21 files changed, 211 insertions(+), 925 deletions(-) delete mode 100644 rust/automerge/src/query/attribute.rs delete mode 100644 rust/automerge/src/query/attribute2.rs delete mode 100644 rust/automerge/src/query/raw_spans.rs delete mode 100644 rust/automerge/src/query/spans.rs diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index 2e804b3c..0de57f27 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -188,12 +188,8 @@ export class Automerge { // marks mark(obj: ObjID, key: string, range: string, value: Value, datatype?: Datatype): void; - unmark(obj: ObjID, mark: ObjID): void; + unmark(obj: ObjID, key: string, start: number, end: numbr): void; marks(obj: ObjID): Mark[]; - rawMarks(obj: ObjID): RawMark[]; - blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; - attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; - attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; // returns a single value - if there is a conflict return the winner get(obj: ObjID, prop: Prop, heads?: Heads): Value | undefined; diff --git a/rust/automerge-wasm/package.json b/rust/automerge-wasm/package.json index 8f79cad2..61123944 100644 --- a/rust/automerge-wasm/package.json +++ b/rust/automerge-wasm/package.json @@ -43,6 +43,7 @@ "devDependencies": { "@types/mocha": "^10.0.1", "@types/node": "^18.11.13", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.46.0", "@typescript-eslint/parser": "^5.46.0", "cross-env": "^7.0.3", @@ -52,7 +53,8 @@ "pako": "^2.1.0", "rimraf": "^3.0.2", "ts-mocha": "^10.0.0", - "typescript": "^4.9.4" + "typescript": "^4.9.4", + "uuid": "^9.0.0" }, "exports": { "browser": "./bundler/automerge_wasm.js", diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index a206e5f6..f3130e78 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -834,7 +834,7 @@ impl Automerge { } } } - Patch::Mark { .. } => Ok(result.into()), + Patch::Mark { .. } | Patch::Unmark { .. } => Ok(result.into()), } } @@ -894,7 +894,7 @@ impl Automerge { //Patch::SpliceText { .. } => Err(to_js_err("cannot Splice into map")), Patch::SpliceText { .. } => Err(error::ApplyPatch::SpliceTextInMap), Patch::PutSeq { .. } => Err(error::ApplyPatch::PutIdxInMap), - Patch::Mark { .. } => Err(error::ApplyPatch::MarkInMap), + Patch::Mark { .. } | Patch::Unmark { .. } => Err(error::ApplyPatch::MarkInMap), } } @@ -1018,15 +1018,6 @@ impl Automerge { } } - pub(crate) fn import_obj(&self, id: JsValue) -> Result { - if let Some(s) = id.as_string() { - // only valid formats is 123@aabbcc - self.doc.import_obj(&s).map_err(error::ImportObj::BadImport) - } else { - Err(error::ImportObj::NotString) - } - } - fn import_path<'a, I: Iterator>( &self, mut obj: ObjId, diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 8da55e11..bee15729 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -824,10 +824,18 @@ impl Automerge { Ok(()) } - pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { + pub fn unmark( + &mut self, + obj: JsValue, + key: JsValue, + start: f64, + end: f64, + ) -> Result<(), JsValue> { let (obj, _) = self.import(obj)?; - let mark = self.import_obj(mark)?; - self.doc.unmark(&obj, &mark).map_err(to_js_err)?; + let key = key.as_string().ok_or("key must be a string")?; + self.doc + .unmark(&obj, &key, start as usize, end as usize) + .map_err(to_js_err)?; Ok(()) } @@ -846,174 +854,6 @@ impl Automerge { } Ok(result.into()) } - - pub fn spans(&mut self, obj: JsValue) -> Result { - let (obj, _) = self.import(obj)?; - let text = self.doc.text(&obj)?; - let spans = self.doc.spans(&obj).map_err(to_js_err)?; - let mut last_pos = 0; - let result = Array::new(); - for s in spans { - let marks = Array::new(); - for m in s.marks { - let mark = Array::new(); - mark.push(&m.0.to_string().into()); // span name - let (datatype, value) = alloc(&am::Value::Scalar(m.1.clone()), self.text_rep); - mark.push(&datatype.into()); - mark.push(&value); - marks.push(&mark.into()); - } - let text_span = &text.get(last_pos..s.pos); //.slice(last_pos, s.pos); - if let Some(t) = text_span { - if !t.is_empty() { - result.push(&t.to_string().into()); - } - } - result.push(&marks); - last_pos = s.pos; - //let obj = Object::new().into(); - //js_set(&obj, "pos", s.pos as i32)?; - //js_set(&obj, "marks", marks)?; - //result.push(&obj.into()); - } - let text_span = &text.get(last_pos..); - if let Some(t) = text_span { - if !t.is_empty() { - result.push(&t.to_string().into()); - } - } - Ok(result.into()) - } - - #[wasm_bindgen(js_name = rawMarks)] - pub fn raw_marks(&mut self, obj: JsValue) -> Result { - let (obj, _) = self.import(obj)?; - let spans = self.doc.raw_spans(obj).map_err(to_js_err)?; - let result = Array::new(); - for s in spans { - //#[allow(deprecated)] - //result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); - result.push(&serde_wasm_bindgen::to_value(&s)?); - } - Ok(result) - } - - pub fn blame( - &mut self, - obj: JsValue, - baseline: JsValue, - change_sets: JsValue, - ) -> Result { - self.attribute(obj, baseline, change_sets) - } - - pub fn attribute( - &mut self, - obj: JsValue, - baseline: JsValue, - change_sets: JsValue, - ) -> Result { - let (obj, _) = self.import(obj)?; - let baseline = baseline.dyn_into::()?; - let baseline = get_heads(Some(baseline))?.unwrap(); // we know its an array - let change_sets = change_sets.dyn_into::()?; - let change_sets = change_sets - .iter() - .filter_map(|o| get_heads(o.dyn_into::().ok()).transpose()) - .collect::, _>>()?; - let result = self.doc.attribute(obj, &baseline, &change_sets)?; - let result = result - .into_iter() - .map(|cs| { - let add = cs - .add - .iter() - .map::, _>(|range| { - let r = Object::new(); - js_set(&r, "start", range.start as f64)?; - js_set(&r, "end", range.end as f64)?; - Ok(JsValue::from(&r)) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - let del = cs - .del - .iter() - .map::, _>(|d| { - let r = Object::new(); - js_set(&r, "pos", d.0 as f64)?; - js_set(&r, "val", &d.1)?; - Ok(JsValue::from(&r)) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - let obj = Object::new(); - js_set(&obj, "add", add)?; - js_set(&obj, "del", del)?; - Ok(obj.into()) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - Ok(result) - } - - pub fn attribute2( - &mut self, - obj: JsValue, - baseline: JsValue, - change_sets: JsValue, - ) -> Result { - let (obj, _) = self.import(obj)?; - let baseline = baseline.dyn_into::()?; - let baseline = get_heads(Some(baseline))?.unwrap(); - let change_sets = change_sets.dyn_into::()?; - let change_sets = change_sets - .iter() - .filter_map(|o| get_heads(o.dyn_into::().ok()).transpose()) - .collect::, _>>()?; - let result = self.doc.attribute2(obj, &baseline, &change_sets)?; - let result = result - .into_iter() - .map(|cs| { - let add = cs - .add - .iter() - .map::, _>(|a| { - let r = Object::new(); - js_set(&r, "actor", a.actor)?; - js_set(&r, "start", a.range.start as f64)?; - js_set(&r, "end", a.range.end as f64)?; - Ok(JsValue::from(&r)) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - let del = cs - .del - .iter() - .map::, _>(|d| { - let r = Object::new(); - js_set(&r, "actor", d.actor)?; - js_set(&r, "pos", d.pos as f64)?; - js_set(&r, "val", &d.span)?; - Ok(JsValue::from(&r)) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - let obj = Object::new(); - js_set(&obj, "add", add)?; - js_set(&obj, "del", del)?; - Ok(obj.into()) - }) - .collect::, JsValue>>()? - .iter() - .collect::(); - Ok(result) - } } #[wasm_bindgen(js_name = create)] diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs index 27c701c2..82654b54 100644 --- a/rust/automerge-wasm/src/observer.rs +++ b/rust/automerge-wasm/src/observer.rs @@ -109,6 +109,13 @@ pub(crate) enum Patch { path: Vec<(ObjId, Prop)>, marks: Vec>, }, + Unmark { + obj: ObjId, + path: Vec<(ObjId, Prop)>, + key: String, + start: usize, + end: usize, + }, } impl OpObserver for Observer { @@ -388,6 +395,27 @@ impl OpObserver for Observer { } } + fn unmark( + &mut self, + doc: &R, + obj: ObjId, + key: &str, + start: usize, + end: usize, + ) { + if self.enabled { + if let Some(path) = self.get_path(doc, &obj) { + self.patches.push(Patch::Unmark { + path, + obj, + key: key.to_string(), + start, + end, + }); + } + } + } + fn text_as_seq(&self) -> bool { self.text_rep == TextRepresentation::Array } @@ -443,6 +471,7 @@ impl Patch { Self::DeleteMap { path, .. } => path.as_slice(), Self::DeleteSeq { path, .. } => path.as_slice(), Self::Mark { path, .. } => path.as_slice(), + Self::Unmark { path, .. } => path.as_slice(), } } @@ -456,6 +485,7 @@ impl Patch { Self::DeleteMap { obj, .. } => obj, Self::DeleteSeq { obj, .. } => obj, Self::Mark { obj, .. } => obj, + Self::Unmark { obj, .. } => obj, } } } @@ -586,6 +616,20 @@ impl TryFrom for JsValue { js_set(&result, "marks", marks_array)?; Ok(result.into()) } + Patch::Unmark { + path, + key, + start, + end, + .. + } => { + js_set(&result, "action", "unmark")?; + js_set(&result, "path", export_just_path(path.as_slice()))?; + js_set(&result, "key", key)?; + js_set(&result, "start", start as i32)?; + js_set(&result, "end", end as i32)?; + Ok(result.into()) + } } } } diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 310783bf..060fe5bd 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -3,6 +3,8 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { create, load, Automerge, encodeChange, decodeChange } from '..' +import { v4 as uuid } from "uuid" + let util = require('util') @@ -22,6 +24,51 @@ describe('Automerge', () => { assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 4, end: 7 }]) }) + it('should handle mark and unmark', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[2..8]", "bold" , true) + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 8 }]) + doc.unmark(list, 'bold', 4, 6) + doc.insert(list, 7, "A") + doc.insert(list, 3, "A") + marks = doc.marks(list); + assert.deepStrictEqual(marks, [ + { key: 'bold', value: true, start: 2, end: 5 }, + { key: 'bold', value: true, start: 7, end: 10 }, + ]) + }) + + it('should handle mark and unmark of overlapping marks', () => { + let doc = create(true) + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[2..6]", "bold" , true) + doc.mark(list, "[5..8]", "bold" , true) + doc.mark(list, "[3..6]", "underline" , true) + let marks = doc.marks(list); + assert.deepStrictEqual(marks, [ + { key: 'underline', value: true, start: 3, end: 6 }, + { key: 'bold', value: true, start: 2, end: 8 }, + ]) + doc.unmark(list, 'bold', 4, 6) + doc.insert(list, 7, "A") + doc.insert(list, 3, "A") + marks = doc.marks(list); + assert.deepStrictEqual(marks, [ + { key: 'bold', value: true, start: 2, end: 5 }, + { key: 'underline', value: true, start: 4, end: 7 }, + { key: 'bold', value: true, start: 7, end: 10 }, + ]) + doc.unmark(list, 'bold', 0, 11) + marks = doc.marks(list); + assert.deepStrictEqual(marks, [ + { key: 'underline', value: true, start: 4, end: 7 } + ]) + }) + it('should handle marks [..] at the beginning of a string', () => { let doc = create(true) let list = doc.putObject("_root", "list", "") @@ -143,39 +190,17 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") doc.mark(list, "[0..37]", "bold" , true) doc.mark(list, "[4..19]", "itallic" , true) - doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + let id = uuid(); // we want each comment to be unique so give it a unique id + doc.mark(list, "[10..13]", `comment:${id}` , "foxes are my favorite animal!") doc.commit("marks"); let marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: 'comment', start: 10, end: 13, value: 'foxes are my favorite animal!' }, + { key: `comment:${id}`, start: 10, end: 13, value: 'foxes are my favorite animal!' }, { key: 'itallic', start: 4, end: 19, value: true }, { key: 'bold', start: 0, end: 37, value: true } ]) let text = doc.text(list); assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); - let rawMarks = doc.rawMarks(list); - assert.deepStrictEqual(rawMarks, - [ - { id: "39@aabbcc", start: 0, end: 37, key: 'bold', value: true }, - { id: "41@aabbcc", start: 4, end: 19, key: 'itallic', value: true }, - { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } - ]); - - doc.unmark(list, "41@aabbcc") - rawMarks = doc.rawMarks(list); - assert.deepStrictEqual(rawMarks, - [ - { id: "39@aabbcc", start: 0, end: 37, key: 'bold', value: true }, - { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } - ]); - // mark sure encode decode can handle marks - - doc.unmark(list, "39@aabbcc") - rawMarks = doc.rawMarks(list); - assert.deepStrictEqual(rawMarks, - [ - { id: "43@aabbcc", start: 10, end: 13, key: 'comment', value: 'foxes are my favorite animal!' } - ]); let all = doc.getChanges([]) let decoded = all.map((c) => decodeChange(c)) @@ -197,10 +222,10 @@ describe('Automerge', () => { let h1 = doc.getHeads() doc.mark(list, "[0..37]", "bold" , true) doc.mark(list, "[4..19]", "itallic" , true) - doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + let id = uuid(); // we want each comment to be unique so give it a unique id + doc.mark(list, "[10..13]", `comment:${id}` , "foxes are my favorite animal!") doc.commit("marks"); let h2 = doc.getHeads() - let x = doc.attribute2(list, [], [h2]); let patches = doc.popPatches(); let util = require('util') assert.deepEqual(patches, [ @@ -214,7 +239,7 @@ describe('Automerge', () => { marks: [ { key: 'bold', value: true, start: 0, end: 37 }, { key: 'itallic', value: true, start: 4, end: 19 }, - { key: 'comment', value: 'foxes are my favorite animal!', start: 10, end: 13 } + { key: `comment:${id}`, value: 'foxes are my favorite animal!', start: 10, end: 13 } ] } ]); diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index c8ced527..02cdd0d2 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -6,7 +6,6 @@ use crate::op_observer::{BranchableObserver, OpObserver}; use crate::sync::SyncDoc; use crate::transaction::{CommitOptions, Transactable}; use crate::{ - query, transaction::{Observation, Observed, TransactionInner, UnObserved}, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, TextEncoding, Value, Values, }; @@ -505,32 +504,6 @@ impl ReadDoc for AutoCommitWithObs { fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&Change> { self.doc.get_change_by_hash(hash) } - - fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { - self.doc.raw_spans(obj) - } - - fn spans>(&self, obj: O) -> Result>, AutomergeError> { - self.doc.spans(obj) - } - - fn attribute>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - self.doc.attribute(obj, baseline, change_sets) - } - - fn attribute2>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - self.doc.attribute2(obj, baseline, change_sets) - } } impl Transactable for AutoCommitWithObs { @@ -675,10 +648,12 @@ impl Transactable for AutoCommitWithObs { ) } - fn unmark, M: AsRef>( + fn unmark>( &mut self, obj: O, - mark: M, + key: &str, + start: usize, + end: usize, ) -> Result<(), AutomergeError> { self.ensure_transaction_open(); let (current, tx) = self.transaction.as_mut().unwrap(); @@ -686,7 +661,9 @@ impl Transactable for AutoCommitWithObs { &mut self.doc, current.observer(), obj.as_ref(), - mark.as_ref(), + key, + start, + end, ) } diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index f431fc16..b9ea2f9a 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -1369,66 +1369,6 @@ impl ReadDoc for Automerge { .collect()) } - fn spans>(&self, obj: O) -> Result>, AutomergeError> { - let obj = self.exid_to_obj(obj.as_ref())?.0; - let mut query = self.ops.search( - &obj, - query::Spans::new(ListEncoding::Text(self.text_encoding)), - ); - query.check_marks(); - Ok(query.spans) - } - - fn attribute>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - let obj = self.exid_to_obj(obj.as_ref())?.0; - let baseline = self.clock_at(baseline); - let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); - let mut query = self - .ops - .search(&obj, query::Attribute::new(baseline, change_sets)); - query.finish(); - log!("ATTRIBUTE query={:?}", query); - Ok(query.change_sets) - } - - fn attribute2>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - let obj = self.exid_to_obj(obj.as_ref())?.0; - let baseline = self.clock_at(baseline); - let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); - let mut query = self - .ops - .search(&obj, query::Attribute2::new(baseline, change_sets)); - query.finish(); - Ok(query.change_sets) - } - - fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { - let obj = self.exid_to_obj(obj.as_ref())?.0; - let query = self.ops.search(&obj, query::RawSpans::new()); - let result = query - .spans - .into_iter() - .map(|s| query::SpanInfo { - id: self.id_to_exid(s.id), - start: s.start, - end: s.end, - key: s.key.to_string(), - value: s.value, - }) - .collect(); - Ok(result) - } - fn get, P: Into>( &self, obj: O, diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index ec8226ef..640a498d 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -438,6 +438,17 @@ mod tests { _mark: M, ) { } + + fn unmark( + &mut self, + _doc: &R, + _objid: crate::ObjId, + _key: &str, + _start: usize, + _end: usize, + ) { + } + } #[test] diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 3dada62d..9e7fbe5a 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -84,7 +84,9 @@ impl<'a> MarkStateMachine<'a> { } else { let mut m = below.clone(); m.end = pos; - result = Some(m); + if !m.value().is_null() { + result = Some(m); + } } } @@ -105,10 +107,14 @@ impl<'a> MarkStateMachine<'a> { Some(below) if below.value() == mark.value() => {} Some(below) => { below.start = pos; - result = Some(mark.clone()); + if !mark.value().is_null() { + result = Some(mark.clone()); + } } None => { - result = Some(mark.clone()); + if !mark.value().is_null() { + result = Some(mark.clone()); + } } } } diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 2294e417..3d1d9f0a 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -120,6 +120,15 @@ pub trait OpObserver { mark: M, ); + fn unmark( + &mut self, + doc: &R, + objid: ExId, + key: &str, + start: usize, + end: usize, + ); + /// Whether to call sequence methods or `splice_text` when encountering changes in text /// /// Returns `false` by default @@ -198,6 +207,16 @@ impl OpObserver for () { ) { } + fn unmark<'a, R: ReadDoc>( + &mut self, + _doc: &'a R, + _objid: ExId, + _key: &str, + _start: usize, + _end: usize, + ) { + } + fn delete_map(&mut self, _doc: &R, _objid: ExId, _key: &str) {} fn delete_seq(&mut self, _doc: &R, _objid: ExId, _index: usize, _num: usize) {} @@ -310,6 +329,16 @@ impl OpObserver for VecOpObserver { ) { } + fn unmark( + &mut self, + _doc: &R, + _objid: ExId, + _key: &str, + _start: usize, + _end: usize, + ) { + } + fn delete_map(&mut self, doc: &R, obj: ExId, key: &str) { if let Ok(p) = doc.parents(&obj) { self.patches.push(Patch::Delete { diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index 64b5cbbc..f581d915 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -97,6 +97,18 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, self.obs2.mark(doc, objid, marks.into_iter()); } + fn unmark( + &mut self, + doc: &R, + objid: crate::ObjId, + key: &str, + start: usize, + end: usize + ) { + self.obs1.unmark(doc, objid.clone(), key, start, end); + self.obs2.unmark(doc, objid, key, start, end); + } + fn delete_map(&mut self, doc: &R, objid: crate::ObjId, key: &str) { self.obs1.delete_map(doc, objid.clone(), key); self.obs2.delete_map(doc, objid, key); diff --git a/rust/automerge/src/query.rs b/rust/automerge/src/query.rs index 0d331b8b..ffa06d3b 100644 --- a/rust/automerge/src/query.rs +++ b/rust/automerge/src/query.rs @@ -1,16 +1,12 @@ -use crate::exid::ExId; use crate::op_tree::{OpSetMetadata, OpTree, OpTreeNode}; use crate::types::{ Clock, Counter, Key, ListEncoding, Op, OpId, OpType, ScalarValue, TextEncoding, }; use fxhash::FxBuildHasher; -use serde::Serialize; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; -mod attribute; -mod attribute2; mod elem_id_pos; mod insert; mod keys; @@ -29,14 +25,10 @@ mod opid; mod opid_vis; mod prop; mod prop_at; -mod raw_spans; mod seek_mark; mod seek_op; mod seek_op_with_patch; -mod spans; -pub(crate) use attribute::{Attribute, ChangeSet}; -pub(crate) use attribute2::{Attribute2, ChangeSet2}; pub(crate) use elem_id_pos::ElemIdPos; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -55,20 +47,9 @@ pub(crate) use opid::OpIdSearch; pub(crate) use opid_vis::OpIdVisSearch; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; -pub(crate) use raw_spans::RawSpans; pub(crate) use seek_mark::SeekMark; pub(crate) use seek_op::SeekOp; pub(crate) use seek_op_with_patch::SeekOpWithPatch; -pub(crate) use spans::{Span, Spans}; - -#[derive(Serialize, Debug, Clone, PartialEq)] -pub struct SpanInfo { - pub id: ExId, - pub start: usize, - pub end: usize, - pub key: String, - pub value: ScalarValue, -} // use a struct for the args for clarity as they are passed up the update chain in the optree #[derive(Debug, Clone)] diff --git a/rust/automerge/src/query/attribute.rs b/rust/automerge/src/query/attribute.rs deleted file mode 100644 index 7dfea5ac..00000000 --- a/rust/automerge/src/query/attribute.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::clock::Clock; -use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, Op}; -use std::fmt::Debug; -use std::ops::Range; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Attribute { - pos: usize, - seen: usize, - last_seen: Option, - baseline: Clock, - pub(crate) change_sets: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ChangeSet { - clock: Clock, - next_add: Option>, - next_del: Option<(usize, String)>, - pub add: Vec>, - pub del: Vec<(usize, String)>, -} - -impl From for ChangeSet { - fn from(clock: Clock) -> Self { - ChangeSet { - clock, - next_add: None, - next_del: None, - add: Vec::new(), - del: Vec::new(), - } - } -} - -impl ChangeSet { - fn cut_add(&mut self) { - if let Some(add) = self.next_add.take() { - self.add.push(add) - } - } - - fn cut_del(&mut self) { - if let Some(del) = self.next_del.take() { - self.del.push(del) - } - } -} - -impl Attribute { - pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { - Attribute { - pos: 0, - seen: 0, - last_seen: None, - baseline, - change_sets: change_sets.into_iter().map(|c| c.into()).collect(), - } - } - - fn update_add(&mut self, element: &Op) { - let baseline = self.baseline.covers(&element.id); - for cs in &mut self.change_sets { - if !baseline && cs.clock.covers(&element.id) { - // is part of the change_set - if let Some(range) = &mut cs.next_add { - range.end += 1; - } else { - cs.next_add = Some(Range { - start: self.seen, - end: self.seen + 1, - }); - } - } else { - cs.cut_add(); - } - cs.cut_del(); - } - } - - // id is in baseline - // succ is not in baseline but is in cs - - fn update_del(&mut self, element: &Op) { - if !self.baseline.covers(&element.id) - || element.succ.iter().any(|id| self.baseline.covers(id)) - { - return; - } - for cs in &mut self.change_sets { - if element.succ.iter().any(|id| cs.clock.covers(id)) { - // was deleted by change set - let s = element.to_str(); - if let Some((_, span)) = &mut cs.next_del { - span.push_str(s); - } else { - cs.next_del = Some((self.seen, s.to_owned())) - } - } - } - } - - pub(crate) fn finish(&mut self) { - for cs in &mut self.change_sets { - cs.cut_add(); - cs.cut_del(); - } - } -} - -impl<'a> TreeQuery<'a> for Attribute { - fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { - if element.insert { - self.last_seen = None; - } - if self.last_seen.is_none() && element.visible() { - self.update_add(element); - self.seen += 1; - self.last_seen = element.elemid(); - } - if !element.succ.is_empty() { - self.update_del(element); - } - self.pos += 1; - QueryResult::Next - } -} diff --git a/rust/automerge/src/query/attribute2.rs b/rust/automerge/src/query/attribute2.rs deleted file mode 100644 index 3bec55a5..00000000 --- a/rust/automerge/src/query/attribute2.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::clock::Clock; -use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, Op}; -use std::fmt::Debug; -use std::ops::Range; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Attribute2 { - pos: usize, - seen: usize, - last_seen: Option, - baseline: Clock, - pub(crate) change_sets: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ChangeSet2 { - clock: Clock, - next_add: Option, - next_del: Option, - pub add: Vec, - pub del: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct CS2Add { - pub actor: usize, - pub range: Range, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct CS2Del { - pub pos: usize, - pub actor: usize, - pub span: String, -} - -impl From for ChangeSet2 { - fn from(clock: Clock) -> Self { - ChangeSet2 { - clock, - next_add: None, - next_del: None, - add: Vec::new(), - del: Vec::new(), - } - } -} - -impl ChangeSet2 { - fn cut_add(&mut self) { - if let Some(add) = self.next_add.take() { - self.add.push(add) - } - } - - fn cut_del(&mut self) { - if let Some(del) = self.next_del.take() { - self.del.push(del) - } - } -} - -impl Attribute2 { - pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { - Attribute2 { - pos: 0, - seen: 0, - last_seen: None, - baseline, - change_sets: change_sets.into_iter().map(|c| c.into()).collect(), - } - } - - fn update_add(&mut self, element: &Op) { - let baseline = self.baseline.covers(&element.id); - for cs in &mut self.change_sets { - if !baseline && cs.clock.covers(&element.id) { - // is part of the change_set - if let Some(CS2Add { range, actor }) = &mut cs.next_add { - if *actor == element.id.actor() { - range.end += 1; - } else { - cs.cut_add(); - cs.next_add = Some(CS2Add { - actor: element.id.actor(), - range: Range { - start: self.seen, - end: self.seen + 1, - }, - }); - } - } else { - cs.next_add = Some(CS2Add { - actor: element.id.actor(), - range: Range { - start: self.seen, - end: self.seen + 1, - }, - }); - } - } else { - cs.cut_add(); - } - cs.cut_del(); - } - } - - // id is in baseline - // succ is not in baseline but is in cs - - fn update_del(&mut self, element: &Op) { - if !self.baseline.covers(&element.id) - || element.succ.iter().any(|id| self.baseline.covers(id)) - { - return; - } - for cs in &mut self.change_sets { - let succ: Vec<_> = element - .succ - .iter() - .filter(|id| cs.clock.covers(id)) - .collect(); - // was deleted by change set - if let Some(suc) = succ.get(0) { - let s = element.to_str(); - if let Some(CS2Del { actor, span, .. }) = &mut cs.next_del { - if suc.actor() == *actor { - span.push_str(s); - } else { - cs.cut_del(); - cs.next_del = Some(CS2Del { - pos: self.seen, - actor: suc.actor(), - span: s.to_owned(), - }) - } - } else { - cs.next_del = Some(CS2Del { - pos: self.seen, - actor: suc.actor(), - span: s.to_owned(), - }) - } - } - } - } - - pub(crate) fn finish(&mut self) { - for cs in &mut self.change_sets { - cs.cut_add(); - cs.cut_del(); - } - } -} - -impl<'a> TreeQuery<'a> for Attribute2 { - fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { - if element.insert { - self.last_seen = None; - } - if self.last_seen.is_none() && element.visible() { - self.update_add(element); - self.seen += 1; - self.last_seen = element.elemid(); - } - if !element.succ.is_empty() { - self.update_del(element); - } - self.pos += 1; - QueryResult::Next - } -} diff --git a/rust/automerge/src/query/raw_spans.rs b/rust/automerge/src/query/raw_spans.rs deleted file mode 100644 index 90535bb1..00000000 --- a/rust/automerge/src/query/raw_spans.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; -use std::fmt::Debug; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct RawSpans { - pos: usize, - seen: usize, - last_seen: Option, - last_insert: Option, - changed: bool, - pub(crate) spans: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct RawSpan { - pub(crate) id: OpId, - pub(crate) start: usize, - pub(crate) end: usize, - pub(crate) key: smol_str::SmolStr, - pub(crate) value: ScalarValue, -} - -impl RawSpans { - pub(crate) fn new() -> Self { - RawSpans { - pos: 0, - seen: 0, - last_seen: None, - last_insert: None, - changed: false, - spans: Vec::new(), - } - } -} - -impl<'a> TreeQuery<'a> for RawSpans { - fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { - // find location to insert - // mark or set - if element.succ.is_empty() { - if let OpType::MarkBegin(_, md) = &element.action { - let pos = self - .spans - .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) - .unwrap_err(); - self.spans.insert( - pos, - RawSpan { - id: element.id, - start: self.seen, - end: 0, - key: md.key.clone(), - value: md.value.clone(), - }, - ); - } - if let OpType::MarkEnd(_) = &element.action { - for s in self.spans.iter_mut() { - if s.id == element.id.prev() { - s.end = self.seen; - break; - } - } - } - } - if element.insert { - self.last_seen = None; - self.last_insert = element.elemid(); - } - if self.last_seen.is_none() && element.visible() { - self.seen += 1; - self.last_seen = element.elemid(); - } - self.pos += 1; - QueryResult::Next - } -} diff --git a/rust/automerge/src/query/spans.rs b/rust/automerge/src/query/spans.rs deleted file mode 100644 index 8fb0cae8..00000000 --- a/rust/automerge/src/query/spans.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, ListEncoding, Op, OpType, ScalarValue}; -use std::borrow::Cow; -use std::collections::HashMap; -use std::fmt::Debug; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Spans<'a> { - pos: usize, - seen: usize, - encoding: ListEncoding, - last_seen: Option, - last_insert: Option, - seen_at_this_mark: Option, - seen_at_last_mark: Option, - ops: Vec<&'a Op>, - marks: HashMap, - changed: bool, - pub(crate) spans: Vec>, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Span<'a> { - pub pos: usize, - pub marks: Vec<(smol_str::SmolStr, Cow<'a, ScalarValue>)>, -} - -impl<'a> Spans<'a> { - pub(crate) fn new(encoding: ListEncoding) -> Self { - Spans { - pos: 0, - seen: 0, - encoding, - last_seen: None, - last_insert: None, - seen_at_last_mark: None, - seen_at_this_mark: None, - changed: false, - ops: Vec::new(), - marks: HashMap::new(), - spans: Vec::new(), - } - } - - pub(crate) fn check_marks(&mut self) { - let mut new_marks = HashMap::new(); - for op in &self.ops { - if let OpType::MarkBegin(_, m) = &op.action { - new_marks.insert(m.key.clone(), &m.value); - } - } - if new_marks != self.marks { - self.changed = true; - self.marks = new_marks; - } - if self.changed - && (self.seen_at_last_mark != self.seen_at_this_mark - || self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none()) - { - self.changed = false; - self.seen_at_last_mark = self.seen_at_this_mark; - let mut marks: Vec<_> = self - .marks - .iter() - .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) - .collect(); - marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); - self.spans.push(Span { - pos: self.seen, - marks, - }); - } - } -} - -impl<'a> TreeQuery<'a> for Spans<'a> { - /* - fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { - unimplemented!() - } - */ - - fn query_element_with_metadata(&mut self, element: &'a Op, m: &OpSetMetadata) -> QueryResult { - // find location to insert - // mark or set - if element.succ.is_empty() { - if let OpType::MarkBegin(_, _) = &element.action { - let pos = self - .ops - .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) - .unwrap_err(); - self.ops.insert(pos, element); - } - if let OpType::MarkEnd(_) = &element.action { - self.ops.retain(|op| op.id != element.id.prev()); - } - } - if element.insert { - self.last_seen = None; - self.last_insert = element.elemid(); - } - if self.last_seen.is_none() && element.visible() { - self.check_marks(); - let last_width = element.width(self.encoding); - self.seen += last_width; - self.last_seen = element.elemid(); - self.seen_at_this_mark = element.elemid(); - } - self.pos += 1; - QueryResult::Next - } -} diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 3596e033..6a5f6515 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -1,7 +1,7 @@ use crate::{ error::AutomergeError, exid::ExId, keys::Keys, keys_at::KeysAt, list_range::ListRange, list_range_at::ListRangeAt, map_range::MapRange, map_range_at::MapRangeAt, marks::Mark, - parents::Parents, query, values::Values, Change, ChangeHash, ObjType, Prop, Value, + parents::Parents, values::Values, Change, ChangeHash, ObjType, Prop, Value, }; use std::ops::RangeBounds; @@ -199,22 +199,4 @@ pub trait ReadDoc { /// Get a change by its hash. fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&Change>; - - fn raw_spans>(&self, obj: O) -> Result, AutomergeError>; - - fn spans>(&self, obj: O) -> Result>, AutomergeError>; - - fn attribute>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError>; - - fn attribute2>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError>; } diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 046a21bb..86de16b9 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use crate::exid::ExId; -use crate::marks::Mark; +use crate::marks::{ Mark }; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -659,70 +659,33 @@ impl TransactionInner { ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; if let Some(obs) = op_observer { - self.do_insert( - doc, - Some(obs), - obj, - mark.start, - OpType::MarkBegin(expand_left, mark.data.clone().into_owned()), - )?; + let action = OpType::MarkBegin(expand_left, mark.data.clone().into_owned()); + self.do_insert(doc, Some(obs), obj, mark.start, action)?; self.do_insert(doc, Some(obs), obj, mark.end, OpType::MarkEnd(expand_right))?; - obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) + if mark.value().is_null() { + obs.unmark(doc, ex_obj.clone(), mark.key(), mark.start, mark.end); + } else { + obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) + } } else { - self.do_insert::( - doc, - None, - obj, - mark.start, - OpType::MarkBegin(expand_left, mark.data.into_owned()), - )?; + let action = OpType::MarkBegin(expand_left, mark.data.into_owned()); + self.do_insert::(doc, None, obj, mark.start, action)?; self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(expand_right))?; } Ok(()) } - pub(crate) fn unmark, M: AsRef, Obs: OpObserver>( + pub(crate) fn unmark( &mut self, doc: &mut Automerge, - _op_observer: Option<&mut Obs>, - obj: O, - mark: M, + op_observer: Option<&mut Obs>, + ex_obj: &ExId, + key: &str, + start: usize, + end: usize, ) -> Result<(), AutomergeError> { - let (obj, _) = doc.exid_to_obj(obj.as_ref())?; - let markid = doc.exid_to_opid(mark.as_ref())?; - let ops = doc.ops_mut(); - let op1 = Op { - id: self.next_id(), - action: OpType::Delete, - key: markid.into(), - succ: Default::default(), - pred: ops.m.sorted_opids(vec![markid].into_iter()), - insert: false, - }; - let q1 = ops.search(&obj, query::SeekOp::new(&op1)); - ops.add_succ(&obj, &q1.succ, &op1); - //for i in q1.succ { - // ops.replace(&obj, i, |old_op| old_op.add_succ(&op1)); - //} - self.operations.push((obj, op1)); - - let markid = markid.next(); - let op2 = Op { - id: self.next_id(), - action: OpType::Delete, - key: markid.into(), - succ: Default::default(), - pred: ops.m.sorted_opids(vec![markid].into_iter()), - insert: false, - }; - let q2 = ops.search(&obj, query::SeekOp::new(&op2)); - - ops.add_succ(&obj, &q2.succ, &op2); - //for i in q2.succ { - // ops.replace(&obj, i, |old_op| old_op.add_succ(&op2)); - //} - self.operations.push((obj, op2)); - Ok(()) + let mark = Mark::new(key.to_string(), ScalarValue::Null, start, end); + self.mark(doc, op_observer, ex_obj, mark, (false, false)) } fn finalize_op( diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 9a8a659e..2fffa6c6 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -4,7 +4,7 @@ use crate::exid::ExId; use crate::marks::Mark; use crate::op_observer::BranchableObserver; use crate::{ - query, Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, + Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, Values, }; use crate::{AutomergeError, Keys}; @@ -245,32 +245,6 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { fn get_change_by_hash(&self, hash: &ChangeHash) -> Option<&crate::Change> { self.doc.get_change_by_hash(hash) } - - fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { - self.doc.raw_spans(obj) - } - - fn spans>(&self, obj: O) -> Result>, AutomergeError> { - self.doc.spans(obj) - } - - fn attribute>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - self.doc.attribute(obj, baseline, change_sets) - } - - fn attribute2>( - &self, - obj: O, - baseline: &[ChangeHash], - change_sets: &[Vec], - ) -> Result, AutomergeError> { - self.doc.attribute2(obj, baseline, change_sets) - } } impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { @@ -371,12 +345,14 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), mark, expand)) } - fn unmark, M: AsRef>( + fn unmark>( &mut self, obj: O, - mark: M, + key: &str, + start: usize, + end: usize, ) -> Result<(), AutomergeError> { - self.do_tx(|tx, doc, obs| tx.unmark(doc, obs, obj.as_ref(), mark.as_ref())) + self.do_tx(|tx, doc, obs| tx.unmark(doc, obs, obj.as_ref(), key, start, end)) } fn base_heads(&self) -> Vec { diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index d8401891..c0781b95 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -96,10 +96,12 @@ pub trait Transactable: ReadDoc { expand: (bool, bool), ) -> Result<(), AutomergeError>; - fn unmark, M: AsRef>( + fn unmark>( &mut self, obj: O, - mark: M, + key: &str, + start: usize, + end: usize, ) -> Result<(), AutomergeError>; /// The heads this transaction will be based on From ba491c6f72aa24df3c2150f81c3a207062f16dbf Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 27 Feb 2023 17:40:12 -0600 Subject: [PATCH 22/26] move wasm observer into automerge --- rust/automerge-wasm/src/interop.rs | 206 +++++- rust/automerge-wasm/src/lib.rs | 30 +- rust/automerge-wasm/src/observer.rs | 635 ------------------ rust/automerge-wasm/test/test.ts | 2 +- rust/automerge/examples/watch.rs | 72 +- rust/automerge/src/autocommit.rs | 8 +- rust/automerge/src/automerge/current_state.rs | 1 - rust/automerge/src/automerge/tests.rs | 46 +- rust/automerge/src/lib.rs | 7 +- rust/automerge/src/marks.rs | 6 +- rust/automerge/src/op_observer.rs | 242 +------ rust/automerge/src/op_observer/compose.rs | 2 +- rust/automerge/src/op_observer/patch.rs | 57 ++ .../src/op_observer/toggle_observer.rs | 178 +++++ .../automerge/src/op_observer/vec_observer.rs | 566 ++++++++++++++++ rust/automerge/src/sequence_tree.rs | 617 +++++++++++++++++ rust/automerge/src/transaction/inner.rs | 9 +- .../src/transaction/manual_transaction.rs | 3 +- rust/automerge/src/types.rs | 24 +- 19 files changed, 1713 insertions(+), 998 deletions(-) delete mode 100644 rust/automerge-wasm/src/observer.rs create mode 100644 rust/automerge/src/op_observer/patch.rs create mode 100644 rust/automerge/src/op_observer/toggle_observer.rs create mode 100644 rust/automerge/src/op_observer/vec_observer.rs create mode 100644 rust/automerge/src/sequence_tree.rs diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index f3130e78..7135e7db 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -12,7 +12,7 @@ use std::fmt::Display; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use crate::{observer::Patch, ObjId, Value}; +use am::{ObjId, Patch, PatchAction, Value}; const RAW_DATA_SYMBOL: &str = "_am_raw_value_"; const DATATYPE_SYMBOL: &str = "_am_datatype_"; @@ -761,13 +761,13 @@ impl Automerge { pub(crate) fn apply_patch_to_array( &self, array: &Object, - patch: &Patch, + patch: &Patch, meta: &JsValue, exposed: &mut HashSet, ) -> Result { let result = Array::from(array); // shallow copy - match patch { - Patch::PutSeq { + match &patch.action { + PatchAction::PutSeq { index, value, expose, @@ -783,13 +783,13 @@ impl Automerge { } Ok(result.into()) } - Patch::DeleteSeq { index, length, .. } => { + PatchAction::DeleteSeq { index, length, .. } => { Ok(self.sub_splice(result, *index, *length, vec![], meta)?) } - Patch::Insert { index, values, .. } => { + PatchAction::Insert { index, values, .. } => { Ok(self.sub_splice(result, *index, 0, values, meta)?) } - Patch::Increment { prop, value, .. } => { + PatchAction::Increment { prop, value, .. } => { if let Prop::Seq(index) = prop { let index = *index as f64; let old_val = js_get(&result, index)?.0; @@ -810,9 +810,9 @@ impl Automerge { Err(error::ApplyPatch::IncrementKeyInSeq) } } - Patch::DeleteMap { .. } => Err(error::ApplyPatch::DeleteKeyFromSeq), - Patch::PutMap { .. } => Err(error::ApplyPatch::PutKeyInSeq), - Patch::SpliceText { index, value, .. } => { + PatchAction::DeleteMap { .. } => Err(error::ApplyPatch::DeleteKeyFromSeq), + PatchAction::PutMap { .. } => Err(error::ApplyPatch::PutKeyInSeq), + PatchAction::SpliceText { index, value, .. } => { match self.text_rep { TextRepresentation::String => Err(error::ApplyPatch::SpliceTextInSeq), TextRepresentation::Array => { @@ -834,20 +834,20 @@ impl Automerge { } } } - Patch::Mark { .. } | Patch::Unmark { .. } => Ok(result.into()), + PatchAction::Mark { .. } | PatchAction::Unmark { .. } => Ok(result.into()), } } pub(crate) fn apply_patch_to_map( &self, map: &Object, - patch: &Patch, + patch: &Patch, meta: &JsValue, exposed: &mut HashSet, ) -> Result { let result = Object::assign(&Object::new(), map); // shallow copy - match patch { - Patch::PutMap { + match &patch.action { + PatchAction::PutMap { key, value, expose, .. } => { if *expose && value.0.is_object() { @@ -860,7 +860,7 @@ impl Automerge { } Ok(result) } - Patch::DeleteMap { key, .. } => { + PatchAction::DeleteMap { key, .. } => { Reflect::delete_property(&result, &key.into()).map_err(|e| { error::Export::Delete { prop: key.to_string(), @@ -869,7 +869,7 @@ impl Automerge { })?; Ok(result) } - Patch::Increment { prop, value, .. } => { + PatchAction::Increment { prop, value, .. } => { if let Prop::Map(key) = prop { let old_val = js_get(&result, key)?.0; let old_val = self.unwrap_scalar(old_val)?; @@ -889,28 +889,30 @@ impl Automerge { Err(error::ApplyPatch::IncrementIndexInMap) } } - Patch::Insert { .. } => Err(error::ApplyPatch::InsertInMap), - Patch::DeleteSeq { .. } => Err(error::ApplyPatch::SpliceInMap), - //Patch::SpliceText { .. } => Err(to_js_err("cannot Splice into map")), - Patch::SpliceText { .. } => Err(error::ApplyPatch::SpliceTextInMap), - Patch::PutSeq { .. } => Err(error::ApplyPatch::PutIdxInMap), - Patch::Mark { .. } | Patch::Unmark { .. } => Err(error::ApplyPatch::MarkInMap), + PatchAction::Insert { .. } => Err(error::ApplyPatch::InsertInMap), + PatchAction::DeleteSeq { .. } => Err(error::ApplyPatch::SpliceInMap), + //PatchAction::SpliceText { .. } => Err(to_js_err("cannot Splice into map")), + PatchAction::SpliceText { .. } => Err(error::ApplyPatch::SpliceTextInMap), + PatchAction::PutSeq { .. } => Err(error::ApplyPatch::PutIdxInMap), + PatchAction::Mark { .. } | PatchAction::Unmark { .. } => { + Err(error::ApplyPatch::MarkInMap) + } } } pub(crate) fn apply_patch( &self, obj: Object, - patch: &Patch, + patch: &Patch, depth: usize, meta: &JsValue, exposed: &mut HashSet, ) -> Result { let (inner, datatype, id) = self.unwrap_object(&obj)?; - let prop = patch.path().get(depth).map(|p| prop_to_js(&p.1)); + let prop = patch.path.get(depth).map(|p| prop_to_js(&p.1)); let result = if let Some(prop) = prop { let subval = js_get(&inner, &prop)?.0; - if subval.is_string() && patch.path().len() - 1 == depth { + if subval.is_string() && patch.path.len() - 1 == depth { if let Ok(s) = subval.dyn_into::() { let new_value = self.apply_patch_to_text(&s, patch)?; let result = shallow_copy(&inner); @@ -931,12 +933,12 @@ impl Automerge { return Ok(obj); } } else if Array::is_array(&inner) { - if &id == patch.obj() { + if id == patch.obj { self.apply_patch_to_array(&inner, patch, meta, exposed) } else { Ok(Array::from(&inner).into()) } - } else if &id == patch.obj() { + } else if id == patch.obj { self.apply_patch_to_map(&inner, patch, meta, exposed) } else { Ok(Object::assign(&Object::new(), &inner)) @@ -949,17 +951,17 @@ impl Automerge { fn apply_patch_to_text( &self, string: &JsString, - patch: &Patch, + patch: &Patch, ) -> Result { - match patch { - Patch::DeleteSeq { index, length, .. } => { + match &patch.action { + PatchAction::DeleteSeq { index, length, .. } => { let index = *index as u32; let before = string.slice(0, index); let after = string.slice(index + *length as u32, string.length()); let result = before.concat(&after); Ok(result.into()) } - Patch::SpliceText { index, value, .. } => { + PatchAction::SpliceText { index, value, .. } => { let index = *index as u32; let length = string.length(); let before = string.slice(0, index); @@ -1222,6 +1224,148 @@ fn set_hidden_value>( Ok(()) } +pub(crate) struct JsPatch(pub(crate) Patch); + +fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array { + let result = Array::new(); + for p in path { + result.push(&prop_to_js(&p.1)); + } + result.push(&prop_to_js(end)); + result +} + +fn export_just_path(path: &[(ObjId, Prop)]) -> Array { + let result = Array::new(); + for p in path { + result.push(&prop_to_js(&p.1)); + } + result +} + +impl TryFrom for JsValue { + type Error = error::Export; + + fn try_from(p: JsPatch) -> Result { + let result = Object::new(); + let path = &p.0.path; + match p.0.action { + PatchAction::PutMap { key, value, .. } => { + js_set(&result, "action", "put")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Map(key)), + )?; + js_set( + &result, + "value", + alloc(&value.0, TextRepresentation::String).1, + )?; + Ok(result.into()) + } + PatchAction::PutSeq { index, value, .. } => { + js_set(&result, "action", "put")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Seq(index)), + )?; + js_set( + &result, + "value", + alloc(&value.0, TextRepresentation::String).1, + )?; + Ok(result.into()) + } + PatchAction::Insert { index, values, .. } => { + js_set(&result, "action", "insert")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Seq(index)), + )?; + js_set( + &result, + "values", + values + .iter() + .map(|v| alloc(&v.0, TextRepresentation::String).1) + .collect::(), + )?; + Ok(result.into()) + } + PatchAction::SpliceText { index, value, .. } => { + js_set(&result, "action", "splice")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Seq(index)), + )?; + let bytes: Vec = value.iter().cloned().collect(); + js_set(&result, "value", String::from_utf16_lossy(bytes.as_slice()))?; + Ok(result.into()) + } + PatchAction::Increment { prop, value, .. } => { + js_set(&result, "action", "inc")?; + js_set(&result, "path", export_path(path.as_slice(), &prop))?; + js_set(&result, "value", &JsValue::from_f64(value as f64))?; + Ok(result.into()) + } + PatchAction::DeleteMap { key, .. } => { + js_set(&result, "action", "del")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Map(key)), + )?; + Ok(result.into()) + } + PatchAction::DeleteSeq { index, length, .. } => { + js_set(&result, "action", "del")?; + js_set( + &result, + "path", + export_path(path.as_slice(), &Prop::Seq(index)), + )?; + if length > 1 { + js_set(&result, "length", length)?; + } + Ok(result.into()) + } + PatchAction::Mark { marks, .. } => { + js_set(&result, "action", "mark")?; + js_set(&result, "path", export_just_path(path.as_slice()))?; + let marks_array = Array::new(); + for m in marks.iter() { + let mark = Object::new(); + js_set(&mark, "key", m.key())?; + js_set( + &mark, + "value", + &alloc(&m.value().into(), TextRepresentation::String).1, + )?; + js_set(&mark, "start", m.start as i32)?; + js_set(&mark, "end", m.end as i32)?; + marks_array.push(&mark); + } + js_set(&result, "marks", marks_array)?; + Ok(result.into()) + } + PatchAction::Unmark { + key, start, end, .. + } => { + js_set(&result, "action", "unmark")?; + js_set(&result, "path", export_just_path(path.as_slice()))?; + js_set(&result, "key", key)?; + js_set(&result, "start", start as i32)?; + js_set(&result, "end", end as i32)?; + Ok(result.into()) + } + } + } +} + fn shallow_copy(obj: &Object) -> Object { if Array::is_array(obj) { Array::from(obj).into() diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index bee15729..113d71a1 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -29,7 +29,8 @@ use am::transaction::CommitOptions; use am::transaction::{Observed, Transactable, UnObserved}; use am::ScalarValue; use automerge as am; -use automerge::{sync::SyncDoc, Change, ObjId, Prop, ReadDoc, TextEncoding, Value, ROOT}; +use automerge::{sync::SyncDoc, Change, Prop, ReadDoc, TextEncoding, Value, ROOT}; +use automerge::{ToggleObserver, VecOpObserver16}; use js_sys::{Array, Function, Object, Uint8Array}; use regex::Regex; use serde::ser::Serialize; @@ -41,13 +42,9 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; mod interop; -mod observer; -mod sequence_tree; mod sync; mod value; -use observer::Observer; - use interop::{alloc, get_heads, import_obj, js_set, to_js_err, to_prop, AR, JS}; use sync::SyncState; use value::Datatype; @@ -61,7 +58,7 @@ macro_rules! log { }; } -type AutoCommit = am::AutoCommitWithObs>; +type AutoCommit = am::AutoCommitWithObs>>; #[cfg(feature = "wee_alloc")] #[global_allocator] @@ -83,6 +80,15 @@ impl std::default::Default for TextRepresentation { } } +impl From for am::op_observer::TextRepresentation { + fn from(tr: TextRepresentation) -> Self { + match tr { + TextRepresentation::Array => am::op_observer::TextRepresentation::Array, + TextRepresentation::String => am::op_observer::TextRepresentation::String, + } + } +} + #[wasm_bindgen] #[derive(Debug)] pub struct Automerge { @@ -98,7 +104,10 @@ impl Automerge { actor: Option, text_rep: TextRepresentation, ) -> Result { - let mut doc = AutoCommit::default().with_encoding(TextEncoding::Utf16); + let mut doc = AutoCommit::default() + .with_observer(ToggleObserver::default().with_text_rep(text_rep.into())) + .with_encoding(TextEncoding::Utf16); + doc.observer().set_text_rep(text_rep.into()); if let Some(a) = actor { let a = automerge::ActorId::from(hex::decode(a)?.to_vec()); doc.set_actor(a); @@ -548,7 +557,7 @@ impl Automerge { .ok_or_else(|| to_js_err("must pass a bool to enablePatches"))?; let heads = self.doc.get_heads(); let old_enabled = self.doc.observer().enable(enable, heads); - self.doc.observer().set_text_rep(self.text_rep); + self.doc.observer().set_text_rep(self.text_rep.into()); Ok(old_enabled.into()) } @@ -603,6 +612,7 @@ impl Automerge { if !patches.is_empty() { let patches: Array = patches .into_iter() + .map(interop::JsPatch) .map(JsValue::try_from) .collect::>()?; let info = Object::new(); @@ -627,7 +637,7 @@ impl Automerge { let (patches, _heads) = self.doc.observer().take_patches(heads); let result = Array::new(); for p in patches { - result.push(&p.try_into()?); + result.push(&interop::JsPatch(p).try_into()?); } Ok(result) } @@ -880,7 +890,7 @@ pub fn load( TextRepresentation::Array }; let mut doc = am::AutoCommitWithObs::::load(&data)? - .with_observer(Observer::default().with_text_rep(text_rep)) + .with_observer(ToggleObserver::default().with_text_rep(text_rep.into())) .with_encoding(TextEncoding::Utf16); if let Some(s) = actor { let actor = diff --git a/rust/automerge-wasm/src/observer.rs b/rust/automerge-wasm/src/observer.rs deleted file mode 100644 index 82654b54..00000000 --- a/rust/automerge-wasm/src/observer.rs +++ /dev/null @@ -1,635 +0,0 @@ -#![allow(dead_code)] - -use std::borrow::Cow; - -use automerge::ChangeHash; - -use crate::{ - interop::{self, alloc, js_set}, - TextRepresentation, -}; -use automerge::{marks::Mark, ObjId, OpObserver, Prop, ReadDoc, ScalarValue, Value}; -use js_sys::{Array, Object}; -use wasm_bindgen::prelude::*; - -use crate::sequence_tree::SequenceTree; - -#[derive(Debug, Clone, Default)] -pub(crate) struct Observer { - enabled: bool, - last_heads: Option>, - patches: Vec, - text_rep: TextRepresentation, -} - -impl Observer { - pub(crate) fn take_patches(&mut self, heads: Vec) -> (Vec, Vec) { - let old_heads = self.last_heads.replace(heads).unwrap_or_default(); - let patches = std::mem::take(&mut self.patches); - (patches, old_heads) - } - - pub(crate) fn enable(&mut self, enable: bool, heads: Vec) -> bool { - if self.enabled && !enable { - self.patches.truncate(0); - self.last_heads = Some(heads); - } - let old_enabled = self.enabled; - self.enabled = enable; - old_enabled - } - - fn get_path(&mut self, doc: &R, obj: &ObjId) -> Option> { - match doc.parents(obj) { - Ok(parents) => parents.visible_path(), - Err(e) => { - automerge::log!("error generating patch : {:?}", e); - None - } - } - } - - pub(crate) fn with_text_rep(mut self, text_rep: TextRepresentation) -> Self { - self.text_rep = text_rep; - self - } - - pub(crate) fn set_text_rep(&mut self, text_rep: TextRepresentation) { - self.text_rep = text_rep; - } -} - -#[derive(Debug, Clone)] -pub(crate) enum Patch { - PutMap { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - key: String, - value: (Value<'static>, ObjId), - expose: bool, - }, - PutSeq { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - index: usize, - value: (Value<'static>, ObjId), - expose: bool, - }, - Insert { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - index: usize, - values: SequenceTree<(Value<'static>, ObjId)>, - }, - SpliceText { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - index: usize, - value: SequenceTree, - }, - Increment { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - prop: Prop, - value: i64, - }, - DeleteMap { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - key: String, - }, - DeleteSeq { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - index: usize, - length: usize, - }, - Mark { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - marks: Vec>, - }, - Unmark { - obj: ObjId, - path: Vec<(ObjId, Prop)>, - key: String, - start: usize, - end: usize, - }, -} - -impl OpObserver for Observer { - fn insert( - &mut self, - doc: &R, - obj: ObjId, - index: usize, - tagged_value: (Value<'_>, ObjId), - _conflict: bool, - ) { - if self.enabled { - let value = (tagged_value.0.to_owned(), tagged_value.1); - if let Some(Patch::Insert { - obj: tail_obj, - index: tail_index, - values, - .. - }) = self.patches.last_mut() - { - let range = *tail_index..=*tail_index + values.len(); - if tail_obj == &obj && range.contains(&index) { - values.insert(index - *tail_index, value); - return; - } - } - if let Some(path) = self.get_path(doc, &obj) { - let mut values = SequenceTree::new(); - values.push(value); - let patch = Patch::Insert { - path, - obj, - index, - values, - }; - self.patches.push(patch); - } - } - } - - fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { - if self.enabled { - if self.text_rep == TextRepresentation::Array { - for (i, c) in value.chars().enumerate() { - self.insert( - doc, - obj.clone(), - index + i, - ( - Value::Scalar(Cow::Owned(ScalarValue::Str(c.to_string().into()))), - ObjId::Root, // We hope this is okay - ), - false, - ); - } - return; - } - if let Some(Patch::SpliceText { - obj: tail_obj, - index: tail_index, - value: prev_value, - .. - }) = self.patches.last_mut() - { - let range = *tail_index..=*tail_index + prev_value.len(); - if tail_obj == &obj && range.contains(&index) { - let i = index - *tail_index; - for (n, ch) in value.encode_utf16().enumerate() { - prev_value.insert(i + n, ch) - } - return; - } - } - if let Some(path) = self.get_path(doc, &obj) { - let mut v = SequenceTree::new(); - for ch in value.encode_utf16() { - v.push(ch) - } - let patch = Patch::SpliceText { - path, - obj, - index, - value: v, - }; - self.patches.push(patch); - } - } - } - - fn delete_seq(&mut self, doc: &R, obj: ObjId, index: usize, length: usize) { - if self.enabled { - match self.patches.last_mut() { - Some(Patch::SpliceText { - obj: tail_obj, - index: tail_index, - value, - .. - }) => { - let range = *tail_index..*tail_index + value.len(); - if tail_obj == &obj - && range.contains(&index) - && range.contains(&(index + length - 1)) - { - for _ in 0..length { - value.remove(index - *tail_index); - } - return; - } - } - Some(Patch::Insert { - obj: tail_obj, - index: tail_index, - values, - .. - }) => { - let range = *tail_index..*tail_index + values.len(); - if tail_obj == &obj - && range.contains(&index) - && range.contains(&(index + length - 1)) - { - for _ in 0..length { - values.remove(index - *tail_index); - } - return; - } - } - Some(Patch::DeleteSeq { - obj: tail_obj, - index: tail_index, - length: tail_length, - .. - }) => { - if tail_obj == &obj && index == *tail_index { - *tail_length += length; - return; - } - } - _ => {} - } - if let Some(path) = self.get_path(doc, &obj) { - let patch = Patch::DeleteSeq { - path, - obj, - index, - length, - }; - self.patches.push(patch) - } - } - } - - fn delete_map(&mut self, doc: &R, obj: ObjId, key: &str) { - if self.enabled { - if let Some(path) = self.get_path(doc, &obj) { - let patch = Patch::DeleteMap { - path, - obj, - key: key.to_owned(), - }; - self.patches.push(patch) - } - } - } - - fn put( - &mut self, - doc: &R, - obj: ObjId, - prop: Prop, - tagged_value: (Value<'_>, ObjId), - _conflict: bool, - ) { - if self.enabled { - let expose = false; - if let Some(path) = self.get_path(doc, &obj) { - let value = (tagged_value.0.to_owned(), tagged_value.1); - let patch = match prop { - Prop::Map(key) => Patch::PutMap { - path, - obj, - key, - value, - expose, - }, - Prop::Seq(index) => Patch::PutSeq { - path, - obj, - index, - value, - expose, - }, - }; - self.patches.push(patch); - } - } - } - - fn expose( - &mut self, - doc: &R, - obj: ObjId, - prop: Prop, - tagged_value: (Value<'_>, ObjId), - _conflict: bool, - ) { - if self.enabled { - let expose = true; - if let Some(path) = self.get_path(doc, &obj) { - let value = (tagged_value.0.to_owned(), tagged_value.1); - let patch = match prop { - Prop::Map(key) => Patch::PutMap { - path, - obj, - key, - value, - expose, - }, - Prop::Seq(index) => Patch::PutSeq { - path, - obj, - index, - value, - expose, - }, - }; - self.patches.push(patch); - } - } - } - - fn increment( - &mut self, - doc: &R, - obj: ObjId, - prop: Prop, - tagged_value: (i64, ObjId), - ) { - if self.enabled { - if let Some(path) = self.get_path(doc, &obj) { - let value = tagged_value.0; - self.patches.push(Patch::Increment { - path, - obj, - prop, - value, - }) - } - } - } - - fn mark<'a, R: ReadDoc, M: Iterator>>( - &mut self, - doc: &'a R, - obj: ObjId, - mark: M, - ) { - if self.enabled { - if let Some(Patch::Mark { - obj: tail_obj, - marks, - .. - }) = self.patches.last_mut() - { - if tail_obj == &obj { - for m in mark { - marks.push(m.into_owned()) - } - return; - } - } - if let Some(path) = self.get_path(doc, &obj) { - let marks: Vec<_> = mark.map(|m| m.into_owned()).collect(); - if !marks.is_empty() { - self.patches.push(Patch::Mark { path, obj, marks }); - } - } - } - } - - fn unmark( - &mut self, - doc: &R, - obj: ObjId, - key: &str, - start: usize, - end: usize, - ) { - if self.enabled { - if let Some(path) = self.get_path(doc, &obj) { - self.patches.push(Patch::Unmark { - path, - obj, - key: key.to_string(), - start, - end, - }); - } - } - } - - fn text_as_seq(&self) -> bool { - self.text_rep == TextRepresentation::Array - } -} - -impl automerge::op_observer::BranchableObserver for Observer { - fn merge(&mut self, other: &Self) { - self.patches.extend_from_slice(other.patches.as_slice()) - } - - fn branch(&self) -> Self { - Observer { - patches: vec![], - last_heads: None, - enabled: self.enabled, - text_rep: self.text_rep, - } - } -} - -fn prop_to_js(p: &Prop) -> JsValue { - match p { - Prop::Map(key) => JsValue::from_str(key), - Prop::Seq(index) => JsValue::from_f64(*index as f64), - } -} - -fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array { - let result = Array::new(); - for p in path { - result.push(&prop_to_js(&p.1)); - } - result.push(&prop_to_js(end)); - result -} - -fn export_just_path(path: &[(ObjId, Prop)]) -> Array { - let result = Array::new(); - for p in path { - result.push(&prop_to_js(&p.1)); - } - result -} - -impl Patch { - pub(crate) fn path(&self) -> &[(ObjId, Prop)] { - match &self { - Self::PutMap { path, .. } => path.as_slice(), - Self::PutSeq { path, .. } => path.as_slice(), - Self::Increment { path, .. } => path.as_slice(), - Self::Insert { path, .. } => path.as_slice(), - Self::SpliceText { path, .. } => path.as_slice(), - Self::DeleteMap { path, .. } => path.as_slice(), - Self::DeleteSeq { path, .. } => path.as_slice(), - Self::Mark { path, .. } => path.as_slice(), - Self::Unmark { path, .. } => path.as_slice(), - } - } - - pub(crate) fn obj(&self) -> &ObjId { - match &self { - Self::PutMap { obj, .. } => obj, - Self::PutSeq { obj, .. } => obj, - Self::Increment { obj, .. } => obj, - Self::Insert { obj, .. } => obj, - Self::SpliceText { obj, .. } => obj, - Self::DeleteMap { obj, .. } => obj, - Self::DeleteSeq { obj, .. } => obj, - Self::Mark { obj, .. } => obj, - Self::Unmark { obj, .. } => obj, - } - } -} - -impl TryFrom for JsValue { - type Error = interop::error::Export; - - fn try_from(p: Patch) -> Result { - let result = Object::new(); - match p { - Patch::PutMap { - path, key, value, .. - } => { - js_set(&result, "action", "put")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Map(key)), - )?; - js_set( - &result, - "value", - alloc(&value.0, TextRepresentation::String).1, - )?; - Ok(result.into()) - } - Patch::PutSeq { - path, index, value, .. - } => { - js_set(&result, "action", "put")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Seq(index)), - )?; - js_set( - &result, - "value", - alloc(&value.0, TextRepresentation::String).1, - )?; - Ok(result.into()) - } - Patch::Insert { - path, - index, - values, - .. - } => { - js_set(&result, "action", "insert")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Seq(index)), - )?; - js_set( - &result, - "values", - values - .iter() - .map(|v| alloc(&v.0, TextRepresentation::String).1) - .collect::(), - )?; - Ok(result.into()) - } - Patch::SpliceText { - path, index, value, .. - } => { - js_set(&result, "action", "splice")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Seq(index)), - )?; - let bytes: Vec = value.iter().cloned().collect(); - js_set(&result, "value", String::from_utf16_lossy(bytes.as_slice()))?; - Ok(result.into()) - } - Patch::Increment { - path, prop, value, .. - } => { - js_set(&result, "action", "inc")?; - js_set(&result, "path", export_path(path.as_slice(), &prop))?; - js_set(&result, "value", &JsValue::from_f64(value as f64))?; - Ok(result.into()) - } - Patch::DeleteMap { path, key, .. } => { - js_set(&result, "action", "del")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Map(key)), - )?; - Ok(result.into()) - } - Patch::DeleteSeq { - path, - index, - length, - .. - } => { - js_set(&result, "action", "del")?; - js_set( - &result, - "path", - export_path(path.as_slice(), &Prop::Seq(index)), - )?; - if length > 1 { - js_set(&result, "length", length)?; - } - Ok(result.into()) - } - Patch::Mark { path, marks, .. } => { - js_set(&result, "action", "mark")?; - js_set(&result, "path", export_just_path(path.as_slice()))?; - let marks_array = Array::new(); - for m in marks.iter() { - let mark = Object::new(); - js_set(&mark, "key", m.key())?; - js_set( - &mark, - "value", - &alloc(&m.value().into(), TextRepresentation::String).1, - )?; - js_set(&mark, "start", m.start as i32)?; - js_set(&mark, "end", m.end as i32)?; - marks_array.push(&mark); - } - js_set(&result, "marks", marks_array)?; - Ok(result.into()) - } - Patch::Unmark { - path, - key, - start, - end, - .. - } => { - js_set(&result, "action", "unmark")?; - js_set(&result, "path", export_just_path(path.as_slice()))?; - js_set(&result, "key", key)?; - js_set(&result, "start", start as i32)?; - js_set(&result, "end", end as i32)?; - Ok(result.into()) - } - } - } -} diff --git a/rust/automerge-wasm/test/test.ts b/rust/automerge-wasm/test/test.ts index 3e57d92c..2a6833a1 100644 --- a/rust/automerge-wasm/test/test.ts +++ b/rust/automerge-wasm/test/test.ts @@ -2162,7 +2162,7 @@ describe('Automerge', () => { assert.deepEqual(mat.text, new FakeText("0abcd")) }) - it("should allow inserting objects in old style text", () => { + it.only("should allow inserting objects in old style text", () => { let doc = create(false); doc.registerDatatype("text", (e: any) => new FakeText(e)) doc.enablePatches(true) diff --git a/rust/automerge/examples/watch.rs b/rust/automerge/examples/watch.rs index 4cd8f4ea..73bdeb27 100644 --- a/rust/automerge/examples/watch.rs +++ b/rust/automerge/examples/watch.rs @@ -1,11 +1,11 @@ +use automerge::op_observer::HasPatches; use automerge::transaction::CommitOptions; use automerge::transaction::Transactable; use automerge::Automerge; use automerge::AutomergeError; -use automerge::Patch; -use automerge::ReadDoc; use automerge::VecOpObserver; use automerge::ROOT; +use automerge::{PatchAction, PatchCtx}; fn main() { let mut doc = Automerge::new(); @@ -42,64 +42,52 @@ fn main() { get_changes(&doc, patches); } -fn get_changes(doc: &Automerge, patches: Vec) { - for patch in patches { - match patch { - Patch::Put { - obj, prop, value, .. - } => { +fn get_changes(_doc: &Automerge, patches: Vec>) { + for PatchCtx { obj, path, action } in patches { + match action { + PatchAction::PutMap { key, value, .. } => { println!( "put {:?} at {:?} in obj {:?}, object path {:?}", - value, - prop, - obj, - doc.path_to_object(&obj) + value, key, obj, path, ) } - Patch::Insert { - obj, index, value, .. - } => { + PatchAction::PutSeq { index, value, .. } => { + println!( + "put {:?} at {:?} in obj {:?}, object path {:?}", + value, index, obj, path, + ) + } + PatchAction::Insert { index, values, .. } => { println!( "insert {:?} at {:?} in obj {:?}, object path {:?}", - value, - index, - obj, - doc.path_to_object(&obj) + values, index, obj, path, ) } - Patch::Splice { - obj, index, value, .. - } => { + PatchAction::SpliceText { index, value, .. } => { println!( "splice '{:?}' at {:?} in obj {:?}, object path {:?}", - value, - index, - obj, - doc.path_to_object(&obj) + value, index, obj, path, ) } - Patch::Increment { - obj, prop, value, .. - } => { + PatchAction::Increment { prop, value, .. } => { println!( "increment {:?} in obj {:?} by {:?}, object path {:?}", - prop, - obj, - value, - doc.path_to_object(&obj) + prop, obj, value, path, ) } - Patch::Delete { obj, prop, .. } => println!( + PatchAction::DeleteMap { key, .. } => { + println!("delete {:?} in obj {:?}, object path {:?}", key, obj, path,) + } + PatchAction::DeleteSeq { index, .. } => println!( "delete {:?} in obj {:?}, object path {:?}", - prop, - obj, - doc.path_to_object(&obj) + index, obj, path, ), - Patch::Expose { obj, prop, .. } => println!( - "expose {:?} in obj {:?}, object path {:?}", - prop, - obj, - doc.path_to_object(&obj) + PatchAction::Mark { marks } => { + println!("mark {:?} in obj {:?}, object path {:?}", marks, obj, path,) + } + PatchAction::Unmark { key, start, end } => println!( + "unmark {:?} from {} to {} in obj {:?}, object path {:?}", + key, start, end, obj, path, ), } } diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 02cdd0d2..adb6bfab 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -5,14 +5,14 @@ use crate::marks::Mark; use crate::op_observer::{BranchableObserver, OpObserver}; use crate::sync::SyncDoc; use crate::transaction::{CommitOptions, Transactable}; -use crate::{ - transaction::{Observation, Observed, TransactionInner, UnObserved}, - ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, TextEncoding, Value, Values, -}; use crate::{ sync, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, ReadDoc, ScalarValue, }; +use crate::{ + transaction::{Observation, Observed, TransactionInner, UnObserved}, + ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, TextEncoding, Value, Values, +}; /// An automerge document that automatically manages transactions. /// diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index 640a498d..95dd5e4e 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -448,7 +448,6 @@ mod tests { _end: usize, ) { } - } #[test] diff --git a/rust/automerge/src/automerge/tests.rs b/rust/automerge/src/automerge/tests.rs index 6e40c9b1..9cbebff1 100644 --- a/rust/automerge/src/automerge/tests.rs +++ b/rust/automerge/src/automerge/tests.rs @@ -7,6 +7,7 @@ use crate::transaction::Transactable; use crate::*; use std::convert::TryInto; +use crate::op_observer::HasPatches; use test_log::test; #[test] @@ -1481,15 +1482,18 @@ fn observe_counter_change_application_overwrite() { assert_eq!( doc3.observer().take_patches(), - vec![Patch::Put { + vec![PatchCtx { obj: ExId::Root, path: vec![], - prop: Prop::Map("counter".into()), - value: ( - ScalarValue::Str("mystring".into()).into(), - ExId::Id(2, doc2.get_actor().clone(), 1) - ), - conflict: false + action: PatchAction::PutMap { + key: "counter".into(), + value: ( + ScalarValue::Str("mystring".into()).into(), + ExId::Id(2, doc2.get_actor().clone(), 1) + ), + conflict: false, + expose: false + } }] ); @@ -1516,29 +1520,29 @@ fn observe_counter_change_application() { new_doc.observer().take_patches(); new_doc.apply_changes(changes).unwrap(); assert_eq!( - new_doc.observer().take_patches(), + new_doc + .observer() + .take_patches() + .into_iter() + .map(|p| p.action) + .collect::>(), vec![ - Patch::Put { - obj: ExId::Root, - path: vec![], - prop: Prop::Map("counter".into()), + PatchAction::PutMap { + key: "counter".into(), value: ( ScalarValue::counter(1).into(), ExId::Id(1, doc.get_actor().clone(), 0) ), - conflict: false + conflict: false, + expose: false, }, - Patch::Increment { - obj: ExId::Root, - path: vec![], + PatchAction::Increment { prop: Prop::Map("counter".into()), - value: (2, ExId::Id(2, doc.get_actor().clone(), 0)), + value: 2, }, - Patch::Increment { - obj: ExId::Root, - path: vec![], + PatchAction::Increment { prop: Prop::Map("counter".into()), - value: (5, ExId::Id(3, doc.get_actor().clone(), 0)), + value: 5, } ] ); diff --git a/rust/automerge/src/lib.rs b/rust/automerge/src/lib.rs index 78d4976c..ff6f6daa 100644 --- a/rust/automerge/src/lib.rs +++ b/rust/automerge/src/lib.rs @@ -265,6 +265,7 @@ mod op_tree; mod parents; mod query; mod read; +mod sequence_tree; mod storage; pub mod sync; pub mod transaction; @@ -289,9 +290,9 @@ pub use list_range::ListRange; pub use list_range_at::ListRangeAt; pub use map_range::MapRange; pub use map_range_at::MapRangeAt; -pub use op_observer::OpObserver; -pub use op_observer::Patch; -pub use op_observer::VecOpObserver; +pub use op_observer::{ + OpObserver, Patch, PatchAction, ToggleObserver, VecOpObserver, VecOpObserver16, +}; pub use parents::{Parent, Parents}; pub use read::ReadDoc; pub use types::{ActorId, ChangeHash, ObjType, OpType, ParseChangeHashError, Prop, TextEncoding}; diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index 9e7fbe5a..eb7b9b38 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -85,7 +85,7 @@ impl<'a> MarkStateMachine<'a> { let mut m = below.clone(); m.end = pos; if !m.value().is_null() { - result = Some(m); + result = Some(m); } } } @@ -108,12 +108,12 @@ impl<'a> MarkStateMachine<'a> { Some(below) => { below.start = pos; if !mark.value().is_null() { - result = Some(mark.clone()); + result = Some(mark.clone()); } } None => { if !mark.value().is_null() { - result = Some(mark.clone()); + result = Some(mark.clone()); } } } diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 3d1d9f0a..1c068ad4 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -5,7 +5,13 @@ use crate::ReadDoc; use crate::Value; mod compose; +mod patch; +mod toggle_observer; +mod vec_observer; pub use compose::compose; +pub use patch::{Patch, PatchAction}; +pub use toggle_observer::ToggleObserver; +pub use vec_observer::{HasPatches, TextRepresentation, VecOpObserver, VecOpObserver16}; /// An observer of operations applied to the document. pub trait OpObserver { @@ -120,14 +126,7 @@ pub trait OpObserver { mark: M, ); - fn unmark( - &mut self, - doc: &R, - objid: ExId, - key: &str, - start: usize, - end: usize, - ); + fn unmark(&mut self, doc: &R, objid: ExId, key: &str, start: usize, end: usize); /// Whether to call sequence methods or `splice_text` when encountering changes in text /// @@ -207,9 +206,9 @@ impl OpObserver for () { ) { } - fn unmark<'a, R: ReadDoc>( + fn unmark( &mut self, - _doc: &'a R, + _doc: &R, _objid: ExId, _key: &str, _start: usize, @@ -226,226 +225,3 @@ impl BranchableObserver for () { fn merge(&mut self, _other: &Self) {} fn branch(&self) -> Self {} } - -/// Capture operations into a [`Vec`] and store them as patches. -#[derive(Default, Debug, Clone)] -pub struct VecOpObserver { - patches: Vec, -} - -impl VecOpObserver { - /// Take the current list of patches, leaving the internal list empty and ready for new - /// patches. - pub fn take_patches(&mut self) -> Vec { - std::mem::take(&mut self.patches) - } -} - -impl OpObserver for VecOpObserver { - fn insert( - &mut self, - doc: &R, - obj: ExId, - index: usize, - (value, id): (Value<'_>, ExId), - conflict: bool, - ) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Insert { - obj, - path: p.path(), - index, - value: (value.into_owned(), id), - conflict, - }); - } - } - - fn splice_text(&mut self, doc: &R, obj: ExId, index: usize, value: &str) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Splice { - obj, - path: p.path(), - index, - value: value.to_string(), - }) - } - } - - fn put( - &mut self, - doc: &R, - obj: ExId, - prop: Prop, - (value, id): (Value<'_>, ExId), - conflict: bool, - ) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Put { - obj, - path: p.path(), - prop, - value: (value.into_owned(), id), - conflict, - }); - } - } - - fn expose( - &mut self, - doc: &R, - obj: ExId, - prop: Prop, - (value, id): (Value<'_>, ExId), - conflict: bool, - ) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Expose { - obj, - path: p.path(), - prop, - value: (value.into_owned(), id), - conflict, - }); - } - } - - fn increment(&mut self, doc: &R, obj: ExId, prop: Prop, tagged_value: (i64, ExId)) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Increment { - obj, - path: p.path(), - prop, - value: tagged_value, - }); - } - } - - fn mark<'a, R: ReadDoc, M: Iterator>>( - &mut self, - _doc: &'a R, - _objid: ExId, - _mark: M, - ) { - } - - fn unmark( - &mut self, - _doc: &R, - _objid: ExId, - _key: &str, - _start: usize, - _end: usize, - ) { - } - - fn delete_map(&mut self, doc: &R, obj: ExId, key: &str) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Delete { - obj, - path: p.path(), - prop: Prop::Map(key.to_owned()), - num: 1, - }) - } - } - - fn delete_seq(&mut self, doc: &R, obj: ExId, index: usize, num: usize) { - if let Ok(p) = doc.parents(&obj) { - self.patches.push(Patch::Delete { - obj, - path: p.path(), - prop: Prop::Seq(index), - num, - }) - } - } -} - -impl BranchableObserver for VecOpObserver { - fn merge(&mut self, other: &Self) { - self.patches.extend_from_slice(other.patches.as_slice()) - } - - fn branch(&self) -> Self { - Self::default() - } -} - -/// A notification to the application that something has changed in a document. -#[derive(Debug, Clone, PartialEq)] -pub enum Patch { - /// Associating a new value with a prop in a map, or an existing list element - Put { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was put into. - obj: ExId, - /// The prop that the new value was put at. - prop: Prop, - /// The value that was put, and the id of the operation that put it there. - value: (Value<'static>, ExId), - /// Whether this put conflicts with another. - conflict: bool, - }, - /// Exposing (via delete) an old but conflicted value with a prop in a map, or a list element - Expose { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was put into. - obj: ExId, - /// The prop that the new value was put at. - prop: Prop, - /// The value that was put, and the id of the operation that put it there. - value: (Value<'static>, ExId), - /// Whether this put conflicts with another. - conflict: bool, - }, - /// Inserting a new element into a list - Insert { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was inserted into. - obj: ExId, - /// The index that the new value was inserted at. - index: usize, - /// The value that was inserted, and the id of the operation that inserted it there. - value: (Value<'static>, ExId), - /// the inserted value has a conflict - only possible to be true on document load - conflict: bool, - }, - /// Splicing a text object - Splice { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was inserted into. - obj: ExId, - /// The index that the new value was inserted at. - index: usize, - /// The value that was spliced - value: String, - }, - /// Incrementing a counter. - Increment { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was incremented in. - obj: ExId, - /// The prop that was incremented. - prop: Prop, - /// The amount that the counter was incremented by, and the id of the operation that - /// did the increment. - value: (i64, ExId), - }, - /// Deleting an element from a list/text - Delete { - /// path to the object - path: Vec<(ExId, Prop)>, - /// The object that was deleted from. - obj: ExId, - /// The prop that was deleted. - prop: Prop, - /// number of items deleted (for seq) - num: usize, - }, -} diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index f581d915..23ccaf8e 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -103,7 +103,7 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, objid: crate::ObjId, key: &str, start: usize, - end: usize + end: usize, ) { self.obs1.unmark(doc, objid.clone(), key, start, end); self.obs2.unmark(doc, objid, key, start, end); diff --git a/rust/automerge/src/op_observer/patch.rs b/rust/automerge/src/op_observer/patch.rs new file mode 100644 index 00000000..af860070 --- /dev/null +++ b/rust/automerge/src/op_observer/patch.rs @@ -0,0 +1,57 @@ +#![allow(dead_code)] + +use crate::{marks::Mark, ObjId, Prop, Value}; +use core::fmt::Debug; + +use crate::sequence_tree::SequenceTree; + +#[derive(Debug, Clone, PartialEq)] +pub struct Patch { + pub obj: ObjId, + pub path: Vec<(ObjId, Prop)>, + pub action: PatchAction, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PatchAction { + PutMap { + key: String, + value: (Value<'static>, ObjId), + expose: bool, + conflict: bool, + }, + PutSeq { + index: usize, + value: (Value<'static>, ObjId), + expose: bool, + conflict: bool, + }, + Insert { + index: usize, + values: SequenceTree<(Value<'static>, ObjId)>, + conflict: bool, + }, + SpliceText { + index: usize, + value: SequenceTree, + }, + Increment { + prop: Prop, + value: i64, + }, + DeleteMap { + key: String, + }, + DeleteSeq { + index: usize, + length: usize, + }, + Mark { + marks: Vec>, + }, + Unmark { + key: String, + start: usize, + end: usize, + }, +} diff --git a/rust/automerge/src/op_observer/toggle_observer.rs b/rust/automerge/src/op_observer/toggle_observer.rs new file mode 100644 index 00000000..f9e42b76 --- /dev/null +++ b/rust/automerge/src/op_observer/toggle_observer.rs @@ -0,0 +1,178 @@ +#![allow(dead_code)] + +use crate::ChangeHash; +use core::fmt::Debug; + +use crate::{marks::Mark, ObjId, OpObserver, Prop, ReadDoc, Value}; + +use crate::op_observer::BranchableObserver; +use crate::op_observer::{HasPatches, TextRepresentation}; + +#[derive(Debug, Clone)] +pub struct ToggleObserver { + enabled: bool, + last_heads: Option>, + observer: T, +} + +impl Default for ToggleObserver { + fn default() -> Self { + Self { + enabled: false, + last_heads: None, + observer: T::default(), + } + } +} + +impl ToggleObserver { + pub fn new(observer: T) -> Self { + ToggleObserver { + enabled: false, + last_heads: None, + observer, + } + } + + pub fn take_patches(&mut self, heads: Vec) -> (T::Patches, Vec) { + let old_heads = self.last_heads.replace(heads).unwrap_or_default(); + let patches = self.observer.take_patches(); + (patches, old_heads) + } + + pub fn with_text_rep(mut self, text_rep: TextRepresentation) -> Self { + self.observer = self.observer.with_text_rep(text_rep); + self + } + + pub fn set_text_rep(&mut self, text_rep: TextRepresentation) { + self.observer.set_text_rep(text_rep) + } + + pub fn enable(&mut self, enable: bool, heads: Vec) -> bool { + if self.enabled && !enable { + self.observer.take_patches(); + self.last_heads = Some(heads); + } + let old_enabled = self.enabled; + self.enabled = enable; + old_enabled + } + + fn get_path(&mut self, doc: &R, obj: &ObjId) -> Option> { + match doc.parents(obj) { + Ok(parents) => parents.visible_path(), + Err(e) => { + log!("error generating patch : {:?}", e); + None + } + } + } +} + +impl OpObserver for ToggleObserver { + fn insert( + &mut self, + doc: &R, + obj: ObjId, + index: usize, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + if self.enabled { + self.observer + .insert(doc, obj, index, tagged_value, conflict) + } + } + + fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { + if self.enabled { + self.observer.splice_text(doc, obj, index, value) + } + } + + fn delete_seq(&mut self, doc: &R, obj: ObjId, index: usize, length: usize) { + if self.enabled { + self.observer.delete_seq(doc, obj, index, length) + } + } + + fn delete_map(&mut self, doc: &R, obj: ObjId, key: &str) { + if self.enabled { + self.observer.delete_map(doc, obj, key) + } + } + + fn put( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + if self.enabled { + self.observer.put(doc, obj, prop, tagged_value, conflict) + } + } + + fn expose( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + if self.enabled { + self.observer.expose(doc, obj, prop, tagged_value, conflict) + } + } + + fn increment( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (i64, ObjId), + ) { + if self.enabled { + self.observer.increment(doc, obj, prop, tagged_value) + } + } + + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + obj: ObjId, + mark: M, + ) { + if self.enabled { + self.observer.mark(doc, obj, mark) + } + } + + fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + if self.enabled { + self.observer.unmark(doc, obj, key, start, end) + } + } + + fn text_as_seq(&self) -> bool { + self.observer.get_text_rep() == TextRepresentation::Array + } +} + +impl BranchableObserver for ToggleObserver { + fn merge(&mut self, other: &Self) { + self.observer.merge(&other.observer) + } + + fn branch(&self) -> Self { + ToggleObserver { + observer: self.observer.branch(), + last_heads: None, + enabled: self.enabled, + } + } +} diff --git a/rust/automerge/src/op_observer/vec_observer.rs b/rust/automerge/src/op_observer/vec_observer.rs new file mode 100644 index 00000000..a937f525 --- /dev/null +++ b/rust/automerge/src/op_observer/vec_observer.rs @@ -0,0 +1,566 @@ +#![allow(dead_code)] + +use core::fmt::Debug; + +use crate::{marks::Mark, ObjId, OpObserver, Prop, ReadDoc, ScalarValue, Value}; + +use crate::sequence_tree::SequenceTree; + +use crate::op_observer::BranchableObserver; +use crate::op_observer::{Patch, PatchAction}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum TextRepresentation { + Array, + String, +} + +impl TextRepresentation { + pub fn is_array(&self) -> bool { + matches!(self, TextRepresentation::Array) + } + + pub fn is_string(&self) -> bool { + matches!(self, TextRepresentation::String) + } +} + +impl std::default::Default for TextRepresentation { + fn default() -> Self { + TextRepresentation::Array + } +} + +pub(crate) trait TextIndex { + type Item: Debug + PartialEq + Clone; + type Iter<'a>: Iterator; + + fn chars(text: &str) -> Self::Iter<'_>; +} + +#[derive(Debug, Clone, Default)] +struct VecOpObserverInner { + pub(crate) patches: Vec>, + pub(crate) text_rep: TextRepresentation, +} + +#[derive(Debug, Clone, Default)] +pub struct VecOpObserver(VecOpObserverInner); + +#[derive(Debug, Clone, Default)] +pub struct VecOpObserver16(VecOpObserverInner); + +#[derive(Debug, Clone, Default)] +pub(crate) struct Utf16TextIndex; + +#[derive(Debug, Clone, Default)] +pub(crate) struct Utf8TextIndex; + +impl TextIndex for Utf8TextIndex { + type Item = char; + type Iter<'a> = std::str::Chars<'a>; + + fn chars(text: &str) -> Self::Iter<'_> { + text.chars() + } +} + +impl TextIndex for Utf16TextIndex { + type Item = u16; + type Iter<'a> = std::str::EncodeUtf16<'a>; + + fn chars(text: &str) -> Self::Iter<'_> { + text.encode_utf16() + } +} + +pub trait HasPatches { + type Patches; + + fn take_patches(&mut self) -> Self::Patches; + fn with_text_rep(self, text_rep: TextRepresentation) -> Self; + fn set_text_rep(&mut self, text_rep: TextRepresentation); + fn get_text_rep(&self) -> TextRepresentation; +} + +impl HasPatches for VecOpObserver { + type Patches = Vec>; + + fn take_patches(&mut self) -> Self::Patches { + std::mem::take(&mut self.0.patches) + } + + fn with_text_rep(mut self, text_rep: TextRepresentation) -> Self { + self.0.text_rep = text_rep; + self + } + + fn set_text_rep(&mut self, text_rep: TextRepresentation) { + self.0.text_rep = text_rep; + } + + fn get_text_rep(&self) -> TextRepresentation { + self.0.text_rep + } +} + +impl HasPatches for VecOpObserver16 { + type Patches = Vec>; + + fn take_patches(&mut self) -> Self::Patches { + std::mem::take(&mut self.0.patches) + } + + fn with_text_rep(mut self, text_rep: TextRepresentation) -> Self { + self.0.text_rep = text_rep; + self + } + + fn set_text_rep(&mut self, text_rep: TextRepresentation) { + self.0.text_rep = text_rep; + } + + fn get_text_rep(&self) -> TextRepresentation { + self.0.text_rep + } +} + +impl VecOpObserverInner { + fn get_path(&mut self, doc: &R, obj: &ObjId) -> Option> { + match doc.parents(obj) { + Ok(parents) => parents.visible_path(), + Err(e) => { + log!("error generating patch : {:?}", e); + None + } + } + } + + fn maybe_append(&mut self, obj: &ObjId) -> Option<&mut PatchAction> { + match self.patches.last_mut() { + Some(Patch { + obj: tail_obj, + action, + .. + }) if obj == tail_obj => Some(action), + _ => None, + } + } +} + +impl OpObserver for VecOpObserverInner { + fn insert( + &mut self, + doc: &R, + obj: ObjId, + index: usize, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + let value = (tagged_value.0.to_owned(), tagged_value.1); + if let Some(PatchAction::Insert { + index: tail_index, + values, + .. + }) = self.maybe_append(&obj) + { + let range = *tail_index..=*tail_index + values.len(); + if range.contains(&index) { + values.insert(index - *tail_index, value); + return; + } + } + if let Some(path) = self.get_path(doc, &obj) { + let mut values = SequenceTree::new(); + values.push(value); + let action = PatchAction::Insert { + index, + values, + conflict, + }; + self.patches.push(Patch { obj, path, action }); + } + } + + fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { + if self.text_rep == TextRepresentation::Array { + for (offset, c) in value.chars().map(ScalarValue::from).enumerate() { + log!( + "-- internal insert index={} value={:?}", + index + offset, + value + ); + let value = (c.into(), ObjId::Root); + self.insert(doc, obj.clone(), index + offset, value, false); + } + return; + } + if let Some(PatchAction::SpliceText { + index: tail_index, + value: prev_value, + .. + }) = self.maybe_append(&obj) + { + let range = *tail_index..=*tail_index + prev_value.len(); + if range.contains(&index) { + let i = index - *tail_index; + for (n, ch) in T::chars(value).enumerate() { + prev_value.insert(i + n, ch) + } + return; + } + } + if let Some(path) = self.get_path(doc, &obj) { + let mut v = SequenceTree::new(); + for ch in T::chars(value) { + v.push(ch) + } + let action = PatchAction::SpliceText { index, value: v }; + self.patches.push(Patch { obj, path, action }); + } + } + + fn delete_seq(&mut self, doc: &R, obj: ObjId, index: usize, length: usize) { + match self.maybe_append(&obj) { + Some(PatchAction::SpliceText { + index: tail_index, + value, + .. + }) => { + let range = *tail_index..*tail_index + value.len(); + if range.contains(&index) && range.contains(&(index + length - 1)) { + for _ in 0..length { + value.remove(index - *tail_index); + } + return; + } + } + Some(PatchAction::Insert { + index: tail_index, + values, + .. + }) => { + let range = *tail_index..*tail_index + values.len(); + if range.contains(&index) && range.contains(&(index + length - 1)) { + for _ in 0..length { + values.remove(index - *tail_index); + } + return; + } + } + Some(PatchAction::DeleteSeq { + index: tail_index, + length: tail_length, + .. + }) => { + if index == *tail_index { + *tail_length += length; + return; + } + } + _ => {} + } + if let Some(path) = self.get_path(doc, &obj) { + let action = PatchAction::DeleteSeq { index, length }; + self.patches.push(Patch { obj, path, action }) + } + } + + fn delete_map(&mut self, doc: &R, obj: ObjId, key: &str) { + if let Some(path) = self.get_path(doc, &obj) { + let action = PatchAction::DeleteMap { + key: key.to_owned(), + }; + self.patches.push(Patch { obj, path, action }) + } + } + + fn put( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + let expose = false; + if let Some(path) = self.get_path(doc, &obj) { + let value = (tagged_value.0.to_owned(), tagged_value.1); + let action = match prop { + Prop::Map(key) => PatchAction::PutMap { + key, + value, + expose, + conflict, + }, + Prop::Seq(index) => PatchAction::PutSeq { + index, + value, + expose, + conflict, + }, + }; + self.patches.push(Patch { obj, path, action }) + } + } + + fn expose( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + let expose = true; + if let Some(path) = self.get_path(doc, &obj) { + let value = (tagged_value.0.to_owned(), tagged_value.1); + let action = match prop { + Prop::Map(key) => PatchAction::PutMap { + key, + value, + expose, + conflict, + }, + Prop::Seq(index) => PatchAction::PutSeq { + index, + value, + expose, + conflict, + }, + }; + self.patches.push(Patch { obj, path, action }) + } + } + + fn increment( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (i64, ObjId), + ) { + if let Some(path) = self.get_path(doc, &obj) { + let value = tagged_value.0; + let action = PatchAction::Increment { prop, value }; + self.patches.push(Patch { obj, path, action }) + } + } + + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + obj: ObjId, + mark: M, + ) { + if let Some(PatchAction::Mark { marks, .. }) = self.maybe_append(&obj) { + for m in mark { + marks.push(m.into_owned()) + } + return; + } + if let Some(path) = self.get_path(doc, &obj) { + let marks: Vec<_> = mark.map(|m| m.into_owned()).collect(); + if !marks.is_empty() { + let action = PatchAction::Mark { marks }; + self.patches.push(Patch { obj, path, action }); + } + } + } + + fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + if let Some(path) = self.get_path(doc, &obj) { + let action = PatchAction::Unmark { + key: key.to_string(), + start, + end, + }; + self.patches.push(Patch { obj, path, action }); + } + } + + fn text_as_seq(&self) -> bool { + self.text_rep == TextRepresentation::Array + } +} + +impl BranchableObserver for VecOpObserverInner { + fn merge(&mut self, other: &Self) { + self.patches.extend_from_slice(other.patches.as_slice()) + } + + fn branch(&self) -> Self { + VecOpObserverInner { + patches: vec![], + text_rep: self.text_rep, + } + } +} + +impl OpObserver for VecOpObserver { + fn insert( + &mut self, + doc: &R, + obj: ObjId, + index: usize, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.insert(doc, obj, index, tagged_value, conflict) + } + + fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { + self.0.splice_text(doc, obj, index, value) + } + + fn delete_seq(&mut self, doc: &R, obj: ObjId, index: usize, length: usize) { + self.0.delete_seq(doc, obj, index, length) + } + + fn delete_map(&mut self, doc: &R, obj: ObjId, key: &str) { + self.0.delete_map(doc, obj, key) + } + + fn put( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.put(doc, obj, prop, tagged_value, conflict) + } + + fn expose( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.expose(doc, obj, prop, tagged_value, conflict) + } + + fn increment( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (i64, ObjId), + ) { + self.0.increment(doc, obj, prop, tagged_value) + } + + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + obj: ObjId, + mark: M, + ) { + self.0.mark(doc, obj, mark) + } + + fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + self.0.unmark(doc, obj, key, start, end) + } + + fn text_as_seq(&self) -> bool { + self.0.text_as_seq() + } +} + +impl OpObserver for VecOpObserver16 { + fn insert( + &mut self, + doc: &R, + obj: ObjId, + index: usize, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.insert(doc, obj, index, tagged_value, conflict) + } + + fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { + self.0.splice_text(doc, obj, index, value) + } + + fn delete_seq(&mut self, doc: &R, obj: ObjId, index: usize, length: usize) { + self.0.delete_seq(doc, obj, index, length) + } + + fn delete_map(&mut self, doc: &R, obj: ObjId, key: &str) { + self.0.delete_map(doc, obj, key) + } + + fn put( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.put(doc, obj, prop, tagged_value, conflict) + } + + fn expose( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (Value<'_>, ObjId), + conflict: bool, + ) { + self.0.expose(doc, obj, prop, tagged_value, conflict) + } + + fn increment( + &mut self, + doc: &R, + obj: ObjId, + prop: Prop, + tagged_value: (i64, ObjId), + ) { + self.0.increment(doc, obj, prop, tagged_value) + } + + fn mark<'a, R: ReadDoc, M: Iterator>>( + &mut self, + doc: &'a R, + obj: ObjId, + mark: M, + ) { + self.0.mark(doc, obj, mark) + } + + fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + self.0.unmark(doc, obj, key, start, end) + } + + fn text_as_seq(&self) -> bool { + self.0.text_as_seq() + } +} + +impl BranchableObserver for VecOpObserver { + fn merge(&mut self, other: &Self) { + self.0.merge(&other.0) + } + + fn branch(&self) -> Self { + VecOpObserver(self.0.branch()) + } +} + +impl BranchableObserver for VecOpObserver16 { + fn merge(&mut self, other: &Self) { + self.0.merge(&other.0) + } + + fn branch(&self) -> Self { + VecOpObserver16(self.0.branch()) + } +} diff --git a/rust/automerge/src/sequence_tree.rs b/rust/automerge/src/sequence_tree.rs new file mode 100644 index 00000000..920eb13f --- /dev/null +++ b/rust/automerge/src/sequence_tree.rs @@ -0,0 +1,617 @@ +use std::{ + cmp::{min, Ordering}, + fmt::Debug, + mem, +}; + +pub(crate) const B: usize = 16; +pub(crate) type SequenceTree = SequenceTreeInternal; + +#[derive(Clone, Debug)] +pub struct SequenceTreeInternal { + root_node: Option>, +} + +#[derive(Clone, Debug, PartialEq)] +struct SequenceTreeNode { + elements: Vec, + children: Vec>, + length: usize, +} + +impl SequenceTreeInternal +where + T: Clone + Debug, +{ + /// Construct a new, empty, sequence. + pub fn new() -> Self { + Self { root_node: None } + } + + /// Get the length of the sequence. + pub fn len(&self) -> usize { + self.root_node.as_ref().map_or(0, |n| n.len()) + } + + /// Create an iterator through the sequence. + pub fn iter(&self) -> Iter<'_, T> { + Iter { + inner: self, + index: 0, + } + } + + /// Insert the `element` into the sequence at `index`. + /// + /// # Panics + /// + /// Panics if `index > len`. + pub fn insert(&mut self, index: usize, element: T) { + let old_len = self.len(); + if let Some(root) = self.root_node.as_mut() { + #[cfg(debug_assertions)] + root.check(); + + if root.is_full() { + let original_len = root.len(); + let new_root = SequenceTreeNode::new(); + + // move new_root to root position + let old_root = mem::replace(root, new_root); + + root.length += old_root.len(); + root.children.push(old_root); + root.split_child(0); + + assert_eq!(original_len, root.len()); + + // after splitting the root has one element and two children, find which child the + // index is in + let first_child_len = root.children[0].len(); + let (child, insertion_index) = if first_child_len < index { + (&mut root.children[1], index - (first_child_len + 1)) + } else { + (&mut root.children[0], index) + }; + root.length += 1; + child.insert_into_non_full_node(insertion_index, element) + } else { + root.insert_into_non_full_node(index, element) + } + } else { + self.root_node = Some(SequenceTreeNode { + elements: vec![element], + children: Vec::new(), + length: 1, + }) + } + assert_eq!(self.len(), old_len + 1, "{:#?}", self); + } + + /// Push the `element` onto the back of the sequence. + pub fn push(&mut self, element: T) { + let l = self.len(); + self.insert(l, element) + } + + /// Get the `element` at `index` in the sequence. + pub fn get(&self, index: usize) -> Option<&T> { + self.root_node.as_ref().and_then(|n| n.get(index)) + } + + /// Removes the element at `index` from the sequence. + /// + /// # Panics + /// + /// Panics if `index` is out of bounds. + pub fn remove(&mut self, index: usize) -> T { + if let Some(root) = self.root_node.as_mut() { + #[cfg(debug_assertions)] + let len = root.check(); + let old = root.remove(index); + + if root.elements.is_empty() { + if root.is_leaf() { + self.root_node = None; + } else { + self.root_node = Some(root.children.remove(0)); + } + } + + #[cfg(debug_assertions)] + debug_assert_eq!(len, self.root_node.as_ref().map_or(0, |r| r.check()) + 1); + old + } else { + panic!("remove from empty tree") + } + } +} + +impl SequenceTreeNode +where + T: Clone + Debug, +{ + fn new() -> Self { + Self { + elements: Vec::new(), + children: Vec::new(), + length: 0, + } + } + + fn len(&self) -> usize { + self.length + } + + fn is_leaf(&self) -> bool { + self.children.is_empty() + } + + fn is_full(&self) -> bool { + self.elements.len() >= 2 * B - 1 + } + + /// Returns the child index and the given index adjusted for the cumulative index before that + /// child. + fn find_child_index(&self, index: usize) -> (usize, usize) { + let mut cumulative_len = 0; + for (child_index, child) in self.children.iter().enumerate() { + if cumulative_len + child.len() >= index { + return (child_index, index - cumulative_len); + } else { + cumulative_len += child.len() + 1; + } + } + panic!("index not found in node") + } + + fn insert_into_non_full_node(&mut self, index: usize, element: T) { + assert!(!self.is_full()); + if self.is_leaf() { + self.length += 1; + self.elements.insert(index, element); + } else { + let (child_index, sub_index) = self.find_child_index(index); + let child = &mut self.children[child_index]; + + if child.is_full() { + self.split_child(child_index); + + // child structure has changed so we need to find the index again + let (child_index, sub_index) = self.find_child_index(index); + let child = &mut self.children[child_index]; + child.insert_into_non_full_node(sub_index, element); + } else { + child.insert_into_non_full_node(sub_index, element); + } + self.length += 1; + } + } + + // A utility function to split the child `full_child_index` of this node + // Note that `full_child_index` must be full when this function is called. + fn split_child(&mut self, full_child_index: usize) { + let original_len_self = self.len(); + + // Create a new node which is going to store (B-1) keys + // of the full child. + let mut successor_sibling = SequenceTreeNode::new(); + + let full_child = &mut self.children[full_child_index]; + let original_len = full_child.len(); + assert!(full_child.is_full()); + + successor_sibling.elements = full_child.elements.split_off(B); + + if !full_child.is_leaf() { + successor_sibling.children = full_child.children.split_off(B); + } + + let middle = full_child.elements.pop().unwrap(); + + full_child.length = + full_child.elements.len() + full_child.children.iter().map(|c| c.len()).sum::(); + + successor_sibling.length = successor_sibling.elements.len() + + successor_sibling + .children + .iter() + .map(|c| c.len()) + .sum::(); + + let z_len = successor_sibling.len(); + + let full_child_len = full_child.len(); + + self.children + .insert(full_child_index + 1, successor_sibling); + + self.elements.insert(full_child_index, middle); + + assert_eq!(full_child_len + z_len + 1, original_len, "{:#?}", self); + + assert_eq!(original_len_self, self.len()); + } + + fn remove_from_leaf(&mut self, index: usize) -> T { + self.length -= 1; + self.elements.remove(index) + } + + fn remove_element_from_non_leaf(&mut self, index: usize, element_index: usize) -> T { + self.length -= 1; + if self.children[element_index].elements.len() >= B { + let total_index = self.cumulative_index(element_index); + // recursively delete index - 1 in predecessor_node + let predecessor = self.children[element_index].remove(index - 1 - total_index); + // replace element with that one + mem::replace(&mut self.elements[element_index], predecessor) + } else if self.children[element_index + 1].elements.len() >= B { + // recursively delete index + 1 in successor_node + let total_index = self.cumulative_index(element_index + 1); + let successor = self.children[element_index + 1].remove(index + 1 - total_index); + // replace element with that one + mem::replace(&mut self.elements[element_index], successor) + } else { + let middle_element = self.elements.remove(element_index); + let successor_child = self.children.remove(element_index + 1); + self.children[element_index].merge(middle_element, successor_child); + + let total_index = self.cumulative_index(element_index); + self.children[element_index].remove(index - total_index) + } + } + + fn cumulative_index(&self, child_index: usize) -> usize { + self.children[0..child_index] + .iter() + .map(|c| c.len() + 1) + .sum() + } + + fn remove_from_internal_child(&mut self, index: usize, mut child_index: usize) -> T { + if self.children[child_index].elements.len() < B + && if child_index > 0 { + self.children[child_index - 1].elements.len() < B + } else { + true + } + && if child_index + 1 < self.children.len() { + self.children[child_index + 1].elements.len() < B + } else { + true + } + { + // if the child and its immediate siblings have B-1 elements merge the child + // with one sibling, moving an element from this node into the new merged node + // to be the median + + if child_index > 0 { + let middle = self.elements.remove(child_index - 1); + + // use the predessor sibling + let successor = self.children.remove(child_index); + child_index -= 1; + + self.children[child_index].merge(middle, successor); + } else { + let middle = self.elements.remove(child_index); + + // use the sucessor sibling + let successor = self.children.remove(child_index + 1); + + self.children[child_index].merge(middle, successor); + } + } else if self.children[child_index].elements.len() < B { + if child_index > 0 + && self + .children + .get(child_index - 1) + .map_or(false, |c| c.elements.len() >= B) + { + let last_element = self.children[child_index - 1].elements.pop().unwrap(); + assert!(!self.children[child_index - 1].elements.is_empty()); + self.children[child_index - 1].length -= 1; + + let parent_element = + mem::replace(&mut self.elements[child_index - 1], last_element); + + self.children[child_index] + .elements + .insert(0, parent_element); + self.children[child_index].length += 1; + + if let Some(last_child) = self.children[child_index - 1].children.pop() { + self.children[child_index - 1].length -= last_child.len(); + self.children[child_index].length += last_child.len(); + self.children[child_index].children.insert(0, last_child); + } + } else if self + .children + .get(child_index + 1) + .map_or(false, |c| c.elements.len() >= B) + { + let first_element = self.children[child_index + 1].elements.remove(0); + self.children[child_index + 1].length -= 1; + + assert!(!self.children[child_index + 1].elements.is_empty()); + + let parent_element = mem::replace(&mut self.elements[child_index], first_element); + + self.children[child_index].length += 1; + self.children[child_index].elements.push(parent_element); + + if !self.children[child_index + 1].is_leaf() { + let first_child = self.children[child_index + 1].children.remove(0); + self.children[child_index + 1].length -= first_child.len(); + self.children[child_index].length += first_child.len(); + + self.children[child_index].children.push(first_child); + } + } + } + self.length -= 1; + let total_index = self.cumulative_index(child_index); + self.children[child_index].remove(index - total_index) + } + + fn check(&self) -> usize { + let l = self.elements.len() + self.children.iter().map(|c| c.check()).sum::(); + assert_eq!(self.len(), l, "{:#?}", self); + + l + } + + fn remove(&mut self, index: usize) -> T { + let original_len = self.len(); + if self.is_leaf() { + let v = self.remove_from_leaf(index); + assert_eq!(original_len, self.len() + 1); + debug_assert_eq!(self.check(), self.len()); + v + } else { + let mut total_index = 0; + for (child_index, child) in self.children.iter().enumerate() { + match (total_index + child.len()).cmp(&index) { + Ordering::Less => { + // should be later on in the loop + total_index += child.len() + 1; + continue; + } + Ordering::Equal => { + let v = self.remove_element_from_non_leaf( + index, + min(child_index, self.elements.len() - 1), + ); + assert_eq!(original_len, self.len() + 1); + debug_assert_eq!(self.check(), self.len()); + return v; + } + Ordering::Greater => { + let v = self.remove_from_internal_child(index, child_index); + assert_eq!(original_len, self.len() + 1); + debug_assert_eq!(self.check(), self.len()); + return v; + } + } + } + panic!( + "index not found to remove {} {} {} {}", + index, + total_index, + self.len(), + self.check() + ); + } + } + + fn merge(&mut self, middle: T, successor_sibling: SequenceTreeNode) { + self.elements.push(middle); + self.elements.extend(successor_sibling.elements); + self.children.extend(successor_sibling.children); + self.length += successor_sibling.length + 1; + assert!(self.is_full()); + } + + fn get(&self, index: usize) -> Option<&T> { + if self.is_leaf() { + return self.elements.get(index); + } else { + let mut cumulative_len = 0; + for (child_index, child) in self.children.iter().enumerate() { + match (cumulative_len + child.len()).cmp(&index) { + Ordering::Less => { + cumulative_len += child.len() + 1; + } + Ordering::Equal => return self.elements.get(child_index), + Ordering::Greater => { + return child.get(index - cumulative_len); + } + } + } + } + None + } +} + +impl Default for SequenceTreeInternal +where + T: Clone + Debug, +{ + fn default() -> Self { + Self::new() + } +} + +impl PartialEq for SequenceTreeInternal +where + T: Clone + Debug + PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b) + } +} + +impl<'a, T> IntoIterator for &'a SequenceTreeInternal +where + T: Clone + Debug, +{ + type Item = &'a T; + + type IntoIter = Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + Iter { + inner: self, + index: 0, + } + } +} + +#[derive(Debug)] +pub struct Iter<'a, T> { + inner: &'a SequenceTreeInternal, + index: usize, +} + +impl<'a, T> Iterator for Iter<'a, T> +where + T: Clone + Debug, +{ + type Item = &'a T; + + fn next(&mut self) -> Option { + self.index += 1; + self.inner.get(self.index - 1) + } + + fn nth(&mut self, n: usize) -> Option { + self.index += n + 1; + self.inner.get(self.index - 1) + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::*; + + #[test] + fn push_back() { + let mut t = SequenceTree::new(); + + t.push(1); + t.push(2); + t.push(3); + t.push(4); + t.push(5); + t.push(6); + t.push(8); + t.push(100); + } + + #[test] + fn insert() { + let mut t = SequenceTree::new(); + + t.insert(0, 1); + t.insert(1, 1); + t.insert(0, 1); + t.insert(0, 1); + t.insert(0, 1); + t.insert(3, 1); + t.insert(4, 1); + } + + #[test] + fn insert_book() { + let mut t = SequenceTree::new(); + + for i in 0..100 { + t.insert(i % 2, ()); + } + } + + #[test] + fn insert_book_vec() { + let mut t = SequenceTree::new(); + let mut v = Vec::new(); + + for i in 0..100 { + t.insert(i % 3, ()); + v.insert(i % 3, ()); + + assert_eq!(v, t.iter().copied().collect::>()) + } + } + + fn arb_indices() -> impl Strategy> { + proptest::collection::vec(any::(), 0..1000).prop_map(|v| { + let mut len = 0; + v.into_iter() + .map(|i| { + len += 1; + i % len + }) + .collect::>() + }) + } + + proptest! { + + #[test] + fn proptest_insert(indices in arb_indices()) { + let mut t = SequenceTreeInternal::::new(); + let mut v = Vec::new(); + + for i in indices{ + if i <= v.len() { + t.insert(i % 3, i); + v.insert(i % 3, i); + } else { + return Err(proptest::test_runner::TestCaseError::reject("index out of bounds")) + } + + assert_eq!(v, t.iter().copied().collect::>()) + } + } + + } + + proptest! { + + // This is a really slow test due to all the copying of the Vecs (i.e. not due to the + // sequencetree) so we only do a few runs + #![proptest_config(ProptestConfig::with_cases(20))] + #[test] + fn proptest_remove(inserts in arb_indices(), removes in arb_indices()) { + let mut t = SequenceTreeInternal::::new(); + let mut v = Vec::new(); + + for i in inserts { + if i <= v.len() { + t.insert(i , i); + v.insert(i , i); + } else { + return Err(proptest::test_runner::TestCaseError::reject("index out of bounds")) + } + + assert_eq!(v, t.iter().copied().collect::>()) + } + + for i in removes { + if i < v.len() { + let tr = t.remove(i); + let vr = v.remove(i); + assert_eq!(tr, vr); + } else { + return Err(proptest::test_runner::TestCaseError::reject("index out of bounds")) + } + + assert_eq!(v, t.iter().copied().collect::>()) + } + } + + } +} diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 86de16b9..8ffcaa1a 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use crate::exid::ExId; -use crate::marks::{ Mark }; +use crate::marks::Mark; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -631,6 +631,7 @@ impl TransactionInner { // handle the observer if let Some(obs) = op_observer.as_mut() { match splice_type { + //SpliceType::Text(text, _) => { //if !obs.text_as_seq() => { SpliceType::Text(text, _) if !obs.text_as_seq() => { obs.splice_text(doc, ex_obj, index, text) } @@ -639,6 +640,7 @@ impl TransactionInner { for (offset, v) in values.iter().enumerate() { let op = &self.operations[start + offset].1; let value = (v.clone().into(), doc.ops().id_to_exid(op.id)); + log!("external insert index={} value={:?}", index, value); obs.insert(doc, ex_obj.clone(), index + offset, value, false) } } @@ -663,9 +665,9 @@ impl TransactionInner { self.do_insert(doc, Some(obs), obj, mark.start, action)?; self.do_insert(doc, Some(obs), obj, mark.end, OpType::MarkEnd(expand_right))?; if mark.value().is_null() { - obs.unmark(doc, ex_obj.clone(), mark.key(), mark.start, mark.end); + obs.unmark(doc, ex_obj.clone(), mark.key(), mark.start, mark.end); } else { - obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) + obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) } } else { let action = OpType::MarkBegin(expand_left, mark.data.into_owned()); @@ -711,6 +713,7 @@ impl TransactionInner { (Some(ObjType::Text), Prop::Seq(index)) => { if op_observer.text_as_seq() { let value = (op.value(), doc.ops().id_to_exid(op.id)); + log!("external insert2 index={} value={:?}", index, value); op_observer.insert(doc, ex_obj, index, value, false) } else { op_observer.splice_text(doc, ex_obj, index, op.to_str()) diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 2fffa6c6..974eccf1 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -4,8 +4,7 @@ use crate::exid::ExId; use crate::marks::Mark; use crate::op_observer::BranchableObserver; use crate::{ - Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, - Values, + Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, Values, }; use crate::{AutomergeError, Keys}; use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 25dd9ade..3eca837c 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -254,6 +254,20 @@ impl OpType { other => Err(error::InvalidOpType::UnknownAction(other)), } } + + pub(crate) fn to_str(&self) -> &str { + if let OpType::Put(ScalarValue::Str(s)) = &self { + s + } else if self.is_mark() { + "" + } else { + "\u{fffc}" + } + } + + pub(crate) fn is_mark(&self) -> bool { + matches!(&self, OpType::MarkBegin(_, _) | OpType::MarkEnd(_)) + } } impl From for OpType { @@ -606,13 +620,7 @@ impl Op { } pub(crate) fn to_str(&self) -> &str { - if let OpType::Put(ScalarValue::Str(s)) = &self.action { - s - } else if self.is_mark() { - "" - } else { - "\u{fffc}" - } + self.action.to_str() } pub(crate) fn visible(&self) -> bool { @@ -656,7 +664,7 @@ impl Op { } pub(crate) fn is_mark(&self) -> bool { - matches!(&self.action, OpType::MarkBegin(_, _) | OpType::MarkEnd(_)) + self.action.is_mark() } pub(crate) fn valid_mark_anchor(&self) -> bool { From 1c28e9656a43281c63f3659a407feedd05c9a7fe Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 28 Feb 2023 17:17:14 -0600 Subject: [PATCH 23/26] add get_marks_at() --- rust/automerge-wasm/index.d.ts | 2 +- rust/automerge-wasm/src/lib.rs | 9 ++++-- rust/automerge-wasm/test/marks.ts | 30 +++++++++++++++++ rust/automerge-wasm/test/test.ts | 2 +- rust/automerge/src/autocommit.rs | 8 +++++ rust/automerge/src/automerge.rs | 32 +++++++++++++++++++ .../automerge/src/op_observer/vec_observer.rs | 5 --- rust/automerge/src/query.rs | 2 +- rust/automerge/src/read.rs | 7 ++++ rust/automerge/src/transaction/inner.rs | 2 -- .../src/transaction/manual_transaction.rs | 8 +++++ 11 files changed, 95 insertions(+), 12 deletions(-) diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index 644d3adb..3da5d85b 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -189,7 +189,7 @@ export class Automerge { // marks mark(obj: ObjID, key: string, range: string, value: Value, datatype?: Datatype): void; unmark(obj: ObjID, key: string, start: number, end: number): void; - marks(obj: ObjID): Mark[]; + marks(obj: ObjID, heads?: Heads): Mark[]; // returns a single value - if there is a conflict return the winner get(obj: ObjID, prop: Prop, heads?: Heads): Value | undefined; diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index 113d71a1..a0fd069b 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -849,9 +849,14 @@ impl Automerge { Ok(()) } - pub fn marks(&mut self, obj: JsValue) -> Result { + pub fn marks(&mut self, obj: JsValue, heads: Option) -> Result { let (obj, _) = self.import(obj)?; - let marks = self.doc.get_marks(obj).map_err(to_js_err)?; + let heads = get_heads(heads)?; + let marks = if let Some(heads) = heads { + self.doc.get_marks_at(obj, &heads).map_err(to_js_err)? + } else { + self.doc.get_marks(obj).map_err(to_js_err)? + }; let result = Array::new(); for m in marks { let mark = Object::new(); diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 060fe5bd..6738b7dc 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -538,5 +538,35 @@ describe('Automerge', () => { ]} ]); }) + it('can get marks at a given heads', () => { + let doc1 : Automerge = create(true, "aabbcc") + doc1.enablePatches(true) + + let list = doc1.putObject("_root", "list", "") + doc1.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + + let heads1 = doc1.getHeads(); + let marks1 = doc1.marks(list); + + doc1.mark(list, "[3..25]", "xxx", "aaa") + + let heads2 = doc1.getHeads(); + let marks2 = doc1.marks(list); + + doc1.mark(list, "[4..11]", "yyy", "bbb") + + let heads3 = doc1.getHeads(); + let marks3 = doc1.marks(list); + + doc1.unmark(list, "xxx", 9, 20) + + let heads4 = doc1.getHeads(); + let marks4 = doc1.marks(list); + + assert.deepEqual(marks1, doc1.marks(list,heads1)) + assert.deepEqual(marks2, doc1.marks(list,heads2)) + assert.deepEqual(marks3, doc1.marks(list,heads3)) + assert.deepEqual(marks4, doc1.marks(list,heads4)) + }) }) }) diff --git a/rust/automerge-wasm/test/test.ts b/rust/automerge-wasm/test/test.ts index 2a6833a1..3e57d92c 100644 --- a/rust/automerge-wasm/test/test.ts +++ b/rust/automerge-wasm/test/test.ts @@ -2162,7 +2162,7 @@ describe('Automerge', () => { assert.deepEqual(mat.text, new FakeText("0abcd")) }) - it.only("should allow inserting objects in old style text", () => { + it("should allow inserting objects in old style text", () => { let doc = create(false); doc.registerDatatype("text", (e: any) => new FakeText(e)) doc.enablePatches(true) diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index adb6bfab..5e39bace 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -451,6 +451,14 @@ impl ReadDoc for AutoCommitWithObs { self.doc.get_marks(obj) } + fn get_marks_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result>, AutomergeError> { + self.doc.get_marks_at(obj, heads) + } + fn text>(&self, obj: O) -> Result { self.doc.text(obj) } diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index ccd02941..36327bef 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -1371,6 +1371,38 @@ impl ReadDoc for Automerge { .collect()) } + fn get_marks_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result>, AutomergeError> { + let (obj, obj_type) = self.exid_to_obj(obj.as_ref())?; + let clock = self.clock_at(heads); + let encoding = ListEncoding::new(obj_type, self.text_encoding); + let ops_by_key = self.ops().iter_ops(&obj).group_by(|o| o.elemid_or_key()); + let mut window = query::VisWindow::default(); + let mut pos = 0; + let mut marks = MarkStateMachine::default(); + + Ok(ops_by_key + .into_iter() + .filter_map(|(_key, key_ops)| { + key_ops + .filter(|o| window.visible_at(o, pos, &clock)) + .last() + .and_then(|o| match &o.action { + OpType::Make(_) | OpType::Put(_) => { + pos += o.width(encoding); + None + } + OpType::MarkBegin(_, data) => marks.mark_begin(o.id, pos, data, self), + OpType::MarkEnd(_) => marks.mark_end(o.id, pos, self), + OpType::Increment(_) | OpType::Delete => None, + }) + }) + .collect()) + } + fn get, P: Into>( &self, obj: O, diff --git a/rust/automerge/src/op_observer/vec_observer.rs b/rust/automerge/src/op_observer/vec_observer.rs index a937f525..dc21d86e 100644 --- a/rust/automerge/src/op_observer/vec_observer.rs +++ b/rust/automerge/src/op_observer/vec_observer.rs @@ -185,11 +185,6 @@ impl OpObserver for VecOpObserverInner { fn splice_text(&mut self, doc: &R, obj: ObjId, index: usize, value: &str) { if self.text_rep == TextRepresentation::Array { for (offset, c) in value.chars().map(ScalarValue::from).enumerate() { - log!( - "-- internal insert index={} value={:?}", - index + offset, - value - ); let value = (c.into(), ObjId::Root); self.insert(doc, obj.clone(), index + offset, value, false); } diff --git a/rust/automerge/src/query.rs b/rust/automerge/src/query.rs index ffa06d3b..c76c3a0e 100644 --- a/rust/automerge/src/query.rs +++ b/rust/automerge/src/query.rs @@ -289,7 +289,7 @@ pub(crate) struct VisWindow { } impl VisWindow { - fn visible_at(&mut self, op: &Op, pos: usize, clock: &Clock) -> bool { + pub(crate) fn visible_at(&mut self, op: &Op, pos: usize, clock: &Clock) -> bool { if !clock.covers(&op.id) { return false; } diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 6a5f6515..966814ad 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -132,6 +132,13 @@ pub trait ReadDoc { /// Get all marks on a current sequence fn get_marks>(&self, obj: O) -> Result>, AutomergeError>; + /// Get all marks on a sequence at a given heads + fn get_marks_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result>, AutomergeError>; + /// Get the string represented by the given text object. fn text>(&self, obj: O) -> Result; diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 8ffcaa1a..4ba6dfe4 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -640,7 +640,6 @@ impl TransactionInner { for (offset, v) in values.iter().enumerate() { let op = &self.operations[start + offset].1; let value = (v.clone().into(), doc.ops().id_to_exid(op.id)); - log!("external insert index={} value={:?}", index, value); obs.insert(doc, ex_obj.clone(), index + offset, value, false) } } @@ -713,7 +712,6 @@ impl TransactionInner { (Some(ObjType::Text), Prop::Seq(index)) => { if op_observer.text_as_seq() { let value = (op.value(), doc.ops().id_to_exid(op.id)); - log!("external insert2 index={} value={:?}", index, value); op_observer.insert(doc, ex_obj, index, value, false) } else { op_observer.splice_text(doc, ex_obj, index, op.to_str()) diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 974eccf1..337c7578 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -195,6 +195,14 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { self.doc.get_marks(obj) } + fn get_marks_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result>, AutomergeError> { + self.doc.get_marks_at(obj, heads) + } + fn get, P: Into>( &self, obj: O, From 2f4cf7b328f41a62d872198102fcb6505e2a4490 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 28 Feb 2023 18:52:26 -0600 Subject: [PATCH 24/26] enable load callback test --- javascript/test/legacy_tests.ts | 43 +++++++++------------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/javascript/test/legacy_tests.ts b/javascript/test/legacy_tests.ts index 11c34490..a078e30b 100644 --- a/javascript/test/legacy_tests.ts +++ b/javascript/test/legacy_tests.ts @@ -1568,7 +1568,7 @@ describe("Automerge", () => { assert.deepStrictEqual(doc, { list: expected }) }) - it.skip("should call patchCallback if supplied to load", () => { + it("should call patchCallback if supplied to load", () => { const s1 = Automerge.change( Automerge.init(), doc => (doc.birds = ["Goldfinch"]) @@ -1577,40 +1577,19 @@ describe("Automerge", () => { const callbacks: Array = [], actor = Automerge.getActorId(s1) const reloaded = Automerge.load(Automerge.save(s2), { - patchCallback(patch, before, after) { - callbacks.push({ patch, before, after }) + patchCallback(patches, opts) { + callbacks.push({ patches, opts }) }, }) assert.strictEqual(callbacks.length, 1) - assert.deepStrictEqual(callbacks[0].patch, { - maxOp: 3, - deps: [decodeChange(Automerge.getAllChanges(s2)[1]).hash], - clock: { [actor]: 2 }, - pendingChanges: 0, - diffs: { - objectId: "_root", - type: "map", - props: { - birds: { - [`1@${actor}`]: { - objectId: `1@${actor}`, - type: "list", - edits: [ - { - action: "multi-insert", - index: 0, - elemId: `2@${actor}`, - values: ["Goldfinch", "Chaffinch"], - }, - ], - }, - }, - }, - }, - }) - assert.deepStrictEqual(callbacks[0].before, {}) - assert.strictEqual(callbacks[0].after, reloaded) - assert.strictEqual(callbacks[0].local, false) + assert.deepStrictEqual(callbacks[0].patches, [ + { action: "put", path: ["birds"], value: [] }, + { action: "insert", path: ["birds", 0], values: ["", ""] }, + { action: "splice", path: ["birds", 0, 0], value: "Goldfinch" }, + { action: "splice", path: ["birds", 1, 0], value: "Chaffinch" }, + ]) + assert.deepStrictEqual(callbacks[0].opts.before, {}) + assert.strictEqual(callbacks[0].opts.after, reloaded) }) }) From c38d9e883eaf8d6429a094cfc07ce2ca03a9aa20 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 1 Mar 2023 10:54:28 -0600 Subject: [PATCH 25/26] add unmark() and marks() to unstable api --- javascript/src/unstable.ts | 37 +++++++++++++++++++++++++++++++++++++ javascript/test/marks.ts | 25 ++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/javascript/src/unstable.ts b/javascript/src/unstable.ts index fcb7a6e0..6386795e 100644 --- a/javascript/src/unstable.ts +++ b/javascript/src/unstable.ts @@ -260,6 +260,43 @@ export function mark( } } +export function unmark( + doc: Doc, + prop: stable.Prop, + key: string, + start: number, + end: number +) { + if (!_is_proxy(doc)) { + throw new RangeError("object cannot be modified outside of a change block") + } + const state = _state(doc, false) + const objectId = _obj(doc) + if (!objectId) { + throw new RangeError("invalid object for unmark") + } + const obj = `${objectId}/${prop}` + try { + return state.handle.unmark(obj, key, start, end) + } catch (e) { + throw new RangeError(`Cannot unmark: ${e}`) + } +} + +export function marks(doc: Doc, prop: stable.Prop) { + const state = _state(doc, false) + const objectId = _obj(doc) + if (!objectId) { + throw new RangeError("invalid object for unmark") + } + const obj = `${objectId}/${prop}` + try { + return state.handle.marks(obj) + } catch (e) { + throw new RangeError(`Cannot call marks(): ${e}`) + } +} + /** * Get the conflicts associated with a property * diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts index 1010101a..222ed8d1 100644 --- a/javascript/test/marks.ts +++ b/javascript/test/marks.ts @@ -15,6 +15,11 @@ describe("Automerge", () => { doc1 = Automerge.change(doc1, d => { Automerge.mark(d, "x", "font-weight", "[5..10]", "bold") }) + + doc1 = Automerge.change(doc1, d => { + Automerge.unmark(d, "x", "font-weight", 7, 9) + }) + assert.deepStrictEqual(callbacks[1], [ { action: "mark", @@ -23,6 +28,16 @@ describe("Automerge", () => { }, ]) + assert.deepStrictEqual(callbacks[2], [ + { + action: "unmark", + path: ["x"], + key: "font-weight", + start: 7, + end: 9, + }, + ]) + callbacks = [] let doc2 = Automerge.init({ @@ -33,8 +48,16 @@ describe("Automerge", () => { assert.deepStrictEqual(callbacks[0][2], { action: "mark", path: ["x"], - marks: [{ key: "font-weight", start: 5, end: 10, value: "bold" }], + marks: [ + { key: "font-weight", start: 5, end: 7, value: "bold" }, + { key: "font-weight", start: 9, end: 10, value: "bold" }, + ], }) + + assert.deepStrictEqual(Automerge.marks(doc2, "x"), [ + { key: "font-weight", value: "bold", start: 5, end: 7 }, + { key: "font-weight", value: "bold", start: 9, end: 10 }, + ]) }) }) }) From 271d5cbead660eda91d2849396e586e3992c46b8 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 10 Mar 2023 12:57:13 -0600 Subject: [PATCH 26/26] cleanup based on alex's comments --- javascript/src/types.ts | 2 +- javascript/src/unstable.ts | 15 +- javascript/src/unstable_types.ts | 1 + javascript/test/marks.ts | 16 +-- rust/automerge-wasm/index.d.ts | 38 ++--- rust/automerge-wasm/src/interop.rs | 6 +- rust/automerge-wasm/src/lib.rs | 12 +- rust/automerge-wasm/test/marks.ts | 136 +++++++++--------- rust/automerge/examples/watch.rs | 4 +- rust/automerge/src/autocommit.rs | 12 +- rust/automerge/src/automerge.rs | 13 +- rust/automerge/src/automerge/current_state.rs | 2 +- rust/automerge/src/change.rs | 4 +- rust/automerge/src/legacy/mod.rs | 6 +- rust/automerge/src/marks.rs | 41 ++++-- rust/automerge/src/op_observer.rs | 4 +- rust/automerge/src/op_observer/compose.rs | 6 +- rust/automerge/src/op_observer/patch.rs | 2 +- .../src/op_observer/toggle_observer.rs | 4 +- .../automerge/src/op_observer/vec_observer.rs | 12 +- rust/automerge/src/query/seek_mark.rs | 14 +- rust/automerge/src/read.rs | 4 +- rust/automerge/src/storage/change.rs | 2 +- .../src/storage/change/change_actors.rs | 4 +- .../src/storage/change/change_op_columns.rs | 52 +++---- .../src/storage/convert/op_as_changeop.rs | 6 +- .../src/storage/convert/op_as_docop.rs | 6 +- .../src/storage/document/doc_op_columns.rs | 42 +++--- .../src/storage/load/reconstruct_document.rs | 2 +- rust/automerge/src/transaction/inner.rs | 27 ++-- .../src/transaction/manual_transaction.rs | 16 +-- .../automerge/src/transaction/transactable.rs | 4 +- rust/automerge/src/types.rs | 6 +- rust/automerge/tests/test.rs | 1 - 34 files changed, 271 insertions(+), 251 deletions(-) diff --git a/javascript/src/types.ts b/javascript/src/types.ts index e7b7fd46..1166ccea 100644 --- a/javascript/src/types.ts +++ b/javascript/src/types.ts @@ -5,7 +5,7 @@ export { Int, Uint, Float64 } from "./numbers" import { Counter } from "./counter" import type { Patch, PatchInfo } from "@automerge/automerge-wasm" -export type { Patch } from "@automerge/automerge-wasm" +export type { Patch, Mark } from "@automerge/automerge-wasm" export type AutomergeValue = | ScalarValue diff --git a/javascript/src/unstable.ts b/javascript/src/unstable.ts index 6386795e..9ce92698 100644 --- a/javascript/src/unstable.ts +++ b/javascript/src/unstable.ts @@ -44,11 +44,12 @@ export { Float64, type Patch, type PatchCallback, + type Mark, type AutomergeValue, type ScalarValue, } from "./unstable_types" -import type { PatchCallback } from "./stable" +import type { ScalarValue, Mark, PatchCallback } from "./stable" import { type UnstableConflicts as Conflicts } from "./conflicts" import { unstableConflictAt } from "./conflicts" @@ -240,9 +241,9 @@ export function splice( export function mark( doc: Doc, prop: stable.Prop, - key: string, + name: string, range: string, - value: string | boolean | number | Uint8Array | null + value: ScalarValue ) { if (!_is_proxy(doc)) { throw new RangeError("object cannot be modified outside of a change block") @@ -254,7 +255,7 @@ export function mark( } const obj = `${objectId}/${prop}` try { - return state.handle.mark(obj, range, key, value) + return state.handle.mark(obj, range, name, value) } catch (e) { throw new RangeError(`Cannot mark: ${e}`) } @@ -263,7 +264,7 @@ export function mark( export function unmark( doc: Doc, prop: stable.Prop, - key: string, + name: string, start: number, end: number ) { @@ -277,13 +278,13 @@ export function unmark( } const obj = `${objectId}/${prop}` try { - return state.handle.unmark(obj, key, start, end) + return state.handle.unmark(obj, name, start, end) } catch (e) { throw new RangeError(`Cannot unmark: ${e}`) } } -export function marks(doc: Doc, prop: stable.Prop) { +export function marks(doc: Doc, prop: stable.Prop): Mark[] { const state = _state(doc, false) const objectId = _obj(doc) if (!objectId) { diff --git a/javascript/src/unstable_types.ts b/javascript/src/unstable_types.ts index 071e2cc4..a89fc8e3 100644 --- a/javascript/src/unstable_types.ts +++ b/javascript/src/unstable_types.ts @@ -8,6 +8,7 @@ export { Float64, type Patch, type PatchCallback, + type Mark, } from "./types" import { RawString } from "./raw_string" diff --git a/javascript/test/marks.ts b/javascript/test/marks.ts index 222ed8d1..538c2db1 100644 --- a/javascript/test/marks.ts +++ b/javascript/test/marks.ts @@ -7,7 +7,7 @@ describe("Automerge", () => { it("should allow marks that can be seen in patches", () => { let callbacks = [] let doc1 = Automerge.init({ - patchCallback: (patches, before, after) => callbacks.push(patches), + patchCallback: (patches, info) => callbacks.push(patches), }) doc1 = Automerge.change(doc1, d => { d.x = "the quick fox jumps over the lazy dog" @@ -24,7 +24,7 @@ describe("Automerge", () => { { action: "mark", path: ["x"], - marks: [{ key: "font-weight", start: 5, end: 10, value: "bold" }], + marks: [{ name: "font-weight", start: 5, end: 10, value: "bold" }], }, ]) @@ -32,7 +32,7 @@ describe("Automerge", () => { { action: "unmark", path: ["x"], - key: "font-weight", + name: "font-weight", start: 7, end: 9, }, @@ -41,7 +41,7 @@ describe("Automerge", () => { callbacks = [] let doc2 = Automerge.init({ - patchCallback: (patches, before, after) => callbacks.push(patches), + patchCallback: (patches, info) => callbacks.push(patches), }) doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1)) @@ -49,14 +49,14 @@ describe("Automerge", () => { action: "mark", path: ["x"], marks: [ - { key: "font-weight", start: 5, end: 7, value: "bold" }, - { key: "font-weight", start: 9, end: 10, value: "bold" }, + { name: "font-weight", start: 5, end: 7, value: "bold" }, + { name: "font-weight", start: 9, end: 10, value: "bold" }, ], }) assert.deepStrictEqual(Automerge.marks(doc2, "x"), [ - { key: "font-weight", value: "bold", start: 5, end: 7 }, - { key: "font-weight", value: "bold", start: 9, end: 10 }, + { name: "font-weight", value: "bold", start: 5, end: 7 }, + { name: "font-weight", value: "bold", start: 9, end: 10 }, ]) }) }) diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index 3da5d85b..4fb1c0c1 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -94,7 +94,7 @@ export type Op = { pred: string[], } -export type Patch = PutPatch | DelPatch | SpliceTextPatch | IncPatch | InsertPatch | MarkPatch; +export type Patch = PutPatch | DelPatch | SpliceTextPatch | IncPatch | InsertPatch | MarkPatch | UnmarkPatch; export type PutPatch = { action: 'put' @@ -109,6 +109,14 @@ export type MarkPatch = { marks: Mark[] } +export type UnmarkPatch = { + action: 'unmark' + path: Prop[], + name: string, + start: number, + end: number +} + export type IncPatch = { action: 'inc' path: Prop[], @@ -134,15 +142,7 @@ export type InsertPatch = { } export type Mark = { - key: string, - value: Value, - start: number, - end: number, -} - -export type RawMark = { - id: ObjID, - key: string, + name: string, value: Value, start: number, end: number, @@ -187,8 +187,8 @@ export class Automerge { delete(obj: ObjID, prop: Prop): void; // marks - mark(obj: ObjID, key: string, range: string, value: Value, datatype?: Datatype): void; - unmark(obj: ObjID, key: string, start: number, end: number): void; + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + unmark(obj: ObjID, name: string, start: number, end: number): void; marks(obj: ObjID, heads?: Heads): Mark[]; // returns a single value - if there is a conflict return the winner @@ -270,17 +270,3 @@ export class SyncState { readonly sharedHeads: Heads; } -export type ChangeSetDeletion = { - pos: number; - val: string; -} - -export type ChangeSetAddition = { - start: number; - end: number; -}; - -export type ChangeSet = { - add: ChangeSetAddition[]; - del: ChangeSetDeletion[]; -}; diff --git a/rust/automerge-wasm/src/interop.rs b/rust/automerge-wasm/src/interop.rs index 7135e7db..fbf252f3 100644 --- a/rust/automerge-wasm/src/interop.rs +++ b/rust/automerge-wasm/src/interop.rs @@ -1339,7 +1339,7 @@ impl TryFrom for JsValue { let marks_array = Array::new(); for m in marks.iter() { let mark = Object::new(); - js_set(&mark, "key", m.key())?; + js_set(&mark, "name", m.name())?; js_set( &mark, "value", @@ -1353,11 +1353,11 @@ impl TryFrom for JsValue { Ok(result.into()) } PatchAction::Unmark { - key, start, end, .. + name, start, end, .. } => { js_set(&result, "action", "unmark")?; js_set(&result, "path", export_just_path(path.as_slice()))?; - js_set(&result, "key", key)?; + js_set(&result, "name", name)?; js_set(&result, "start", start as i32)?; js_set(&result, "end", end as i32)?; Ok(result.into()) diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index a0fd069b..37ef737b 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -815,8 +815,8 @@ impl Automerge { let cap = re.captures_iter(&range).next().ok_or(format!("(range={}) range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal",range))?; let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; - let start_sticky = &cap[1] == "("; - let end_sticky = &cap[4] == ")"; + let left_sticky = &cap[1] == "("; + let right_sticky = &cap[4] == ")"; let name = name .as_string() .ok_or("invalid mark name") @@ -828,7 +828,7 @@ impl Automerge { .mark( &obj, am::marks::Mark::new(name, value, start, end), - (start_sticky, end_sticky), + am::marks::ExpandMark::from(left_sticky, right_sticky), ) .map_err(to_js_err)?; Ok(()) @@ -853,15 +853,15 @@ impl Automerge { let (obj, _) = self.import(obj)?; let heads = get_heads(heads)?; let marks = if let Some(heads) = heads { - self.doc.get_marks_at(obj, &heads).map_err(to_js_err)? + self.doc.marks_at(obj, &heads).map_err(to_js_err)? } else { - self.doc.get_marks(obj).map_err(to_js_err)? + self.doc.marks(obj).map_err(to_js_err)? }; let result = Array::new(); for m in marks { let mark = Object::new(); let (_datatype, value) = alloc(&m.value().clone().into(), self.text_rep); - js_set(&mark, "key", m.key())?; + js_set(&mark, "name", m.name())?; js_set(&mark, "value", value)?; js_set(&mark, "start", m.start as i32)?; js_set(&mark, "end", m.end as i32)?; diff --git a/rust/automerge-wasm/test/marks.ts b/rust/automerge-wasm/test/marks.ts index 6738b7dc..0a2d4433 100644 --- a/rust/automerge-wasm/test/marks.ts +++ b/rust/automerge-wasm/test/marks.ts @@ -17,11 +17,11 @@ describe('Automerge', () => { doc.mark(list, "[3..6]", "bold" , true) let text = doc.text(list) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 6 }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 4, end: 7 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 4, end: 7 }]) }) it('should handle mark and unmark', () => { @@ -30,14 +30,14 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[2..8]", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 8 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 2, end: 8 }]) doc.unmark(list, 'bold', 4, 6) doc.insert(list, 7, "A") doc.insert(list, 3, "A") marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: 'bold', value: true, start: 2, end: 5 }, - { key: 'bold', value: true, start: 7, end: 10 }, + { name: 'bold', value: true, start: 2, end: 5 }, + { name: 'bold', value: true, start: 7, end: 10 }, ]) }) @@ -50,22 +50,22 @@ describe('Automerge', () => { doc.mark(list, "[3..6]", "underline" , true) let marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: 'underline', value: true, start: 3, end: 6 }, - { key: 'bold', value: true, start: 2, end: 8 }, + { name: 'underline', value: true, start: 3, end: 6 }, + { name: 'bold', value: true, start: 2, end: 8 }, ]) doc.unmark(list, 'bold', 4, 6) doc.insert(list, 7, "A") doc.insert(list, 3, "A") marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: 'bold', value: true, start: 2, end: 5 }, - { key: 'underline', value: true, start: 4, end: 7 }, - { key: 'bold', value: true, start: 7, end: 10 }, + { name: 'bold', value: true, start: 2, end: 5 }, + { name: 'underline', value: true, start: 4, end: 7 }, + { name: 'bold', value: true, start: 7, end: 10 }, ]) doc.unmark(list, 'bold', 0, 11) marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: 'underline', value: true, start: 4, end: 7 } + { name: 'underline', value: true, start: 4, end: 7 } ]) }) @@ -75,14 +75,14 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.insert(list, 0, "A") doc2.insert(list, 4, "B") doc.merge(doc2) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 1, end: 4 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 1, end: 4 }]) }) it('should handle marks [..] with splice', () => { @@ -91,14 +91,14 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.splice(list, 0, 2, "AAA") doc2.splice(list, 4, 0, "BBB") doc.merge(doc2) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 4 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 4 }]) }) it('should handle marks across multiple forks', () => { @@ -107,7 +107,7 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 0, end: 3 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 0, end: 3 }]) let doc2 = doc.fork() doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. @@ -119,7 +119,7 @@ describe('Automerge', () => { doc.merge(doc3) marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 6 }]) }) it('should handle marks with deleted ends [..]', () => { @@ -129,17 +129,17 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 6 }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 3 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 2, end: 3 }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 4 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 4 }]) }) it('should handle sticky marks (..)', () => { @@ -148,11 +148,11 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 6 }]) doc.insert(list, 6, "A") doc.insert(list, 3, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 8 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 8 }]) }) it('should handle sticky marks with deleted ends (..)', () => { @@ -161,24 +161,24 @@ describe('Automerge', () => { doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) let marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 3, end: 6 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 3, end: 6 }]) doc.delete(list,5); doc.delete(list,5); doc.delete(list,2); doc.delete(list,2); marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 3 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 2, end: 3 }]) doc.insert(list, 3, "A") doc.insert(list, 2, "A") marks = doc.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 5 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 2, end: 5 }]) // make sure save/load can handle marks let saved = doc.save() let doc2 = load(saved,true) marks = doc2.marks(list); - assert.deepStrictEqual(marks, [{ key: 'bold', value: true, start: 2, end: 5 }]) + assert.deepStrictEqual(marks, [{ name: 'bold', value: true, start: 2, end: 5 }]) assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) assert.deepStrictEqual(doc.save(), doc2.save()) @@ -195,9 +195,9 @@ describe('Automerge', () => { doc.commit("marks"); let marks = doc.marks(list); assert.deepStrictEqual(marks, [ - { key: `comment:${id}`, start: 10, end: 13, value: 'foxes are my favorite animal!' }, - { key: 'itallic', start: 4, end: 19, value: true }, - { key: 'bold', start: 0, end: 37, value: true } + { name: `comment:${id}`, start: 10, end: 13, value: 'foxes are my favorite animal!' }, + { name: 'itallic', start: 4, end: 19, value: true }, + { name: 'bold', start: 0, end: 37, value: true } ]) let text = doc.text(list); assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); @@ -237,9 +237,9 @@ describe('Automerge', () => { { action: 'mark', path: [ 'list' ], marks: [ - { key: 'bold', value: true, start: 0, end: 37 }, - { key: 'itallic', value: true, start: 4, end: 19 }, - { key: `comment:${id}`, value: 'foxes are my favorite animal!', start: 10, end: 13 } + { name: 'bold', value: true, start: 0, end: 37 }, + { name: 'itallic', value: true, start: 4, end: 19 }, + { name: `comment:${id}`, value: 'foxes are my favorite animal!', start: 10, end: 13 } ] } ]); @@ -273,10 +273,10 @@ describe('Automerge', () => { action: 'mark', path: [ 'list' ], marks: [ - { key: 'x', value: 'a', start: 0, end: 5 }, - { key: 'x', value: 'b', start: 8, end: 11 }, - { key: 'x', value: 'c', start: 5, end: 8 }, - { key: 'x', value: 'c', start: 11, end: 13 }, + { name: 'x', value: 'a', start: 0, end: 5 }, + { name: 'x', value: 'b', start: 8, end: 11 }, + { name: 'x', value: 'c', start: 5, end: 8 }, + { name: 'x', value: 'c', start: 11, end: 13 }, ] }, ]); @@ -294,7 +294,7 @@ describe('Automerge', () => { let patches1 = doc1.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches1, [{ - action: 'mark', path: [ 'list' ], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 10 }], + action: 'mark', path: [ 'list' ], marks: [ { name: 'xxx', value: 'aaa', start: 5, end: 10 }], }]); let doc2 : Automerge = create(true); @@ -304,7 +304,7 @@ describe('Automerge', () => { let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches2, [{ - action: 'mark', path: ['list'], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 10}], + action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', start: 5, end: 10}], }]); }) @@ -322,9 +322,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 15 }, - { key: 'xxx', value: 'aaa', start: 10, end: 20 }, - { key: 'xxx', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end: 15 }, + { name: 'xxx', value: 'aaa', start: 10, end: 20 }, + { name: 'xxx', value: 'aaa', start: 15, end: 25 }, ] }, ]); @@ -335,7 +335,7 @@ describe('Automerge', () => { let patches2 = doc2.popPatches().filter((p:any) => p.action == "mark") assert.deepEqual(patches2, [ - { action: 'mark', path: ['list'], marks: [ { key: 'xxx', value: 'aaa', start: 5, end: 25}] }, + { action: 'mark', path: ['list'], marks: [ { name: 'xxx', value: 'aaa', start: 5, end: 25}] }, ]); }) @@ -353,9 +353,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 15 }, - { key: 'xxx', value: 'bbb', start: 10, end: 20 }, - { key: 'xxx', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end: 15 }, + { name: 'xxx', value: 'bbb', start: 10, end: 20 }, + { name: 'xxx', value: 'aaa', start: 15, end: 25 }, ]} ]); @@ -367,9 +367,9 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: ['list'], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 10 }, - { key: 'xxx', value: 'bbb', start: 10, end: 15 }, - { key: 'xxx', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end: 10 }, + { name: 'xxx', value: 'bbb', start: 10, end: 15 }, + { name: 'xxx', value: 'aaa', start: 15, end: 25 }, ]}, ]); }) @@ -388,9 +388,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end:15 }, - { key: 'yyy', value: 'aaa', start: 10, end: 20 }, - { key: 'zzz', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end:15 }, + { name: 'yyy', value: 'aaa', start: 10, end: 20 }, + { name: 'zzz', value: 'aaa', start: 15, end: 25 }, ]} ]); @@ -402,9 +402,9 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 15 }, - { key: 'yyy', value: 'aaa', start: 10, end: 20 }, - { key: 'zzz', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end: 15 }, + { name: 'yyy', value: 'aaa', start: 10, end: 20 }, + { name: 'zzz', value: 'aaa', start: 15, end: 25 }, ]} ]); }) @@ -431,10 +431,10 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 10, end: 20 }, - { key: 'xxx', value: 'aaa', start: 15, end: 25 }, - { key: 'xxx', value: 'bbb', start: 5, end: 10 }, - { key: 'xxx', value: 'bbb', start: 25, end: 30 }, + { name: 'xxx', value: 'aaa', start: 10, end: 20 }, + { name: 'xxx', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'bbb', start: 5, end: 10 }, + { name: 'xxx', value: 'bbb', start: 25, end: 30 }, ] }, ]); @@ -448,9 +448,9 @@ describe('Automerge', () => { let marks = doc3.marks(list) assert.deepEqual(marks, [ - { key: 'xxx', value: 'bbb', start: 5, end: 10 }, - { key: 'xxx', value: 'aaa', start: 10, end: 25 }, - { key: 'xxx', value: 'bbb', start: 25, end: 30 }, + { name: 'xxx', value: 'bbb', start: 5, end: 10 }, + { name: 'xxx', value: 'aaa', start: 10, end: 25 }, + { name: 'xxx', value: 'bbb', start: 25, end: 30 }, ]); assert.deepEqual(patches2, [{ action: 'mark', path: [ 'list' ], marks }]); @@ -478,8 +478,8 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 10, end: 20 }, - { key: 'xxx', value: 'aaa', start: 15, end: 25 }, + { name: 'xxx', value: 'aaa', start: 10, end: 20 }, + { name: 'xxx', value: 'aaa', start: 15, end: 25 }, ] }, ]); @@ -492,7 +492,7 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 10, end: 25 }, + { name: 'xxx', value: 'aaa', start: 10, end: 25 }, ]} ]); }) @@ -519,9 +519,9 @@ describe('Automerge', () => { assert.deepEqual(patches1, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 11 }, - { key: 'xxx', value: 'aaa', start: 19, end: 25 }, - { key: 'xxx', value: 'aaa', start: 11, end: 19 }, + { name: 'xxx', value: 'aaa', start: 5, end: 11 }, + { name: 'xxx', value: 'aaa', start: 19, end: 25 }, + { name: 'xxx', value: 'aaa', start: 11, end: 19 }, ] }, ]); @@ -534,7 +534,7 @@ describe('Automerge', () => { assert.deepEqual(patches2, [ { action: 'mark', path: [ 'list' ], marks: [ - { key: 'xxx', value: 'aaa', start: 5, end: 25 }, + { name: 'xxx', value: 'aaa', start: 5, end: 25 }, ]} ]); }) diff --git a/rust/automerge/examples/watch.rs b/rust/automerge/examples/watch.rs index 82d27271..c2b909ee 100644 --- a/rust/automerge/examples/watch.rs +++ b/rust/automerge/examples/watch.rs @@ -85,9 +85,9 @@ fn get_changes(_doc: &Automerge, patches: Vec>) { PatchAction::Mark { marks } => { println!("mark {:?} in obj {:?}, object path {:?}", marks, obj, path,) } - PatchAction::Unmark { key, start, end } => println!( + PatchAction::Unmark { name, start, end } => println!( "unmark {:?} from {} to {} in obj {:?}, object path {:?}", - key, start, end, obj, path, + name, start, end, obj, path, ), } } diff --git a/rust/automerge/src/autocommit.rs b/rust/automerge/src/autocommit.rs index 5e39bace..a5685f48 100644 --- a/rust/automerge/src/autocommit.rs +++ b/rust/automerge/src/autocommit.rs @@ -1,7 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::Mark; +use crate::marks::{ExpandMark, Mark}; use crate::op_observer::{BranchableObserver, OpObserver}; use crate::sync::SyncDoc; use crate::transaction::{CommitOptions, Transactable}; @@ -447,16 +447,16 @@ impl ReadDoc for AutoCommitWithObs { self.doc.object_type(obj) } - fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { - self.doc.get_marks(obj) + fn marks>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.marks(obj) } - fn get_marks_at>( + fn marks_at>( &self, obj: O, heads: &[ChangeHash], ) -> Result>, AutomergeError> { - self.doc.get_marks_at(obj, heads) + self.doc.marks_at(obj, heads) } fn text>(&self, obj: O) -> Result { @@ -643,7 +643,7 @@ impl Transactable for AutoCommitWithObs { &mut self, obj: O, mark: Mark<'_>, - expand: (bool, bool), + expand: ExpandMark, ) -> Result<(), AutomergeError> { self.ensure_transaction_open(); let (current, tx) = self.transaction.as_mut().unwrap(); diff --git a/rust/automerge/src/automerge.rs b/rust/automerge/src/automerge.rs index a69d5df6..c0fcfe28 100644 --- a/rust/automerge/src/automerge.rs +++ b/rust/automerge/src/automerge.rs @@ -733,7 +733,10 @@ impl Automerge { Op { id, action: OpType::from_action_and_value( - c.action, c.val, c.mark_key, c.expand, + c.action, + c.val, + c.mark_name, + c.expand, ), key, succ: Default::default(), @@ -988,8 +991,8 @@ impl Automerge { OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), OpType::Delete => format!("del{}", 0), - OpType::MarkBegin(_, MarkData { key, value }) => { - format!("mark({},{})", key, value) + OpType::MarkBegin(_, MarkData { name, value }) => { + format!("mark({},{})", name, value) } OpType::MarkEnd(_) => "/mark".to_string(), }; @@ -1341,7 +1344,7 @@ impl ReadDoc for Automerge { Ok(buffer) } - fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { + fn marks>(&self, obj: O) -> Result>, AutomergeError> { let (obj, obj_type) = self.exid_to_obj(obj.as_ref())?; let encoding = ListEncoding::new(obj_type, self.text_encoding); let ops_by_key = self.ops().iter_ops(&obj).group_by(|o| o.elemid_or_key()); @@ -1367,7 +1370,7 @@ impl ReadDoc for Automerge { .collect()) } - fn get_marks_at>( + fn marks_at>( &self, obj: O, heads: &[ChangeHash], diff --git a/rust/automerge/src/automerge/current_state.rs b/rust/automerge/src/automerge/current_state.rs index d2d9093f..bb741a76 100644 --- a/rust/automerge/src/automerge/current_state.rs +++ b/rust/automerge/src/automerge/current_state.rs @@ -445,7 +445,7 @@ mod tests { &mut self, _doc: &R, _objid: crate::ObjId, - _key: &str, + _name: &str, _start: usize, _end: usize, ) { diff --git a/rust/automerge/src/change.rs b/rust/automerge/src/change.rs index 657ce574..a6ab112a 100644 --- a/rust/automerge/src/change.rs +++ b/rust/automerge/src/change.rs @@ -260,7 +260,7 @@ mod convert_expanded { self.action.expand() } - fn mark_key(&self) -> Option> { + fn mark_name(&self) -> Option> { if let legacy::OpType::MarkBegin(legacy::MarkData { name, .. }) = &self.action { Some(Cow::Borrowed(name)) } else { @@ -294,7 +294,7 @@ impl From<&Change> for crate::ExpandedChange { action: o.action, value: o.val, expand: o.expand, - mark_key: o.mark_key, + mark_name: o.mark_name, }), insert: o.insert, key: match o.key { diff --git a/rust/automerge/src/legacy/mod.rs b/rust/automerge/src/legacy/mod.rs index 4c731513..d4fb1fd0 100644 --- a/rust/automerge/src/legacy/mod.rs +++ b/rust/automerge/src/legacy/mod.rs @@ -208,7 +208,7 @@ pub(crate) struct OpTypeParts { pub(crate) action: u64, pub(crate) value: ScalarValue, pub(crate) expand: bool, - pub(crate) mark_key: Option, + pub(crate) mark_name: Option, } // Like `types::OpType` except using a String for mark names @@ -239,7 +239,7 @@ impl OpType { action, value, expand, - mark_key, + mark_name, }: OpTypeParts, ) -> Self { match action { @@ -254,7 +254,7 @@ impl OpType { _ => panic!("non numeric value for integer action"), }, 6 => Self::Make(ObjType::Table), - 7 => match mark_key { + 7 => match mark_name { Some(name) => Self::MarkBegin(MarkData { name, value, diff --git a/rust/automerge/src/marks.rs b/rust/automerge/src/marks.rs index eb7b9b38..cb9520d9 100644 --- a/rust/automerge/src/marks.rs +++ b/rust/automerge/src/marks.rs @@ -16,14 +16,14 @@ pub struct Mark<'a> { impl<'a> Mark<'a> { pub fn new>( - key: String, + name: String, value: V, start: usize, end: usize, ) -> Mark<'static> { Mark { data: Cow::Owned(MarkData { - key: key.into(), + name: name.into(), value: value.into(), }), start, @@ -47,8 +47,8 @@ impl<'a> Mark<'a> { } } - pub fn key(&self) -> &str { - self.data.key.as_str() + pub fn name(&self) -> &str { + self.data.name.as_str() } pub fn value(&self) -> &ScalarValue { @@ -136,7 +136,7 @@ impl<'a> MarkStateMachine<'a> { Some( &state[index..] .iter() - .find(|(_, m)| m.key() == mark.key())? + .find(|(_, m)| m.name() == mark.name())? .1, ) } @@ -149,7 +149,7 @@ impl<'a> MarkStateMachine<'a> { Some( &mut state[0..index] .iter_mut() - .filter(|(_, m)| m.data.key == mark.data.key) + .filter(|(_, m)| m.data.name == mark.data.name) .last()? .1, ) @@ -158,12 +158,37 @@ impl<'a> MarkStateMachine<'a> { #[derive(PartialEq, Debug, Clone)] pub struct MarkData { - pub key: SmolStr, + pub name: SmolStr, pub value: ScalarValue, } impl Display for MarkData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "key={} value={}", self.key, self.value) + write!(f, "name={} value={}", self.name, self.value) + } +} + +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum ExpandMark { + Left, + Right, + Both, + None, +} + +impl ExpandMark { + pub fn from(left: bool, right: bool) -> Self { + match (left, right) { + (true, true) => Self::Both, + (false, true) => Self::Right, + (true, false) => Self::Left, + (false, false) => Self::None, + } + } + pub fn left(&self) -> bool { + matches!(self, Self::Left | Self::Both) + } + pub fn right(&self) -> bool { + matches!(self, Self::Right | Self::Both) } } diff --git a/rust/automerge/src/op_observer.rs b/rust/automerge/src/op_observer.rs index 1c068ad4..6870c136 100644 --- a/rust/automerge/src/op_observer.rs +++ b/rust/automerge/src/op_observer.rs @@ -126,7 +126,7 @@ pub trait OpObserver { mark: M, ); - fn unmark(&mut self, doc: &R, objid: ExId, key: &str, start: usize, end: usize); + fn unmark(&mut self, doc: &R, objid: ExId, name: &str, start: usize, end: usize); /// Whether to call sequence methods or `splice_text` when encountering changes in text /// @@ -210,7 +210,7 @@ impl OpObserver for () { &mut self, _doc: &R, _objid: ExId, - _key: &str, + _name: &str, _start: usize, _end: usize, ) { diff --git a/rust/automerge/src/op_observer/compose.rs b/rust/automerge/src/op_observer/compose.rs index 23ccaf8e..0dfc6542 100644 --- a/rust/automerge/src/op_observer/compose.rs +++ b/rust/automerge/src/op_observer/compose.rs @@ -101,12 +101,12 @@ impl<'a, O1: OpObserver, O2: OpObserver> OpObserver for ComposeObservers<'a, O1, &mut self, doc: &R, objid: crate::ObjId, - key: &str, + name: &str, start: usize, end: usize, ) { - self.obs1.unmark(doc, objid.clone(), key, start, end); - self.obs2.unmark(doc, objid, key, start, end); + self.obs1.unmark(doc, objid.clone(), name, start, end); + self.obs2.unmark(doc, objid, name, start, end); } fn delete_map(&mut self, doc: &R, objid: crate::ObjId, key: &str) { diff --git a/rust/automerge/src/op_observer/patch.rs b/rust/automerge/src/op_observer/patch.rs index af860070..717f680b 100644 --- a/rust/automerge/src/op_observer/patch.rs +++ b/rust/automerge/src/op_observer/patch.rs @@ -50,7 +50,7 @@ pub enum PatchAction { marks: Vec>, }, Unmark { - key: String, + name: String, start: usize, end: usize, }, diff --git a/rust/automerge/src/op_observer/toggle_observer.rs b/rust/automerge/src/op_observer/toggle_observer.rs index f9e42b76..6afcee9f 100644 --- a/rust/automerge/src/op_observer/toggle_observer.rs +++ b/rust/automerge/src/op_observer/toggle_observer.rs @@ -152,9 +152,9 @@ impl OpObserver for ToggleObserver { } } - fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + fn unmark(&mut self, doc: &R, obj: ObjId, name: &str, start: usize, end: usize) { if self.enabled { - self.observer.unmark(doc, obj, key, start, end) + self.observer.unmark(doc, obj, name, start, end) } } diff --git a/rust/automerge/src/op_observer/vec_observer.rs b/rust/automerge/src/op_observer/vec_observer.rs index dc21d86e..9887699a 100644 --- a/rust/automerge/src/op_observer/vec_observer.rs +++ b/rust/automerge/src/op_observer/vec_observer.rs @@ -363,10 +363,10 @@ impl OpObserver for VecOpObserverInner { } } - fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { + fn unmark(&mut self, doc: &R, obj: ObjId, name: &str, start: usize, end: usize) { if let Some(path) = self.get_path(doc, &obj) { let action = PatchAction::Unmark { - key: key.to_string(), + name: name.to_string(), start, end, }; @@ -457,8 +457,8 @@ impl OpObserver for VecOpObserver { self.0.mark(doc, obj, mark) } - fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { - self.0.unmark(doc, obj, key, start, end) + fn unmark(&mut self, doc: &R, obj: ObjId, name: &str, start: usize, end: usize) { + self.0.unmark(doc, obj, name, start, end) } fn text_as_seq(&self) -> bool { @@ -531,8 +531,8 @@ impl OpObserver for VecOpObserver16 { self.0.mark(doc, obj, mark) } - fn unmark(&mut self, doc: &R, obj: ObjId, key: &str, start: usize, end: usize) { - self.0.unmark(doc, obj, key, start, end) + fn unmark(&mut self, doc: &R, obj: ObjId, name: &str, start: usize, end: usize) { + self.0.unmark(doc, obj, name, start, end) } fn text_as_seq(&self) -> bool { diff --git a/rust/automerge/src/query/seek_mark.rs b/rust/automerge/src/query/seek_mark.rs index 6706167c..c5834ab8 100644 --- a/rust/automerge/src/query/seek_mark.rs +++ b/rust/automerge/src/query/seek_mark.rs @@ -13,7 +13,7 @@ pub(crate) struct SeekMark<'a> { end: usize, encoding: ListEncoding, found: bool, - mark_key: smol_str::SmolStr, + mark_name: smol_str::SmolStr, next_mark: Option>, pos: usize, seen: usize, @@ -30,7 +30,7 @@ impl<'a> SeekMark<'a> { end, found: false, next_mark: None, - mark_key: "".into(), + mark_name: "".into(), pos: 0, seen: 0, last_seen: None, @@ -58,20 +58,20 @@ impl<'a> TreeQuery<'a> for SeekMark<'a> { return QueryResult::Finish; } self.found = true; - self.mark_key = data.key.clone(); + self.mark_name = data.name.clone(); // retain the name and the value self.next_mark = Some(Mark::from_data(self.seen, self.seen, data)); // change id to the end id self.id = self.id.next(); // remove all marks that dont match - self.super_marks.retain(|_, v| v == &data.key); + self.super_marks.retain(|_, v| v == &data.name); } OpType::MarkBegin(_, mark) => { if m.lamport_cmp(op.id, self.id) == Ordering::Greater { if let Some(next_mark) = &mut self.next_mark { // gather marks of the same type that supersede us - if mark.key == self.mark_key { - self.super_marks.insert(op.id.next(), mark.key.clone()); + if mark.name == self.mark_name { + self.super_marks.insert(op.id.next(), mark.name.clone()); if self.super_marks.len() == 1 { // complete a mark next_mark.end = self.seen; @@ -80,7 +80,7 @@ impl<'a> TreeQuery<'a> for SeekMark<'a> { } } else { // gather all marks until we know what our mark's name is - self.super_marks.insert(op.id.next(), mark.key.clone()); + self.super_marks.insert(op.id.next(), mark.name.clone()); } } } diff --git a/rust/automerge/src/read.rs b/rust/automerge/src/read.rs index 966814ad..9b3a6a77 100644 --- a/rust/automerge/src/read.rs +++ b/rust/automerge/src/read.rs @@ -130,10 +130,10 @@ pub trait ReadDoc { fn object_type>(&self, obj: O) -> Result; /// Get all marks on a current sequence - fn get_marks>(&self, obj: O) -> Result>, AutomergeError>; + fn marks>(&self, obj: O) -> Result>, AutomergeError>; /// Get all marks on a sequence at a given heads - fn get_marks_at>( + fn marks_at>( &self, obj: O, heads: &[ChangeHash], diff --git a/rust/automerge/src/storage/change.rs b/rust/automerge/src/storage/change.rs index 0e8f9000..a420da39 100644 --- a/rust/automerge/src/storage/change.rs +++ b/rust/automerge/src/storage/change.rs @@ -438,7 +438,7 @@ pub(crate) trait AsChangeOp<'a> { fn val(&self) -> Cow<'a, ScalarValue>; fn pred(&self) -> Self::PredIter; fn expand(&self) -> bool; - fn mark_key(&self) -> Option>; + fn mark_name(&self) -> Option>; } impl ChangeBuilder, Set, Set, Set> { diff --git a/rust/automerge/src/storage/change/change_actors.rs b/rust/automerge/src/storage/change/change_actors.rs index afd3204c..951201dd 100644 --- a/rust/automerge/src/storage/change/change_actors.rs +++ b/rust/automerge/src/storage/change/change_actors.rs @@ -252,8 +252,8 @@ where self.op.expand() } - fn mark_key(&self) -> Option> { - self.op.mark_key() + fn mark_name(&self) -> Option> { + self.op.mark_name() } } diff --git a/rust/automerge/src/storage/change/change_op_columns.rs b/rust/automerge/src/storage/change/change_op_columns.rs index 92e90b0b..145073e6 100644 --- a/rust/automerge/src/storage/change/change_op_columns.rs +++ b/rust/automerge/src/storage/change/change_op_columns.rs @@ -44,7 +44,7 @@ pub(crate) struct ChangeOp { pub(crate) action: u64, pub(crate) obj: ObjId, pub(crate) expand: bool, - pub(crate) mark_key: Option, + pub(crate) mark_name: Option, } impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { @@ -64,7 +64,7 @@ impl<'a, A: AsChangeOp<'a, ActorId = usize, OpId = OpId>> From for ChangeOp { insert: a.insert(), action: a.action(), expand: a.expand(), - mark_key: a.mark_key().map(|n| n.into_owned()), + mark_name: a.mark_name().map(|n| n.into_owned()), } } } @@ -110,8 +110,8 @@ impl<'a> AsChangeOp<'a> for &'a ChangeOp { self.expand } - fn mark_key(&self) -> Option> { - self.mark_key.as_ref().map(Cow::Borrowed) + fn mark_name(&self) -> Option> { + self.mark_name.as_ref().map(Cow::Borrowed) } } @@ -124,7 +124,7 @@ pub(crate) struct ChangeOpsColumns { val: ValueRange, pred: OpIdListRange, expand: MaybeBooleanRange, - mark_key: RleRange, + mark_name: RleRange, } impl ChangeOpsColumns { @@ -138,7 +138,7 @@ impl ChangeOpsColumns { val: self.val.iter(data), pred: self.pred.iter(data), expand: self.expand.decoder(data), - mark_key: self.mark_key.decoder(data), + mark_name: self.mark_name.decoder(data), } } @@ -173,8 +173,8 @@ impl ChangeOpsColumns { let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let pred = OpIdListRange::encode(ops.clone().map(|o| o.pred()), out); let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); - let mark_key = - RleRange::encode::, _>(ops.map(|o| o.mark_key()), out); + let mark_name = + RleRange::encode::, _>(ops.map(|o| o.mark_name()), out); Self { obj, key, @@ -183,7 +183,7 @@ impl ChangeOpsColumns { val, pred, expand, - mark_key, + mark_name, } } @@ -200,7 +200,7 @@ impl ChangeOpsColumns { let mut val = ValueEncoder::new(); let mut pred = OpIdListEncoder::new(); let mut expand = MaybeBooleanEncoder::new(); - let mut mark_key = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); + let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { tracing::trace!(expand=?op.expand(), "expand"); obj.append(op.obj()); @@ -210,7 +210,7 @@ impl ChangeOpsColumns { val.append(&op.val()); pred.append(op.pred()); expand.append(op.expand()); - mark_key.append(op.mark_key()); + mark_name.append(op.mark_name()); } let obj = obj.finish(out); let key = key.finish(out); @@ -233,10 +233,10 @@ impl ChangeOpsColumns { out.extend(expand); let expand = MaybeBooleanRange::from(expand_start..out.len()); - let mark_key_start = out.len(); - let (mark_key, _) = mark_key.finish(); - out.extend(mark_key); - let mark_key = RleRange::from(mark_key_start..out.len()); + let mark_name_start = out.len(); + let (mark_name, _) = mark_name.finish(); + out.extend(mark_name); + let mark_name = RleRange::from(mark_name_start..out.len()); Self { obj, @@ -246,7 +246,7 @@ impl ChangeOpsColumns { val, pred, expand, - mark_key, + mark_name, } } @@ -319,10 +319,10 @@ impl ChangeOpsColumns { self.expand.clone().into(), )); } - if !self.mark_key.is_empty() { + if !self.mark_name.is_empty() { cols.push(RawColumn::new( ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), - self.mark_key.clone().into(), + self.mark_name.clone().into(), )); } cols.into_iter().collect() @@ -350,7 +350,7 @@ pub(crate) struct ChangeOpsIter<'a> { val: ValueIter<'a>, pred: OpIdListIter<'a>, expand: MaybeBooleanDecoder<'a>, - mark_key: RleDecoder<'a, smol_str::SmolStr>, + mark_name: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> ChangeOpsIter<'a> { @@ -373,7 +373,7 @@ impl<'a> ChangeOpsIter<'a> { let val = self.val.next_in_col("value")?; let pred = self.pred.next_in_col("pred")?; let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); - let mark_key = self.mark_key.maybe_next_in_col("mark_key")?; + let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; // This check is necessary to ensure that OpType::from_action_and_value // cannot panic later in the process. @@ -387,7 +387,7 @@ impl<'a> ChangeOpsIter<'a> { val, pred, expand, - mark_key, + mark_name, })) } } @@ -435,7 +435,7 @@ impl TryFrom for ChangeOpsColumns { let mut pred_actor: Option> = None; let mut pred_ctr: Option = None; let mut expand: Option = None; - let mut mark_key: Option> = None; + let mut mark_name: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -491,7 +491,7 @@ impl TryFrom for ChangeOpsColumns { _ => return Err(ParseChangeColumnsError::MismatchingColumn { index }), }, (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), - (MARK_NAME_COL_ID, ColumnType::String) => mark_key = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), (other_type, other_col) => { tracing::warn!(typ=?other_type, id=?other_col, "unknown column"); other.append(col); @@ -518,7 +518,7 @@ impl TryFrom for ChangeOpsColumns { val: val.unwrap_or_else(|| ValueRange::new((0..0).into(), (0..0).into())), pred, expand: expand.unwrap_or_else(|| (0..0).into()), - mark_key: mark_key.unwrap_or_else(|| (0..0).into()), + mark_name: mark_name.unwrap_or_else(|| (0..0).into()), }) } } @@ -536,7 +536,7 @@ mod tests { pred in proptest::collection::vec(opid(), 0..20), action in 0_u64..6, obj in opid(), - mark_key in proptest::option::of(any::().prop_map(|s| s.into())), + mark_name in proptest::option::of(any::().prop_map(|s| s.into())), expand in any::(), insert in any::()) -> ChangeOp { @@ -551,7 +551,7 @@ mod tests { action, insert, expand, - mark_key, + mark_name, } } } diff --git a/rust/automerge/src/storage/convert/op_as_changeop.rs b/rust/automerge/src/storage/convert/op_as_changeop.rs index 150c782e..c08fa063 100644 --- a/rust/automerge/src/storage/convert/op_as_changeop.rs +++ b/rust/automerge/src/storage/convert/op_as_changeop.rs @@ -136,9 +136,9 @@ impl<'a> AsChangeOp<'a> for OpWithMetadata<'a> { ) } - fn mark_key(&self) -> Option> { - if let OpType::MarkBegin(_, MarkData { key, .. }) = &self.op.action { - Some(Cow::Owned(key.clone())) + fn mark_name(&self) -> Option> { + if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { + Some(Cow::Owned(name.clone())) } else { None } diff --git a/rust/automerge/src/storage/convert/op_as_docop.rs b/rust/automerge/src/storage/convert/op_as_docop.rs index cb6b7b90..09598b67 100644 --- a/rust/automerge/src/storage/convert/op_as_docop.rs +++ b/rust/automerge/src/storage/convert/op_as_docop.rs @@ -119,9 +119,9 @@ impl<'a> AsDocOp<'a> for OpAsDocOp<'a> { } } - fn mark_key(&self) -> Option> { - if let OpType::MarkBegin(_, MarkData { key, .. }) = &self.op.action { - Some(Cow::Owned(key.clone())) + fn mark_name(&self) -> Option> { + if let OpType::MarkBegin(_, MarkData { name, .. }) = &self.op.action { + Some(Cow::Owned(name.clone())) } else { None } diff --git a/rust/automerge/src/storage/document/doc_op_columns.rs b/rust/automerge/src/storage/document/doc_op_columns.rs index 1ce16f85..7248b1b3 100644 --- a/rust/automerge/src/storage/document/doc_op_columns.rs +++ b/rust/automerge/src/storage/document/doc_op_columns.rs @@ -42,7 +42,7 @@ pub(crate) struct DocOp { pub(crate) value: ScalarValue, pub(crate) succ: Vec, pub(crate) expand: bool, - pub(crate) mark_key: Option, + pub(crate) mark_name: Option, } #[derive(Debug, Clone)] @@ -57,7 +57,7 @@ pub(crate) struct DocOpColumns { #[allow(dead_code)] other: Columns, expand: MaybeBooleanRange, - mark_key: RleRange, + mark_name: RleRange, } struct DocId { @@ -97,7 +97,7 @@ pub(crate) trait AsDocOp<'a> { fn val(&self) -> Cow<'a, ScalarValue>; fn succ(&self) -> Self::SuccIter; fn expand(&self) -> bool; - fn mark_key(&self) -> Option>; + fn mark_name(&self) -> Option>; } impl DocOpColumns { @@ -128,7 +128,7 @@ impl DocOpColumns { let val = ValueRange::encode(ops.clone().map(|o| o.val()), out); let succ = OpIdListRange::encode(ops.clone().map(|o| o.succ()), out); let expand = MaybeBooleanRange::encode(ops.clone().map(|o| o.expand()), out); - let mark_key = RleRange::encode(ops.map(|o| o.mark_key()), out); + let mark_name = RleRange::encode(ops.map(|o| o.mark_name()), out); Self { obj, key, @@ -138,7 +138,7 @@ impl DocOpColumns { val, succ, expand, - mark_key, + mark_name, other: Columns::empty(), } } @@ -157,7 +157,7 @@ impl DocOpColumns { let mut val = ValueEncoder::new(); let mut succ = OpIdListEncoder::new(); let mut expand = MaybeBooleanEncoder::new(); - let mut mark_key = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); + let mut mark_name = RleEncoder::<_, smol_str::SmolStr>::new(Vec::new()); for op in ops { obj.append(op.obj()); key.append(op.key()); @@ -167,7 +167,7 @@ impl DocOpColumns { val.append(&op.val()); succ.append(op.succ()); expand.append(op.expand()); - mark_key.append(op.mark_key()); + mark_name.append(op.mark_name()); } let obj = obj.finish(out); let key = key.finish(out); @@ -191,10 +191,10 @@ impl DocOpColumns { out.extend(expand_out); let expand = MaybeBooleanRange::from(expand_start..out.len()); - let mark_key_start = out.len(); - let (mark_key_out, _) = mark_key.finish(); - out.extend(mark_key_out); - let mark_key = RleRange::from(mark_key_start..out.len()); + let mark_name_start = out.len(); + let (mark_name_out, _) = mark_name.finish(); + out.extend(mark_name_out); + let mark_name = RleRange::from(mark_name_start..out.len()); DocOpColumns { obj, @@ -205,7 +205,7 @@ impl DocOpColumns { val, succ, expand, - mark_key, + mark_name, other: Columns::empty(), } } @@ -220,7 +220,7 @@ impl DocOpColumns { value: self.val.iter(data), succ: self.succ.iter(data), expand: self.expand.decoder(data), - mark_key: self.mark_key.decoder(data), + mark_name: self.mark_name.decoder(data), } } @@ -301,10 +301,10 @@ impl DocOpColumns { self.expand.clone().into(), )); } - if !self.mark_key.is_empty() { + if !self.mark_name.is_empty() { cols.push(RawColumn::new( ColumnSpec::new(MARK_NAME_COL_ID, ColumnType::String, false), - self.mark_key.clone().into(), + self.mark_name.clone().into(), )); } cols.into_iter().collect() @@ -321,7 +321,7 @@ pub(crate) struct DocOpColumnIter<'a> { value: ValueIter<'a>, succ: OpIdListIter<'a>, expand: MaybeBooleanDecoder<'a>, - mark_key: RleDecoder<'a, smol_str::SmolStr>, + mark_name: RleDecoder<'a, smol_str::SmolStr>, } impl<'a> DocOpColumnIter<'a> { @@ -367,7 +367,7 @@ impl<'a> DocOpColumnIter<'a> { let succ = self.succ.next_in_col("succ")?; let insert = self.insert.next_in_col("insert")?; let expand = self.expand.maybe_next_in_col("expand")?.unwrap_or(false); - let mark_key = self.mark_key.maybe_next_in_col("mark_key")?; + let mark_name = self.mark_name.maybe_next_in_col("mark_name")?; Ok(Some(DocOp { id, value, @@ -377,7 +377,7 @@ impl<'a> DocOpColumnIter<'a> { succ, insert, expand, - mark_key, + mark_name, })) } } @@ -413,7 +413,7 @@ impl TryFrom for DocOpColumns { let mut succ_actor: Option> = None; let mut succ_ctr: Option = None; let mut expand: Option = None; - let mut mark_key: Option> = None; + let mut mark_name: Option> = None; let mut other = Columns::empty(); for (index, col) in columns.into_iter().enumerate() { @@ -468,7 +468,7 @@ impl TryFrom for DocOpColumns { _ => return Err(Error::MismatchingColumn { index }), }, (EXPAND_COL_ID, ColumnType::Boolean) => expand = Some(col.range().into()), - (MARK_NAME_COL_ID, ColumnType::String) => mark_key = Some(col.range().into()), + (MARK_NAME_COL_ID, ColumnType::String) => mark_name = Some(col.range().into()), (other_col, other_type) => { tracing::warn!(id=?other_col, typ=?other_type, "unknown column type"); other.append(col) @@ -498,7 +498,7 @@ impl TryFrom for DocOpColumns { succ_ctr.unwrap_or_else(|| (0..0).into()), ), expand: expand.unwrap_or_else(|| (0..0).into()), - mark_key: mark_key.unwrap_or_else(|| (0..0).into()), + mark_name: mark_name.unwrap_or_else(|| (0..0).into()), other, }) } diff --git a/rust/automerge/src/storage/load/reconstruct_document.rs b/rust/automerge/src/storage/load/reconstruct_document.rs index b7203dbc..1f80a85d 100644 --- a/rust/automerge/src/storage/load/reconstruct_document.rs +++ b/rust/automerge/src/storage/load/reconstruct_document.rs @@ -345,7 +345,7 @@ fn import_op(m: &mut OpSetMetadata, op: DocOp) -> Result { return Err(Error::MissingActor); } } - let action = OpType::from_action_and_value(op.action, op.value, op.mark_key, op.expand); + let action = OpType::from_action_and_value(op.action, op.value, op.mark_name, op.expand); Ok(Op { id: check_opid(m, op.id)?, action, diff --git a/rust/automerge/src/transaction/inner.rs b/rust/automerge/src/transaction/inner.rs index 4ba6dfe4..5afd2e1d 100644 --- a/rust/automerge/src/transaction/inner.rs +++ b/rust/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use std::num::NonZeroU64; use crate::exid::ExId; -use crate::marks::Mark; +use crate::marks::{ExpandMark, Mark}; use crate::query::{self, OpIdSearch}; use crate::storage::Change as StoredChange; use crate::types::{Key, ListEncoding, ObjId, OpId, OpIds, TextEncoding}; @@ -631,7 +631,6 @@ impl TransactionInner { // handle the observer if let Some(obs) = op_observer.as_mut() { match splice_type { - //SpliceType::Text(text, _) => { //if !obs.text_as_seq() => { SpliceType::Text(text, _) if !obs.text_as_seq() => { obs.splice_text(doc, ex_obj, index, text) } @@ -656,22 +655,28 @@ impl TransactionInner { op_observer: Option<&mut Obs>, ex_obj: &ExId, mark: Mark<'_>, - (expand_left, expand_right): (bool, bool), + expand: ExpandMark, ) -> Result<(), AutomergeError> { let (obj, _obj_type) = doc.exid_to_obj(ex_obj)?; if let Some(obs) = op_observer { - let action = OpType::MarkBegin(expand_left, mark.data.clone().into_owned()); + let action = OpType::MarkBegin(expand.left(), mark.data.clone().into_owned()); self.do_insert(doc, Some(obs), obj, mark.start, action)?; - self.do_insert(doc, Some(obs), obj, mark.end, OpType::MarkEnd(expand_right))?; + self.do_insert( + doc, + Some(obs), + obj, + mark.end, + OpType::MarkEnd(expand.right()), + )?; if mark.value().is_null() { - obs.unmark(doc, ex_obj.clone(), mark.key(), mark.start, mark.end); + obs.unmark(doc, ex_obj.clone(), mark.name(), mark.start, mark.end); } else { obs.mark(doc, ex_obj.clone(), Some(mark).into_iter()) } } else { - let action = OpType::MarkBegin(expand_left, mark.data.into_owned()); + let action = OpType::MarkBegin(expand.left(), mark.data.into_owned()); self.do_insert::(doc, None, obj, mark.start, action)?; - self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(expand_right))?; + self.do_insert::(doc, None, obj, mark.end, OpType::MarkEnd(expand.right()))?; } Ok(()) } @@ -681,12 +686,12 @@ impl TransactionInner { doc: &mut Automerge, op_observer: Option<&mut Obs>, ex_obj: &ExId, - key: &str, + name: &str, start: usize, end: usize, ) -> Result<(), AutomergeError> { - let mark = Mark::new(key.to_string(), ScalarValue::Null, start, end); - self.mark(doc, op_observer, ex_obj, mark, (false, false)) + let mark = Mark::new(name.to_string(), ScalarValue::Null, start, end); + self.mark(doc, op_observer, ex_obj, mark, ExpandMark::None) } fn finalize_op( diff --git a/rust/automerge/src/transaction/manual_transaction.rs b/rust/automerge/src/transaction/manual_transaction.rs index 337c7578..52fdd0d7 100644 --- a/rust/automerge/src/transaction/manual_transaction.rs +++ b/rust/automerge/src/transaction/manual_transaction.rs @@ -1,7 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; -use crate::marks::Mark; +use crate::marks::{ExpandMark, Mark}; use crate::op_observer::BranchableObserver; use crate::{ Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ReadDoc, ScalarValue, Value, Values, @@ -191,16 +191,16 @@ impl<'a, Obs: observation::Observation> ReadDoc for Transaction<'a, Obs> { self.doc.text_at(obj, heads) } - fn get_marks>(&self, obj: O) -> Result>, AutomergeError> { - self.doc.get_marks(obj) + fn marks>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.marks(obj) } - fn get_marks_at>( + fn marks_at>( &self, obj: O, heads: &[ChangeHash], ) -> Result>, AutomergeError> { - self.doc.get_marks_at(obj, heads) + self.doc.marks_at(obj, heads) } fn get, P: Into>( @@ -347,7 +347,7 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { &mut self, obj: O, mark: Mark<'_>, - expand: (bool, bool), + expand: ExpandMark, ) -> Result<(), AutomergeError> { self.do_tx(|tx, doc, obs| tx.mark(doc, obs, obj.as_ref(), mark, expand)) } @@ -355,11 +355,11 @@ impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> { fn unmark>( &mut self, obj: O, - key: &str, + name: &str, start: usize, end: usize, ) -> Result<(), AutomergeError> { - self.do_tx(|tx, doc, obs| tx.unmark(doc, obs, obj.as_ref(), key, start, end)) + self.do_tx(|tx, doc, obs| tx.unmark(doc, obs, obj.as_ref(), name, start, end)) } fn base_heads(&self) -> Vec { diff --git a/rust/automerge/src/transaction/transactable.rs b/rust/automerge/src/transaction/transactable.rs index c0781b95..81b83dff 100644 --- a/rust/automerge/src/transaction/transactable.rs +++ b/rust/automerge/src/transaction/transactable.rs @@ -1,5 +1,5 @@ use crate::exid::ExId; -use crate::marks::Mark; +use crate::marks::{ExpandMark, Mark}; use crate::{AutomergeError, ChangeHash, ObjType, Prop, ReadDoc, ScalarValue}; /// A way of mutating a document within a single change. @@ -93,7 +93,7 @@ pub trait Transactable: ReadDoc { &mut self, obj: O, mark: Mark<'_>, - expand: (bool, bool), + expand: ExpandMark, ) -> Result<(), AutomergeError>; fn unmark>( diff --git a/rust/automerge/src/types.rs b/rust/automerge/src/types.rs index 3afa90d7..468605b6 100644 --- a/rust/automerge/src/types.rs +++ b/rust/automerge/src/types.rs @@ -239,7 +239,7 @@ impl OpType { pub(crate) fn from_action_and_value( action: u64, value: ScalarValue, - mark_key: Option, + mark_name: Option, expand: bool, ) -> OpType { match action { @@ -254,8 +254,8 @@ impl OpType { _ => unreachable!("validate_action_and_value returned NonNumericInc"), }, 6 => Self::Make(ObjType::Table), - 7 => match mark_key { - Some(key) => Self::MarkBegin(expand, MarkData { key, value }), + 7 => match mark_name { + Some(name) => Self::MarkBegin(expand, MarkData { name, value }), None => Self::MarkEnd(expand), }, _ => unreachable!("validate_action_and_value returned UnknownAction"), diff --git a/rust/automerge/tests/test.rs b/rust/automerge/tests/test.rs index 3e6cde70..3ba1df40 100644 --- a/rust/automerge/tests/test.rs +++ b/rust/automerge/tests/test.rs @@ -1283,7 +1283,6 @@ fn test_change_encoding_expanded_change_round_trip() { let change = automerge::Change::try_from(&change_bytes[..]).unwrap(); assert_eq!(change.raw_bytes(), change_bytes); let expanded = automerge::ExpandedChange::from(&change); - println!("{:?}", expanded); let unexpanded: automerge::Change = expanded.try_into().unwrap(); assert_eq!(unexpanded.raw_bytes(), change_bytes); }