Compare commits

...

14 Commits

Author SHA1 Message Date
Kevin Jahns
c67428d715 13.5.4 2021-04-02 23:13:24 +02:00
Kevin Jahns
45a9af96af Merge pull request #289 from jhaynie/main
fix crash when the walker didn't match because n will be null
2021-04-02 23:11:48 +02:00
Jeff Haynie
249c4f9c45 switch order for type to get picked up 2021-04-01 14:59:34 -05:00
Jeff Haynie
cdc7d3ffe6 fix crash when the walker didnt match because n will be null 2021-04-01 14:54:37 -05:00
Kevin Jahns
ac6a0e7667 Merge pull request #285 from damz/patch-1
Trivial editing of README.md
2021-03-25 14:02:38 +01:00
Kevin Jahns
12881e2be7 13.5.3 2021-03-21 21:32:38 +01:00
Kevin Jahns
77958da657 unify Y.Array & Y.Text deltas so event.changes.delta is equal to event.delta 2021-03-21 21:31:28 +01:00
Kevin Jahns
8a8a60efde remove unpkg entry 2021-03-21 15:31:17 +01:00
Damien Tournoud
7a1d648e79 Trivial editing of README.md 2021-03-15 07:24:36 -07:00
Kevin Jahns
3af420e790 add package.json to exports so other packages can consume it 2021-03-15 15:08:19 +01:00
Kevin Jahns
4f2d13e3ce 13.5.2 2021-03-11 18:54:02 +01:00
Kevin Jahns
e0b76cd2f4 [UndoManager] stop tracking unrelated insertions - yjs/y-monaco#10 2021-03-11 18:52:35 +01:00
Kevin Jahns
d812636c5b Implement doc suggestions - closes #249 2021-03-03 12:20:53 +01:00
Kevin Jahns
21fee0fe96 spelling issue lib0 repetition 2021-02-28 16:09:54 +01:00
7 changed files with 151 additions and 125 deletions

View File

@@ -320,6 +320,8 @@ or any of its children.
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b> <b><code>parent:Y.AbstractType|null</code></b>
<dd></dd> <dd></dd>
<b><code>size: number</code></b>
<dd>Total number of key/value pairs.</dd>
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b> <b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
<dd></dd> <dd></dd>
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b> <b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
@@ -410,7 +412,7 @@ YTextEvents compute changes as deltas.
<dd></dd> <dd></dd>
<b><code>format(index:number, length:number, formattingAttributes:Object&lt;string,string&gt;)</code></b> <b><code>format(index:number, length:number, formattingAttributes:Object&lt;string,string&gt;)</code></b>
<dd>Assign formatting attributes to a range in the text</dd> <dd>Assign formatting attributes to a range in the text</dd>
<b><code>applyDelta(delta, opts:Object&lt;string,any&gt;)</code></b> <b><code>applyDelta(delta: Delta, opts:Object&lt;string,any&gt;)</code></b>
<dd> <dd>
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a> See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
Can set options for preventing remove ending newLines, default is true. Can set options for preventing remove ending newLines, default is true.
@@ -483,6 +485,8 @@ or any of its children.
<dd>Get the XML serialization of all descendants.</dd> <dd>Get the XML serialization of all descendants.</dd>
<b><code>toJSON():string</code></b> <b><code>toJSON():string</code></b>
<dd>See <code>toString</code>.</dd> <dd>See <code>toString</code>.</dd>
<b><code>createTreeWalker(filter: function(AbstractType&lt;any&gt;):boolean):Iterable</code></b>
<dd>Create an Iterable that walks through the children.</dd>
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b> <b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
<dd> <dd>
Adds an event listener to this type that will be called synchronously every time Adds an event listener to this type that will be called synchronously every time
@@ -540,7 +544,7 @@ content and be actually XML compliant.
<dd></dd> <dd></dd>
<b><code>getAttribute(attributeName:string):string</code></b> <b><code>getAttribute(attributeName:string):string</code></b>
<dd></dd> <dd></dd>
<b><code>getAttributes(attributeName:string):Object&lt;string,string&gt;</code></b> <b><code>getAttributes():Object&lt;string,string&gt;</code></b>
<dd></dd> <dd></dd>
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b> <b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
<dd>Retrieve the i-th element.</dd> <dd>Retrieve the i-th element.</dd>
@@ -609,7 +613,10 @@ parameter that is stored on <code>transaction.origin</code> and
</dd> </dd>
<b><code>toJSON():any</code></b> <b><code>toJSON():any</code></b>
<dd> <dd>
Converts the entire document into a js object, recursively traversing each yjs type. Deprecated: It is recommended to call toJSON directly on the shared types.
Converts the entire document into a js object, recursively traversing each yjs
type. Doesn't log types that have not been defined (using
<code>ydoc.getType(..)</code>).
</dd> </dd>
<b><code>get(string, Y.[TypeClass]):[Type]</code></b> <b><code>get(string, Y.[TypeClass]):[Type]</code></b>
<dd>Define a shared type.</dd> <dd>Define a shared type.</dd>
@@ -691,7 +698,7 @@ Y.applyUpdate(ydoc2, state1)
This example shows how to sync two clients with the minimal amount of exchanged This example shows how to sync two clients with the minimal amount of exchanged
data by computing only the differences using the state vector of the remote data by computing only the differences using the state vector of the remote
client. Syncing clients using the state vector requires another roundtrip, but client. Syncing clients using the state vector requires another roundtrip, but
can safe a lot of bandwidth. can save a lot of bandwidth.
```js ```js
const stateVector1 = Y.encodeStateVector(ydoc1) const stateVector1 = Y.encodeStateVector(ydoc1)

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.1", "version": "13.5.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,10 +1,9 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.1", "version": "13.5.4",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
"unpkg": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts", "types": "./dist/src/index.d.ts",
"sideEffects": false, "sideEffects": false,
"funding": { "funding": {
@@ -12,14 +11,14 @@
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
}, },
"scripts": { "scripts": {
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50", "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc", "dist": "rm -rf dist && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
"serve-docs": "npm run docs && http-server ./docs/", "serve-docs": "npm run docs && http-server ./docs/",
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repitition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
"debug": "concurrently 'http-server -o test.html' 'npm run watch'", "debug": "concurrently 'http-server -o test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs", "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
@@ -31,7 +30,8 @@
"require": "./dist/yjs.cjs" "require": "./dist/yjs.cjs"
}, },
"./src/index.js": "./src/index.js", "./src/index.js": "./src/index.js",
"./tests/testHelper.js": "./tests/testHelper.js" "./tests/testHelper.js": "./tests/testHelper.js",
"./package.json": "./package.json"
}, },
"files": [ "files": [
"dist/yjs.*", "dist/yjs.*",

View File

@@ -502,14 +502,6 @@ const deleteText = (transaction, currPos, length) => {
* @typedef {Object} TextAttributes * @typedef {Object} TextAttributes
*/ */
/**
* @typedef {Object} DeltaItem
* @property {number|undefined} DeltaItem.delete
* @property {number|undefined} DeltaItem.retain
* @property {string|undefined} DeltaItem.insert
* @property {Object<string,any>} DeltaItem.attributes
*/
/** /**
* Event that describes the changes on a YText type. * Event that describes the changes on a YText type.
*/ */
@@ -521,10 +513,6 @@ export class YTextEvent extends YEvent {
*/ */
constructor (ytext, transaction, subs) { constructor (ytext, transaction, subs) {
super(ytext, transaction) super(ytext, transaction)
/**
* @type {Array<DeltaItem>|null}
*/
this._delta = null
/** /**
* Whether the children changed. * Whether the children changed.
* @type {Boolean} * @type {Boolean}
@@ -545,20 +533,41 @@ export class YTextEvent extends YEvent {
}) })
} }
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
*/
get changes () {
if (this._changes === null) {
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
*/
const changes = {
keys: this.keys,
delta: this.delta,
added: new Set(),
deleted: new Set()
}
this._changes = changes
}
return /** @type {any} */ (this._changes)
}
/** /**
* Compute the changes in the delta format. * Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
* *
* @type {Array<DeltaItem>} * @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* *
* @public * @public
*/ */
get delta () { get delta () {
if (this._delta === null) { if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc) const y = /** @type {Doc} */ (this.target.doc)
this._delta = [] /**
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*/
const delta = []
transact(y, transaction => { transact(y, transaction => {
const delta = /** @type {Array<DeltaItem>} */ (this._delta)
const currentAttributes = new Map() // saves all current attributes for insert const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map() const oldAttributes = new Map()
let item = this.target._start let item = this.target._start
@@ -728,8 +737,9 @@ export class YTextEvent extends YEvent {
} }
} }
}) })
this._delta = delta
} }
return this._delta return /** @type {any} */ (this._delta)
} }
} }

View File

@@ -83,7 +83,7 @@ export class YXmlTreeWalker {
* @type {Item|null} * @type {Item|null}
*/ */
let n = this._currentNode let n = this._currentNode
let type = /** @type {any} */ (n.content).type let type = n && n.content && /** @type {any} */ (n.content).type
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do { do {
type = /** @type {any} */ (n.content).type type = /** @type {any} */ (n.content).type

View File

@@ -5,11 +5,11 @@ import {
transact, transact,
createID, createID,
redoItem, redoItem,
iterateStructs,
isParentOf, isParentOf,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
getState, isDeleted,
addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -18,14 +18,12 @@ import { Observable } from 'lib0/observable.js'
class StackItem { class StackItem {
/** /**
* @param {DeleteSet} ds * @param {DeleteSet} deletions
* @param {Map<number,number>} beforeState * @param {DeleteSet} insertions
* @param {Map<number,number>} afterState
*/ */
constructor (ds, beforeState, afterState) { constructor (deletions, insertions) {
this.ds = ds this.insertions = insertions
this.beforeState = beforeState this.deletions = deletions
this.afterState = afterState
/** /**
* Use this to save and restore metadata like selection range * Use this to save and restore metadata like selection range
*/ */
@@ -65,48 +63,26 @@ const popStackItem = (undoManager, stack, eventType) => {
*/ */
const itemsToDelete = [] const itemsToDelete = []
let performedChange = false let performedChange = false
stackItem.afterState.forEach((endClock, client) => { iterateDeletedStructs(transaction, stackItem.insertions, struct => {
const startClock = stackItem.beforeState.get(client) || 0 if (struct instanceof Item) {
const len = endClock - startClock if (struct.redone !== null) {
// @todo iterateStructs should not need the structs parameter let { item, diff } = followRedone(store, struct.id)
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client)) if (diff > 0) {
if (startClock !== endClock) { item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
// this must be executed before deleted structs are iterated.
getItemCleanStart(transaction, createID(client, startClock))
if (endClock < getState(doc.store, client)) {
getItemCleanStart(transaction, createID(client, endClock))
}
iterateStructs(transaction, structs, startClock, len, struct => {
if (struct instanceof Item) {
if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id)
if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
}
if (item.length > len) {
getItemCleanStart(transaction, createID(item.id.client, endClock))
}
struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
} }
}) struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
} }
}) })
iterateDeletedStructs(transaction, stackItem.ds, struct => { iterateDeletedStructs(transaction, stackItem.deletions, struct => {
const id = struct.id
const clock = id.clock
const client = id.client
const startClock = stackItem.beforeState.get(client) || 0
const endClock = stackItem.afterState.get(client) || 0
if ( if (
struct instanceof Item && struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) && scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval. // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!(clock >= startClock && clock < endClock) !isDeleted(stackItem.insertions, struct.id)
) { ) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
@@ -201,17 +177,23 @@ export class UndoManager extends Observable {
// neither undoing nor redoing: delete redoStack // neither undoing nor redoing: delete redoStack
this.redoStack = [] this.redoStack = []
} }
const beforeState = transaction.beforeState const insertions = new DeleteSet()
const afterState = transaction.afterState transaction.afterState.forEach((endClock, client) => {
const startClock = transaction.beforeState.get(client) || 0
const len = endClock - startClock
if (len > 0) {
addToDeleteSet(insertions, client, startClock, len)
}
})
const now = time.getUnixTime() const now = time.getUnixTime()
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet]) lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
lastOp.afterState = afterState lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
} else { } else {
// create a new stack op // create a new stack op
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState)) stack.push(new StackItem(transaction.deleteSet, insertions))
} }
if (!undoing && !redoing) { if (!undoing && !redoing) {
this.lastChange = now this.lastChange = now
@@ -232,7 +214,7 @@ export class UndoManager extends Observable {
* @param {StackItem} stackItem * @param {StackItem} stackItem
*/ */
const clearItem = stackItem => { const clearItem = stackItem => {
iterateDeletedStructs(transaction, stackItem.ds, item => { iterateDeletedStructs(transaction, stackItem.deletions, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, false) keepItem(item, false)
} }

View File

@@ -35,6 +35,14 @@ export class YEvent {
* @type {Object|null} * @type {Object|null}
*/ */
this._changes = null this._changes = null
/**
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
this._keys = null
/**
* @type {null | Array<{ insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/
this._delta = null
} }
/** /**
@@ -67,6 +75,66 @@ export class YEvent {
return isDeleted(this.transaction.deleteSet, struct.id) return isDeleted(this.transaction.deleteSet, struct.id)
} }
/**
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
get keys () {
if (this._keys === null) {
const keys = new Map()
const target = this.target
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
changed.forEach(key => {
if (key !== null) {
const item = /** @type {Item} */ (target._map.get(key))
/**
* @type {'delete' | 'add' | 'update'}
*/
let action
let oldValue
if (this.adds(item)) {
let prev = item.left
while (prev !== null && this.adds(prev)) {
prev = prev.left
}
if (this.deletes(item)) {
if (prev !== null && this.deletes(prev)) {
action = 'delete'
oldValue = array.last(prev.content.getContent())
} else {
return
}
} else {
if (prev !== null && this.deletes(prev)) {
action = 'update'
oldValue = array.last(prev.content.getContent())
} else {
action = 'add'
oldValue = undefined
}
}
} else {
if (this.deletes(item)) {
action = 'delete'
oldValue = array.last(/** @type {Item} */ item.content.getContent())
} else {
return // nop
}
}
keys.set(key, { action, oldValue })
}
})
this._keys = keys
}
return this._keys
}
/**
* @type {Array<{insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/
get delta () {
return this.changes.delta
}
/** /**
* Check if a struct is added by this event. * Check if a struct is added by this event.
* *
@@ -80,7 +148,7 @@ export class YEvent {
} }
/** /**
* @return {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert:Array<any>}|{delete:number}|{retain:number}>}} * @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
*/ */
get changes () { get changes () {
let changes = this._changes let changes = this._changes
@@ -92,12 +160,11 @@ export class YEvent {
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>} * @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/ */
const delta = [] const delta = []
/**
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
*/
const keys = new Map()
changes = { changes = {
added, deleted, delta, keys added,
deleted,
delta,
keys: this.keys
} }
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target)) const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
if (changed.has(null)) { if (changed.has(null)) {
@@ -141,46 +208,6 @@ export class YEvent {
packOp() packOp()
} }
} }
changed.forEach(key => {
if (key !== null) {
const item = /** @type {Item} */ (target._map.get(key))
/**
* @type {'delete' | 'add' | 'update'}
*/
let action
let oldValue
if (this.adds(item)) {
let prev = item.left
while (prev !== null && this.adds(prev)) {
prev = prev.left
}
if (this.deletes(item)) {
if (prev !== null && this.deletes(prev)) {
action = 'delete'
oldValue = array.last(prev.content.getContent())
} else {
return
}
} else {
if (prev !== null && this.deletes(prev)) {
action = 'update'
oldValue = array.last(prev.content.getContent())
} else {
action = 'add'
oldValue = undefined
}
}
} else {
if (this.deletes(item)) {
action = 'delete'
oldValue = array.last(/** @type {Item} */ item.content.getContent())
} else {
return // nop
}
}
keys.set(key, { action, oldValue })
}
})
this._changes = changes this._changes = changes
} }
return /** @type {any} */ (changes) return /** @type {any} */ (changes)