Compare commits
	
		
			70 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 98328e2ecb | ||
|  | ee50b4a3ca | ||
|  | 64e7efd843 | ||
|  | 50cea87c82 | ||
|  | 8859806da5 | ||
|  | 72c09c3cb9 | ||
|  | cf97432df3 | ||
|  | 8982b96c41 | ||
|  | d0b34a7dde | ||
|  | c43dc18493 | ||
|  | 71977451b6 | ||
|  | 5be67d10cb | ||
|  | bce30fa9b2 | ||
|  | 38f3bcb401 | ||
|  | 21a7bd91dc | ||
|  | 4e304d11c6 | ||
|  | 08e6a86f28 | ||
|  | 979b9fd362 | ||
|  | c149da3a6d | ||
|  | af02ba6b86 | ||
|  | 657bd22d61 | ||
|  | 2663e0315c | ||
|  | bebd310ab6 | ||
|  | bc98b1ecc9 | ||
|  | 84619d8331 | ||
|  | 5d4e1f0c42 | ||
|  | 25afa0b12b | ||
|  | 0cf54c36a8 | ||
|  | 99b1127f5c | ||
|  | ae87d7bc00 | ||
|  | ce9771b29c | ||
|  | e00797c512 | ||
|  | 57a0f62b75 | ||
|  | a0f78561c4 | ||
|  | ff1a20c626 | ||
|  | b14d874dfc | ||
|  | aad4852e30 | ||
|  | 63b4c96e71 | ||
|  | 1b1d50dfaf | ||
|  | d02737ad12 | ||
|  | 8f4c1fc209 | ||
|  | 304195d720 | ||
|  | b81e0fd619 | ||
|  | 22b62b14b5 | ||
|  | cbf1ac03b2 | ||
|  | 4094e82f04 | ||
|  | 42446fa5c2 | ||
|  | 6d5f16c9cd | ||
|  | dbbdd616fd | ||
|  | 523af57a26 | ||
|  | d195a81d49 | ||
|  | 4c11c86532 | ||
|  | 42b6ffe9d8 | ||
|  | b21b59e6a1 | ||
|  | c1be06a6c7 | ||
|  | e07211278f | ||
|  | 3c3f411329 | ||
|  | 5aad691e31 | ||
|  | 872efc5756 | ||
|  | e37395f975 | ||
|  | a84fa64554 | ||
|  | a37d4a6870 | ||
|  | 5eb5714c13 | ||
|  | 4f9b95b5b8 | ||
|  | 36b4f08d20 | ||
|  | 015e8ce465 | ||
|  | ea2f29d681 | ||
|  | c8cd069e51 | ||
|  | 2ba2da95a8 | ||
|  | 561cad44e3 | 
					 37 changed files with 1748 additions and 48 deletions
				
			
		|  | @ -3,6 +3,7 @@ | ||||||
| //const CACHE     = Symbol('_cache')     // map from objectId to immutable object
 | //const CACHE     = Symbol('_cache')     // map from objectId to immutable object
 | ||||||
| export const STATE      = Symbol.for('_am_state')     // object containing metadata about current state (e.g. sequence numbers)
 | export const STATE      = Symbol.for('_am_state')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
| export const HEADS      = Symbol.for('_am_heads')     // object containing metadata about current state (e.g. sequence numbers)
 | export const HEADS      = Symbol.for('_am_heads')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
|  | export const TRACE      = Symbol.for('_am_trace')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
| export const OBJECT_ID  = Symbol.for('_am_objectId')     // object containing metadata about current state (e.g. sequence numbers)
 | export const OBJECT_ID  = Symbol.for('_am_objectId')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
| export const READ_ONLY  = Symbol.for('_am_readOnly')     // object containing metadata about current state (e.g. sequence numbers)
 | export const READ_ONLY  = Symbol.for('_am_readOnly')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
| export const FROZEN     = Symbol.for('_am_frozen')     // object containing metadata about current state (e.g. sequence numbers)
 | export const FROZEN     = Symbol.for('_am_frozen')     // object containing metadata about current state (e.g. sequence numbers)
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| export { uuid } from './uuid' | export { uuid } from './uuid' | ||||||
| 
 | 
 | ||||||
| import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies" | import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies" | ||||||
| import { STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN  } from "./constants" | import { STATE, HEADS, TRACE, OBJECT_ID, READ_ONLY, FROZEN  } from "./constants" | ||||||
| 
 | 
 | ||||||
