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/index.d.ts b/automerge-wasm/index.d.ts index b9249b41..7b97583e 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -98,6 +98,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/package.json b/automerge-wasm/package.json index 92eb79f8..d1e39f12 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -4,24 +4,27 @@ "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.14", "license": "MIT", "files": [ "README.md", - "LICENSE", "package.json", - "automerge_wasm_bg.wasm", - "automerge_wasm.js" + "index.d.ts", + "node/index.js", + "node/index_bg.wasm", + "web/index.js", + "web/index_bg.wasm" ], - "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 && 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", + "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 --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" }, "dependencies": {}, diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index c31c925d..7ad1befe 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -4,6 +4,7 @@ use am::transaction::Transactable; 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; @@ -332,6 +333,86 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: JsValue, + range: JsValue, + name: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result<(), JsValue> { + let obj = self.import(obj)?; + let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); + let range = range.as_string().ok_or("range must be a string")?; + let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; + let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; + let start_sticky = &cap[1] == "("; + let end_sticky = &cap[4] == ")"; + let name = name + .as_string() + .ok_or("invalid mark name") + .map_err(to_js_err)?; + let value = self + .import_scalar(&value, &datatype.as_string()) + .ok_or_else(|| to_js_err("invalid value"))?; + self.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.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(); + 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() { + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.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() { + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); + } + Ok(result.into()) + } + + 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(); + 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..76702caf --- /dev/null +++ b/automerge-wasm/test/marks.ts @@ -0,0 +1,195 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, loadDoc, Automerge, encodeChange, decodeChange } from '..' + +describe('Automerge', () => { + describe('marks', () => { + it('should handle marks [..]', () => { + let doc = create() + 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) + 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 [..] at the beginning of a string', () => { + let doc = create() + 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) + 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.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) + 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.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) + 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.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) + 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.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) + 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.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) + 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.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) + 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-wasm/test/test.ts b/automerge-wasm/test/test.ts index af6b2315..9d7a8960 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,9 +3,8 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index' -import { DecodedSyncMessage } from '../index'; -import { Hash } from '../dev/index'; +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 15e4c098..fb5e4d45 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)] @@ -284,6 +283,114 @@ 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()); + query.check_marks(); + 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()); + let result = query + .spans + .into_iter() + .map(|s| query::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))?; + + 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? @@ -823,6 +930,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(); @@ -851,17 +960,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::*; @@ -1124,8 +1222,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/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/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 84a70c49..e5514ea7 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -1,10 +1,13 @@ +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; +mod blame; mod insert; mod keys; mod keys_at; @@ -17,8 +20,11 @@ mod nth_at; mod opid; mod prop; mod prop_at; +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; @@ -31,7 +37,20 @@ pub(crate) use nth_at::NthAt; pub(crate) use opid::OpIdSearch; 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}; + +#[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 { 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 + } +} 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..95aafc56 --- /dev/null +++ b/automerge/src/query/raw_spans.rs @@ -0,0 +1,80 @@ +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/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/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/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()) }