From 561cad44e3aeee4e84d36345b3d79b8349d6a7a8 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 11:42:58 -0500 Subject: [PATCH 01/15] Revert "remove marks" This reverts commit c8c695618b678b63aac31364cf4203d7a6f507b2. --- automerge-wasm/index.d.ts | 5 + automerge-wasm/src/lib.rs | 73 +++++++++++ automerge-wasm/test/marks.ts | 138 ++++++++++++++++++++ automerge/src/automerge.rs | 88 ++++++++++++- automerge/src/columnar.rs | 46 ++++++- automerge/src/legacy/serde_impls/op.rs | 36 +++++ automerge/src/legacy/serde_impls/op_type.rs | 2 + automerge/src/query.rs | 4 + automerge/src/query/insert.rs | 4 + automerge/src/query/raw_spans.rs | 71 ++++++++++ automerge/src/query/spans.rs | 108 +++++++++++++++ automerge/src/types.rs | 39 +++++- automerge/tests/test.rs | 96 +++++++------- 13 files changed, 658 insertions(+), 52 deletions(-) create mode 100644 automerge-wasm/test/marks.ts create mode 100644 automerge/src/query/raw_spans.rs create mode 100644 automerge/src/query/spans.rs diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 20189dab..8a7e9408 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -101,6 +101,11 @@ export class Automerge { text(obj: ObjID, heads?: Heads): string; length(obj: ObjID, heads?: Heads): number; + // experimental spans api - unstable! + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + spans(obj: ObjID): any; + raw_spans(obj: ObjID): any; + // transactions commit(message?: string, time?: number): Heads; merge(other: Automerge): Heads; diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index b4973f6f..b2f8e277 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -2,6 +2,7 @@ use automerge as am; use automerge::{Change, ObjId, Prop, Value, ROOT}; use js_sys::{Array, Object, Uint8Array}; +use regex::Regex; use std::convert::TryInto; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -305,6 +306,78 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: String, + 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.0 + .mark(&obj, start, start_sticky, end, end_sticky, &name, value) + .map_err(to_js_err)?; + Ok(()) + } + + pub fn spans(&mut self, obj: String) -> Result { + let obj = self.import(obj)?; + let text = self.0.text(&obj).map_err(to_js_err)?; + let spans = self.0.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()); + mark.push(&datatype(&m.1).into()); + mark.push(&ScalarValue(m.1).into()); + marks.push(&mark.into()); + } + let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); + if !text_span.is_empty() { + result.push(&text_span.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[last_pos..]; + if !text_span.is_empty() { + result.push(&text_span.into()); + } + Ok(result.into()) + } + + pub fn raw_spans(&mut self, obj: String) -> Result { + let obj = self.import(obj)?; + let spans = self.0.raw_spans(&obj).map_err(to_js_err)?; + let result = Array::new(); + for s in spans { + result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); + } + Ok(result) + } + pub fn save(&mut self) -> Result { self.0 .save() diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts new file mode 100644 index 00000000..61951056 --- /dev/null +++ b/automerge-wasm/test/marks.ts @@ -0,0 +1,138 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '../dev/index' + +describe('Automerge', () => { + describe('marks', () => { + it('should handle marks [..]', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); + }) + + it('should handle marks with deleted ends [..]', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + + 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.del(list,5); + doc.del(list,5); + doc.del(list,2); + doc.del(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('should handle sticky marks (..)', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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('should handle sticky marks with deleted ends (..)', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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.del(list,5); + doc.del(list,5); + doc.del(list,2); + doc.del(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 = loadDoc(doc.save()) + 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('should handle overlapping marks', () => { + let doc : Automerge = create("aabbcc") + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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",999); + 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", time: 999, start: 0, end: 37, type: 'bold', value: true }, + { id: "41@aabbcc", time: 999, start: 4, end: 19, type: 'itallic', value: true }, + { id: "43@aabbcc", time: 999, start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + // mark sure encode decode can handle marks + + let all = doc.getChanges([]) + let decoded = all.map((c) => decodeChange(c)) + let encoded = decoded.map((c) => encodeChange(c)) + let doc2 = create(); + doc2.applyChanges(encoded) + + assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + }) +}) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index c3918354..122fa884 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -453,6 +453,90 @@ impl Automerge { Ok(buffer) } + pub fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let mut query = self.ops.search(obj, query::Spans::new()); + query.check_marks(); + Ok(query.spans) + } + + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let query = self.ops.search(obj, query::RawSpans::new()); + let result = query.spans.into_iter().map(|s| SpanInfo { + id: self.id_to_exid(s.id), + time: self.history[s.change].time, + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }).collect(); + Ok(result) + } + + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = self.exid_to_obj(obj)?; + + self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; + self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; + + /* + let (a, b) = query.ops()?; + let (pos, key) = a; + let id = self.next_id(); + let op = Op { + change: self.history.len(), + id, + action: OpType::Mark(MarkData { name: mark.into(), expand: expand_start, value}), + obj, + key, + succ: Default::default(), + pred: Default::default(), + insert: true, + }; + self.ops.insert(pos, op.clone()); + self.tx().operations.push(op); + + let (pos, key) = b; + let id = self.next_id(); + let op = Op { + change: self.history.len(), + id, + action: OpType::Unmark(expand_end), + obj, + key, + succ: Default::default(), + pred: Default::default(), + insert: true, + }; + self.ops.insert(pos, op.clone()); + self.tx().operations.push(op); + */ + + Ok(()) + } + + pub fn unmark( + &self, + _obj: &ExId, + _start: usize, + _end: usize, + _inclusive: bool, + _mark: &str, + ) -> Result { + unimplemented!() + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? @@ -1114,6 +1198,8 @@ impl Automerge { OpType::Set(value) => format!("{}", value), OpType::Make(obj) => format!("make({})", obj), OpType::Inc(obj) => format!("inc({})", obj), + OpType::MarkBegin(m) => format!("mark({}={})", m.name, m.value), + OpType::MarkEnd(_) => "/mark".into(), OpType::Del => format!("del{}", 0), }; let pred: Vec<_> = i.pred.iter().map(|id| self.to_string(*id)).collect(); @@ -1384,8 +1470,6 @@ mod tests { assert!(doc.value_at(&list, 0, &heads2)?.unwrap().0 == Value::int(10)); assert!(doc.length_at(&list, &heads3) == 2); - doc.dump(); - //log!("{:?}", doc.value_at(&list, 0, &heads3)?.unwrap().0); assert!(doc.value_at(&list, 0, &heads3)?.unwrap().0 == Value::int(30)); assert!(doc.value_at(&list, 1, &heads3)?.unwrap().0 == Value::int(20)); diff --git a/automerge/src/columnar.rs b/automerge/src/columnar.rs index 53a9d488..28aca822 100644 --- a/automerge/src/columnar.rs +++ b/automerge/src/columnar.rs @@ -134,6 +134,15 @@ impl<'a> Iterator for OperationIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Del, Action::Inc => OpType::Inc(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.to_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(amp::Op { action, @@ -175,6 +184,15 @@ impl<'a> Iterator for DocOpIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Del, Action::Inc => OpType::Inc(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.to_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(DocOp { actor, @@ -1064,6 +1082,16 @@ impl DocOpEncoder { self.val.append_null(); Action::Del } + amp::OpType::MarkBegin(m) => { + self.val.append_value(&m.name.clone().into(), actors); + self.val.append_value(&m.expand.into(), actors); + self.val.append_value(&m.value.clone(), actors); + Action::MarkBegin + } + amp::OpType::MarkEnd(s) => { + self.val.append_value(&(*s).into(), actors); + Action::MarkEnd + } amp::OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1170,6 +1198,16 @@ impl ColumnEncoder { self.val.append_null(); Action::Del } + OpType::MarkBegin(m) => { + self.val.append_value2(&m.name.clone().into(), actors); + self.val.append_value2(&m.expand.into(), actors); + self.val.append_value2(&m.value.clone(), actors); + Action::MarkBegin + } + OpType::MarkEnd(s) => { + self.val.append_value2(&(*s).into(), actors); + Action::MarkEnd + } OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1275,8 +1313,11 @@ pub(crate) enum Action { MakeText, Inc, MakeTable, + MarkBegin, + Unused, // final bit is used to mask `Make` actions + MarkEnd, } -const ACTIONS: [Action; 7] = [ +const ACTIONS: [Action; 10] = [ Action::MakeMap, Action::Set, Action::MakeList, @@ -1284,6 +1325,9 @@ const ACTIONS: [Action; 7] = [ Action::MakeText, Action::Inc, Action::MakeTable, + Action::MarkBegin, + Action::Unused, + Action::MarkEnd, ]; impl Decodable for Action { diff --git a/automerge/src/legacy/serde_impls/op.rs b/automerge/src/legacy/serde_impls/op.rs index 1d2a4125..b91ae7e8 100644 --- a/automerge/src/legacy/serde_impls/op.rs +++ b/automerge/src/legacy/serde_impls/op.rs @@ -49,6 +49,12 @@ impl Serialize for Op { match &self.action { OpType::Inc(n) => op.serialize_field("value", &n)?, OpType::Set(value) => op.serialize_field("value", &value)?, + OpType::MarkBegin(m) => { + op.serialize_field("name", &m.name)?; + op.serialize_field("expand", &m.expand)?; + op.serialize_field("value", &m.value)?; + } + OpType::MarkEnd(s) => op.serialize_field("expand", &s)?, _ => {} } op.serialize_field("pred", &self.pred)?; @@ -70,6 +76,8 @@ pub(crate) enum RawOpType { Del, Inc, Set, + MarkBegin, + MarkEnd, } impl Serialize for RawOpType { @@ -85,6 +93,8 @@ impl Serialize for RawOpType { RawOpType::Del => "del", RawOpType::Inc => "inc", RawOpType::Set => "set", + RawOpType::MarkBegin => "mark_begin", + RawOpType::MarkEnd => "mark_end", }; serializer.serialize_str(s) } @@ -116,6 +126,8 @@ impl<'de> Deserialize<'de> for RawOpType { "del" => Ok(RawOpType::Del), "inc" => Ok(RawOpType::Inc), "set" => Ok(RawOpType::Set), + "mark_begin" => Ok(RawOpType::MarkBegin), + "mark_end" => Ok(RawOpType::MarkEnd), other => Err(Error::unknown_variant(other, VARIANTS)), } } @@ -188,6 +200,30 @@ impl<'de> Deserialize<'de> for Op { RawOpType::MakeList => OpType::Make(ObjType::List), RawOpType::MakeText => OpType::Make(ObjType::Text), RawOpType::Del => OpType::Del, + RawOpType::MarkBegin => { + let name = name.ok_or_else(|| Error::missing_field("mark(name)"))?; + let expand = expand.unwrap_or(false); + 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::mark(name, expand, value) + } + RawOpType::MarkEnd => { + let expand = expand.unwrap_or(true); + OpType::MarkEnd(expand) + } RawOpType::Set => { let value = if let Some(datatype) = datatype { let raw_value = value diff --git a/automerge/src/legacy/serde_impls/op_type.rs b/automerge/src/legacy/serde_impls/op_type.rs index 19849674..0959b11d 100644 --- a/automerge/src/legacy/serde_impls/op_type.rs +++ b/automerge/src/legacy/serde_impls/op_type.rs @@ -15,6 +15,8 @@ impl Serialize for OpType { OpType::Make(ObjType::Table) => RawOpType::MakeTable, OpType::Make(ObjType::List) => RawOpType::MakeList, OpType::Make(ObjType::Text) => RawOpType::MakeText, + OpType::MarkBegin(_) => RawOpType::MarkBegin, + OpType::MarkEnd(_) => RawOpType::MarkEnd, OpType::Del => RawOpType::Del, OpType::Inc(_) => RawOpType::Inc, OpType::Set(_) => RawOpType::Set, diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 7911e1bb..ff97532e 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -17,6 +17,8 @@ mod nth_at; mod prop; mod prop_at; mod seek_op; +mod spans; +mod raw_spans; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -30,6 +32,8 @@ pub(crate) use nth_at::NthAt; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; pub(crate) use seek_op::SeekOp; +pub(crate) use spans::{Span, Spans}; +pub(crate) use raw_spans::RawSpans; #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { diff --git a/automerge/src/query/insert.rs b/automerge/src/query/insert.rs index 62da48f9..38a58e45 100644 --- a/automerge/src/query/insert.rs +++ b/automerge/src/query/insert.rs @@ -85,6 +85,10 @@ impl TreeQuery for InsertNth { self.last_seen = None; self.last_insert = element.elemid(); } + if self.valid.is_some() && element.valid_mark_anchor() { + self.last_valid_insert = element.elemid(); + self.valid = None; + } if self.last_seen.is_none() && element.visible() { if self.seen >= self.target { return QueryResult::Finish; diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs new file mode 100644 index 00000000..77a45741 --- /dev/null +++ b/automerge/src/query/raw_spans.rs @@ -0,0 +1,71 @@ +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 spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpan { + pub id: OpId, + pub change: usize, + pub start: usize, + pub end: usize, + pub name: String, + pub value: ScalarValue, +} + +impl RawSpans { + pub fn new() -> Self { + RawSpans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + changed: false, + spans: Vec::new(), + } + } +} + +impl TreeQuery 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, change: element.change, 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/automerge/src/query/spans.rs b/automerge/src/query/spans.rs new file mode 100644 index 00000000..589dba03 --- /dev/null +++ b/automerge/src/query/spans.rs @@ -0,0 +1,108 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpType, ScalarValue}; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Spans { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec, + marks: HashMap, + changed: bool, + pub spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Span { + pub pos: usize, + pub marks: Vec<(String, ScalarValue)>, +} + +impl Spans { + pub fn new() -> Self { + Spans { + pos: 0, + seen: 0, + 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 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.clone()); + } + } + 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(), val.clone())) + .collect(); + marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); + self.spans.push(Span { + pos: self.seen, + marks, + }); + } + } +} + +impl TreeQuery for Spans { + /* + fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { + unimplemented!() + } + */ + + 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(_) = &element.action { + let pos = self + .ops + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.ops.insert(pos, element.clone()); + } + 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(); + self.seen += 1; + self.last_seen = element.elemid(); + self.seen_at_this_mark = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 4494f6d9..54192908 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -158,6 +158,25 @@ pub enum OpType { Del, Inc(i64), Set(ScalarValue), + MarkBegin(MarkData), + MarkEnd(bool), +} + +impl OpType { + pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> Self { + OpType::MarkBegin(MarkData { + name, + expand, + value, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: String, + pub value: ScalarValue, + pub expand: bool, } #[derive(Debug)] @@ -180,6 +199,10 @@ impl OpId { pub fn actor(&self) -> usize { self.1 } + #[inline] + pub fn prev(&self) -> OpId { + OpId(self.0 - 1, self.1) + } } impl Exportable for ObjId { @@ -376,7 +399,7 @@ impl Op { } pub 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() @@ -401,6 +424,18 @@ impl Op { matches!(&self.action, OpType::Inc(_)) } + pub fn valid_mark_anchor(&self) -> bool { + self.succ.is_empty() + && matches!( + &self.action, + OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) + ) + } + + pub fn is_mark(&self) -> bool { + matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) + } + pub fn is_counter(&self) -> bool { matches!(&self.action, OpType::Set(ScalarValue::Counter(_))) } @@ -435,6 +470,8 @@ impl Op { OpType::Set(value) if self.insert => format!("i:{}", value), OpType::Set(value) => format!("s:{}", value), OpType::Make(obj) => format!("make{}", obj), + OpType::MarkBegin(m) => format!("mark{}={}", m.name, m.value), + OpType::MarkEnd(_) => "unmark".into(), OpType::Inc(val) => format!("inc:{}", val), OpType::Del => "del".to_string(), } diff --git a/automerge/tests/test.rs b/automerge/tests/test.rs index 03d5a5d2..d8637283 100644 --- a/automerge/tests/test.rs +++ b/automerge/tests/test.rs @@ -54,10 +54,10 @@ fn repeated_map_assignment_which_resolves_conflict_not_ignored() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "field", 123).unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&automerge::ROOT, "field", 456).unwrap(); doc1.set(&automerge::ROOT, "field", 789).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_eq!(doc1.values(&automerge::ROOT, "field").unwrap().len(), 2); doc1.set(&automerge::ROOT, "field", 123).unwrap(); @@ -78,9 +78,9 @@ fn repeated_list_assignment_which_resolves_conflict_not_ignored() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, 123).unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&list_id, 0, 456).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc1.set(&list_id, 0, 789).unwrap(); assert_doc!( @@ -123,7 +123,7 @@ fn merge_concurrent_map_prop_updates() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "foo", "bar").unwrap(); doc2.set(&automerge::ROOT, "hello", "world").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_eq!( doc1.value(&automerge::ROOT, "foo").unwrap().unwrap().0, "bar".into() @@ -135,7 +135,7 @@ fn merge_concurrent_map_prop_updates() { "hello" => { "world" }, } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -152,10 +152,10 @@ fn add_concurrent_increments_of_same_property() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "counter", mk_counter(0)) .unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.inc(&automerge::ROOT, "counter", 1).unwrap(); doc2.inc(&automerge::ROOT, "counter", 2).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, map! { @@ -181,7 +181,7 @@ fn add_increments_only_to_preceeded_values() { doc2.inc(&automerge::ROOT, "counter", 3).unwrap(); // The two values should be conflicting rather than added - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -201,7 +201,7 @@ fn concurrent_updates_of_same_field() { doc1.set(&automerge::ROOT, "field", "one").unwrap(); doc2.set(&automerge::ROOT, "field", "two").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -223,11 +223,11 @@ fn concurrent_updates_of_same_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "finch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&list_id, 0, "greenfinch").unwrap(); doc2.set(&list_id, 0, "goldfinch").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -252,8 +252,8 @@ fn assignment_conflicts_of_different_types() { .unwrap(); doc3.set(&automerge::ROOT, "field", automerge::Value::map()) .unwrap(); - doc1.merge(&mut doc2).unwrap(); - doc1.merge(&mut doc3).unwrap(); + doc1.merge(&mut doc2); + doc1.merge(&mut doc3); assert_doc!( &doc1, @@ -277,7 +277,7 @@ fn changes_within_conflicting_map_field() { .unwrap() .unwrap(); doc2.set(&map_id, "innerKey", 42).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -304,7 +304,7 @@ fn changes_within_conflicting_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "hello").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); let map_in_doc1 = doc1 .set(&list_id, 0, automerge::Value::map()) @@ -317,11 +317,11 @@ fn changes_within_conflicting_list_element() { .set(&list_id, 0, automerge::Value::map()) .unwrap() .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc2.set(&map_in_doc2, "map2", true).unwrap(); doc2.set(&map_in_doc2, "key", 2).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -361,7 +361,7 @@ fn concurrently_assigned_nested_maps_should_not_merge() { .unwrap(); doc2.set(&doc2_map_id, "logo_url", "logo.png").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -392,11 +392,11 @@ fn concurrent_insertions_at_different_list_positions() { doc1.insert(&list_id, 0, "one").unwrap(); doc1.insert(&list_id, 1, "three").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice(&list_id, 1, 0, vec!["two".into()]).unwrap(); doc2.insert(&list_id, 2, "four").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -426,10 +426,10 @@ fn concurrent_insertions_at_same_list_position() { .unwrap(); doc1.insert(&list_id, 0, "parakeet").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.insert(&list_id, 1, "starling").unwrap(); doc2.insert(&list_id, 1, "chaffinch").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -456,11 +456,11 @@ fn concurrent_assignment_and_deletion_of_a_map_entry() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "bestBird", "robin").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&automerge::ROOT, "bestBird").unwrap(); doc2.set(&automerge::ROOT, "bestBird", "magpie").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -483,7 +483,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { doc1.insert(&list_id, 0, "blackbird").unwrap(); doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&list_id, 1, "starling").unwrap(); doc2.del(&list_id, 1).unwrap(); @@ -508,7 +508,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { } ); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -535,14 +535,14 @@ fn insertion_after_a_deleted_list_element() { doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice(&list_id, 1, 2, Vec::new()).unwrap(); doc2.splice(&list_id, 2, 0, vec!["starling".into()]) .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -554,7 +554,7 @@ fn insertion_after_a_deleted_list_element() { } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -579,13 +579,13 @@ fn concurrent_deletion_of_same_list_element() { doc1.insert(&list_id, 1, "buzzard").unwrap(); doc1.insert(&list_id, 2, "cormorant").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&list_id, 1).unwrap(); doc2.del(&list_id, 1).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -597,7 +597,7 @@ fn concurrent_deletion_of_same_list_element() { } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -631,12 +631,12 @@ fn concurrent_updates_at_different_levels() { .unwrap(); doc1.insert(&mammals, 0, "badger").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&birds, "brown", "sparrow").unwrap(); doc2.del(&animals, "birds").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_obj!( &doc1, @@ -676,13 +676,13 @@ fn concurrent_updates_of_concurrently_deleted_objects() { .unwrap(); doc1.set(&blackbird, "feathers", "black").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&birds, "blackbird").unwrap(); doc2.set(&blackbird, "beak", "orange").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -704,7 +704,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { .set(&automerge::ROOT, "wisdom", automerge::Value::list()) .unwrap() .unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice( &wisdom, @@ -734,7 +734,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { ) .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -767,7 +767,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_greater_actor_id( .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -793,7 +793,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_lesser_actor_id() .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -817,11 +817,11 @@ fn insertion_consistent_with_causality() { .unwrap() .unwrap(); doc1.insert(&list, 0, "four").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "three").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -861,11 +861,11 @@ fn save_restore_complex() { doc1.set(&first_todo, "done", false).unwrap(); let mut doc2 = new_doc(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&first_todo, "title", "weed plants").unwrap(); doc1.set(&first_todo, "title", "kill plants").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); let reloaded = Automerge::load(&doc1.save().unwrap()).unwrap(); @@ -918,8 +918,8 @@ fn list_counter_del() -> Result<(), automerge::AutomergeError> { doc1.inc(&list, 1, 1)?; doc1.inc(&list, 2, 1)?; - doc1.merge(&mut doc2).unwrap(); - doc1.merge(&mut doc3).unwrap(); + doc1.merge(&mut doc2); + doc1.merge(&mut doc3); let values = doc1.values(&list, 1)?; assert_eq!(values.len(), 3); From 2ba2da95a8b03d473036d697c2102f1ed2c0f000 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 14:41:57 -0500 Subject: [PATCH 02/15] attempt at new packaging --- automerge-wasm/.gitignore | 2 + automerge-wasm/Cargo.toml | 6 +- automerge-wasm/README.md | 689 ----------------------------------- automerge-wasm/package.json | 24 +- automerge-wasm/test/marks.ts | 2 +- automerge-wasm/test/test.ts | 6 +- 6 files changed, 23 insertions(+), 706 deletions(-) diff --git a/automerge-wasm/.gitignore b/automerge-wasm/.gitignore index a5ef445c..90f5b649 100644 --- a/automerge-wasm/.gitignore +++ b/automerge-wasm/.gitignore @@ -1,5 +1,7 @@ /node_modules /dev +/node +/web /target Cargo.lock yarn.lock diff --git a/automerge-wasm/Cargo.toml b/automerge-wasm/Cargo.toml index 8225d811..5bc82cd7 100644 --- a/automerge-wasm/Cargo.toml +++ b/automerge-wasm/Cargo.toml @@ -3,7 +3,7 @@ name = "automerge-wasm" description = "An js/wasm wrapper for the rust implementation of automerge-backend" # repository = "https://github.com/automerge/automerge-rs" -version = "0.1.0" +version = "0.0.4" authors = ["Alex Good ","Orion Henry ", "Martin Kleppmann"] categories = ["wasm"] readme = "README.md" @@ -40,10 +40,10 @@ version = "^0.2" features = ["serde-serialize", "std"] [package.metadata.wasm-pack.profile.release] -# wasm-opt = false +wasm-opt = true [package.metadata.wasm-pack.profile.profiling] -wasm-opt = false +wasm-opt = true # The `web-sys` crate allows you to interact with the various browser APIs, # like the DOM. diff --git a/automerge-wasm/README.md b/automerge-wasm/README.md index 985955a3..bee84123 100644 --- a/automerge-wasm/README.md +++ b/automerge-wasm/README.md @@ -2,692 +2,3 @@ This is a low level automerge library written in rust exporting a javascript API via WASM. This low level api is the underpinning to the `automerge-js` library that reimplements the Automerge API via these low level functions. -### Static Functions - -### Methods - - `doc.clone(actor?: string)` : Make a complete - - `doc.free()` : deallocate WASM memory associated with a document - -#[wasm_bindgen] - pub fn free(self) {} - - #[wasm_bindgen(js_name = pendingOps)] - pub fn pending_ops(&self) -> JsValue { - (self.0.pending_ops() as u32).into() - } - - pub fn commit(&mut self, message: Option, time: Option) -> Array { - let heads = self.0.commit(message, time.map(|n| n as i64)); - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - heads - } - - pub fn rollback(&mut self) -> f64 { - self.0.rollback() as f64 - } - - pub fn keys(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - let result = if let Some(heads) = get_heads(heads) { - self.0.keys_at(&obj, &heads) - } else { - self.0.keys(&obj) - } - .iter() - .map(|s| JsValue::from_str(s)) - .collect(); - Ok(result) - } - - pub fn text(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - if let Some(heads) = get_heads(heads) { - self.0.text_at(&obj, &heads) - } else { - self.0.text(&obj) - } - .map_err(to_js_err) - } - - pub fn splice( - &mut self, - obj: String, - start: f64, - delete_count: f64, - text: JsValue, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let start = start as usize; - let delete_count = delete_count as usize; - let mut vals = vec![]; - if let Some(t) = text.as_string() { - self.0 - .splice_text(&obj, start, delete_count, &t) - .map_err(to_js_err)?; - Ok(None) - } else { - if let Ok(array) = text.dyn_into::() { - for i in array.iter() { - if let Ok(array) = i.clone().dyn_into::() { - let value = array.get(1); - let datatype = array.get(2); - let value = self.import_value(value, datatype.as_string())?; - vals.push(value); - } else { - let value = self.import_value(i, None)?; - vals.push(value); - } - } - } - let result = self - .0 - .splice(&obj, start, delete_count, vals) - .map_err(to_js_err)?; - if result.is_empty() { - Ok(None) - } else { - let result: Array = result - .iter() - .map(|r| JsValue::from(r.to_string())) - .collect(); - Ok(result.into()) - } - } - } - - pub fn push( - &mut self, - obj: String, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let value = self.import_value(value, datatype)?; - let index = self.0.length(&obj); - let opid = self.0.insert(&obj, index, value).map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn insert( - &mut self, - obj: String, - index: f64, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let index = index as f64; - let value = self.import_value(value, datatype)?; - let opid = self - .0 - .insert(&obj, index as usize, value) - .map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn set( - &mut self, - obj: String, - prop: JsValue, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let prop = self.import_prop(prop)?; - let value = self.import_value(value, datatype)?; - let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn make( - &mut self, - obj: String, - prop: JsValue, - value: JsValue, - ) -> Result { - let obj = self.import(obj)?; - let prop = self.import_prop(prop)?; - let value = self.import_value(value, None)?; - if value.is_object() { - let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?; - Ok(opid.unwrap().to_string()) - } else { - Err("invalid object type".into()) - } - } - - pub fn inc(&mut self, obj: String, prop: JsValue, value: JsValue) -> Result<(), JsValue> { - let obj = self.import(obj)?; - let prop = self.import_prop(prop)?; - let value: f64 = value - .as_f64() - .ok_or("inc needs a numberic value") - .map_err(to_js_err)?; - self.0.inc(&obj, prop, value as i64).map_err(to_js_err)?; - Ok(()) - } - - pub fn value( - &mut self, - obj: String, - prop: JsValue, - heads: Option, - ) -> Result { - let obj = self.import(obj)?; - let result = Array::new(); - let prop = to_prop(prop); - let heads = get_heads(heads); - if let Ok(prop) = prop { - let value = if let Some(h) = heads { - self.0.value_at(&obj, prop, &h) - } else { - self.0.value(&obj, prop) - } - .map_err(to_js_err)?; - match value { - Some((Value::Object(obj_type), obj_id)) => { - result.push(&obj_type.to_string().into()); - result.push(&obj_id.to_string().into()); - } - Some((Value::Scalar(value), _)) => { - result.push(&datatype(&value).into()); - result.push(&ScalarValue(value).into()); - } - None => {} - } - } - Ok(result) - } - - pub fn values( - &mut self, - obj: String, - arg: JsValue, - heads: Option, - ) -> Result { - let obj = self.import(obj)?; - let result = Array::new(); - let prop = to_prop(arg); - if let Ok(prop) = prop { - let values = if let Some(heads) = get_heads(heads) { - self.0.values_at(&obj, prop, &heads) - } else { - self.0.values(&obj, prop) - } - .map_err(to_js_err)?; - for value in values { - match value { - (Value::Object(obj_type), obj_id) => { - let sub = Array::new(); - sub.push(&obj_type.to_string().into()); - sub.push(&obj_id.to_string().into()); - result.push(&sub.into()); - } - (Value::Scalar(value), id) => { - let sub = Array::new(); - sub.push(&datatype(&value).into()); - sub.push(&ScalarValue(value).into()); - sub.push(&id.to_string().into()); - result.push(&sub.into()); - } - } - } - } - Ok(result) - } - - pub fn length(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - if let Some(heads) = get_heads(heads) { - Ok(self.0.length_at(&obj, &heads) as f64) - } else { - Ok(self.0.length(&obj) as f64) - } - } - - pub fn del(&mut self, obj: String, prop: JsValue) -> Result<(), JsValue> { - let obj = self.import(obj)?; - let prop = to_prop(prop)?; - self.0.del(&obj, prop).map_err(to_js_err)?; - 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())?; - self.0 - .mark(&obj, start, start_sticky, end, end_sticky, &name, value) - .map_err(to_js_err)?; - Ok(()) - } - - pub fn spans(&mut self, obj: JsValue) -> Result { - let obj = self.import(obj)?; - let text = self.0.text(&obj).map_err(to_js_err)?; - let spans = self.0.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()); - mark.push(&datatype(&m.1).into()); - mark.push(&ScalarValue(m.1).into()); - marks.push(&mark.into()); - } - let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); - if text_span.len() > 0 { - result.push(&text_span.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[last_pos..]; - if text_span.len() > 0 { - result.push(&text_span.into()); - } - Ok(result.into()) - } - - pub fn save(&mut self) -> Result { - self.0 - .save() - .map(|v| Uint8Array::from(v.as_slice())) - .map_err(to_js_err) - } - - #[wasm_bindgen(js_name = saveIncremental)] - pub fn save_incremental(&mut self) -> Uint8Array { - let bytes = self.0.save_incremental(); - Uint8Array::from(bytes.as_slice()) - } - - #[wasm_bindgen(js_name = loadIncremental)] - pub fn load_incremental(&mut self, data: Uint8Array) -> Result { - let data = data.to_vec(); - let len = self.0.load_incremental(&data).map_err(to_js_err)?; - Ok(len as f64) - } - - #[wasm_bindgen(js_name = applyChanges)] - pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> { - let changes: Vec<_> = JS(changes).try_into()?; - self.0.apply_changes(&changes).map_err(to_js_err)?; - Ok(()) - } - - #[wasm_bindgen(js_name = getChanges)] - pub fn get_changes(&mut self, have_deps: JsValue) -> Result { - let deps: Vec<_> = JS(have_deps).try_into()?; - let changes = self.0.get_changes(&deps); - let changes: Array = changes - .iter() - .map(|c| Uint8Array::from(c.raw_bytes())) - .collect(); - Ok(changes) - } - - #[wasm_bindgen(js_name = getChangesAdded)] - pub fn get_changes_added(&mut self, other: &Automerge) -> Result { - let changes = self.0.get_changes_added(&other.0); - let changes: Array = changes - .iter() - .map(|c| Uint8Array::from(c.raw_bytes())) - .collect(); - Ok(changes) - } - - #[wasm_bindgen(js_name = getHeads)] - pub fn get_heads(&mut self) -> Array { - let heads = self.0.get_heads(); - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - heads - } - - #[wasm_bindgen(js_name = getActorId)] - pub fn get_actor_id(&mut self) -> String { - let actor = self.0.get_actor(); - actor.to_string() - } - - #[wasm_bindgen(js_name = getLastLocalChange)] - pub fn get_last_local_change(&mut self) -> Result, JsValue> { - if let Some(change) = self.0.get_last_local_change() { - Ok(Some(Uint8Array::from(change.raw_bytes()))) - } else { - Ok(None) - } - } - - pub fn dump(&self) { - self.0.dump() - } - - #[wasm_bindgen(js_name = getMissingDeps)] - pub fn get_missing_deps(&mut self, heads: Option) -> Result { - let heads = get_heads(heads).unwrap_or_default(); - let deps = self.0.get_missing_deps(&heads); - let deps: Array = deps - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - Ok(deps) - } - - #[wasm_bindgen(js_name = receiveSyncMessage)] - pub fn receive_sync_message( - &mut self, - state: &mut SyncState, - message: Uint8Array, - ) -> Result<(), JsValue> { - let message = message.to_vec(); - let message = am::SyncMessage::decode(message.as_slice()).map_err(to_js_err)?; - self.0 - .receive_sync_message(&mut state.0, message) - .map_err(to_js_err)?; - Ok(()) - } - - #[wasm_bindgen(js_name = generateSyncMessage)] - pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result { - if let Some(message) = self.0.generate_sync_message(&mut state.0) { - Ok(Uint8Array::from(message.encode().map_err(to_js_err)?.as_slice()).into()) - } else { - Ok(JsValue::null()) - } - } - - #[wasm_bindgen(js_name = toJS)] - pub fn to_js(&self) -> JsValue { - map_to_js(&self.0, &ROOT) - } - - fn import(&self, id: String) -> Result { - self.0.import(&id).map_err(to_js_err) - } - - fn import_prop(&mut self, prop: JsValue) -> Result { - if let Some(s) = prop.as_string() { - Ok(s.into()) - } else if let Some(n) = prop.as_f64() { - Ok((n as usize).into()) - } else { - Err(format!("invalid prop {:?}", prop).into()) - } - } - - fn import_scalar( - &mut self, - value: &JsValue, - datatype: Option, - ) -> Result { - match datatype.as_deref() { - Some("boolean") => value - .as_bool() - .ok_or_else(|| "value must be a bool".into()) - .map(am::ScalarValue::Boolean), - Some("int") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Int(v as i64)), - Some("uint") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Uint(v as u64)), - Some("f64") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(am::ScalarValue::F64), - Some("bytes") => Ok(am::ScalarValue::Bytes( - value.clone().dyn_into::().unwrap().to_vec(), - )), - Some("counter") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::counter(v as i64)), - Some("timestamp") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Timestamp(v as i64)), - /* - Some("bytes") => unimplemented!(), - Some("cursor") => unimplemented!(), - */ - Some("null") => Ok(am::ScalarValue::Null), - Some(_) => Err(format!("unknown datatype {:?}", datatype).into()), - None => { - if value.is_null() { - Ok(am::ScalarValue::Null) - } else if let Some(b) = value.as_bool() { - Ok(am::ScalarValue::Boolean(b)) - } else if let Some(s) = value.as_string() { - // FIXME - we need to detect str vs int vs float vs bool here :/ - Ok(am::ScalarValue::Str(s.into())) - } else if let Some(n) = value.as_f64() { - if (n.round() - n).abs() < f64::EPSILON { - Ok(am::ScalarValue::Int(n as i64)) - } else { - Ok(am::ScalarValue::F64(n)) - } - // } else if let Some(o) = to_objtype(&value) { - // Ok(o.into()) - } else if let Ok(d) = value.clone().dyn_into::() { - Ok(am::ScalarValue::Timestamp(d.get_time() as i64)) - } else if let Ok(o) = &value.clone().dyn_into::() { - Ok(am::ScalarValue::Bytes(o.to_vec())) - } else { - Err("value is invalid".into()) - } - } - } - } - - fn import_value(&mut self, value: JsValue, datatype: Option) -> Result { - match self.import_scalar(&value, datatype) { - Ok(val) => Ok(val.into()), - Err(err) => { - if let Some(o) = to_objtype(&value) { - Ok(o.into()) - } else { - Err(err) - } - } - } - /* - match datatype.as_deref() { - Some("boolean") => value - .as_bool() - .ok_or_else(|| "value must be a bool".into()) - .map(|v| am::ScalarValue::Boolean(v).into()), - Some("int") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Int(v as i64).into()), - Some("uint") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Uint(v as u64).into()), - Some("f64") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|n| am::ScalarValue::F64(n).into()), - Some("bytes") => { - Ok(am::ScalarValue::Bytes(value.dyn_into::().unwrap().to_vec()).into()) - } - Some("counter") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::counter(v as i64).into()), - Some("timestamp") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Timestamp(v as i64).into()), - Some("null") => Ok(am::ScalarValue::Null.into()), - Some(_) => Err(format!("unknown datatype {:?}", datatype).into()), - None => { - if value.is_null() { - Ok(am::ScalarValue::Null.into()) - } else if let Some(b) = value.as_bool() { - Ok(am::ScalarValue::Boolean(b).into()) - } else if let Some(s) = value.as_string() { - // FIXME - we need to detect str vs int vs float vs bool here :/ - Ok(am::ScalarValue::Str(s.into()).into()) - } else if let Some(n) = value.as_f64() { - if (n.round() - n).abs() < f64::EPSILON { - Ok(am::ScalarValue::Int(n as i64).into()) - } else { - Ok(am::ScalarValue::F64(n).into()) - } - } else if let Some(o) = to_objtype(&value) { - Ok(o.into()) - } else if let Ok(d) = value.clone().dyn_into::() { - Ok(am::ScalarValue::Timestamp(d.get_time() as i64).into()) - } else if let Ok(o) = &value.dyn_into::() { - Ok(am::ScalarValue::Bytes(o.to_vec()).into()) - } else { - Err("value is invalid".into()) - } - } - } - */ - } -} - -#[wasm_bindgen(js_name = create)] -pub fn init(actor: Option) -> Result { - console_error_panic_hook::set_once(); - Automerge::new(actor) -} - -#[wasm_bindgen(js_name = loadDoc)] -pub fn load(data: Uint8Array, actor: Option) -> Result { - let data = data.to_vec(); - let mut automerge = am::Automerge::load(&data).map_err(to_js_err)?; - if let Some(s) = actor { - let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); - automerge.set_actor(actor) - } - Ok(Automerge(automerge)) -} - -#[wasm_bindgen(js_name = encodeChange)] -pub fn encode_change(change: JsValue) -> Result { - let change: am::ExpandedChange = change.into_serde().map_err(to_js_err)?; - let change: Change = change.into(); - Ok(Uint8Array::from(change.raw_bytes())) -} - -#[wasm_bindgen(js_name = decodeChange)] -pub fn decode_change(change: Uint8Array) -> Result { - let change = Change::from_bytes(change.to_vec()).map_err(to_js_err)?; - let change: am::ExpandedChange = change.decode(); - JsValue::from_serde(&change).map_err(to_js_err) -} - -#[wasm_bindgen(js_name = initSyncState)] -pub fn init_sync_state() -> SyncState { - SyncState(am::SyncState::new()) -} - -// this is needed to be compatible with the automerge-js api -#[wasm_bindgen(js_name = importSyncState)] -pub fn import_sync_state(state: JsValue) -> Result { - Ok(SyncState(JS(state).try_into()?)) -} - -// this is needed to be compatible with the automerge-js api -#[wasm_bindgen(js_name = exportSyncState)] -pub fn export_sync_state(state: SyncState) -> JsValue { - JS::from(state.0).into() -} - -#[wasm_bindgen(js_name = encodeSyncMessage)] -pub fn encode_sync_message(message: JsValue) -> Result { - let heads = js_get(&message, "heads")?.try_into()?; - let need = js_get(&message, "need")?.try_into()?; - let changes = js_get(&message, "changes")?.try_into()?; - let have = js_get(&message, "have")?.try_into()?; - Ok(Uint8Array::from( - am::SyncMessage { - heads, - need, - have, - changes, - } - .encode() - .unwrap() - .as_slice(), - )) -} - -#[wasm_bindgen(js_name = decodeSyncMessage)] -pub fn decode_sync_message(msg: Uint8Array) -> Result { - let data = msg.to_vec(); - let msg = am::SyncMessage::decode(&data).map_err(to_js_err)?; - let heads = AR::from(msg.heads.as_slice()); - let need = AR::from(msg.need.as_slice()); - let changes = AR::from(msg.changes.as_slice()); - let have = AR::from(msg.have.as_slice()); - let obj = Object::new().into(); - js_set(&obj, "heads", heads)?; - js_set(&obj, "need", need)?; - js_set(&obj, "have", have)?; - js_set(&obj, "changes", changes)?; - Ok(obj) -} - -#[wasm_bindgen(js_name = encodeSyncState)] -pub fn encode_sync_state(state: SyncState) -> Result { - let state = state.0; - Ok(Uint8Array::from( - state.encode().map_err(to_js_err)?.as_slice(), - )) -} - -#[wasm_bindgen(js_name = decodeSyncState)] -pub fn decode_sync_state(data: Uint8Array) -> Result { - SyncState::decode(data) -} - -#[wasm_bindgen(js_name = MAP)] -pub struct Map {} - -#[wasm_bindgen(js_name = LIST)] -pub struct List {} - -#[wasm_bindgen(js_name = TEXT)] -pub struct Text {} - -#[wasm_bindgen(js_name = TABLE)] -pub struct Table {} diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index c39299eb..9fa9bbf2 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -4,25 +4,29 @@ "Alex Good ", "Martin Kleppmann" ], - "name": "automerge-wasm", + "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.1", + "version": "0.0.5", "license": "MIT", "files": [ "README.md", "LICENSE", "package.json", - "automerge_wasm_bg.wasm", + "types.d.ts", + "node/index.js", + "node/index_bg.wasm", + "web/index.js", + "web/index_bg.wasm", "automerge_wasm.js" ], - "module": "./pkg/index.js", - "main": "./dev/index.js", + "types": "index.d.ts", + "module": "./web/index.js", + "main": "./node/index.js", "scripts": { - "build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev && cp index.d.ts dev", - "release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && yarn opt && cp index.d.ts dev", - "pkg": "rimraf ./pkg && wasm-pack build --target web --release --out-name index -d pkg && cp index.d.ts pkg && cd pkg && yarn pack && mv automerge-wasm*tgz ..", - "prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev", - "opt": "wasm-opt -Oz dev/index_bg.wasm -o tmp.wasm && mv tmp.wasm dev/index_bg.wasm", + "build": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", + "release-w": "rimraf ./web && wasm-pack build --target web --release --out-name index -d web && cp index.d.ts web", + "release-n": "rimraf ./node && wasm-pack build --target nodejs --release --out-name index -d node && cp index.d.ts node", + "release": "yarn release-w && yarn release-n", "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, "dependencies": {}, diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 61951056..ab08636f 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -2,7 +2,7 @@ import { describe, it } from 'mocha'; //@ts-ignore import assert from 'assert' //@ts-ignore -import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '../dev/index' +import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '..' describe('Automerge', () => { describe('marks', () => { diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index f72d0979..51e50789 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,9 +3,9 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index' -import { DecodedSyncMessage } from '../index'; -import { Hash } from '../dev/index'; +import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage } from '..'; +import { Hash } from '..'; function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) { const MAX_ITER = 10 From c8cd069e51bf655d022aab1cb452194b6f90fd0e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 19:15:02 -0500 Subject: [PATCH 03/15] tweak files --- automerge-wasm/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 9fa9bbf2..e74cf803 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -10,14 +10,12 @@ "license": "MIT", "files": [ "README.md", - "LICENSE", "package.json", - "types.d.ts", + "index.d.ts", "node/index.js", "node/index_bg.wasm", "web/index.js", - "web/index_bg.wasm", - "automerge_wasm.js" + "web/index_bg.wasm" ], "types": "index.d.ts", "module": "./web/index.js", From ea2f29d681edfeebff7ec13190bd0e3a8024fb41 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 21:08:28 -0500 Subject: [PATCH 04/15] wasm to 0.0.6 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index e74cf803..6d487f51 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "files": [ "README.md", From 015e8ce465cba4c8369f5995e6b1093f77caaf64 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 15 Feb 2022 14:40:40 -0500 Subject: [PATCH 05/15] choking on bad value function --- automerge-wasm/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index b2f8e277..3ea5dff6 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -11,7 +11,9 @@ mod interop; mod sync; mod value; -use interop::{get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS}; +use interop::{ + get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, +}; use sync::SyncState; use value::{datatype, ScalarValue}; From 36b4f08d202f4e57b89e16dd57d784cf9a4a2c0c Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 21:08:28 -0500 Subject: [PATCH 06/15] wasm to 0.0.7 --- automerge-wasm/package.json | 6 +++--- automerge-wasm/src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 6d487f51..1a135c50 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.6", + "version": "0.0.8", "license": "MIT", "files": [ "README.md", @@ -22,8 +22,8 @@ "main": "./node/index.js", "scripts": { "build": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", - "release-w": "rimraf ./web && wasm-pack build --target web --release --out-name index -d web && cp index.d.ts web", - "release-n": "rimraf ./node && wasm-pack build --target nodejs --release --out-name index -d node && cp index.d.ts node", + "release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", + "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", "release": "yarn release-w && yarn release-n", "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 3ea5dff6..2c61368d 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -310,7 +310,7 @@ impl Automerge { pub fn mark( &mut self, - obj: String, + obj: JsValue, range: JsValue, name: JsValue, value: JsValue, @@ -337,7 +337,7 @@ impl Automerge { Ok(()) } - pub fn spans(&mut self, obj: String) -> Result { + pub fn spans(&mut self, obj: JsValue) -> Result { let obj = self.import(obj)?; let text = self.0.text(&obj).map_err(to_js_err)?; let spans = self.0.spans(&obj).map_err(to_js_err)?; @@ -370,7 +370,7 @@ impl Automerge { Ok(result.into()) } - pub fn raw_spans(&mut self, obj: String) -> Result { + pub fn raw_spans(&mut self, obj: JsValue) -> Result { let obj = self.import(obj)?; let spans = self.0.raw_spans(&obj).map_err(to_js_err)?; let result = Array::new(); From 4f9b95b5b83b702c66c7f04dd81913ac6cc3ecb5 Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Wed, 23 Feb 2022 10:11:43 -0800 Subject: [PATCH 07/15] add test for merge behaviour of marks --- automerge-wasm/test/marks.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index ab08636f..9597e9f4 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -55,6 +55,23 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); }) + it('should handle sticky marks at the beginning of a string (..)', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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('should handle sticky marks with deleted ends (..)', () => { let doc = create() let list = doc.set("_root", "list", TEXT) From 5eb5714c131bb539571e838247ca4353c889172a Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Wed, 23 Feb 2022 21:30:47 -0800 Subject: [PATCH 08/15] add failing test for marks handling in 3-way merge scenario --- automerge-wasm/test/marks.ts | 74 +++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 9597e9f4..8064f953 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -20,6 +20,63 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); }) + it('should handle marks [..] at the beginning of a string', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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('should handle marks [..] with splice', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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('should handle marks across multiple forks', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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('should handle marks with deleted ends [..]', () => { let doc = create() let list = doc.set("_root", "list", TEXT) @@ -55,23 +112,6 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); }) - it('should handle sticky marks at the beginning of a string (..)', () => { - let doc = create() - let list = doc.set("_root", "list", TEXT) - if (!list) throw new Error('should not be undefined') - 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('should handle sticky marks with deleted ends (..)', () => { let doc = create() let list = doc.set("_root", "list", TEXT) From a37d4a6870f6b2ed50c359a67d54b116f38dc640 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 16:41:01 -0500 Subject: [PATCH 09/15] spans will now respect non-graphmem values --- automerge-wasm/package.json | 2 +- automerge-wasm/src/lib.rs | 18 ++++++++----- automerge/src/automerge.rs | 45 ++++++++++++++++++++++++++------ automerge/src/query.rs | 4 +-- automerge/src/query/raw_spans.rs | 21 ++++++++++----- automerge/src/value.rs | 14 ++++++++++ 6 files changed, 81 insertions(+), 23 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 1a135c50..30e0690a 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -25,7 +25,7 @@ "release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", "release": "yarn release-w && yarn release-n", - "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" + "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/marks.ts" }, "dependencies": {}, "devDependencies": { diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 2c61368d..7d886788 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -11,9 +11,7 @@ mod interop; mod sync; mod value; -use interop::{ - get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, -}; +use interop::{get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS}; use sync::SyncState; use value::{datatype, ScalarValue}; @@ -339,7 +337,7 @@ impl Automerge { pub fn spans(&mut self, obj: JsValue) -> Result { let obj = self.import(obj)?; - let text = self.0.text(&obj).map_err(to_js_err)?; + let text = self.0.list(&obj).map_err(to_js_err)?; let spans = self.0.spans(&obj).map_err(to_js_err)?; let mut last_pos = 0; let result = Array::new(); @@ -354,7 +352,11 @@ impl Automerge { } let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); if !text_span.is_empty() { - result.push(&text_span.into()); + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); } result.push(&marks); last_pos = s.pos; @@ -365,7 +367,11 @@ impl Automerge { } let text_span = &text[last_pos..]; if !text_span.is_empty() { - result.push(&text_span.into()); + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); } Ok(result.into()) } diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 122fa884..b0f84d5f 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -453,6 +453,31 @@ impl Automerge { Ok(buffer) } + pub fn list(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let query = self.ops.search(obj, query::ListVals::new()); + Ok(query + .ops + .iter() + .map(|o| (o.value(), self.id_to_exid(o.id))) + .collect()) + } + + pub fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let clock = self.clock_at(heads); + let query = self.ops.search(obj, query::ListValsAt::new(clock)); + Ok(query + .ops + .iter() + .map(|o| (o.value(), self.id_to_exid(o.id))) + .collect()) + } + pub fn spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let mut query = self.ops.search(obj, query::Spans::new()); @@ -463,14 +488,18 @@ impl Automerge { pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); - let result = query.spans.into_iter().map(|s| SpanInfo { - id: self.id_to_exid(s.id), - time: self.history[s.change].time, - start: s.start, - end: s.end, - span_type: s.name, - value: s.value, - }).collect(); + let result = query + .spans + .into_iter() + .map(|s| SpanInfo { + id: self.id_to_exid(s.id), + time: self.history[s.change].time, + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }) + .collect(); Ok(result) } diff --git a/automerge/src/query.rs b/automerge/src/query.rs index ff97532e..e630d080 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -16,9 +16,9 @@ mod nth; mod nth_at; mod prop; mod prop_at; +mod raw_spans; mod seek_op; mod spans; -mod raw_spans; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -31,9 +31,9 @@ pub(crate) use nth::Nth; pub(crate) use nth_at::NthAt; 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 spans::{Span, Spans}; -pub(crate) use raw_spans::RawSpans; #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs index 77a45741..95aafc56 100644 --- a/automerge/src/query/raw_spans.rs +++ b/automerge/src/query/raw_spans.rs @@ -36,7 +36,6 @@ impl RawSpans { } impl TreeQuery for RawSpans { - fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { // find location to insert // mark or set @@ -46,14 +45,24 @@ impl TreeQuery for RawSpans { .spans .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) .unwrap_err(); - self.spans.insert(pos, RawSpan { id: element.id, change: element.change, start: self.seen, end: 0, name: md.name.clone(), value: md.value.clone() }); + self.spans.insert( + pos, + RawSpan { + id: element.id, + change: element.change, + 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 s.id == element.id.prev() { + s.end = self.seen; + break; + } } } } diff --git a/automerge/src/value.rs b/automerge/src/value.rs index ac26033c..16364f34 100644 --- a/automerge/src/value.rs +++ b/automerge/src/value.rs @@ -18,6 +18,13 @@ impl Value { } } + pub fn as_string(&self) -> Option { + match self { + Value::Scalar(val) => val.as_string(), + _ => None, + } + } + pub fn map() -> Value { Value::Object(ObjType::Map) } @@ -394,6 +401,13 @@ impl ScalarValue { } } + pub fn as_string(&self) -> Option { + match self { + ScalarValue::Str(s) => Some(s.to_string()), + _ => None, + } + } + pub fn counter(n: i64) -> ScalarValue { ScalarValue::Counter(n.into()) } From a84fa6455428e725ab03be40299f274552e8da2e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 23 Feb 2022 19:43:13 -0500 Subject: [PATCH 10/15] change MAP,LIST,TEXT to be {},[],'' - allow recursion --- automerge-js/src/proxies.js | 25 ++++++----- automerge-wasm/index.d.ts | 17 +++----- automerge-wasm/src/interop.rs | 72 +++++++++++++++++++++++-------- automerge-wasm/src/lib.rs | 80 +++++++++++++++++++++-------------- automerge-wasm/test/test.ts | 54 ++++++++++++++--------- edit-trace/automerge-wasm.js | 4 +- 6 files changed, 158 insertions(+), 94 deletions(-) diff --git a/automerge-js/src/proxies.js b/automerge-js/src/proxies.js index 878ae101..ed3a4b97 100644 --- a/automerge-js/src/proxies.js +++ b/automerge-js/src/proxies.js @@ -4,7 +4,6 @@ const { Int, Uint, Float64 } = require("./numbers"); const { Counter, getWriteableCounter } = require("./counter"); const { Text } = require("./text"); const { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY } = require("./constants") -const { MAP, LIST, TABLE, TEXT } = require("automerge-wasm") function parseListIndex(key) { if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) @@ -135,21 +134,21 @@ const MapHandler = { } switch (datatype) { case "list": - const list = context.set(objectId, key, LIST) + const list = context.set(objectId, key, []) const proxyList = listProxy(context, list, [ ... path, key ], readonly ); for (let i = 0; i < value.length; i++) { proxyList[i] = value[i] } break; case "text": - const text = context.set(objectId, key, TEXT) + const text = context.set(objectId, key, "", "text") const proxyText = textProxy(context, text, [ ... path, key ], readonly ); for (let i = 0; i < value.length; i++) { proxyText[i] = value.get(i) } break; case "map": - const map = context.set(objectId, key, MAP) + const map = context.set(objectId, key, {}) const proxyMap = mapProxy(context, map, [ ... path, key ], readonly ); for (const key in value) { proxyMap[key] = value[key] @@ -252,9 +251,9 @@ const ListHandler = { case "list": let list if (index >= context.length(objectId)) { - list = context.insert(objectId, index, LIST) + list = context.insert(objectId, index, []) } else { - list = context.set(objectId, index, LIST) + list = context.set(objectId, index, []) } const proxyList = listProxy(context, list, [ ... path, index ], readonly); proxyList.splice(0,0,...value) @@ -262,9 +261,9 @@ const ListHandler = { case "text": let text if (index >= context.length(objectId)) { - text = context.insert(objectId, index, TEXT) + text = context.insert(objectId, index, "", "text") } else { - text = context.set(objectId, index, TEXT) + text = context.set(objectId, index, "", "text") } const proxyText = textProxy(context, text, [ ... path, index ], readonly); proxyText.splice(0,0,...value) @@ -272,9 +271,9 @@ const ListHandler = { case "map": let map if (index >= context.length(objectId)) { - map = context.insert(objectId, index, MAP) + map = context.insert(objectId, index, {}) } else { - map = context.set(objectId, index, MAP) + map = context.set(objectId, index, {}) } const proxyMap = mapProxy(context, map, [ ... path, index ], readonly); for (const key in value) { @@ -479,17 +478,17 @@ function listMethods(target) { for (let [value,datatype] of values) { switch (datatype) { case "list": - const list = context.insert(objectId, index, LIST) + const list = context.insert(objectId, index, []) const proxyList = listProxy(context, list, [ ... path, index ], readonly); proxyList.splice(0,0,...value) break; case "text": - const text = context.insert(objectId, index, TEXT) + const text = context.insert(objectId, index, "", "text") const proxyText = textProxy(context, text, [ ... path, index ], readonly); proxyText.splice(0,0,...value) break; case "map": - const map = context.insert(objectId, index, MAP) + const map = context.insert(objectId, index, {}) const proxyMap = mapProxy(context, map, [ ... path, index ], readonly); for (const key in value) { proxyMap[key] = value[key] diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 8a7e9408..7b97583e 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -6,8 +6,7 @@ export type SyncMessage = Uint8Array; export type Prop = string | number; export type Hash = string; export type Heads = Hash[]; -export type ObjectType = string; // opaque ?? -export type Value = string | number | boolean | null | Date | Uint8Array | ObjectType; +export type Value = string | number | boolean | null | Date | Uint8Array | Array | Object; export type FullValue = ["str", string] | ["int", number] | @@ -23,11 +22,6 @@ export type FullValue = ["text", ObjID] | ["table", ObjID] -export const LIST : ObjectType; -export const MAP : ObjectType; -export const TABLE : ObjectType; -export const TEXT : ObjectType; - export enum ObjTypeName { list = "list", map = "map", @@ -44,7 +38,10 @@ export type Datatype = "null" | "timestamp" | "counter" | - "bytes"; + "bytes" | + "map" | + "text" | + "list"; export type DecodedSyncMessage = { heads: Heads, @@ -86,10 +83,10 @@ export function decodeSyncState(data: Uint8Array): SyncState; export class Automerge { // change state set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID | undefined; - make(obj: ObjID, prop: Prop, value: ObjectType): ObjID; + make(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID; insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): ObjID | undefined; push(obj: ObjID, value: Value, datatype?: Datatype): ObjID | undefined; - splice(obj: ObjID, start: number, delete_count: number, text?: string | Array): ObjID[] | undefined; + splice(obj: ObjID, start: number, delete_count: number, text?: string | Array): ObjID[] | undefined; inc(obj: ObjID, prop: Prop, value: number): void; del(obj: ObjID, prop: Prop): void; diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index fc4c39f9..17a46251 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -3,6 +3,7 @@ use automerge::{Change, ChangeHash, Prop}; use js_sys::{Array, Object, Reflect, Uint8Array}; use std::collections::HashSet; use std::fmt::Display; +use unicode_segmentation::UnicodeSegmentation; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -257,23 +258,60 @@ pub(crate) fn to_prop(p: JsValue) -> Result { } } -pub(crate) fn to_objtype(a: &JsValue) -> Option { - if !a.is_function() { - return None; - } - let f: js_sys::Function = a.clone().try_into().unwrap(); - let f = f.to_string(); - if f.starts_with("class MAP", 0) { - Some(am::ObjType::Map) - } else if f.starts_with("class LIST", 0) { - Some(am::ObjType::List) - } else if f.starts_with("class TEXT", 0) { - Some(am::ObjType::Text) - } else if f.starts_with("class TABLE", 0) { - Some(am::ObjType::Table) - } else { - am::log!("to_objtype(function) -> {}", f); - None +pub(crate) fn to_objtype( + value: &JsValue, + datatype: &Option, +) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> { + match datatype.as_deref() { + Some("map") => { + let map = value.clone().dyn_into::().ok()?; + // FIXME unwrap + let map = js_sys::Object::keys(&map) + .iter() + .zip(js_sys::Object::values(&map).iter()) + .map(|(key, val)| (key.as_string().unwrap().into(), val)) + .collect(); + Some((am::ObjType::Map, map)) + } + Some("list") => { + let list = value.clone().dyn_into::().ok()?; + let list = list + .iter() + .enumerate() + .map(|(i, e)| (i.into(), e)) + .collect(); + Some((am::ObjType::List, list)) + } + Some("text") => { + let text = value.as_string()?; + let text = text + .graphemes(true) + .enumerate() + .map(|(i, ch)| (i.into(), ch.into())) + .collect(); + Some((am::ObjType::Text, text)) + } + Some(_) => None, + None => { + if let Ok(list) = value.clone().dyn_into::() { + let list = list + .iter() + .enumerate() + .map(|(i, e)| (i.into(), e)) + .collect(); + Some((am::ObjType::List, list)) + } else if let Ok(map) = value.clone().dyn_into::() { + // FIXME unwrap + let map = js_sys::Object::keys(&map) + .iter() + .zip(js_sys::Object::values(&map).iter()) + .map(|(key, val)| (key.as_string().unwrap().into(), val)) + .collect(); + Some((am::ObjType::Map, map)) + } else { + None + } + } } } diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 7d886788..4fc2c384 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -132,15 +132,11 @@ impl Automerge { } else { if let Ok(array) = text.dyn_into::() { for i in array.iter() { - if let Ok(array) = i.clone().dyn_into::() { - let value = array.get(1); - let datatype = array.get(2); - let value = self.import_value(value, datatype)?; - vals.push(value); - } else { - let value = self.import_value(i, JsValue::null())?; - vals.push(value); + let (value, subvals) = self.import_value(&i, None)?; + if !subvals.is_empty() { + return Err(to_js_err("splice must be shallow")); } + vals.push(value); } } let result = self.0.splice(&obj, start, delete_count, vals)?; @@ -163,9 +159,10 @@ impl Automerge { datatype: JsValue, ) -> Result, JsValue> { let obj = self.import(obj)?; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let index = self.0.length(&obj); let opid = self.0.insert(&obj, index, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string())) } @@ -178,8 +175,9 @@ impl Automerge { ) -> Result, JsValue> { let obj = self.import(obj)?; let index = index as f64; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let opid = self.0.insert(&obj, index as usize, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string())) } @@ -192,17 +190,44 @@ impl Automerge { ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let opid = self.0.set(&obj, prop, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string()).into()) } - pub fn make(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result { + fn subset( + &mut self, + obj: &Option, + vals: Vec<(am::Prop, JsValue)>, + ) -> Result<(), JsValue> { + if let Some(id) = obj { + for (p, v) in vals { + let (value, subvals) = self.import_value(&v, None)?; + //let opid = self.0.set(id, p, value)?; + let opid = match p { + Prop::Map(s) => self.0.set(id, s, value)?, + Prop::Seq(i) => self.0.insert(id, i, value)?, + }; + self.subset(&opid, subvals)?; + } + } + Ok(()) + } + + pub fn make( + &mut self, + obj: JsValue, + prop: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let value = self.import_value(value, JsValue::null())?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; if value.is_object() { let opid = self.0.set(&obj, prop, value)?; + self.subset(&opid, subvals)?; Ok(opid.unwrap().to_string()) } else { Err(to_js_err("invalid object type")) @@ -561,15 +586,18 @@ impl Automerge { } } - fn import_value(&mut self, value: JsValue, datatype: JsValue) -> Result { - let d = datatype.as_string(); - match self.import_scalar(&value, &d) { - Some(val) => Ok(val.into()), + fn import_value( + &mut self, + value: &JsValue, + datatype: Option, + ) -> Result<(Value, Vec<(Prop, JsValue)>), JsValue> { + match self.import_scalar(value, &datatype) { + Some(val) => Ok((val.into(), vec![])), None => { - if let Some(o) = to_objtype(&value) { - Ok(o.into()) + if let Some((o, subvals)) = to_objtype(value, &datatype) { + Ok((o.into(), subvals)) } else { - web_sys::console::log_3(&"Invalid value".into(), &value, &datatype); + web_sys::console::log_2(&"Invalid value".into(), value); Err(to_js_err("invalid value")) } } @@ -672,15 +700,3 @@ pub fn encode_sync_state(state: SyncState) -> Result { pub fn decode_sync_state(data: Uint8Array) -> Result { SyncState::decode(data) } - -#[wasm_bindgen(js_name = MAP)] -pub struct Map {} - -#[wasm_bindgen(js_name = LIST)] -pub struct List {} - -#[wasm_bindgen(js_name = TEXT)] -pub struct Text {} - -#[wasm_bindgen(js_name = TABLE)] -pub struct Table {} diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 51e50789..93547b8e 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' import { DecodedSyncMessage } from '..'; import { Hash } from '..'; @@ -64,7 +64,7 @@ describe('Automerge', () => { doc.set(root, "bool", true) doc.set(root, "time1", 1000, "timestamp") doc.set(root, "time2", new Date(1001)) - doc.set(root, "list", LIST); + doc.set(root, "list", []); doc.set(root, "null", null) result = doc.value(root,"hello") @@ -124,7 +124,7 @@ describe('Automerge', () => { let root = "_root" let result - let submap = doc.set(root, "submap", MAP) + let submap = doc.set(root, "submap", {}) if (!submap) throw new Error('should be not null') doc.set(submap, "number", 6, "uint") assert.strictEqual(doc.pendingOps(),2) @@ -141,7 +141,7 @@ describe('Automerge', () => { let doc = create() let root = "_root" - let submap = doc.set(root, "numbers", LIST) + let submap = doc.set(root, "numbers", []) if (!submap) throw new Error('should be not null') doc.insert(submap, 0, "a"); doc.insert(submap, 1, "b"); @@ -165,7 +165,7 @@ describe('Automerge', () => { let doc = create() let root = "_root" - let submap = doc.set(root, "letters", LIST) + let submap = doc.set(root, "letters", []) if (!submap) throw new Error('should be not null') doc.insert(submap, 0, "a"); doc.insert(submap, 0, "b"); @@ -230,11 +230,11 @@ describe('Automerge', () => { let doc = create() let root = "_root"; - let text = doc.set(root, "text", TEXT); + let text = doc.set(root, "text", "", "text"); if (!text) throw new Error('should not be undefined') doc.splice(text, 0, 0, "hello ") doc.splice(text, 6, 0, ["w","o","r","l","d"]) - doc.splice(text, 11, 0, [["str","!"],["str","?"]]) + doc.splice(text, 11, 0, ["!","?"]) assert.deepEqual(doc.value(text, 0),["str","h"]) assert.deepEqual(doc.value(text, 1),["str","e"]) assert.deepEqual(doc.value(text, 9),["str","l"]) @@ -282,7 +282,7 @@ describe('Automerge', () => { it('should be able to splice text', () => { let doc = create() - let text = doc.set("_root", "text", TEXT); + let text = doc.set("_root", "text", "", "text"); if (!text) throw new Error('should not be undefined') doc.splice(text, 0, 0, "hello world"); let heads1 = doc.commit(); @@ -331,7 +331,7 @@ describe('Automerge', () => { it('local inc increments all visible counters in a sequence', () => { let doc1 = create("aaaa") - let seq = doc1.set("_root", "seq", LIST) + let seq = doc1.set("_root", "seq", []) if (!seq) throw new Error('Should not be undefined') doc1.insert(seq, 0, "hello") let doc2 = loadDoc(doc1.save(), "bbbb"); @@ -363,18 +363,32 @@ describe('Automerge', () => { doc4.free() }) + it('recursive sets are possible', () => { + let doc = create("aaaa") + let l1 = doc.make("_root","list",[{ foo: "bar"}, [1,2,3]]) + let l2 = doc.insert(l1, 0, { zip: ["a", "b"] }) + let l3 = doc.set("_root","info1","hello world","text") + let l4 = doc.set("_root","info2","hello world") + assert.deepEqual(doc.toJS(), { + "list": [ { zip: ["a", "b"] }, { foo: "bar"}, [ 1,2,3]], + "info1": "hello world".split(""), + "info2": "hello world" + }) + doc.free() + }) + it('only returns an object id when objects are created', () => { let doc = create("aaaa") let r1 = doc.set("_root","foo","bar") - let r2 = doc.set("_root","list",LIST) + let r2 = doc.set("_root","list",[]) let r3 = doc.set("_root","counter",10, "counter") let r4 = doc.inc("_root","counter",1) let r5 = doc.del("_root","counter") if (!r2) throw new Error('should not be undefined') let r6 = doc.insert(r2,0,10); - let r7 = doc.insert(r2,0,MAP); + let r7 = doc.insert(r2,0,{}); let r8 = doc.splice(r2,1,0,["a","b","c"]); - let r9 = doc.splice(r2,1,0,["a",LIST,MAP,"d"]); + let r9 = doc.splice(r2,1,0,["a",[],{},"d"]); assert.deepEqual(r1,null); assert.deepEqual(r2,"2@aaaa"); assert.deepEqual(r3,null); @@ -389,11 +403,11 @@ describe('Automerge', () => { it('objects without properties are preserved', () => { let doc1 = create("aaaa") - let a = doc1.set("_root","a",MAP); + let a = doc1.set("_root","a",{}); if (!a) throw new Error('should not be undefined') - let b = doc1.set("_root","b",MAP); + let b = doc1.set("_root","b",{}); if (!b) throw new Error('should not be undefined') - let c = doc1.set("_root","c",MAP); + let c = doc1.set("_root","c",{}); if (!c) throw new Error('should not be undefined') let d = doc1.set(c,"d","dd"); let saved = doc1.save(); @@ -411,7 +425,7 @@ describe('Automerge', () => { it('should handle merging text conflicts then saving & loading', () => { let A = create("aabbcc") - let At = A.make('_root', 'text', TEXT) + let At = A.make('_root', 'text', "", "text") A.splice(At, 0, 0, 'hello') let B = A.fork() @@ -462,7 +476,7 @@ describe('Automerge', () => { let s1 = initSyncState(), s2 = initSyncState() // make two nodes with the same changes - let list = n1.set("_root","n", LIST) + let list = n1.set("_root","n", []) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -486,7 +500,7 @@ describe('Automerge', () => { let n1 = create(), n2 = create() // make changes for n1 that n2 should request - let list = n1.set("_root","n",LIST) + let list = n1.set("_root","n",[]) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -503,7 +517,7 @@ describe('Automerge', () => { let n1 = create(), n2 = create() // make changes for n1 that n2 should request - let list = n1.set("_root","n",LIST) + let list = n1.set("_root","n",[]) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -660,7 +674,7 @@ describe('Automerge', () => { let n1 = create('01234567'), n2 = create('89abcdef') let s1 = initSyncState(), s2 = initSyncState(), message = null - let items = n1.set("_root", "items", LIST) + let items = n1.set("_root", "items", []) if (!items) throw new Error('undefined') n1.commit("",0) diff --git a/edit-trace/automerge-wasm.js b/edit-trace/automerge-wasm.js index 02130686..3680efc0 100644 --- a/edit-trace/automerge-wasm.js +++ b/edit-trace/automerge-wasm.js @@ -10,8 +10,8 @@ const Automerge = require('../automerge-wasm') const start = new Date() -let doc = Automerge.init(); -let text = doc.set("_root", "text", Automerge.TEXT) +let doc = Automerge.create(); +let text = doc.set("_root", "text", "", "text") for (let i = 0; i < edits.length; i++) { let edit = edits[i] From e37395f975dc07b6a1a5e1e0ea2b544ca1173b7f Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 00:22:56 -0500 Subject: [PATCH 11/15] make() defaults to text --- automerge-wasm/src/interop.rs | 7 +++++++ automerge-wasm/src/lib.rs | 11 ++--------- automerge-wasm/test/test.ts | 8 +++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index 17a46251..61cc81e4 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -308,6 +308,13 @@ pub(crate) fn to_objtype( .map(|(key, val)| (key.as_string().unwrap().into(), val)) .collect(); Some((am::ObjType::Map, map)) + } else if let Some(text) = value.as_string() { + let text = text + .graphemes(true) + .enumerate() + .map(|(i, ch)| (i.into(), ch.into())) + .collect(); + Some((am::ObjType::Text, text)) } else { None } diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 4fc2c384..296a503b 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -215,17 +215,10 @@ impl Automerge { Ok(()) } - pub fn make( - &mut self, - obj: JsValue, - prop: JsValue, - value: JsValue, - datatype: JsValue, - ) -> Result { + pub fn make(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let (value, subvals) = self.import_value(&value, datatype.as_string())?; - if value.is_object() { + if let Some((value, subvals)) = to_objtype(&value, &None) { let opid = self.0.set(&obj, prop, value)?; self.subset(&opid, subvals)?; Ok(opid.unwrap().to_string()) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 93547b8e..9b721f90 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -367,12 +367,14 @@ describe('Automerge', () => { let doc = create("aaaa") let l1 = doc.make("_root","list",[{ foo: "bar"}, [1,2,3]]) let l2 = doc.insert(l1, 0, { zip: ["a", "b"] }) - let l3 = doc.set("_root","info1","hello world","text") - let l4 = doc.set("_root","info2","hello world") + let l3 = doc.make("_root","info1","hello world") // 'text' + let l4 = doc.set("_root","info2","hello world") // 'str' + let l5 = doc.set("_root","info3","hello world", "text") assert.deepEqual(doc.toJS(), { "list": [ { zip: ["a", "b"] }, { foo: "bar"}, [ 1,2,3]], "info1": "hello world".split(""), - "info2": "hello world" + "info2": "hello world", + "info3": "hello world".split("") }) doc.free() }) From 872efc5756a524f11cfc1dd37a76287f8080b12d Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 00:23:32 -0500 Subject: [PATCH 12/15] v10 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 30e0690a..bef3f24c 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.8", + "version": "0.0.10", "license": "MIT", "files": [ "README.md", From 3c3f411329785b013c59d4e2851252a29a1bfbb9 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 18:43:44 -0500 Subject: [PATCH 13/15] update to new autotransaction api --- automerge-wasm/package.json | 4 +- automerge-wasm/test/marks.ts | 18 +-- automerge-wasm/test/test.ts | 1 + automerge/src/autocommit.rs | 49 ++++++++- automerge/src/automerge.rs | 103 +++++------------- automerge/src/query.rs | 13 +++ automerge/src/transaction/inner.rs | 36 +++++- .../src/transaction/manual_transaction.rs | 45 +++++++- automerge/src/transaction/transactable.rs | 32 +++++- automerge/tests/test.rs | 96 ++++++++-------- 10 files changed, 256 insertions(+), 141 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index bef3f24c..88912336 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.10", + "version": "0.0.11", "license": "MIT", "files": [ "README.md", @@ -25,7 +25,7 @@ "release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", "release": "yarn release-w && yarn release-n", - "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/marks.ts" + "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, "dependencies": {}, "devDependencies": { diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 8064f953..76702caf 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -2,13 +2,13 @@ import { describe, it } from 'mocha'; //@ts-ignore import assert from 'assert' //@ts-ignore -import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '..' +import { create, loadDoc, Automerge, encodeChange, decodeChange } from '..' describe('Automerge', () => { describe('marks', () => { it('should handle marks [..]', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) @@ -22,7 +22,7 @@ describe('Automerge', () => { it('should handle marks [..] at the beginning of a string', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -39,7 +39,7 @@ describe('Automerge', () => { it('should handle marks [..] with splice', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -56,7 +56,7 @@ describe('Automerge', () => { it('should handle marks across multiple forks', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -79,7 +79,7 @@ describe('Automerge', () => { it('should handle marks with deleted ends [..]', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") @@ -100,7 +100,7 @@ describe('Automerge', () => { it('should handle sticky marks (..)', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) @@ -114,7 +114,7 @@ describe('Automerge', () => { it('should handle sticky marks with deleted ends (..)', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) @@ -143,7 +143,7 @@ describe('Automerge', () => { it('should handle overlapping marks', () => { let doc : Automerge = create("aabbcc") - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") doc.mark(list, "[0..37]", "bold" , true) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 8375e625..9d7a8960 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -4,6 +4,7 @@ import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage, Hash } from '..' function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) { const MAX_ITER = 10 diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index f54fea03..a5673a57 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -2,8 +2,8 @@ use crate::exid::ExId; use crate::transaction::{CommitOptions, Transactable}; use crate::types::Patch; use crate::{ - change::export_change, transaction::TransactionInner, ActorId, Automerge, AutomergeError, - Change, ChangeHash, Prop, Value, + change::export_change, query, transaction::TransactionInner, ActorId, Automerge, + AutomergeError, Change, ChangeHash, Prop, ScalarValue, Value, }; use crate::{SyncMessage, SyncState}; @@ -347,6 +347,31 @@ impl Transactable for AutoCommit { tx.insert(&mut self.doc, obj, index, value) } + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let tx = self.transaction.as_mut().unwrap(); + tx.mark( + &mut self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + fn inc>( &mut self, obj: &ExId, @@ -386,6 +411,26 @@ impl Transactable for AutoCommit { self.doc.text_at(obj, heads) } + fn list(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 4db083e8..5a0ef99d 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -13,7 +13,6 @@ use crate::types::{ }; use crate::{legacy, query, types, ObjType}; use crate::{AutomergeError, Change, Prop}; -use serde::Serialize; /// An automerge document. #[derive(Debug, Clone)] @@ -316,13 +315,13 @@ impl Automerge { Ok(query.spans) } - pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); let result = query .spans .into_iter() - .map(|s| SpanInfo { + .map(|s| query::SpanInfo { id: self.id_to_exid(s.id), time: self.history[s.change].time, start: s.start, @@ -334,68 +333,37 @@ impl Automerge { Ok(result) } - #[allow(clippy::too_many_arguments)] - pub fn mark( - &mut self, - obj: &ExId, - start: usize, - expand_start: bool, - end: usize, - expand_end: bool, - mark: &str, - value: ScalarValue, - ) -> Result<(), AutomergeError> { - let obj = self.exid_to_obj(obj)?; + /* + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = self.exid_to_obj(obj)?; - self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; - self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; + self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; + self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; - /* - let (a, b) = query.ops()?; - let (pos, key) = a; - let id = self.next_id(); - let op = Op { - change: self.history.len(), - id, - action: OpType::Mark(MarkData { name: mark.into(), expand: expand_start, value}), - obj, - key, - succ: Default::default(), - pred: Default::default(), - insert: true, - }; - self.ops.insert(pos, op.clone()); - self.tx().operations.push(op); + Ok(()) + } - let (pos, key) = b; - let id = self.next_id(); - let op = Op { - change: self.history.len(), - id, - action: OpType::Unmark(expand_end), - obj, - key, - succ: Default::default(), - pred: Default::default(), - insert: true, - }; - self.ops.insert(pos, op.clone()); - self.tx().operations.push(op); - */ - - Ok(()) - } - - pub fn unmark( - &self, - _obj: &ExId, - _start: usize, - _end: usize, - _inclusive: bool, - _mark: &str, - ) -> Result { - unimplemented!() - } + pub fn unmark( + &self, + _obj: &ExId, + _start: usize, + _end: usize, + _inclusive: bool, + _mark: &str, + ) -> Result { + unimplemented!() + } + */ // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } @@ -966,17 +934,6 @@ impl Default for Automerge { } } -#[derive(Serialize, Debug, Clone, PartialEq)] -pub struct SpanInfo { - pub id: ExId, - pub time: i64, - pub start: usize, - pub end: usize, - #[serde(rename = "type")] - pub span_type: String, - pub value: ScalarValue, -} - #[cfg(test)] mod tests { use super::*; diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 1b97ec51..19ea30ce 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -1,6 +1,8 @@ +use crate::exid::ExId; use crate::op_tree::{OpSetMetadata, OpTreeNode}; use crate::types::{Clock, Counter, ElemId, Op, OpId, OpType, ScalarValue}; use fxhash::FxBuildHasher; +use serde::Serialize; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; @@ -37,6 +39,17 @@ pub(crate) use raw_spans::RawSpans; pub(crate) use seek_op::SeekOp; pub(crate) use spans::{Span, Spans}; +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct SpanInfo { + pub id: ExId, + pub time: i64, + pub start: usize, + pub end: usize, + #[serde(rename = "type")] + pub span_type: String, + pub value: ScalarValue, +} + #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { pos: usize, diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index aaa26a99..d476e4e9 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use crate::exid::ExId; use crate::query::{self, OpIdSearch}; use crate::types::{Key, ObjId, OpId}; -use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, Value}; +use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, ScalarValue, Value}; use crate::{AutomergeError, OpType}; #[derive(Debug, Clone)] @@ -116,6 +116,7 @@ impl TransactionInner { value: V, ) -> Result, AutomergeError> { let obj = doc.exid_to_obj(obj)?; + let value = value.into(); if let Some(id) = self.do_insert(doc, obj, index, value)? { Ok(Some(doc.id_to_exid(id))) } else { @@ -123,20 +124,45 @@ impl TransactionInner { } } - fn do_insert>( + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + doc: &mut Automerge, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj)?; + + self.do_insert( + doc, + obj, + start, + OpType::mark(mark.into(), expand_start, value), + )?; + self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?; + + Ok(()) + } + + fn do_insert>( &mut self, doc: &mut Automerge, obj: ObjId, index: usize, - value: V, + action: V, ) -> Result, AutomergeError> { let id = self.next_id(); let query = doc.ops.search(obj, query::InsertNth::new(index)); let key = query.key()?; - let value = value.into(); - let action = value.into(); + //let value = value.into(); + let action = action.into(); let is_make = matches!(&action, OpType::Make(_)); let op = Op { diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index d52b9219..cd341702 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -1,6 +1,6 @@ use crate::exid::ExId; use crate::AutomergeError; -use crate::{Automerge, ChangeHash, Prop, Value}; +use crate::{query, Automerge, ChangeHash, Prop, ScalarValue, Value}; use super::{CommitOptions, Transactable, TransactionInner}; @@ -106,6 +106,29 @@ impl<'a> Transactable for Transaction<'a> { .insert(self.doc, obj, index, value) } + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.inner.as_mut().unwrap().mark( + self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + fn inc>( &mut self, obj: &ExId, @@ -158,6 +181,26 @@ impl<'a> Transactable for Transaction<'a> { self.doc.text_at(obj, heads) } + fn list(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + fn value>( &self, obj: &ExId, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index dbce1d14..5e4b21f5 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -1,5 +1,6 @@ use crate::exid::ExId; -use crate::{AutomergeError, ChangeHash, Prop, Value}; +use crate::query; +use crate::{AutomergeError, ChangeHash, Prop, ScalarValue, Value}; use unicode_segmentation::UnicodeSegmentation; /// A way of mutating a document within a single change. @@ -35,6 +36,19 @@ pub trait Transactable { value: V, ) -> Result, AutomergeError>; + /// Set a mark within a range on a list + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError>; + /// Increment the counter at the prop in the object by `value`. fn inc>(&mut self, obj: &ExId, prop: P, value: i64) -> Result<(), AutomergeError>; @@ -85,6 +99,22 @@ pub trait Transactable { /// Get the string that this text object represents at a point in history. fn text_at(&self, obj: &ExId, heads: &[ChangeHash]) -> Result; + /// Get the string that this text object represents. + fn list(&self, obj: &ExId) -> Result, AutomergeError>; + + /// Get the string that this text object represents at a point in history. + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError>; + + /// test spans api for mark/span experiment + fn spans(&self, obj: &ExId) -> Result, AutomergeError>; + + /// test raw_spans api for mark/span experiment + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError>; + /// Get the value at this prop in the object. fn value>( &self, diff --git a/automerge/tests/test.rs b/automerge/tests/test.rs index 7d272a88..34a9777b 100644 --- a/automerge/tests/test.rs +++ b/automerge/tests/test.rs @@ -55,10 +55,10 @@ fn repeated_map_assignment_which_resolves_conflict_not_ignored() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "field", 123).unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&automerge::ROOT, "field", 456).unwrap(); doc1.set(&automerge::ROOT, "field", 789).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_eq!(doc1.values(&automerge::ROOT, "field").unwrap().len(), 2); doc1.set(&automerge::ROOT, "field", 123).unwrap(); @@ -79,9 +79,9 @@ fn repeated_list_assignment_which_resolves_conflict_not_ignored() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, 123).unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&list_id, 0, 456).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc1.set(&list_id, 0, 789).unwrap(); assert_doc!( @@ -124,7 +124,7 @@ fn merge_concurrent_map_prop_updates() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "foo", "bar").unwrap(); doc2.set(&automerge::ROOT, "hello", "world").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_eq!( doc1.value(&automerge::ROOT, "foo").unwrap().unwrap().0, "bar".into() @@ -136,7 +136,7 @@ fn merge_concurrent_map_prop_updates() { "hello" => { "world" }, } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -153,10 +153,10 @@ fn add_concurrent_increments_of_same_property() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "counter", mk_counter(0)) .unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.inc(&automerge::ROOT, "counter", 1).unwrap(); doc2.inc(&automerge::ROOT, "counter", 2).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), map! { @@ -182,7 +182,7 @@ fn add_increments_only_to_preceeded_values() { doc2.inc(&automerge::ROOT, "counter", 3).unwrap(); // The two values should be conflicting rather than added - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -202,7 +202,7 @@ fn concurrent_updates_of_same_field() { doc1.set(&automerge::ROOT, "field", "one").unwrap(); doc2.set(&automerge::ROOT, "field", "two").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -224,11 +224,11 @@ fn concurrent_updates_of_same_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "finch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&list_id, 0, "greenfinch").unwrap(); doc2.set(&list_id, 0, "goldfinch").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -253,8 +253,8 @@ fn assignment_conflicts_of_different_types() { .unwrap(); doc3.set(&automerge::ROOT, "field", automerge::Value::map()) .unwrap(); - doc1.merge(&mut doc2); - doc1.merge(&mut doc3); + doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc3).unwrap(); assert_doc!( doc1.document(), @@ -278,7 +278,7 @@ fn changes_within_conflicting_map_field() { .unwrap() .unwrap(); doc2.set(&map_id, "innerKey", 42).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -305,7 +305,7 @@ fn changes_within_conflicting_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "hello").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); let map_in_doc1 = doc1 .set(&list_id, 0, automerge::Value::map()) @@ -318,11 +318,11 @@ fn changes_within_conflicting_list_element() { .set(&list_id, 0, automerge::Value::map()) .unwrap() .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc2.set(&map_in_doc2, "map2", true).unwrap(); doc2.set(&map_in_doc2, "key", 2).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -362,7 +362,7 @@ fn concurrently_assigned_nested_maps_should_not_merge() { .unwrap(); doc2.set(&doc2_map_id, "logo_url", "logo.png").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -393,11 +393,11 @@ fn concurrent_insertions_at_different_list_positions() { doc1.insert(&list_id, 0, "one").unwrap(); doc1.insert(&list_id, 1, "three").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice(&list_id, 1, 0, vec!["two".into()]).unwrap(); doc2.insert(&list_id, 2, "four").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -427,10 +427,10 @@ fn concurrent_insertions_at_same_list_position() { .unwrap(); doc1.insert(&list_id, 0, "parakeet").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.insert(&list_id, 1, "starling").unwrap(); doc2.insert(&list_id, 1, "chaffinch").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -457,11 +457,11 @@ fn concurrent_assignment_and_deletion_of_a_map_entry() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "bestBird", "robin").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&automerge::ROOT, "bestBird").unwrap(); doc2.set(&automerge::ROOT, "bestBird", "magpie").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -484,7 +484,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { doc1.insert(&list_id, 0, "blackbird").unwrap(); doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&list_id, 1, "starling").unwrap(); doc2.del(&list_id, 1).unwrap(); @@ -509,7 +509,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { } ); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -536,14 +536,14 @@ fn insertion_after_a_deleted_list_element() { doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice(&list_id, 1, 2, Vec::new()).unwrap(); doc2.splice(&list_id, 2, 0, vec!["starling".into()]) .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -555,7 +555,7 @@ fn insertion_after_a_deleted_list_element() { } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -580,13 +580,13 @@ fn concurrent_deletion_of_same_list_element() { doc1.insert(&list_id, 1, "buzzard").unwrap(); doc1.insert(&list_id, 2, "cormorant").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&list_id, 1).unwrap(); doc2.del(&list_id, 1).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -598,7 +598,7 @@ fn concurrent_deletion_of_same_list_element() { } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -632,12 +632,12 @@ fn concurrent_updates_at_different_levels() { .unwrap(); doc1.insert(&mammals, 0, "badger").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&birds, "brown", "sparrow").unwrap(); doc2.del(&animals, "birds").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_obj!( doc1.document(), @@ -677,13 +677,13 @@ fn concurrent_updates_of_concurrently_deleted_objects() { .unwrap(); doc1.set(&blackbird, "feathers", "black").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&birds, "blackbird").unwrap(); doc2.set(&blackbird, "beak", "orange").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -705,7 +705,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { .set(&automerge::ROOT, "wisdom", automerge::Value::list()) .unwrap() .unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice( &wisdom, @@ -735,7 +735,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { ) .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -768,7 +768,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_greater_actor_id( .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -794,7 +794,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_lesser_actor_id() .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -818,11 +818,11 @@ fn insertion_consistent_with_causality() { .unwrap() .unwrap(); doc1.insert(&list, 0, "four").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "three").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -862,11 +862,11 @@ fn save_restore_complex() { doc1.set(&first_todo, "done", false).unwrap(); let mut doc2 = new_doc(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&first_todo, "title", "weed plants").unwrap(); doc1.set(&first_todo, "title", "kill plants").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); let reloaded = Automerge::load(&doc1.save().unwrap()).unwrap(); @@ -919,8 +919,8 @@ fn list_counter_del() -> Result<(), automerge::AutomergeError> { doc1.inc(&list, 1, 1)?; doc1.inc(&list, 2, 1)?; - doc1.merge(&mut doc2); - doc1.merge(&mut doc3); + doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc3).unwrap(); let values = doc1.values(&list, 1)?; assert_eq!(values.len(), 3); From e07211278f6c5ccac7fb272fbaaf3f85ecd67f4b Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 18:44:42 -0500 Subject: [PATCH 14/15] v0.0.14 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 88912336..d1e39f12 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.11", + "version": "0.0.14", "license": "MIT", "files": [ "README.md", From c1be06a6c79bb635e55ed29c2067687deaaf44b2 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 28 Feb 2022 19:02:28 -0500 Subject: [PATCH 15/15] blame wip 1 --- automerge/src/automerge.rs | 26 +++++++++++++ automerge/src/clock.rs | 7 ++++ automerge/src/query.rs | 2 + automerge/src/query/blame.rs | 71 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 automerge/src/query/blame.rs diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 5a0ef99d..fb5e4d45 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -315,6 +315,32 @@ impl Automerge { Ok(query.spans) } + pub fn blame( + &self, + obj: &ExId, + base: &[ChangeHash], + points: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let base = self.clock_at(base); + let points: Vec = points.iter().map(|p| self.clock_at(p)).collect(); + let points: Vec = points + .iter() + .enumerate() + .map(|(j, _)| { + points + .iter() + .enumerate() + .filter(|(i, _)| *i != j) + .fold(base.clone(), |acc, (_, c)| acc.union(c)) + }) + .collect(); + + let query = self.ops.search(obj, query::Blame::new(points)); + //Ok(query.points) + unimplemented!() + } + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); diff --git a/automerge/src/clock.rs b/automerge/src/clock.rs index d01c7748..4d95918d 100644 --- a/automerge/src/clock.rs +++ b/automerge/src/clock.rs @@ -18,6 +18,13 @@ impl Clock { .or_insert(n); } + pub fn union(mut self, other: &Clock) -> Clock { + for (key, val) in &other.0 { + self.include(*key, *val) + } + self + } + pub fn covers(&self, id: &OpId) -> bool { if let Some(val) = self.0.get(&id.1) { val >= &id.0 diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 19ea30ce..e5514ea7 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -7,6 +7,7 @@ use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +mod blame; mod insert; mod keys; mod keys_at; @@ -23,6 +24,7 @@ mod raw_spans; mod seek_op; mod spans; +pub(crate) use blame::Blame; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; pub(crate) use keys_at::KeysAt; diff --git a/automerge/src/query/blame.rs b/automerge/src/query/blame.rs new file mode 100644 index 00000000..744c19f5 --- /dev/null +++ b/automerge/src/query/blame.rs @@ -0,0 +1,71 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpType, ScalarValue}; +use crate::clock::Clock; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Blame { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec, + points: Vec, + changed: bool, +} + +impl Blame { + pub fn new(points: Vec) -> Self { + Blame { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + seen_at_last_mark: None, + seen_at_this_mark: None, + changed: false, + points: Vec::new(), + ops: Vec::new(), + } + } +} + +impl TreeQuery for Blame { + /* + fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { + unimplemented!() + } + */ + + 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(_) = &element.action { + let pos = self + .ops + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.ops.insert(pos, element.clone()); + } + 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(); + self.seen += 1; + self.last_seen = element.elemid(); + self.seen_at_this_mark = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +}