From 06f0b201c9fdc9012d196197ebe7ecdfd24466b4 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 11:42:58 -0500 Subject: [PATCH] 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 44774ef6..189b9370 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; @@ -307,6 +308,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);