| import { AutomergeValue, Counter } from "./types" | import { AutomergeValue, Counter } from "./types" | ||||||
| export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types" | export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types" | ||||||
|  | @ -48,6 +48,20 @@ function _heads<T>(doc: Doc<T>) : Heads | undefined { | ||||||
|   return Reflect.get(doc,HEADS) |   return Reflect.get(doc,HEADS) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function _trace<T>(doc: Doc<T>) : string | undefined { | ||||||
|  |   return Reflect.get(doc,TRACE) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function _set_heads<T>(doc: Doc<T>, heads: Heads) { | ||||||
|  |   Reflect.set(doc,HEADS,heads) | ||||||
|  |   Reflect.set(doc,TRACE,(new Error()).stack) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function _clear_heads<T>(doc: Doc<T>) { | ||||||
|  |   Reflect.set(doc,HEADS,undefined) | ||||||
|  |   Reflect.set(doc,TRACE,undefined) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function _obj<T>(doc: Doc<T>) : ObjID { | function _obj<T>(doc: Doc<T>) : ObjID { | ||||||
|   return Reflect.get(doc,OBJECT_ID) |   return Reflect.get(doc,OBJECT_ID) | ||||||
| } | } | ||||||
|  | @ -104,7 +118,7 @@ function _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>): | ||||||
|     throw new RangeError("Attempting to use an outdated Automerge document") |     throw new RangeError("Attempting to use an outdated Automerge document") | ||||||
|   } |   } | ||||||
|   if (!!_heads(doc) === true) { |   if (!!_heads(doc) === true) { | ||||||
|     throw new RangeError("Attempting to change an out of date document"); |     throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc)); | ||||||
|   } |   } | ||||||
|   if (_readonly(doc) === false) { |   if (_readonly(doc) === false) { | ||||||
|     throw new RangeError("Calls to Automerge.change cannot be nested") |     throw new RangeError("Calls to Automerge.change cannot be nested") | ||||||
|  | @ -112,13 +126,13 @@ function _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>): | ||||||
|   const state = _state(doc) |   const state = _state(doc) | ||||||
|   const heads = state.getHeads() |   const heads = state.getHeads() | ||||||
|   try { |   try { | ||||||
|     Reflect.set(doc,HEADS,heads) |     _set_heads(doc,heads) | ||||||
|     Reflect.set(doc,FROZEN,true) |     Reflect.set(doc,FROZEN,true) | ||||||
|     const root : T = rootProxy(state); |     const root : T = rootProxy(state); | ||||||
|     callback(root) |     callback(root) | ||||||
|     if (state.pendingOps() === 0) { |     if (state.pendingOps() === 0) { | ||||||
|       Reflect.set(doc,FROZEN,false) |       Reflect.set(doc,FROZEN,false) | ||||||
|       Reflect.set(doc,HEADS,undefined) |       _clear_heads(doc) | ||||||
|       return doc |       return doc | ||||||
|     } else { |     } else { | ||||||
|       state.commit(options.message, options.time) |       state.commit(options.message, options.time) | ||||||
|  | @ -127,7 +141,7 @@ function _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>): | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     //console.log("ERROR: ",e)
 |     //console.log("ERROR: ",e)
 | ||||||
|     Reflect.set(doc,FROZEN,false) |     Reflect.set(doc,FROZEN,false) | ||||||
|     Reflect.set(doc,HEADS,undefined) |     _clear_heads(doc) | ||||||
|     state.rollback() |     state.rollback() | ||||||
|     throw e |     throw e | ||||||
|   } |   } | ||||||
|  | @ -168,14 +182,14 @@ export function save<T>(doc: Doc<T>) : Uint8Array  { | ||||||
| 
 | 
 | ||||||
| export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> { | export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> { | ||||||
|   if (!!_heads(local) === true) { |   if (!!_heads(local) === true) { | ||||||
|     throw new RangeError("Attempting to change an out of date document"); |     throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc)); | ||||||
|   } |   } | ||||||
|   const localState = _state(local) |   const localState = _state(local) | ||||||
|   const heads = localState.getHeads() |   const heads = localState.getHeads() | ||||||
|   const remoteState = _state(remote) |   const remoteState = _state(remote) | ||||||
|   const changes = localState.getChangesAdded(remoteState) |   const changes = localState.getChangesAdded(remoteState) | ||||||
|   localState.applyChanges(changes) |   localState.applyChanges(changes) | ||||||
|   Reflect.set(local,HEADS,heads) |   _set_heads(local,heads) | ||||||
|   return rootProxy(localState, true) |   return rootProxy(localState, true) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -267,7 +281,7 @@ export function applyChanges<T>(doc: Doc<T>, changes: Change[]) : [Doc<T>] { | ||||||
|   const state = _state(doc) |   const state = _state(doc) | ||||||
|   const heads = state.getHeads() |   const heads = state.getHeads() | ||||||
|   state.applyChanges(changes) |   state.applyChanges(changes) | ||||||
|   Reflect.set(doc,HEADS,heads) |   _set_heads(doc,heads) | ||||||
|   return [rootProxy(state, true)]; |   return [rootProxy(state, true)]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -322,7 +336,7 @@ export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: | ||||||
|     throw new RangeError("Attempting to use an outdated Automerge document") |     throw new RangeError("Attempting to use an outdated Automerge document") | ||||||
|   } |   } | ||||||
|   if (!!_heads(doc) === true) { |   if (!!_heads(doc) === true) { | ||||||
|     throw new RangeError("Attempting to change an out of date document"); |     throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc)); | ||||||
|   } |   } | ||||||
|   if (_readonly(doc) === false) { |   if (_readonly(doc) === false) { | ||||||
|     throw new RangeError("Calls to Automerge.change cannot be nested") |     throw new RangeError("Calls to Automerge.change cannot be nested") | ||||||
|  | @ -330,7 +344,7 @@ export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: | ||||||
|   const state = _state(doc) |   const state = _state(doc) | ||||||
|   const heads = state.getHeads() |   const heads = state.getHeads() | ||||||
|   state.receiveSyncMessage(syncState, message) |   state.receiveSyncMessage(syncState, message) | ||||||
|   Reflect.set(doc,HEADS,heads) |   _set_heads(doc,heads) | ||||||
|   const outState = ApiHandler.exportSyncState(syncState) |   const outState = ApiHandler.exportSyncState(syncState) | ||||||
|   return [rootProxy(state, true), outState, null]; |   return [rootProxy(state, true), outState, null]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { AutomergeValue, ScalarValue, MapValue, ListValue, TextValue } from "./t | ||||||
| import { Int, Uint, Float64 } from "./numbers" | import { Int, Uint, Float64 } from "./numbers" | ||||||
| import { Counter, getWriteableCounter } from "./counter" | import { Counter, getWriteableCounter } from "./counter" | ||||||
| import { Text } from "./text" | import { Text } from "./text" | ||||||
| import { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants" | import { STATE, HEADS, TRACE, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants" | ||||||
| 
 | 
 | ||||||
| function parseListIndex(key) { | function parseListIndex(key) { | ||||||
|   if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) |   if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) | ||||||
|  | @ -108,6 +108,7 @@ const MapHandler = { | ||||||
|     if (key === READ_ONLY) return readonly |     if (key === READ_ONLY) return readonly | ||||||
|     if (key === FROZEN) return frozen |     if (key === FROZEN) return frozen | ||||||
|     if (key === HEADS) return heads |     if (key === HEADS) return heads | ||||||
|  |     if (key === TRACE) return target.trace | ||||||
|     if (key === STATE) return context; |     if (key === STATE) return context; | ||||||
|     if (!cache[key]) { |     if (!cache[key]) { | ||||||
|       cache[key] = valueAt(target, key) |       cache[key] = valueAt(target, key) | ||||||
|  | @ -129,6 +130,10 @@ const MapHandler = { | ||||||
|       target.heads = val |       target.heads = val | ||||||
|       return true |       return true | ||||||
|     } |     } | ||||||
|  |     if (key === TRACE) { | ||||||
|  |       target.trace = val | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|     const [ value, datatype ] = import_value(val) |     const [ value, datatype ] = import_value(val) | ||||||
|     if (frozen) { |     if (frozen) { | ||||||
|       throw new RangeError("Attempting to use an outdated Automerge document") |       throw new RangeError("Attempting to use an outdated Automerge document") | ||||||
|  | @ -211,6 +216,7 @@ const ListHandler = { | ||||||
|     if (index === READ_ONLY) return readonly |     if (index === READ_ONLY) return readonly | ||||||
|     if (index === FROZEN) return frozen |     if (index === FROZEN) return frozen | ||||||
|     if (index === HEADS) return heads |     if (index === HEADS) return heads | ||||||
|  |     if (index === TRACE) return target.trace | ||||||
|     if (index === STATE) return context; |     if (index === STATE) return context; | ||||||
|     if (index === 'length') return context.length(objectId, heads); |     if (index === 'length') return context.length(objectId, heads); | ||||||
|     if (index === Symbol.iterator) { |     if (index === Symbol.iterator) { | ||||||
|  | @ -246,6 +252,10 @@ const ListHandler = { | ||||||
|       target.heads = val |       target.heads = val | ||||||
|       return true |       return true | ||||||
|     } |     } | ||||||
|  |     if (index === TRACE) { | ||||||
|  |       target.trace = val | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|     if (typeof index == "string") { |     if (typeof index == "string") { | ||||||
|       throw new RangeError('list index must be a number') |       throw new RangeError('list index must be a number') | ||||||
|     } |     } | ||||||
|  | @ -356,6 +366,7 @@ const TextHandler = Object.assign({}, ListHandler, { | ||||||
|     if (index === READ_ONLY) return readonly |     if (index === READ_ONLY) return readonly | ||||||
|     if (index === FROZEN) return frozen |     if (index === FROZEN) return frozen | ||||||
|     if (index === HEADS) return heads |     if (index === HEADS) return heads | ||||||
|  |     if (index === TRACE) return target.trace | ||||||
|     if (index === STATE) return context; |     if (index === STATE) return context; | ||||||
|     if (index === 'length') return context.length(objectId, heads); |     if (index === 'length') return context.length(objectId, heads); | ||||||
|     if (index === Symbol.iterator) { |     if (index === Symbol.iterator) { | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								automerge-wasm/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								automerge-wasm/.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,7 @@ | ||||||
| /node_modules | /node_modules | ||||||
| /dev | /dev | ||||||
|  | /node | ||||||
|  | /web | ||||||
| /target | /target | ||||||
| Cargo.lock | Cargo.lock | ||||||
| yarn.lock | yarn.lock | ||||||
|  |  | ||||||
|  | @ -40,10 +40,10 @@ version = "^0.2" | ||||||
| features = ["serde-serialize", "std"] | features = ["serde-serialize", "std"] | ||||||
| 
 | 
 | ||||||
| [package.metadata.wasm-pack.profile.release] | [package.metadata.wasm-pack.profile.release] | ||||||
| # wasm-opt = false | wasm-opt = true | ||||||
| 
 | 
 | ||||||
| [package.metadata.wasm-pack.profile.profiling] | [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, | # The `web-sys` crate allows you to interact with the various browser APIs, | ||||||
| # like the DOM. | # like the DOM. | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								automerge-wasm/attr_bug.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								automerge-wasm/attr_bug.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										36
									
								
								automerge-wasm/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								automerge-wasm/index.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,2 +1,38 @@ | ||||||
|  | import { Automerge as VanillaAutomerge } from "automerge-types" | ||||||
|  | 
 | ||||||
| export * from "automerge-types" | export * from "automerge-types" | ||||||
| export { default } from "automerge-types" | export { default } from "automerge-types" | ||||||
|  | 
 | ||||||
|  | export class Automerge extends VanillaAutomerge { | ||||||
|  |   // experimental spans api - unstable!
 | ||||||
|  |   mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; | ||||||
|  |   unmark(obj: ObjID, mark: ObjID): void; | ||||||
|  |   spans(obj: ObjID): any; | ||||||
|  |   raw_spans(obj: ObjID): any; | ||||||
|  |   blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; | ||||||
|  |   attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; | ||||||
|  |   attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; | ||||||
|  | 
 | ||||||
|  |   // override old methods that return automerge
 | ||||||
|  |   clone(actor?: string): Automerge; | ||||||
|  |   fork(actor?: string): Automerge; | ||||||
|  |   forkAt(heads: Heads, actor?: string): Automerge; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type ChangeSetDeletion = { | ||||||
|  |   pos: number; | ||||||
|  |   val: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type ChangeSetAddition = { | ||||||
|  |   start: number; | ||||||
|  |   end: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type ChangeSet = { | ||||||
|  |   add: ChangeSetAddition[]; | ||||||
|  |   del: ChangeSetDeletion[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export function create(actor?: Actor): Automerge; | ||||||
|  | export function load(data: Uint8Array, actor?: Actor): Automerge; | ||||||
|  |  | ||||||
|  | @ -2,6 +2,4 @@ let wasm = require("./bindgen") | ||||||
| module.exports = wasm | module.exports = wasm | ||||||
| module.exports.load = module.exports.loadDoc | module.exports.load = module.exports.loadDoc | ||||||
| delete module.exports.loadDoc | delete module.exports.loadDoc | ||||||
| Object.defineProperty(module.exports, "__esModule", { value: true }) |  | ||||||
| module.exports.init = () => (new Promise((resolve,reject) => { resolve(module.exports) })) | module.exports.init = () => (new Promise((resolve,reject) => { resolve(module.exports) })) | ||||||
| module.exports.default = module.exports.init |  | ||||||
|  |  | ||||||
|  | @ -4,11 +4,11 @@ | ||||||
|     "Alex Good <alex@memoryandthought.me>", |     "Alex Good <alex@memoryandthought.me>", | ||||||
|     "Martin Kleppmann" |     "Martin Kleppmann" | ||||||
|   ], |   ], | ||||||
|   "name": "automerge-wasm", |   "name": "automerge-wasm-pack", | ||||||
|   "description": "wasm-bindgen bindings to the automerge rust implementation", |   "description": "wasm-bindgen bindings to the automerge rust implementation", | ||||||
|   "homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm", |   "homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm", | ||||||
|   "repository": "github:automerge/automerge-rs", |   "repository": "github:automerge/automerge-rs", | ||||||
|   "version": "0.1.6", |   "version": "0.1.8", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "files": [ |   "files": [ | ||||||
|     "README.md", |     "README.md", | ||||||
|  |  | ||||||
|  | @ -352,6 +352,15 @@ pub(crate) fn get_heads(heads: Option<Array>) -> Option<Vec<ChangeHash>> { | ||||||
|     heads.ok() |     heads.ok() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | pub(crate) fn get_js_heads(heads: JsValue) -> Result<Vec<ChangeHash>, JsValue> { | ||||||
|  |     let heads = heads.dyn_into::<Array>()?; | ||||||
|  |     heads | ||||||
|  |         .iter() | ||||||
|  |         .map(|j| j.into_serde()) | ||||||
|  |         .collect::<Result<Vec<_>, _>>() | ||||||
|  |         .map_err(to_js_err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { | pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { | ||||||
|     let keys = doc.keys(obj); |     let keys = doc.keys(obj); | ||||||
|     let map = Object::new(); |     let map = Object::new(); | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ use automerge::Patch; | ||||||
| use automerge::VecOpObserver; | use automerge::VecOpObserver; | ||||||
| use automerge::{Change, ObjId, Prop, Value, ROOT}; | use automerge::{Change, ObjId, Prop, Value, ROOT}; | ||||||
| use js_sys::{Array, Object, Uint8Array}; | use js_sys::{Array, Object, Uint8Array}; | ||||||
|  | use regex::Regex; | ||||||
| use std::convert::TryInto; | use std::convert::TryInto; | ||||||
| use wasm_bindgen::prelude::*; | use wasm_bindgen::prelude::*; | ||||||
| use wasm_bindgen::JsCast; | use wasm_bindgen::JsCast; | ||||||
|  | @ -43,8 +44,8 @@ mod sync; | ||||||
| mod value; | mod value; | ||||||
| 
 | 
 | ||||||
| use interop::{ | use interop::{ | ||||||
|     get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, to_js_err, |     get_heads, get_js_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, | ||||||
|     to_objtype, to_prop, AR, JS, |     to_js_err, to_objtype, to_prop, AR, JS, | ||||||
| }; | }; | ||||||
| use sync::SyncState; | use sync::SyncState; | ||||||
| use value::{datatype, ScalarValue}; | use value::{datatype, ScalarValue}; | ||||||
|  | @ -161,12 +162,9 @@ impl Automerge { | ||||||
|         } else { |         } else { | ||||||
|             ApplyOptions::default() |             ApplyOptions::default() | ||||||
|         }; |         }; | ||||||
|         let heads = self.doc.merge_with(&mut other.doc, options)?; |         let objs = self.doc.merge_with(&mut other.doc, options)?; | ||||||
|         let heads: Array = heads |         let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); | ||||||
|             .iter() |         Ok(objs) | ||||||
|             .map(|h| JsValue::from_str(&hex::encode(&h.0))) |  | ||||||
|             .collect(); |  | ||||||
|         Ok(heads) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn rollback(&mut self) -> f64 { |     pub fn rollback(&mut self) -> f64 { | ||||||
|  | @ -292,6 +290,18 @@ impl Automerge { | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub fn make( | ||||||
|  |         &mut self, | ||||||
|  |         obj: JsValue, | ||||||
|  |         prop: JsValue, | ||||||
|  |         value: JsValue, | ||||||
|  |         _datatype: JsValue, | ||||||
|  |     ) -> Result<JsValue, JsValue> { | ||||||
|  |         // remove this
 | ||||||
|  |         am::log!("doc.make() is depricated - please use doc.set_object() or doc.insert_object()"); | ||||||
|  |         self.put_object(obj, prop, value) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     #[wasm_bindgen(js_name = putObject)] |     #[wasm_bindgen(js_name = putObject)] | ||||||
|     pub fn put_object( |     pub fn put_object( | ||||||
|         &mut self, |         &mut self, | ||||||
|  | @ -311,7 +321,7 @@ impl Automerge { | ||||||
|     fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> { |     fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> { | ||||||
|         for (p, v) in vals { |         for (p, v) in vals { | ||||||
|             let (value, subvals) = self.import_value(&v, None)?; |             let (value, subvals) = self.import_value(&v, None)?; | ||||||
|             //let opid = self.0.set(id, p, value)?;
 |             //let opid = self.doc.set(id, p, value)?;
 | ||||||
|             let opid = match (p, value) { |             let opid = match (p, value) { | ||||||
|                 (Prop::Map(s), Value::Object(objtype)) => { |                 (Prop::Map(s), Value::Object(objtype)) => { | ||||||
|                     Some(self.doc.put_object(obj, s, objtype)?) |                     Some(self.doc.put_object(obj, s, objtype)?) | ||||||
|  | @ -551,6 +561,209 @@ impl Automerge { | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub fn mark( | ||||||
|  |         &mut self, | ||||||
|  |         obj: JsValue, | ||||||
|  |         range: JsValue, | ||||||
|  |         name: JsValue, | ||||||
|  |         value: JsValue, | ||||||
|  |         datatype: JsValue, | ||||||
|  |     ) -> Result<(), JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); | ||||||
|  |         let range = range.as_string().ok_or("range must be a string")?; | ||||||
|  |         let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; | ||||||
|  |         let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; | ||||||
|  |         let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; | ||||||
|  |         let start_sticky = &cap[1] == "("; | ||||||
|  |         let end_sticky = &cap[4] == ")"; | ||||||
|  |         let name = name | ||||||
|  |             .as_string() | ||||||
|  |             .ok_or("invalid mark name") | ||||||
|  |             .map_err(to_js_err)?; | ||||||
|  |         let value = self | ||||||
|  |             .import_scalar(&value, &datatype.as_string()) | ||||||
|  |             .ok_or_else(|| to_js_err("invalid value"))?; | ||||||
|  |         self.doc | ||||||
|  |             .mark(&obj, start, start_sticky, end, end_sticky, &name, value) | ||||||
|  |             .map_err(to_js_err)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let mark = self.import(mark)?; | ||||||
|  |         self.doc.unmark(&obj, &mark).map_err(to_js_err)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn spans(&mut self, obj: JsValue) -> Result<JsValue, JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let text: Vec<_> = self.doc.list_range(&obj, ..).collect(); | ||||||
|  |         let spans = self.doc.spans(&obj).map_err(to_js_err)?; | ||||||
|  |         let mut last_pos = 0; | ||||||
|  |         let result = Array::new(); | ||||||
|  |         for s in spans { | ||||||
|  |             let marks = Array::new(); | ||||||
|  |             for m in s.marks { | ||||||
|  |                 let mark = Array::new(); | ||||||
|  |                 mark.push(&m.0.into()); | ||||||
|  |                 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<Array, JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let spans = self.doc.raw_spans(&obj).map_err(to_js_err)?; | ||||||
|  |         let result = Array::new(); | ||||||
|  |         for s in spans { | ||||||
|  |             result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); | ||||||
|  |         } | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn blame( | ||||||
|  |         &mut self, | ||||||
|  |         obj: JsValue, | ||||||
|  |         baseline: JsValue, | ||||||
|  |         change_sets: JsValue, | ||||||
|  |     ) -> Result<Array, JsValue> { | ||||||
|  |         am::log!("doc.blame() is depricated - please use doc.attribute()"); | ||||||
|  |         self.attribute(obj, baseline, change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn attribute( | ||||||
|  |         &mut self, | ||||||
|  |         obj: JsValue, | ||||||
|  |         baseline: JsValue, | ||||||
|  |         change_sets: JsValue, | ||||||
|  |     ) -> Result<Array, JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let baseline = get_js_heads(baseline)?; | ||||||
|  |         let change_sets = change_sets.dyn_into::<Array>()?; | ||||||
|  |         let change_sets = change_sets | ||||||
|  |             .iter() | ||||||
|  |             .map(get_js_heads) | ||||||
|  |             .collect::<Result<Vec<_>, _>>()?; | ||||||
|  |         let result = self.doc.attribute(&obj, &baseline, &change_sets)?; | ||||||
|  |         let result = result | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|cs| { | ||||||
|  |                 let add = cs | ||||||
|  |                     .add | ||||||
|  |                     .iter() | ||||||
|  |                     .map::<Result<JsValue, JsValue>, _>(|range| { | ||||||
|  |                         let r = Object::new(); | ||||||
|  |                         js_set(&r, "start", range.start as f64)?; | ||||||
|  |                         js_set(&r, "end", range.end as f64)?; | ||||||
|  |                         Ok(JsValue::from(&r)) | ||||||
|  |                     }) | ||||||
|  |                     .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |                     .iter() | ||||||
|  |                     .collect::<Array>(); | ||||||
|  |                 let del = cs | ||||||
|  |                     .del | ||||||
|  |                     .iter() | ||||||
|  |                     .map::<Result<JsValue, JsValue>, _>(|d| { | ||||||
|  |                         let r = Object::new(); | ||||||
|  |                         js_set(&r, "pos", d.0 as f64)?; | ||||||
|  |                         js_set(&r, "val", &d.1)?; | ||||||
|  |                         Ok(JsValue::from(&r)) | ||||||
|  |                     }) | ||||||
|  |                     .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |                     .iter() | ||||||
|  |                     .collect::<Array>(); | ||||||
|  |                 let obj = Object::new(); | ||||||
|  |                 js_set(&obj, "add", add)?; | ||||||
|  |                 js_set(&obj, "del", del)?; | ||||||
|  |                 Ok(obj.into()) | ||||||
|  |             }) | ||||||
|  |             .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |             .iter() | ||||||
|  |             .collect::<Array>(); | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn attribute2( | ||||||
|  |         &mut self, | ||||||
|  |         obj: JsValue, | ||||||
|  |         baseline: JsValue, | ||||||
|  |         change_sets: JsValue, | ||||||
|  |     ) -> Result<Array, JsValue> { | ||||||
|  |         let obj = self.import(obj)?; | ||||||
|  |         let baseline = get_js_heads(baseline)?; | ||||||
|  |         let change_sets = change_sets.dyn_into::<Array>()?; | ||||||
|  |         let change_sets = change_sets | ||||||
|  |             .iter() | ||||||
|  |             .map(get_js_heads) | ||||||
|  |             .collect::<Result<Vec<_>, _>>()?; | ||||||
|  |         let result = self.doc.attribute2(&obj, &baseline, &change_sets)?; | ||||||
|  |         let result = result | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|cs| { | ||||||
|  |                 let add = cs | ||||||
|  |                     .add | ||||||
|  |                     .iter() | ||||||
|  |                     .map::<Result<JsValue, JsValue>, _>(|a| { | ||||||
|  |                         let r = Object::new(); | ||||||
|  |                         js_set(&r, "actor", &self.doc.actor_to_str(a.actor))?; | ||||||
|  |                         js_set(&r, "start", a.range.start as f64)?; | ||||||
|  |                         js_set(&r, "end", a.range.end as f64)?; | ||||||
|  |                         Ok(JsValue::from(&r)) | ||||||
|  |                     }) | ||||||
|  |                     .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |                     .iter() | ||||||
|  |                     .collect::<Array>(); | ||||||
|  |                 let del = cs | ||||||
|  |                     .del | ||||||
|  |                     .iter() | ||||||
|  |                     .map::<Result<JsValue, JsValue>, _>(|d| { | ||||||
|  |                         let r = Object::new(); | ||||||
|  |                         js_set(&r, "actor", &self.doc.actor_to_str(d.actor))?; | ||||||
|  |                         js_set(&r, "pos", d.pos as f64)?; | ||||||
|  |                         js_set(&r, "val", &d.span)?; | ||||||
|  |                         Ok(JsValue::from(&r)) | ||||||
|  |                     }) | ||||||
|  |                     .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |                     .iter() | ||||||
|  |                     .collect::<Array>(); | ||||||
|  |                 let obj = Object::new(); | ||||||
|  |                 js_set(&obj, "add", add)?; | ||||||
|  |                 js_set(&obj, "del", del)?; | ||||||
|  |                 Ok(obj.into()) | ||||||
|  |             }) | ||||||
|  |             .collect::<Result<Vec<JsValue>, JsValue>>()? | ||||||
|  |             .iter() | ||||||
|  |             .collect::<Array>(); | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub fn save(&mut self) -> Uint8Array { |     pub fn save(&mut self) -> Uint8Array { | ||||||
|         self.ensure_transaction_closed(); |         self.ensure_transaction_closed(); | ||||||
|         Uint8Array::from(self.doc.save().as_slice()) |         Uint8Array::from(self.doc.save().as_slice()) | ||||||
|  |  | ||||||
							
								
								
									
										188
									
								
								automerge-wasm/test/attribute.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								automerge-wasm/test/attribute.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,188 @@ | ||||||
|  | import { describe, it } from 'mocha'; | ||||||
|  | //@ts-ignore
 | ||||||
|  | import assert from 'assert' | ||||||
|  | //@ts-ignore
 | ||||||
|  | import { BloomFilter } from './helpers/sync' | ||||||
|  | import { create, load, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' | ||||||
|  | import { DecodedSyncMessage, Hash } from '..' | ||||||
|  | 
 | ||||||
|  | describe('Automerge', () => { | ||||||
|  |   describe('attribute', () => { | ||||||
|  |     it('should be able to attribute text segments on change sets', () => { | ||||||
|  |       let doc1 = create() | ||||||
|  |       let text = doc1.putObject("_root", "notes","hello little world") | ||||||
|  |       let h1 = doc1.getHeads(); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc1.fork(); | ||||||
|  |       doc2.splice(text, 5, 7, " big"); | ||||||
|  |       doc2.text(text) | ||||||
|  |       let h2 = doc2.getHeads(); | ||||||
|  |       assert.deepEqual(doc2.text(text), "hello big world") | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc1.fork(); | ||||||
|  |       doc3.splice(text, 0, 0, "Well, "); | ||||||
|  |       let h3 = doc3.getHeads(); | ||||||
|  |       assert.deepEqual(doc3.text(text), "Well, hello little world") | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc2) | ||||||
|  |       doc1.merge(doc3) | ||||||
|  |       assert.deepEqual(doc1.text(text), "Well, hello big world") | ||||||
|  |       let attribute = doc1.attribute(text, h1, [h2, h3]) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(attribute, [ | ||||||
|  |         { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, | ||||||
|  |         { add: [ { start: 0,  end: 6  } ], del: [] } | ||||||
|  |       ]) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should be able to hand complex attribute change sets', () => { | ||||||
|  |       let doc1 = create("aaaa") | ||||||
|  |       let text = doc1.putObject("_root", "notes","AAAAAA") | ||||||
|  |       let h1 = doc1.getHeads(); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc1.fork("bbbb"); | ||||||
|  |       doc2.splice(text, 0, 2, "BB"); | ||||||
|  |       doc2.commit() | ||||||
|  |       doc2.splice(text, 2, 2, "BB"); | ||||||
|  |       doc2.commit() | ||||||
|  |       doc2.splice(text, 6, 0, "BB"); | ||||||
|  |       doc2.commit() | ||||||
|  |       let h2 = doc2.getHeads(); | ||||||
|  |       assert.deepEqual(doc2.text(text), "BBBBAABB") | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc1.fork("cccc"); | ||||||
|  |       doc3.splice(text, 1, 1, "C"); | ||||||
|  |       doc3.commit() | ||||||
|  |       doc3.splice(text, 3, 1, "C"); | ||||||
|  |       doc3.commit() | ||||||
|  |       doc3.splice(text, 5, 1, "C"); | ||||||
|  |       doc3.commit() | ||||||
|  |       let h3 = doc3.getHeads(); | ||||||
|  |       // with tombstones its 
 | ||||||
|  |       // AC.AC.AC.
 | ||||||
|  |       assert.deepEqual(doc3.text(text), "ACACAC") | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc2) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(doc1.attribute(text, h1, [h2]), [ | ||||||
|  |         { add: [ {start:0, end: 4}, { start: 6, end: 8 } ], del: [ { pos: 4, val: 'AAAA' } ] }, | ||||||
|  |       ]) | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc3) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(doc1.text(text), "BBBBCCACBB") | ||||||
|  | 
 | ||||||
|  |       // with tombstones its 
 | ||||||
|  |       // BBBB.C..C.AC.BB
 | ||||||
|  |       assert.deepEqual(doc1.attribute(text, h1, [h2,h3]), [   | ||||||
|  |         { add: [ {start:0, end: 4}, { start: 8, end: 10 } ], del: [ { pos: 4, val: 'A' }, { pos: 5, val: 'AA' }, { pos: 6, val: 'A' } ] }, | ||||||
|  |         { add: [ {start:4, end: 6}, { start: 7, end: 8 } ], del: [ { pos: 5, val: 'A' }, { pos: 6, val: 'A' }, { pos: 8, val: 'A' } ] } | ||||||
|  |       ]) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should not include attribution of text that is inserted and deleted only within change sets', () => { | ||||||
|  |       let doc1 = create() | ||||||
|  |       let text = doc1.putObject("_root", "notes","hello little world") | ||||||
|  |       let h1 = doc1.getHeads(); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc1.fork(); | ||||||
|  |       doc2.splice(text, 5, 7, " big"); | ||||||
|  |       doc2.splice(text, 9, 0, " bad"); | ||||||
|  |       doc2.splice(text, 9, 4) | ||||||
|  |       doc2.text(text) | ||||||
|  |       let h2 = doc2.getHeads(); | ||||||
|  |       assert.deepEqual(doc2.text(text), "hello big world") | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc1.fork(); | ||||||
|  |       doc3.splice(text, 0, 0, "Well, HI THERE"); | ||||||
|  |       doc3.splice(text, 6, 8, "") | ||||||
|  |       let h3 = doc3.getHeads(); | ||||||
|  |       assert.deepEqual(doc3.text(text), "Well, hello little world") | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc2) | ||||||
|  |       doc1.merge(doc3) | ||||||
|  |       assert.deepEqual(doc1.text(text), "Well, hello big world") | ||||||
|  |       let attribute = doc1.attribute(text, h1, [h2, h3]) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(attribute, [ | ||||||
|  |         { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, | ||||||
|  |         { add: [ { start: 0,  end: 6  } ], del: [] } | ||||||
|  |       ]) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |   }) | ||||||
|  |   describe('attribute2', () => { | ||||||
|  |     it('should be able to attribute text segments on change sets', () => { | ||||||
|  |       let doc1 = create("aaaa") | ||||||
|  |       let text = doc1.putObject("_root", "notes","hello little world") | ||||||
|  |       let h1 = doc1.getHeads(); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc1.fork("bbbb"); | ||||||
|  |       doc2.splice(text, 5, 7, " big"); | ||||||
|  |       doc2.text(text) | ||||||
|  |       let h2 = doc2.getHeads(); | ||||||
|  |       assert.deepEqual(doc2.text(text), "hello big world") | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc1.fork("cccc"); | ||||||
|  |       doc3.splice(text, 0, 0, "Well, "); | ||||||
|  |       let doc4 = doc3.fork("dddd") | ||||||
|  |       doc4.splice(text, 0, 0, "Gee, "); | ||||||
|  |       let h3 = doc4.getHeads(); | ||||||
|  |       assert.deepEqual(doc4.text(text), "Gee, Well, hello little world") | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc2) | ||||||
|  |       doc1.merge(doc4) | ||||||
|  |       assert.deepEqual(doc1.text(text), "Gee, Well, hello big world") | ||||||
|  |       let attribute = doc1.attribute2(text, h1, [h2, h3]) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(attribute, [ | ||||||
|  |         { add: [ { actor: "bbbb", start: 16, end: 20 } ], del: [ { actor: "bbbb", pos: 20, val: ' little' } ] }, | ||||||
|  |         { add: [ { actor: "dddd", start:0, end: 5 }, { actor: "cccc", start: 5,  end: 11  } ], del: [] } | ||||||
|  |       ]) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should not include attribution of text that is inserted and deleted only within change sets', () => { | ||||||
|  |       let doc1 = create("aaaa") | ||||||
|  |       let text = doc1.putObject("_root", "notes","hello little world") | ||||||
|  |       let h1 = doc1.getHeads(); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc1.fork("bbbb"); | ||||||
|  |       doc2.splice(text, 5, 7, " big"); | ||||||
|  |       doc2.splice(text, 9, 0, " bad"); | ||||||
|  |       doc2.splice(text, 9, 4) | ||||||
|  |       doc2.text(text) | ||||||
|  |       let h2 = doc2.getHeads(); | ||||||
|  |       assert.deepEqual(doc2.text(text), "hello big world") | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc1.fork("cccc"); | ||||||
|  |       doc3.splice(text, 0, 0, "Well, HI THERE"); | ||||||
|  |       doc3.splice(text, 6, 8, "") | ||||||
|  |       let h3 = doc3.getHeads(); | ||||||
|  |       assert.deepEqual(doc3.text(text), "Well, hello little world") | ||||||
|  | 
 | ||||||
|  |       doc1.merge(doc2) | ||||||
|  |       doc1.merge(doc3) | ||||||
|  |       assert.deepEqual(doc1.text(text), "Well, hello big world") | ||||||
|  |       let attribute = doc1.attribute2(text, h1, [h2, h3]) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(attribute, [ | ||||||
|  |         { add: [ { start: 11, end: 15, actor: "bbbb" } ], del: [ { pos: 15, val: ' little', actor: "bbbb" } ] }, | ||||||
|  |         { add: [ { start: 0,  end: 6,  actor: "cccc" } ], del: [] } | ||||||
|  |       ]) | ||||||
|  | 
 | ||||||
|  |       let h4 = doc1.getHeads() | ||||||
|  | 
 | ||||||
|  |       doc3.splice(text, 24, 0, "!!!") | ||||||
|  |       doc1.merge(doc3) | ||||||
|  | 
 | ||||||
|  |       let h5 = doc1.getHeads() | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(doc1.text(text), "Well, hello big world!!!") | ||||||
|  |       attribute = doc1.attribute2(text, h4, [h5]) | ||||||
|  | 
 | ||||||
|  |       assert.deepEqual(attribute, [ | ||||||
|  |         { add: [ { start: 21, end: 24, actor: "cccc" } ], del: [] }, | ||||||
|  |       ]) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										201
									
								
								automerge-wasm/test/marks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								automerge-wasm/test/marks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | ||||||
|  | import { describe, it } from 'mocha'; | ||||||
|  | //@ts-ignore
 | ||||||
|  | import assert from 'assert' | ||||||
|  | //@ts-ignore
 | ||||||
|  | import { create, load, Automerge, encodeChange, decodeChange } from '..' | ||||||
|  | 
 | ||||||
|  | describe('Automerge', () => { | ||||||
|  |   describe('marks', () => { | ||||||
|  |     it('should handle marks [..]', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "[3..6]", "bold" , true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); | ||||||
|  |       doc.insert(list, 6, "A") | ||||||
|  |       doc.insert(list, 3, "A") | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should handle marks [..] at the beginning of a string', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "[0..3]", "bold", true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc.fork() | ||||||
|  |       doc2.insert(list, 0, "A") | ||||||
|  |       doc2.insert(list, 4, "B") | ||||||
|  |       doc.merge(doc2) | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should handle marks [..] with splice', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "[0..3]", "bold", true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc.fork() | ||||||
|  |       doc2.splice(list, 0, 2, "AAA") | ||||||
|  |       doc2.splice(list, 4, 0, "BBB") | ||||||
|  |       doc.merge(doc2) | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should handle marks across multiple forks', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "[0..3]", "bold", true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); | ||||||
|  | 
 | ||||||
|  |       let doc2 = doc.fork() | ||||||
|  |       doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark.
 | ||||||
|  | 
 | ||||||
|  |       let doc3 = doc.fork() | ||||||
|  |       doc3.insert(list, 0, "AAA") // should not be included in mark.
 | ||||||
|  | 
 | ||||||
|  |       doc.merge(doc2) | ||||||
|  |       doc.merge(doc3) | ||||||
|  | 
 | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     it('should handle marks with deleted ends [..]', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  | 
 | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "[3..6]", "bold" , true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); | ||||||
|  |       doc.delete(list,5); | ||||||
|  |       doc.delete(list,5); | ||||||
|  |       doc.delete(list,2); | ||||||
|  |       doc.delete(list,2); | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) | ||||||
|  |       doc.insert(list, 3, "A") | ||||||
|  |       doc.insert(list, 2, "A") | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should handle sticky marks (..)', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "(3..6)", "bold" , true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); | ||||||
|  |       doc.insert(list, 6, "A") | ||||||
|  |       doc.insert(list, 3, "A") | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it('should handle sticky marks with deleted ends (..)', () => { | ||||||
|  |       let doc = create() | ||||||
|  |       let list = doc.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "aaabbbccc") | ||||||
|  |       doc.mark(list, "(3..6)", "bold" , true) | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); | ||||||
|  |       doc.delete(list,5); | ||||||
|  |       doc.delete(list,5); | ||||||
|  |       doc.delete(list,2); | ||||||
|  |       doc.delete(list,2); | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) | ||||||
|  |       doc.insert(list, 3, "A") | ||||||
|  |       doc.insert(list, 2, "A") | ||||||
|  |       spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) | ||||||
|  | 
 | ||||||
|  |       // make sure save/load can handle marks
 | ||||||
|  | 
 | ||||||
|  |       let doc2 = load(doc.save()) | ||||||
|  |       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.putObject("_root", "list", "") | ||||||
|  |       doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") | ||||||
|  |       doc.mark(list, "[0..37]", "bold" , true) | ||||||
|  |       doc.mark(list, "[4..19]", "itallic" , true) | ||||||
|  |       doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") | ||||||
|  |       doc.commit("marks"); | ||||||
|  |       let spans = doc.spans(list); | ||||||
|  |       assert.deepStrictEqual(spans, | ||||||
|  |         [ | ||||||
|  |           [ [ 'bold', 'boolean', true ] ], | ||||||
|  |           'the ', | ||||||
|  |           [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], | ||||||
|  |           'quick ', | ||||||
|  |           [ | ||||||
|  |             [ 'bold', 'boolean', true ], | ||||||
|  |             [ 'comment', 'str', 'foxes are my favorite animal!' ], | ||||||
|  |             [ 'itallic', 'boolean', true ] | ||||||
|  |           ], | ||||||
|  |           'fox', | ||||||
|  |           [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], | ||||||
|  |           ' jumps', | ||||||
|  |           [ [ 'bold', 'boolean', true ] ], | ||||||
|  |           ' over the lazy dog', | ||||||
|  |           [], | ||||||
|  |         ] | ||||||
|  |       ) | ||||||
|  |       let text = doc.text(list); | ||||||
|  |       assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); | ||||||
|  |       let raw_spans = doc.raw_spans(list); | ||||||
|  |       assert.deepStrictEqual(raw_spans, | ||||||
|  |         [ | ||||||
|  |           { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, | ||||||
|  |           { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, | ||||||
|  |           { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |       doc.unmark(list, "41@aabbcc") | ||||||
|  |       raw_spans = doc.raw_spans(list); | ||||||
|  |       assert.deepStrictEqual(raw_spans, | ||||||
|  |         [ | ||||||
|  |           { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, | ||||||
|  |           { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } | ||||||
|  |         ]); | ||||||
|  |       // mark sure encode decode can handle marks
 | ||||||
|  | 
 | ||||||
|  |       doc.unmark(list, "39@aabbcc") | ||||||
|  |       raw_spans = doc.raw_spans(list); | ||||||
|  |       assert.deepStrictEqual(raw_spans, | ||||||
|  |         [ | ||||||
|  |           { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |       let all = doc.getChanges([]) | ||||||
|  |       let decoded = all.map((c) => decodeChange(c)) | ||||||
|  |       let encoded = decoded.map((c) => encodeChange(c)) | ||||||
|  |       let doc2 = create(); | ||||||
|  |       doc2.applyChanges(encoded) | ||||||
|  | 
 | ||||||
|  |       assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) | ||||||
|  |       assert.deepStrictEqual(doc.save(), doc2.save()) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | @ -396,6 +396,8 @@ describe('Automerge', () => { | ||||||
|       assert.deepEqual(change2, null) |       assert.deepEqual(change2, null) | ||||||
|       if (change1 === null) { throw new RangeError("change1 should not be null") } |       if (change1 === null) { throw new RangeError("change1 should not be null") } | ||||||
|       assert.deepEqual(decodeChange(change1).hash, head1[0]) |       assert.deepEqual(decodeChange(change1).hash, head1[0]) | ||||||
|  |       assert.deepEqual(head1.some((hash) => doc1.getChangeByHash(hash) === null), false) | ||||||
|  |       assert.deepEqual(head2.some((hash) => doc1.getChangeByHash(hash) === null), true) | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     it('recursive sets are possible', () => { |     it('recursive sets are possible', () => { | ||||||
|  | @ -1654,7 +1656,7 @@ describe('Automerge', () => { | ||||||
|         if (m2 === null) { throw new RangeError("message should not be null") } |         if (m2 === null) { throw new RangeError("message should not be null") } | ||||||
|         n1.receiveSyncMessage(s1, m2) |         n1.receiveSyncMessage(s1, m2) | ||||||
|         n2.receiveSyncMessage(s2, m1) |         n2.receiveSyncMessage(s2, m1) | ||||||
| 
 |          | ||||||
|         // Then n1 and n2 send each other their changes, except for the false positive
 |         // Then n1 and n2 send each other their changes, except for the false positive
 | ||||||
|         m1 = n1.generateSyncMessage(s1) |         m1 = n1.generateSyncMessage(s1) | ||||||
|         m2 = n2.generateSyncMessage(s2) |         m2 = n2.generateSyncMessage(s2) | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								automerge-wasm/types/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								automerge-wasm/types/index.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -205,5 +205,5 @@ export class SyncState { | ||||||
|   readonly sharedHeads: Heads; |   readonly sharedHeads: Heads; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function init (): Promise<API>; |  | ||||||
| export function init (): Promise<API>; | export function init (): Promise<API>; | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -47,7 +47,3 @@ export function init() { | ||||||
|   })) |   })) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // depricating default export
 |  | ||||||
| export default function() { |  | ||||||
|   return init() |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -3,13 +3,14 @@ use std::ops::RangeBounds; | ||||||
| use crate::exid::ExId; | use crate::exid::ExId; | ||||||
| use crate::op_observer::OpObserver; | use crate::op_observer::OpObserver; | ||||||
| use crate::transaction::{CommitOptions, Transactable}; | use crate::transaction::{CommitOptions, Transactable}; | ||||||
|  | use crate::Parents; | ||||||
| use crate::{ | use crate::{ | ||||||
|     sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, |     query, transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, | ||||||
|     Parents, ScalarValue, |     Prop, Value, Values, | ||||||
| }; | }; | ||||||
| use crate::{ | use crate::{ | ||||||
|     transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, |     sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, | ||||||
|     Value, Values, |     ScalarValue, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /// An automerge document that automatically manages transactions.
 | /// An automerge document that automatically manages transactions.
 | ||||||
|  | @ -33,6 +34,11 @@ impl AutoCommit { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // FIXME : temp
 | ||||||
|  |     pub fn actor_to_str(&self, actor: usize) -> String { | ||||||
|  |         self.doc.ops.m.actors.cache[actor].to_hex_string() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Get the inner document.
 |     /// Get the inner document.
 | ||||||
|     #[doc(hidden)] |     #[doc(hidden)] | ||||||
|     pub fn document(&mut self) -> &Automerge { |     pub fn document(&mut self) -> &Automerge { | ||||||
|  | @ -404,6 +410,37 @@ impl Transactable for AutoCommit { | ||||||
|         tx.insert(&mut self.doc, obj.as_ref(), index, value) |         tx.insert(&mut self.doc, obj.as_ref(), index, value) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(clippy::too_many_arguments)] | ||||||
|  |     fn mark<O: AsRef<ExId>>( | ||||||
|  |         &mut self, | ||||||
|  |         obj: O, | ||||||
|  |         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 unmark<O: AsRef<ExId>>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { | ||||||
|  |         self.ensure_transaction_open(); | ||||||
|  |         let tx = self.transaction.as_mut().unwrap(); | ||||||
|  |         tx.unmark(&mut self.doc, obj, mark) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fn insert_object<O: AsRef<ExId>>( |     fn insert_object<O: AsRef<ExId>>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         obj: O, |         obj: O, | ||||||
|  | @ -462,6 +499,32 @@ impl Transactable for AutoCommit { | ||||||
|         self.doc.text_at(obj, heads) |         self.doc.text_at(obj, heads) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::Span<'_>>, AutomergeError> { | ||||||
|  |         self.doc.spans(obj) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn raw_spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::SpanInfo>, AutomergeError> { | ||||||
|  |         self.doc.raw_spans(obj) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn attribute<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet>, AutomergeError> { | ||||||
|  |         self.doc.attribute(obj, baseline, change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn attribute2<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet2>, AutomergeError> { | ||||||
|  |         self.doc.attribute2(obj, baseline, change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // TODO - I need to return these OpId's here **only** to get
 |     // TODO - I need to return these OpId's here **only** to get
 | ||||||
|     // the legacy conflicts format of { [opid]: value }
 |     // the legacy conflicts format of { [opid]: value }
 | ||||||
|     // Something better?
 |     // Something better?
 | ||||||
|  |  | ||||||
|  | @ -452,6 +452,28 @@ impl Automerge { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub(crate) fn exid_to_obj_tmp_unchecked(&self, id: &ExId) -> Result<ObjId, AutomergeError> { | ||||||
|  |         match id { | ||||||
|  |             ExId::Root => Ok(ObjId::root()), | ||||||
|  |             ExId::Id(ctr, actor, idx) => { | ||||||
|  |                 // do a direct get here b/c this could be foriegn and not be within the array
 | ||||||
|  |                 // bounds
 | ||||||
|  |                 if self.ops.m.actors.cache.get(*idx) == Some(actor) { | ||||||
|  |                     Ok(ObjId(OpId(*ctr, *idx))) | ||||||
|  |                 } else { | ||||||
|  |                     // FIXME - make a real error
 | ||||||
|  |                     let idx = self | ||||||
|  |                         .ops | ||||||
|  |                         .m | ||||||
|  |                         .actors | ||||||
|  |                         .lookup(actor) | ||||||
|  |                         .ok_or(AutomergeError::Fail)?; | ||||||
|  |                     Ok(ObjId(OpId(*ctr, idx))) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub(crate) fn id_to_exid(&self, id: OpId) -> ExId { |     pub(crate) fn id_to_exid(&self, id: OpId) -> ExId { | ||||||
|         self.ops.id_to_exid(id) |         self.ops.id_to_exid(id) | ||||||
|     } |     } | ||||||
|  | @ -491,6 +513,71 @@ impl Automerge { | ||||||
|         Ok(buffer) |         Ok(buffer) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub fn spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::Span<'_>>, AutomergeError> { | ||||||
|  |         let obj = self.exid_to_obj(obj.as_ref())?; | ||||||
|  |         let mut query = self.ops.search(&obj, query::Spans::new()); | ||||||
|  |         query.check_marks(); | ||||||
|  |         Ok(query.spans) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn attribute<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet>, AutomergeError> { | ||||||
|  |         let obj = self.exid_to_obj(obj.as_ref())?; | ||||||
|  |         let baseline = self.clock_at(baseline); | ||||||
|  |         let change_sets: Vec<Clock> = change_sets | ||||||
|  |             .iter() | ||||||
|  |             .map(|p| self.clock_at(p).unwrap()) | ||||||
|  |             .collect(); | ||||||
|  |         let mut query = self | ||||||
|  |             .ops | ||||||
|  |             .search(&obj, query::Attribute::new(baseline.unwrap(), change_sets)); | ||||||
|  |         query.finish(); | ||||||
|  |         Ok(query.change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn attribute2<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet2>, AutomergeError> { | ||||||
|  |         let obj = self.exid_to_obj(obj.as_ref())?; | ||||||
|  |         let baseline = self.clock_at(baseline); | ||||||
|  |         let change_sets: Vec<Clock> = change_sets | ||||||
|  |             .iter() | ||||||
|  |             .map(|p| self.clock_at(p).unwrap()) | ||||||
|  |             .collect(); | ||||||
|  |         let mut query = self | ||||||
|  |             .ops | ||||||
|  |             .search(&obj, query::Attribute2::new(baseline.unwrap(), change_sets)); | ||||||
|  |         query.finish(); | ||||||
|  |         Ok(query.change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn raw_spans<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |     ) -> Result<Vec<query::SpanInfo>, AutomergeError> { | ||||||
|  |         let obj = self.exid_to_obj(obj.as_ref())?; | ||||||
|  |         let query = self.ops.search(&obj, query::RawSpans::new()); | ||||||
|  |         let result = query | ||||||
|  |             .spans | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|s| query::SpanInfo { | ||||||
|  |                 id: self.id_to_exid(s.id), | ||||||
|  |                 start: s.start, | ||||||
|  |                 end: s.end, | ||||||
|  |                 span_type: s.name, | ||||||
|  |                 value: s.value, | ||||||
|  |             }) | ||||||
|  |             .collect(); | ||||||
|  |         Ok(result) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // TODO - I need to return these OpId's here **only** to get
 |     // TODO - I need to return these OpId's here **only** to get
 | ||||||
|     // the legacy conflicts format of { [opid]: value }
 |     // the legacy conflicts format of { [opid]: value }
 | ||||||
|     // Something better?
 |     // Something better?
 | ||||||
|  | @ -1041,6 +1128,8 @@ impl Automerge { | ||||||
|                 OpType::Put(value) => format!("{}", value), |                 OpType::Put(value) => format!("{}", value), | ||||||
|                 OpType::Make(obj) => format!("make({})", obj), |                 OpType::Make(obj) => format!("make({})", obj), | ||||||
|                 OpType::Increment(obj) => format!("inc({})", obj), |                 OpType::Increment(obj) => format!("inc({})", obj), | ||||||
|  |                 OpType::MarkBegin(m) => format!("mark({}={})", m.name, m.value), | ||||||
|  |                 OpType::MarkEnd(_) => "/mark".into(), | ||||||
|                 OpType::Delete => format!("del{}", 0), |                 OpType::Delete => format!("del{}", 0), | ||||||
|             }; |             }; | ||||||
|             let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); |             let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); | ||||||
|  |  | ||||||
|  | @ -137,6 +137,15 @@ impl<'a> Iterator for OperationIterator<'a> { | ||||||
|             Action::MakeTable => OpType::Make(ObjType::Table), |             Action::MakeTable => OpType::Make(ObjType::Table), | ||||||
|             Action::Del => OpType::Delete, |             Action::Del => OpType::Delete, | ||||||
|             Action::Inc => OpType::Increment(value.to_i64()?), |             Action::Inc => OpType::Increment(value.to_i64()?), | ||||||
|  |             Action::MarkBegin => { | ||||||
|  |                 // mark has 3 things in the val column
 | ||||||
|  |                 let name = value.as_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 { |         Some(amp::Op { | ||||||
|             action, |             action, | ||||||
|  | @ -178,6 +187,15 @@ impl<'a> Iterator for DocOpIterator<'a> { | ||||||
|             Action::MakeTable => OpType::Make(ObjType::Table), |             Action::MakeTable => OpType::Make(ObjType::Table), | ||||||
|             Action::Del => OpType::Delete, |             Action::Del => OpType::Delete, | ||||||
|             Action::Inc => OpType::Increment(value.to_i64()?), |             Action::Inc => OpType::Increment(value.to_i64()?), | ||||||
|  |             Action::MarkBegin => { | ||||||
|  |                 // mark has 3 things in the val column
 | ||||||
|  |                 let name = value.as_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 { |         Some(DocOp { | ||||||
|             actor, |             actor, | ||||||
|  | @ -1082,6 +1100,16 @@ impl DocOpEncoder { | ||||||
|                     self.val.append_null(); |                     self.val.append_null(); | ||||||
|                     Action::Del |                     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) => { |                 amp::OpType::Make(kind) => { | ||||||
|                     self.val.append_null(); |                     self.val.append_null(); | ||||||
|                     match kind { |                     match kind { | ||||||
|  | @ -1191,6 +1219,16 @@ impl ColumnEncoder { | ||||||
|                 self.val.append_null(); |                 self.val.append_null(); | ||||||
|                 Action::Del |                 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) => { |             OpType::Make(kind) => { | ||||||
|                 self.val.append_null(); |                 self.val.append_null(); | ||||||
|                 match kind { |                 match kind { | ||||||
|  | @ -1296,8 +1334,11 @@ pub(crate) enum Action { | ||||||
|     MakeText, |     MakeText, | ||||||
|     Inc, |     Inc, | ||||||
|     MakeTable, |     MakeTable, | ||||||
|  |     MarkBegin, | ||||||
|  |     Unused, // final bit is used to mask `Make` actions
 | ||||||
|  |     MarkEnd, | ||||||
| } | } | ||||||
| const ACTIONS: [Action; 7] = [ | const ACTIONS: [Action; 10] = [ | ||||||
|     Action::MakeMap, |     Action::MakeMap, | ||||||
|     Action::Set, |     Action::Set, | ||||||
|     Action::MakeList, |     Action::MakeList, | ||||||
|  | @ -1305,6 +1346,9 @@ const ACTIONS: [Action; 7] = [ | ||||||
|     Action::MakeText, |     Action::MakeText, | ||||||
|     Action::Inc, |     Action::Inc, | ||||||
|     Action::MakeTable, |     Action::MakeTable, | ||||||
|  |     Action::MarkBegin, | ||||||
|  |     Action::Unused, | ||||||
|  |     Action::MarkEnd, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| impl Decodable for Action { | impl Decodable for Action { | ||||||
|  |  | ||||||
|  | @ -50,6 +50,12 @@ impl Serialize for Op { | ||||||
|             OpType::Increment(n) => op.serialize_field("value", &n)?, |             OpType::Increment(n) => op.serialize_field("value", &n)?, | ||||||
|             OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?, |             OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?, | ||||||
|             OpType::Put(value) => op.serialize_field("value", &value)?, |             OpType::Put(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)?; |         op.serialize_field("pred", &self.pred)?; | ||||||
|  | @ -71,6 +77,8 @@ pub(crate) enum RawOpType { | ||||||
|     Del, |     Del, | ||||||
|     Inc, |     Inc, | ||||||
|     Set, |     Set, | ||||||
|  |     MarkBegin, | ||||||
|  |     MarkEnd, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Serialize for RawOpType { | impl Serialize for RawOpType { | ||||||
|  | @ -86,6 +94,8 @@ impl Serialize for RawOpType { | ||||||
|             RawOpType::Del => "del", |             RawOpType::Del => "del", | ||||||
|             RawOpType::Inc => "inc", |             RawOpType::Inc => "inc", | ||||||
|             RawOpType::Set => "set", |             RawOpType::Set => "set", | ||||||
|  |             RawOpType::MarkBegin => "mark_begin", | ||||||
|  |             RawOpType::MarkEnd => "mark_end", | ||||||
|         }; |         }; | ||||||
|         serializer.serialize_str(s) |         serializer.serialize_str(s) | ||||||
|     } |     } | ||||||
|  | @ -117,6 +127,8 @@ impl<'de> Deserialize<'de> for RawOpType { | ||||||
|             "del" => Ok(RawOpType::Del), |             "del" => Ok(RawOpType::Del), | ||||||
|             "inc" => Ok(RawOpType::Inc), |             "inc" => Ok(RawOpType::Inc), | ||||||
|             "set" => Ok(RawOpType::Set), |             "set" => Ok(RawOpType::Set), | ||||||
|  |             "mark_begin" => Ok(RawOpType::MarkBegin), | ||||||
|  |             "mark_end" => Ok(RawOpType::MarkEnd), | ||||||
|             other => Err(Error::unknown_variant(other, VARIANTS)), |             other => Err(Error::unknown_variant(other, VARIANTS)), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -189,6 +201,30 @@ impl<'de> Deserialize<'de> for Op { | ||||||
|                     RawOpType::MakeList => OpType::Make(ObjType::List), |                     RawOpType::MakeList => OpType::Make(ObjType::List), | ||||||
|                     RawOpType::MakeText => OpType::Make(ObjType::Text), |                     RawOpType::MakeText => OpType::Make(ObjType::Text), | ||||||
|                     RawOpType::Del => OpType::Delete, |                     RawOpType::Del => OpType::Delete, | ||||||
|  |                     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 => { |                     RawOpType::Set => { | ||||||
|                         let value = if let Some(datatype) = datatype { |                         let value = if let Some(datatype) = datatype { | ||||||
|                             let raw_value = value |                             let raw_value = value | ||||||
|  |  | ||||||
|  | @ -15,6 +15,8 @@ impl Serialize for OpType { | ||||||
|             OpType::Make(ObjType::Table) => RawOpType::MakeTable, |             OpType::Make(ObjType::Table) => RawOpType::MakeTable, | ||||||
|             OpType::Make(ObjType::List) => RawOpType::MakeList, |             OpType::Make(ObjType::List) => RawOpType::MakeList, | ||||||
|             OpType::Make(ObjType::Text) => RawOpType::MakeText, |             OpType::Make(ObjType::Text) => RawOpType::MakeText, | ||||||
|  |             OpType::MarkBegin(_) => RawOpType::MarkBegin, | ||||||
|  |             OpType::MarkEnd(_) => RawOpType::MarkEnd, | ||||||
|             OpType::Delete => RawOpType::Del, |             OpType::Delete => RawOpType::Del, | ||||||
|             OpType::Increment(_) => RawOpType::Inc, |             OpType::Increment(_) => RawOpType::Inc, | ||||||
|             OpType::Put(_) => RawOpType::Set, |             OpType::Put(_) => RawOpType::Set, | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
|  | use crate::exid::ExId; | ||||||
| use crate::op_tree::{OpSetMetadata, OpTreeNode}; | use crate::op_tree::{OpSetMetadata, OpTreeNode}; | ||||||
| use crate::types::{Clock, Counter, Key, Op, OpId, OpType, ScalarValue}; | use crate::types::{Clock, Counter, Key, Op, OpId, OpType, ScalarValue}; | ||||||
| use fxhash::FxBuildHasher; | use fxhash::FxBuildHasher; | ||||||
|  | use serde::Serialize; | ||||||
| use std::cmp::Ordering; | use std::cmp::Ordering; | ||||||
| use std::collections::{HashMap, HashSet}; | use std::collections::{HashMap, HashSet}; | ||||||
| use std::fmt::Debug; | use std::fmt::Debug; | ||||||
| 
 | 
 | ||||||
|  | mod attribute; | ||||||
|  | mod attribute2; | ||||||
| mod elem_id_pos; | mod elem_id_pos; | ||||||
| mod insert; | mod insert; | ||||||
| mod keys; | mod keys; | ||||||
|  | @ -22,9 +26,13 @@ mod nth_at; | ||||||
| mod opid; | mod opid; | ||||||
| mod prop; | mod prop; | ||||||
| mod prop_at; | mod prop_at; | ||||||
|  | mod raw_spans; | ||||||
| mod seek_op; | mod seek_op; | ||||||
| mod seek_op_with_patch; | mod seek_op_with_patch; | ||||||
|  | mod spans; | ||||||
| 
 | 
 | ||||||
|  | pub(crate) use attribute::{Attribute, ChangeSet}; | ||||||
|  | pub(crate) use attribute2::{Attribute2, ChangeSet2}; | ||||||
| pub(crate) use elem_id_pos::ElemIdPos; | pub(crate) use elem_id_pos::ElemIdPos; | ||||||
| pub(crate) use insert::InsertNth; | pub(crate) use insert::InsertNth; | ||||||
| pub(crate) use keys::Keys; | pub(crate) use keys::Keys; | ||||||
|  | @ -42,8 +50,20 @@ pub(crate) use nth_at::NthAt; | ||||||
| pub(crate) use opid::OpIdSearch; | pub(crate) use opid::OpIdSearch; | ||||||
| pub(crate) use prop::Prop; | pub(crate) use prop::Prop; | ||||||
| pub(crate) use prop_at::PropAt; | pub(crate) use prop_at::PropAt; | ||||||
|  | pub(crate) use raw_spans::RawSpans; | ||||||
| pub(crate) use seek_op::SeekOp; | pub(crate) use seek_op::SeekOp; | ||||||
| pub(crate) use seek_op_with_patch::SeekOpWithPatch; | pub(crate) use seek_op_with_patch::SeekOpWithPatch; | ||||||
|  | pub(crate) use spans::{Span, Spans}; | ||||||
|  | 
 | ||||||
|  | #[derive(Serialize, Debug, Clone, PartialEq)] | ||||||
|  | pub struct SpanInfo { | ||||||
|  |     pub id: ExId, | ||||||
|  |     pub start: usize, | ||||||
|  |     pub end: usize, | ||||||
|  |     #[serde(rename = "type")] | ||||||
|  |     pub span_type: String, | ||||||
|  |     pub value: ScalarValue, | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // use a struct for the args for clarity as they are passed up the update chain in the optree
 | // use a struct for the args for clarity as they are passed up the update chain in the optree
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
|  |  | ||||||
							
								
								
									
										129
									
								
								automerge/src/query/attribute.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								automerge/src/query/attribute.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | ||||||
|  | use crate::clock::Clock; | ||||||
|  | use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; | ||||||
|  | use crate::types::{ElemId, Op}; | ||||||
|  | use std::fmt::Debug; | ||||||
|  | use std::ops::Range; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub(crate) struct Attribute { | ||||||
|  |     pos: usize, | ||||||
|  |     seen: usize, | ||||||
|  |     last_seen: Option<ElemId>, | ||||||
|  |     baseline: Clock, | ||||||
|  |     pub(crate) change_sets: Vec<ChangeSet>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub struct ChangeSet { | ||||||
|  |     clock: Clock, | ||||||
|  |     next_add: Option<Range<usize>>, | ||||||
|  |     next_del: Option<(usize, String)>, | ||||||
|  |     pub add: Vec<Range<usize>>, | ||||||
|  |     pub del: Vec<(usize, String)>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<Clock> for ChangeSet { | ||||||
|  |     fn from(clock: Clock) -> Self { | ||||||
|  |         ChangeSet { | ||||||
|  |             clock, | ||||||
|  |             next_add: None, | ||||||
|  |             next_del: None, | ||||||
|  |             add: Vec::new(), | ||||||
|  |             del: Vec::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ChangeSet { | ||||||
|  |     fn cut_add(&mut self) { | ||||||
|  |         if let Some(add) = self.next_add.take() { | ||||||
|  |             self.add.push(add) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn cut_del(&mut self) { | ||||||
|  |         if let Some(del) = self.next_del.take() { | ||||||
|  |             self.del.push(del) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Attribute { | ||||||
|  |     pub(crate) fn new(baseline: Clock, change_sets: Vec<Clock>) -> Self { | ||||||
|  |         Attribute { | ||||||
|  |             pos: 0, | ||||||
|  |             seen: 0, | ||||||
|  |             last_seen: None, | ||||||
|  |             baseline, | ||||||
|  |             change_sets: change_sets.into_iter().map(|c| c.into()).collect(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn update_add(&mut self, element: &Op) { | ||||||
|  |         let baseline = self.baseline.covers(&element.id); | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             if !baseline && cs.clock.covers(&element.id) { | ||||||
|  |                 // is part of the change_set
 | ||||||
|  |                 if let Some(range) = &mut cs.next_add { | ||||||
|  |                     range.end += 1; | ||||||
|  |                 } else { | ||||||
|  |                     cs.next_add = Some(Range { | ||||||
|  |                         start: self.seen, | ||||||
|  |                         end: self.seen + 1, | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 cs.cut_add(); | ||||||
|  |             } | ||||||
|  |             cs.cut_del(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // id is in baseline
 | ||||||
|  |     // succ is not in baseline but is in cs
 | ||||||
|  | 
 | ||||||
|  |     fn update_del(&mut self, element: &Op) { | ||||||
|  |         if !self.baseline.covers(&element.id) | ||||||
|  |             || element.succ.iter().any(|id| self.baseline.covers(id)) | ||||||
|  |         { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             if element.succ.iter().any(|id| cs.clock.covers(id)) { | ||||||
|  |                 // was deleted by change set
 | ||||||
|  |                 if let Some(s) = element.as_string() { | ||||||
|  |                     if let Some((_, span)) = &mut cs.next_del { | ||||||
|  |                         span.push_str(&s); | ||||||
|  |                     } else { | ||||||
|  |                         cs.next_del = Some((self.seen, s)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub(crate) fn finish(&mut self) { | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             cs.cut_add(); | ||||||
|  |             cs.cut_del(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> TreeQuery<'a> for Attribute { | ||||||
|  |     fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { | ||||||
|  |         if element.insert { | ||||||
|  |             self.last_seen = None; | ||||||
|  |         } | ||||||
|  |         if self.last_seen.is_none() && element.visible() { | ||||||
|  |             self.update_add(element); | ||||||
|  |             self.seen += 1; | ||||||
|  |             self.last_seen = element.elemid(); | ||||||
|  |         } | ||||||
|  |         if !element.succ.is_empty() { | ||||||
|  |             self.update_del(element); | ||||||
|  |         } | ||||||
|  |         self.pos += 1; | ||||||
|  |         QueryResult::Next | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										174
									
								
								automerge/src/query/attribute2.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								automerge/src/query/attribute2.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,174 @@ | ||||||
|  | use crate::clock::Clock; | ||||||
|  | use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; | ||||||
|  | use crate::types::{ElemId, Op}; | ||||||
|  | use std::fmt::Debug; | ||||||
|  | use std::ops::Range; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub(crate) struct Attribute2 { | ||||||
|  |     pos: usize, | ||||||
|  |     seen: usize, | ||||||
|  |     last_seen: Option<ElemId>, | ||||||
|  |     baseline: Clock, | ||||||
|  |     pub(crate) change_sets: Vec<ChangeSet2>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub struct ChangeSet2 { | ||||||
|  |     clock: Clock, | ||||||
|  |     next_add: Option<CS2Add>, | ||||||
|  |     next_del: Option<CS2Del>, | ||||||
|  |     pub add: Vec<CS2Add>, | ||||||
|  |     pub del: Vec<CS2Del>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub struct CS2Add { | ||||||
|  |     pub actor: usize, | ||||||
|  |     pub range: Range<usize>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub struct CS2Del { | ||||||
|  |     pub pos: usize, | ||||||
|  |     pub actor: usize, | ||||||
|  |     pub span: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<Clock> for ChangeSet2 { | ||||||
|  |     fn from(clock: Clock) -> Self { | ||||||
|  |         ChangeSet2 { | ||||||
|  |             clock, | ||||||
|  |             next_add: None, | ||||||
|  |             next_del: None, | ||||||
|  |             add: Vec::new(), | ||||||
|  |             del: Vec::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ChangeSet2 { | ||||||
|  |     fn cut_add(&mut self) { | ||||||
|  |         if let Some(add) = self.next_add.take() { | ||||||
|  |             self.add.push(add) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn cut_del(&mut self) { | ||||||
|  |         if let Some(del) = self.next_del.take() { | ||||||
|  |             self.del.push(del) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Attribute2 { | ||||||
|  |     pub(crate) fn new(baseline: Clock, change_sets: Vec<Clock>) -> Self { | ||||||
|  |         Attribute2 { | ||||||
|  |             pos: 0, | ||||||
|  |             seen: 0, | ||||||
|  |             last_seen: None, | ||||||
|  |             baseline, | ||||||
|  |             change_sets: change_sets.into_iter().map(|c| c.into()).collect(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn update_add(&mut self, element: &Op) { | ||||||
|  |         let baseline = self.baseline.covers(&element.id); | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             if !baseline && cs.clock.covers(&element.id) { | ||||||
|  |                 // is part of the change_set
 | ||||||
|  |                 if let Some(CS2Add { range, actor }) = &mut cs.next_add { | ||||||
|  |                     if *actor == element.id.actor() { | ||||||
|  |                         range.end += 1; | ||||||
|  |                     } else { | ||||||
|  |                         cs.cut_add(); | ||||||
|  |                         cs.next_add = Some(CS2Add { | ||||||
|  |                             actor: element.id.actor(), | ||||||
|  |                             range: Range { | ||||||
|  |                                 start: self.seen, | ||||||
|  |                                 end: self.seen + 1, | ||||||
|  |                             }, | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     cs.next_add = Some(CS2Add { | ||||||
|  |                         actor: element.id.actor(), | ||||||
|  |                         range: Range { | ||||||
|  |                             start: self.seen, | ||||||
|  |                             end: self.seen + 1, | ||||||
|  |                         }, | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 cs.cut_add(); | ||||||
|  |             } | ||||||
|  |             cs.cut_del(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // id is in baseline
 | ||||||
|  |     // succ is not in baseline but is in cs
 | ||||||
|  | 
 | ||||||
|  |     fn update_del(&mut self, element: &Op) { | ||||||
|  |         if !self.baseline.covers(&element.id) | ||||||
|  |             || element.succ.iter().any(|id| self.baseline.covers(id)) | ||||||
|  |         { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             let succ: Vec<_> = element | ||||||
|  |                 .succ | ||||||
|  |                 .iter() | ||||||
|  |                 .filter(|id| cs.clock.covers(id)) | ||||||
|  |                 .collect(); | ||||||
|  |             // was deleted by change set
 | ||||||
|  |             if let Some(suc) = succ.get(0) { | ||||||
|  |                 if let Some(s) = element.as_string() { | ||||||
|  |                     if let Some(CS2Del { actor, span, .. }) = &mut cs.next_del { | ||||||
|  |                         if suc.actor() == *actor { | ||||||
|  |                             span.push_str(&s); | ||||||
|  |                         } else { | ||||||
|  |                             cs.cut_del(); | ||||||
|  |                             cs.next_del = Some(CS2Del { | ||||||
|  |                                 pos: self.seen, | ||||||
|  |                                 actor: suc.actor(), | ||||||
|  |                                 span: s, | ||||||
|  |                             }) | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         cs.next_del = Some(CS2Del { | ||||||
|  |                             pos: self.seen, | ||||||
|  |                             actor: suc.actor(), | ||||||
|  |                             span: s, | ||||||
|  |                         }) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub(crate) fn finish(&mut self) { | ||||||
|  |         for cs in &mut self.change_sets { | ||||||
|  |             cs.cut_add(); | ||||||
|  |             cs.cut_del(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> TreeQuery<'a> for Attribute2 { | ||||||
|  |     fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { | ||||||
|  |         if element.insert { | ||||||
|  |             self.last_seen = None; | ||||||
|  |         } | ||||||
|  |         if self.last_seen.is_none() && element.visible() { | ||||||
|  |             self.update_add(element); | ||||||
|  |             self.seen += 1; | ||||||
|  |             self.last_seen = element.elemid(); | ||||||
|  |         } | ||||||
|  |         if !element.succ.is_empty() { | ||||||
|  |             self.update_del(element); | ||||||
|  |         } | ||||||
|  |         self.pos += 1; | ||||||
|  |         QueryResult::Next | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -99,6 +99,10 @@ impl<'a> TreeQuery<'a> for InsertNth { | ||||||
|             self.last_seen = None; |             self.last_seen = None; | ||||||
|             self.last_insert = element.elemid(); |             self.last_insert = element.elemid(); | ||||||
|         } |         } | ||||||
|  |         if self.valid.is_some() && element.valid_mark_anchor() { | ||||||
|  |             self.last_valid_insert = Some(element.elemid_or_key()); | ||||||
|  |             self.valid = None; | ||||||
|  |         } | ||||||
|         if self.last_seen.is_none() && element.visible() { |         if self.last_seen.is_none() && element.visible() { | ||||||
|             if self.seen >= self.target { |             if self.seen >= self.target { | ||||||
|                 return QueryResult::Finish; |                 return QueryResult::Finish; | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ impl ListVals { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<'a> TreeQuery<'a> for ListVals { | impl<'a> TreeQuery<'a> for ListVals { | ||||||
|     fn query_node(&mut self, child: &OpTreeNode) -> QueryResult { |     fn query_node(&mut self, child: &'a OpTreeNode) -> QueryResult { | ||||||
|         let start = 0; |         let start = 0; | ||||||
|         for pos in start..child.len() { |         for pos in start..child.len() { | ||||||
|             let op = child.get(pos).unwrap(); |             let op = child.get(pos).unwrap(); | ||||||
|  |  | ||||||
							
								
								
									
										78
									
								
								automerge/src/query/raw_spans.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								automerge/src/query/raw_spans.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | ||||||
|  | use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; | ||||||
|  | use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; | ||||||
|  | use std::fmt::Debug; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub(crate) struct RawSpans { | ||||||
|  |     pos: usize, | ||||||
|  |     seen: usize, | ||||||
|  |     last_seen: Option<ElemId>, | ||||||
|  |     last_insert: Option<ElemId>, | ||||||
|  |     changed: bool, | ||||||
|  |     pub(crate) spans: Vec<RawSpan>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub(crate) struct RawSpan { | ||||||
|  |     pub(crate) id: OpId, | ||||||
|  |     pub(crate) start: usize, | ||||||
|  |     pub(crate) end: usize, | ||||||
|  |     pub(crate) name: String, | ||||||
|  |     pub(crate) value: ScalarValue, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl RawSpans { | ||||||
|  |     pub(crate) fn new() -> Self { | ||||||
|  |         RawSpans { | ||||||
|  |             pos: 0, | ||||||
|  |             seen: 0, | ||||||
|  |             last_seen: None, | ||||||
|  |             last_insert: None, | ||||||
|  |             changed: false, | ||||||
|  |             spans: Vec::new(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> TreeQuery<'a> for RawSpans { | ||||||
|  |     fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { | ||||||
|  |         // find location to insert
 | ||||||
|  |         // mark or set
 | ||||||
|  |         if element.succ.is_empty() { | ||||||
|  |             if let OpType::MarkBegin(md) = &element.action { | ||||||
|  |                 let pos = self | ||||||
|  |                     .spans | ||||||
|  |                     .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) | ||||||
|  |                     .unwrap_err(); | ||||||
|  |                 self.spans.insert( | ||||||
|  |                     pos, | ||||||
|  |                     RawSpan { | ||||||
|  |                         id: element.id, | ||||||
|  |                         start: self.seen, | ||||||
|  |                         end: 0, | ||||||
|  |                         name: md.name.clone(), | ||||||
|  |                         value: md.value.clone(), | ||||||
|  |                     }, | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             if let OpType::MarkEnd(_) = &element.action { | ||||||
|  |                 for s in self.spans.iter_mut() { | ||||||
|  |                     if s.id == element.id.prev() { | ||||||
|  |                         s.end = self.seen; | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if element.insert { | ||||||
|  |             self.last_seen = None; | ||||||
|  |             self.last_insert = element.elemid(); | ||||||
|  |         } | ||||||
|  |         if self.last_seen.is_none() && element.visible() { | ||||||
|  |             self.seen += 1; | ||||||
|  |             self.last_seen = element.elemid(); | ||||||
|  |         } | ||||||
|  |         self.pos += 1; | ||||||
|  |         QueryResult::Next | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								automerge/src/query/spans.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								automerge/src/query/spans.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,109 @@ | ||||||
|  | use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; | ||||||
|  | use crate::types::{ElemId, Op, OpType, ScalarValue}; | ||||||
|  | use std::borrow::Cow; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::fmt::Debug; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub(crate) struct Spans<'a> { | ||||||
|  |     pos: usize, | ||||||
|  |     seen: usize, | ||||||
|  |     last_seen: Option<ElemId>, | ||||||
|  |     last_insert: Option<ElemId>, | ||||||
|  |     seen_at_this_mark: Option<ElemId>, | ||||||
|  |     seen_at_last_mark: Option<ElemId>, | ||||||
|  |     ops: Vec<&'a Op>, | ||||||
|  |     marks: HashMap<String, &'a ScalarValue>, | ||||||
|  |     changed: bool, | ||||||
|  |     pub(crate) spans: Vec<Span<'a>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, PartialEq)] | ||||||
|  | pub struct Span<'a> { | ||||||
|  |     pub pos: usize, | ||||||
|  |     pub marks: Vec<(String, Cow<'a, ScalarValue>)>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> Spans<'a> { | ||||||
|  |     pub(crate) fn new() -> 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(crate) fn check_marks(&mut self) { | ||||||
|  |         let mut new_marks = HashMap::new(); | ||||||
|  |         for op in &self.ops { | ||||||
|  |             if let OpType::MarkBegin(m) = &op.action { | ||||||
|  |                 new_marks.insert(m.name.clone(), &m.value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if new_marks != self.marks { | ||||||
|  |             self.changed = true; | ||||||
|  |             self.marks = new_marks; | ||||||
|  |         } | ||||||
|  |         if self.changed | ||||||
|  |             && (self.seen_at_last_mark != self.seen_at_this_mark | ||||||
|  |                 || self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none()) | ||||||
|  |         { | ||||||
|  |             self.changed = false; | ||||||
|  |             self.seen_at_last_mark = self.seen_at_this_mark; | ||||||
|  |             let mut marks: Vec<_> = self | ||||||
|  |                 .marks | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) | ||||||
|  |                 .collect(); | ||||||
|  |             marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); | ||||||
|  |             self.spans.push(Span { | ||||||
|  |                 pos: self.seen, | ||||||
|  |                 marks, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<'a> TreeQuery<'a> for Spans<'a> { | ||||||
|  |     /* | ||||||
|  |     fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { | ||||||
|  |         unimplemented!() | ||||||
|  |     } | ||||||
|  |     */ | ||||||
|  | 
 | ||||||
|  |     fn query_element_with_metadata(&mut self, element: &'a Op, m: &OpSetMetadata) -> QueryResult { | ||||||
|  |         // find location to insert
 | ||||||
|  |         // mark or set
 | ||||||
|  |         if element.succ.is_empty() { | ||||||
|  |             if let OpType::MarkBegin(_) = &element.action { | ||||||
|  |                 let pos = self | ||||||
|  |                     .ops | ||||||
|  |                     .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) | ||||||
|  |                     .unwrap_err(); | ||||||
|  |                 self.ops.insert(pos, element); | ||||||
|  |             } | ||||||
|  |             if let OpType::MarkEnd(_) = &element.action { | ||||||
|  |                 self.ops.retain(|op| op.id != element.id.prev()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if element.insert { | ||||||
|  |             self.last_seen = None; | ||||||
|  |             self.last_insert = element.elemid(); | ||||||
|  |         } | ||||||
|  |         if self.last_seen.is_none() && element.visible() { | ||||||
|  |             self.check_marks(); | ||||||
|  |             self.seen += 1; | ||||||
|  |             self.last_seen = element.elemid(); | ||||||
|  |             self.seen_at_this_mark = element.elemid(); | ||||||
|  |         } | ||||||
|  |         self.pos += 1; | ||||||
|  |         QueryResult::Next | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,3 +1,6 @@ | ||||||
|  | use crate::{ | ||||||
|  |     decoding, decoding::Decoder, encoding::Encodable, Automerge, AutomergeError, Change, ChangeHash, | ||||||
|  | }; | ||||||
| use itertools::Itertools; | use itertools::Itertools; | ||||||
| use std::{ | use std::{ | ||||||
|     borrow::Cow, |     borrow::Cow, | ||||||
|  | @ -6,10 +9,7 @@ use std::{ | ||||||
|     io::Write, |     io::Write, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{types::HASH_SIZE, ApplyOptions, OpObserver}; | ||||||
|     decoding, decoding::Decoder, encoding::Encodable, types::HASH_SIZE, ApplyOptions, Automerge, |  | ||||||
|     AutomergeError, Change, ChangeHash, OpObserver, |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| mod bloom; | mod bloom; | ||||||
| mod state; | mod state; | ||||||
|  |  | ||||||
|  | @ -171,6 +171,73 @@ impl TransactionInner { | ||||||
|         self.operations.push((obj, prop, op)); |         self.operations.push((obj, prop, op)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(clippy::too_many_arguments)] | ||||||
|  |     pub(crate) fn mark<O: AsRef<ExId>>( | ||||||
|  |         &mut self, | ||||||
|  |         doc: &mut Automerge, | ||||||
|  |         obj: O, | ||||||
|  |         start: usize, | ||||||
|  |         expand_start: bool, | ||||||
|  |         end: usize, | ||||||
|  |         expand_end: bool, | ||||||
|  |         mark: &str, | ||||||
|  |         value: ScalarValue, | ||||||
|  |     ) -> Result<(), AutomergeError> { | ||||||
|  |         let obj = doc.exid_to_obj(obj.as_ref())?; | ||||||
|  | 
 | ||||||
|  |         self.do_insert( | ||||||
|  |             doc, | ||||||
|  |             obj, | ||||||
|  |             start, | ||||||
|  |             OpType::mark(mark.into(), expand_start, value), | ||||||
|  |         )?; | ||||||
|  |         self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub(crate) fn unmark<O: AsRef<ExId>>( | ||||||
|  |         &mut self, | ||||||
|  |         doc: &mut Automerge, | ||||||
|  |         obj: O, | ||||||
|  |         mark: O, | ||||||
|  |     ) -> Result<(), AutomergeError> { | ||||||
|  |         let obj = doc.exid_to_obj(obj.as_ref())?; | ||||||
|  |         let markid = doc.exid_to_obj_tmp_unchecked(mark.as_ref())?.0; | ||||||
|  |         let op1 = Op { | ||||||
|  |             id: self.next_id(), | ||||||
|  |             action: OpType::Delete, | ||||||
|  |             key: markid.into(), | ||||||
|  |             succ: Default::default(), | ||||||
|  |             pred: doc.ops.m.sorted_opids(vec![markid].into_iter()), | ||||||
|  |             insert: false, | ||||||
|  |         }; | ||||||
|  |         let q1 = doc.ops.search(&obj, query::SeekOp::new(&op1)); | ||||||
|  |         doc.ops.add_succ(&obj, q1.succ.into_iter(), &op1); | ||||||
|  |         //for i in q1.succ {
 | ||||||
|  |         //    doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op1));
 | ||||||
|  |         //}
 | ||||||
|  |         self.operations.push((obj, Prop::Map("".into()), op1)); | ||||||
|  | 
 | ||||||
|  |         let markid = markid.next(); | ||||||
|  |         let op2 = Op { | ||||||
|  |             id: self.next_id(), | ||||||
|  |             action: OpType::Delete, | ||||||
|  |             key: markid.into(), | ||||||
|  |             succ: Default::default(), | ||||||
|  |             pred: doc.ops.m.sorted_opids(vec![markid].into_iter()), | ||||||
|  |             insert: false, | ||||||
|  |         }; | ||||||
|  |         let q2 = doc.ops.search(&obj, query::SeekOp::new(&op2)); | ||||||
|  | 
 | ||||||
|  |         doc.ops.add_succ(&obj, q2.succ.into_iter(), &op2); | ||||||
|  |         //for i in q2.succ {
 | ||||||
|  |         //    doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op2));
 | ||||||
|  |         //}
 | ||||||
|  |         self.operations.push((obj, Prop::Map("".into()), op2)); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub(crate) fn insert<V: Into<ScalarValue>>( |     pub(crate) fn insert<V: Into<ScalarValue>>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         doc: &mut Automerge, |         doc: &mut Automerge, | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| use std::ops::RangeBounds; | use std::ops::RangeBounds; | ||||||
| 
 | 
 | ||||||
|  | use super::{CommitOptions, Transactable, TransactionInner}; | ||||||
| use crate::exid::ExId; | use crate::exid::ExId; | ||||||
|  | use crate::query; | ||||||
| use crate::{Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ScalarValue, Value, Values}; | use crate::{Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ScalarValue, Value, Values}; | ||||||
| use crate::{AutomergeError, Keys}; | use crate::{AutomergeError, Keys}; | ||||||
| use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; | use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; | ||||||
| 
 | 
 | ||||||
| use super::{CommitOptions, Transactable, TransactionInner}; |  | ||||||
| 
 |  | ||||||
| /// A transaction on a document.
 | /// A transaction on a document.
 | ||||||
| /// Transactions group operations into a single change so that no other operations can happen
 | /// Transactions group operations into a single change so that no other operations can happen
 | ||||||
| /// in-between.
 | /// in-between.
 | ||||||
|  | @ -129,6 +129,33 @@ impl<'a> Transactable for Transaction<'a> { | ||||||
|             .insert(self.doc, obj.as_ref(), index, value) |             .insert(self.doc, obj.as_ref(), index, value) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     #[allow(clippy::too_many_arguments)] | ||||||
|  |     fn mark<O: AsRef<ExId>>( | ||||||
|  |         &mut self, | ||||||
|  |         obj: O, | ||||||
|  |         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 unmark<O: AsRef<ExId>>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { | ||||||
|  |         self.inner.as_mut().unwrap().unmark(self.doc, obj, mark) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fn insert_object<O: AsRef<ExId>>( |     fn insert_object<O: AsRef<ExId>>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         obj: O, |         obj: O, | ||||||
|  | @ -253,6 +280,32 @@ impl<'a> Transactable for Transaction<'a> { | ||||||
|         self.doc.text_at(obj, heads) |         self.doc.text_at(obj, heads) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::Span<'_>>, AutomergeError> { | ||||||
|  |         self.doc.spans(obj) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn raw_spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::SpanInfo>, AutomergeError> { | ||||||
|  |         self.doc.raw_spans(obj) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn attribute<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet>, AutomergeError> { | ||||||
|  |         self.doc.attribute(obj, baseline, change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn attribute2<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet2>, AutomergeError> { | ||||||
|  |         self.doc.attribute2(obj, baseline, change_sets) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fn get<O: AsRef<ExId>, P: Into<Prop>>( |     fn get<O: AsRef<ExId>, P: Into<Prop>>( | ||||||
|         &self, |         &self, | ||||||
|         obj: O, |         obj: O, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| use std::ops::RangeBounds; | use std::ops::RangeBounds; | ||||||
| 
 | 
 | ||||||
| use crate::exid::ExId; | use crate::exid::ExId; | ||||||
|  | use crate::query; | ||||||
| use crate::{ | use crate::{ | ||||||
|     AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, |     AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, | ||||||
|     ObjType, Parents, Prop, ScalarValue, Value, Values, |     ObjType, Parents, Prop, ScalarValue, Value, Values, | ||||||
|  | @ -61,6 +62,21 @@ pub trait Transactable { | ||||||
|         object: ObjType, |         object: ObjType, | ||||||
|     ) -> Result<ExId, AutomergeError>; |     ) -> Result<ExId, AutomergeError>; | ||||||
| 
 | 
 | ||||||
|  |     /// Set a mark within a range on a list
 | ||||||
|  |     #[allow(clippy::too_many_arguments)] | ||||||
|  |     fn mark<O: AsRef<ExId>>( | ||||||
|  |         &mut self, | ||||||
|  |         obj: O, | ||||||
|  |         start: usize, | ||||||
|  |         expand_start: bool, | ||||||
|  |         end: usize, | ||||||
|  |         expand_end: bool, | ||||||
|  |         mark: &str, | ||||||
|  |         value: ScalarValue, | ||||||
|  |     ) -> Result<(), AutomergeError>; | ||||||
|  | 
 | ||||||
|  |     fn unmark<O: AsRef<ExId>>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError>; | ||||||
|  | 
 | ||||||
|     /// Increment the counter at the prop in the object by `value`.
 |     /// Increment the counter at the prop in the object by `value`.
 | ||||||
|     fn increment<O: AsRef<ExId>, P: Into<Prop>>( |     fn increment<O: AsRef<ExId>, P: Into<Prop>>( | ||||||
|         &mut self, |         &mut self, | ||||||
|  | @ -151,6 +167,28 @@ pub trait Transactable { | ||||||
|         heads: &[ChangeHash], |         heads: &[ChangeHash], | ||||||
|     ) -> Result<String, AutomergeError>; |     ) -> Result<String, AutomergeError>; | ||||||
| 
 | 
 | ||||||
|  |     /// test spans api for mark/span experiment
 | ||||||
|  |     fn spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::Span<'_>>, AutomergeError>; | ||||||
|  | 
 | ||||||
|  |     /// test raw_spans api for mark/span experiment
 | ||||||
|  |     fn raw_spans<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<query::SpanInfo>, AutomergeError>; | ||||||
|  | 
 | ||||||
|  |     /// test attribute api for mark/span experiment
 | ||||||
|  |     fn attribute<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet>, AutomergeError>; | ||||||
|  | 
 | ||||||
|  |     /// test attribute api for mark/span experiment
 | ||||||
|  |     fn attribute2<O: AsRef<ExId>>( | ||||||
|  |         &self, | ||||||
|  |         obj: O, | ||||||
|  |         baseline: &[ChangeHash], | ||||||
|  |         change_sets: &[Vec<ChangeHash>], | ||||||
|  |     ) -> Result<Vec<query::ChangeSet2>, AutomergeError>; | ||||||
|  | 
 | ||||||
|     /// Get the value at this prop in the object.
 |     /// Get the value at this prop in the object.
 | ||||||
|     fn get<O: AsRef<ExId>, P: Into<Prop>>( |     fn get<O: AsRef<ExId>, P: Into<Prop>>( | ||||||
|         &self, |         &self, | ||||||
|  |  | ||||||
|  | @ -182,9 +182,29 @@ impl fmt::Display for ObjType { | ||||||
| #[derive(PartialEq, Debug, Clone)] | #[derive(PartialEq, Debug, Clone)] | ||||||
| pub enum OpType { | pub enum OpType { | ||||||
|     Make(ObjType), |     Make(ObjType), | ||||||
|  |     /// Perform a deletion, expanding the operation to cover `n` deletions (multiOp).
 | ||||||
|     Delete, |     Delete, | ||||||
|     Increment(i64), |     Increment(i64), | ||||||
|     Put(ScalarValue), |     Put(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, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl From<ObjType> for OpType { | impl From<ObjType> for OpType { | ||||||
|  | @ -219,6 +239,14 @@ impl OpId { | ||||||
|     pub(crate) fn actor(&self) -> usize { |     pub(crate) fn actor(&self) -> usize { | ||||||
|         self.1 |         self.1 | ||||||
|     } |     } | ||||||
|  |     #[inline] | ||||||
|  |     pub(crate) fn prev(&self) -> OpId { | ||||||
|  |         OpId(self.0 - 1, self.1) | ||||||
|  |     } | ||||||
|  |     #[inline] | ||||||
|  |     pub(crate) fn next(&self) -> OpId { | ||||||
|  |         OpId(self.0 + 1, self.1) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Exportable for ObjId { | impl Exportable for ObjId { | ||||||
|  | @ -419,7 +447,7 @@ impl Op { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub(crate) fn visible(&self) -> bool { |     pub(crate) fn visible(&self) -> bool { | ||||||
|         if self.is_inc() { |         if self.is_inc() || self.is_mark() { | ||||||
|             false |             false | ||||||
|         } else if self.is_counter() { |         } else if self.is_counter() { | ||||||
|             self.succ.len() <= self.incs() |             self.succ.len() <= self.incs() | ||||||
|  | @ -444,6 +472,18 @@ impl Op { | ||||||
|         matches!(&self.action, OpType::Increment(_)) |         matches!(&self.action, OpType::Increment(_)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub(crate) fn valid_mark_anchor(&self) -> bool { | ||||||
|  |         self.succ.is_empty() | ||||||
|  |             && matches!( | ||||||
|  |                 &self.action, | ||||||
|  |                 OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) | ||||||
|  |             ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub(crate) fn is_mark(&self) -> bool { | ||||||
|  |         matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub(crate) fn is_counter(&self) -> bool { |     pub(crate) fn is_counter(&self) -> bool { | ||||||
|         matches!(&self.action, OpType::Put(ScalarValue::Counter(_))) |         matches!(&self.action, OpType::Put(ScalarValue::Counter(_))) | ||||||
|     } |     } | ||||||
|  | @ -472,6 +512,13 @@ impl Op { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub(crate) fn as_string(&self) -> Option<String> { | ||||||
|  |         match &self.action { | ||||||
|  |             OpType::Put(scalar) => scalar.as_string(), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub(crate) fn get_increment_value(&self) -> Option<i64> { |     pub(crate) fn get_increment_value(&self) -> Option<i64> { | ||||||
|         if let OpType::Increment(i) = self.action { |         if let OpType::Increment(i) = self.action { | ||||||
|             Some(i) |             Some(i) | ||||||
|  | @ -484,6 +531,8 @@ impl Op { | ||||||
|         match &self.action { |         match &self.action { | ||||||
|             OpType::Make(obj_type) => Value::Object(*obj_type), |             OpType::Make(obj_type) => Value::Object(*obj_type), | ||||||
|             OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), |             OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), | ||||||
|  |             OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned(format!("markBegin[{}]={}",mark.name, mark.value).into())), | ||||||
|  |             OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), | ||||||
|             _ => panic!("cant convert op into a value - {:?}", self), |             _ => panic!("cant convert op into a value - {:?}", self), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -492,6 +541,8 @@ impl Op { | ||||||
|         match &self.action { |         match &self.action { | ||||||
|             OpType::Make(obj_type) => Value::Object(*obj_type), |             OpType::Make(obj_type) => Value::Object(*obj_type), | ||||||
|             OpType::Put(scalar) => Value::Scalar(Cow::Owned(scalar.clone())), |             OpType::Put(scalar) => Value::Scalar(Cow::Owned(scalar.clone())), | ||||||
|  |             OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned(format!("markBegin[{}]={}",mark.name, mark.value).into())), | ||||||
|  |             OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), | ||||||
|             _ => panic!("cant convert op into a value - {:?}", self), |             _ => panic!("cant convert op into a value - {:?}", self), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -502,6 +553,8 @@ impl Op { | ||||||
|             OpType::Put(value) if self.insert => format!("i:{}", value), |             OpType::Put(value) if self.insert => format!("i:{}", value), | ||||||
|             OpType::Put(value) => format!("s:{}", value), |             OpType::Put(value) => format!("s:{}", value), | ||||||
|             OpType::Make(obj) => format!("make{}", obj), |             OpType::Make(obj) => format!("make{}", obj), | ||||||
|  |             OpType::MarkBegin(m) => format!("mark{}={}", m.name, m.value), | ||||||
|  |             OpType::MarkEnd(_) => "unmark".into(), | ||||||
|             OpType::Increment(val) => format!("inc:{}", val), |             OpType::Increment(val) => format!("inc:{}", val), | ||||||
|             OpType::Delete => "del".to_string(), |             OpType::Delete => "del".to_string(), | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,13 @@ pub enum Value<'a> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<'a> Value<'a> { | impl<'a> Value<'a> { | ||||||
|  |     pub fn as_string(&self) -> Option<String> { | ||||||
|  |         match self { | ||||||
|  |             Value::Scalar(val) => val.as_string(), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub fn map() -> Value<'a> { |     pub fn map() -> Value<'a> { | ||||||
|         Value::Object(ObjType::Map) |         Value::Object(ObjType::Map) | ||||||
|     } |     } | ||||||
|  | @ -629,6 +636,13 @@ impl ScalarValue { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub fn as_string(&self) -> Option<String> { | ||||||
|  |         match self { | ||||||
|  |             ScalarValue::Str(s) => Some(s.to_string()), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub fn counter(n: i64) -> ScalarValue { |     pub fn counter(n: i64) -> ScalarValue { | ||||||
|         ScalarValue::Counter(n.into()) |         ScalarValue::Counter(n.into()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -238,6 +238,8 @@ impl OpTableRow { | ||||||
|             crate::OpType::Put(v) => format!("set {}", v), |             crate::OpType::Put(v) => format!("set {}", v), | ||||||
|             crate::OpType::Make(obj) => format!("make {}", obj), |             crate::OpType::Make(obj) => format!("make {}", obj), | ||||||
|             crate::OpType::Increment(v) => format!("inc {}", v), |             crate::OpType::Increment(v) => format!("inc {}", v), | ||||||
|  |             crate::OpType::MarkBegin(v) => format!("mark {}={}", v.name, v.value), | ||||||
|  |             crate::OpType::MarkEnd(v) => format!("/mark {}", v), | ||||||
|         }; |         }; | ||||||
|         let prop = match op.key { |         let prop = match op.key { | ||||||
|             crate::types::Key::Map(k) => metadata.props[k].clone(), |             crate::types::Key::Map(k) => metadata.props[k].clone(), | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								automerge/tests/attribute.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								automerge/tests/attribute.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | use automerge::transaction::Transactable; | ||||||
|  | use automerge::{AutoCommit, AutomergeError, ROOT}; | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | mod helpers; | ||||||
|  | use helpers::{ | ||||||
|  |     pretty_print, realize, realize_obj, | ||||||
|  |     RealizedObject, | ||||||
|  | }; | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn simple_attribute_text() -> Result<(), AutomergeError> { | ||||||
|  |     let mut doc = AutoCommit::new(); | ||||||
|  |     let note = doc.put_object(&ROOT, "note", automerge::ObjType::Text)?; | ||||||
|  |     doc.splice_text(¬e, 0, 0, "hello little world")?; | ||||||
|  |     let baseline = doc.get_heads(); | ||||||
|  |     assert!(doc.text(¬e).unwrap() == "hello little world"); | ||||||
|  |     let mut doc2 = doc.fork(); | ||||||
|  |     doc2.splice_text(¬e, 5, 7, " big")?; | ||||||
|  |     let h2 = doc2.get_heads(); | ||||||
|  |     assert!(doc2.text(¬e)? == "hello big world"); | ||||||
|  |     let mut doc3 = doc.fork(); | ||||||
|  |     doc3.splice_text(¬e, 0, 0, "Well, ")?; | ||||||
|  |     let h3 = doc3.get_heads(); | ||||||
|  |     assert!(doc3.text(¬e)? == "Well, hello little world"); | ||||||
|  |     doc.merge(&mut doc2)?; | ||||||
|  |     doc.merge(&mut doc3)?; | ||||||
|  |     let text = doc.text(¬e)?; | ||||||
|  |     assert!(text == "Well, hello big world"); | ||||||
|  |     let cs = vec![h2, h3]; | ||||||
|  |     let attribute = doc.attribute(¬e, &baseline, &cs)?; | ||||||
|  |     assert!(&text[attribute[0].add[0].clone()] == " big"); | ||||||
|  |     assert!(attribute[0].del[0] == (15, " little".to_owned())); | ||||||
|  |     //println!("{:?} == {:?}", attribute[0].del[0] , (15, " little".to_owned()));
 | ||||||
|  |     assert!(&text[attribute[1].add[0].clone()] == "Well, "); | ||||||
|  |     //println!("- ------- attribute = {:?}", attribute);
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | @ -7,9 +7,9 @@ yarn --cwd $WASM_PROJECT install; | ||||||
| yarn --cwd $WASM_PROJECT build; | yarn --cwd $WASM_PROJECT build; | ||||||
| # If the dependencies are already installed we delete automerge-wasm. This makes | # If the dependencies are already installed we delete automerge-wasm. This makes | ||||||
| # this script usable for iterative development. | # this script usable for iterative development. | ||||||
| if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then | #if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then | ||||||
|     rm -rf $JS_PROJECT/node_modules/automerge-wasm | #    rm -rf $JS_PROJECT/node_modules/automerge-wasm | ||||||
| fi | #fi | ||||||
| # --check-files forces yarn to check if the local dep has changed | # --check-files forces yarn to check if the local dep has changed | ||||||
| yarn --cwd $JS_PROJECT install --check-files; | yarn --cwd $JS_PROJECT install --check-files; | ||||||
| yarn --cwd $JS_PROJECT test; | yarn --cwd $JS_PROJECT test; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue