mirror of
https://github.com/yjs/yjs.git
synced 2026-02-23 19:49:59 +01:00
attributions filtering, undo of contentIds
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
"types": "./dist/src/internals.d.ts",
|
||||
"default": "./src/internals.js"
|
||||
},
|
||||
"/meta": {
|
||||
"./meta": {
|
||||
"types": "./dist/src/utils/meta.d.ts",
|
||||
"default": "./src/utils/meta.js"
|
||||
},
|
||||
|
||||
@@ -116,7 +116,13 @@ export {
|
||||
decodeIdMap,
|
||||
diffDocsToDelta,
|
||||
getPathTo,
|
||||
Attributions
|
||||
Attributions,
|
||||
filterIdMap,
|
||||
undoContentIds,
|
||||
createContentIds,
|
||||
createContentMap,
|
||||
createContentIdsFromContentMap,
|
||||
createContentMapFromContentIds
|
||||
} from './internals.js'
|
||||
|
||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from './utils/StructSet.js'
|
||||
export * from './utils/IdMap.js'
|
||||
export * from './utils/AttributionManager.js'
|
||||
export * from './utils/delta-helpers.js'
|
||||
export * from './utils/meta.js'
|
||||
export * from './ytype.js'
|
||||
export * from './structs/AbstractStruct.js'
|
||||
export * from './structs/GC.js'
|
||||
|
||||
@@ -149,7 +149,7 @@ export const splitStruct = (transaction, leftStruct, diff) => {
|
||||
* @param {Array<StackItem>} stack
|
||||
* @param {ID} id
|
||||
*/
|
||||
const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => s.deletions.hasId(id))
|
||||
const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => s.deletes.hasId(id))
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
|
||||
@@ -468,7 +468,7 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
this._prevDoc.transact(tr => {
|
||||
applyUpdate(this._prevDoc, encodeStateAsUpdate(this._nextDoc))
|
||||
const um = new UndoManager(this._prevDoc)
|
||||
um.undoStack.push(new StackItem(tr.deleteSet, tr.insertSet))
|
||||
um.undoStack.push(new StackItem(tr.insertSet, tr.deleteSet))
|
||||
um.undo()
|
||||
um.destroy()
|
||||
})
|
||||
@@ -497,7 +497,7 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
writeStructsFromIdSet(encoder, this._nextDoc.store, inserts)
|
||||
writeIdSet(encoder, deletes)
|
||||
const um = new UndoManager(this._nextDoc)
|
||||
um.undoStack.push(new StackItem(deletes, inserts))
|
||||
um.undoStack.push(new StackItem(inserts, deletes))
|
||||
um.undo()
|
||||
um.destroy()
|
||||
})
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
_deleteRangeFromIdSet,
|
||||
DSDecoderV1, DSDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSet, ID, // eslint-disable-line
|
||||
_insertIntoIdSet,
|
||||
_intersectSets
|
||||
_intersectSets,
|
||||
createIdSet,
|
||||
IdRanges
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
@@ -294,6 +296,22 @@ export const createIdMapFromIdSet = (idset, attrs) => {
|
||||
return idmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IdSet from an IdMap by stripping the attributes.
|
||||
*
|
||||
* @param {IdMap<any>} idmap
|
||||
* @return {IdSet}
|
||||
*/
|
||||
export const createIdSetFromIdMap = idmap => {
|
||||
const idset = createIdSet()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
const idRanges = new IdRanges([])
|
||||
ranges.getIds().forEach(range => idRanges.add(range.clock, range.len))
|
||||
idset.clients.set(client, idRanges)
|
||||
})
|
||||
return idset
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*/
|
||||
@@ -324,6 +342,10 @@ export class IdMap {
|
||||
})
|
||||
}
|
||||
|
||||
isEmpty () {
|
||||
return this.clients.size === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @return {boolean}
|
||||
@@ -616,3 +638,36 @@ export const diffIdMap = (set, exclude) => {
|
||||
}
|
||||
|
||||
export const intersectMaps = _intersectSets
|
||||
|
||||
/**
|
||||
* Filter attributes in an IdMap based on a predicate function.
|
||||
* Returns a new IdMap containing idranges that match the predicate.
|
||||
*
|
||||
* @template Attrs
|
||||
* @param {IdMap<Attrs>} idmap
|
||||
* @param {(attr: ContentAttribute<Attrs>) => boolean} predicate
|
||||
* @return {IdMap<Attrs>}
|
||||
*/
|
||||
export const filterIdMap = (idmap, predicate) => {
|
||||
const filtered = createIdMap()
|
||||
idmap.clients.forEach((ranges, client) => {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const attrRanges = []
|
||||
ranges.getIds().forEach((range) => {
|
||||
if (range.attrs.some(predicate)) {
|
||||
const rangeCpy = range.copyWith(range.clock, range.len)
|
||||
attrRanges.push(rangeCpy)
|
||||
rangeCpy.attrs.forEach(attr => {
|
||||
filtered.attrs.add(attr)
|
||||
filtered.attrsH.set(attr.hash(), attr)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (attrRanges.length > 0) {
|
||||
filtered.clients.set(client, new AttrRanges(attrRanges))
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export class IdRanges {
|
||||
*/
|
||||
add (clock, length) {
|
||||
const last = this._ids[this._ids.length - 1]
|
||||
if (last.clock + last.len === clock) {
|
||||
if (last != null && last.clock + last.len === clock) {
|
||||
if (this._lastIsUsed) {
|
||||
this._ids[this._ids.length - 1] = new IdRange(last.clock, last.len + length)
|
||||
this._lastIsUsed = false
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
isParentOf,
|
||||
followRedone,
|
||||
getItemCleanStart,
|
||||
YEvent, Transaction, Doc, Item, GC, IdSet, YType // eslint-disable-line
|
||||
YEvent, Transaction, Doc, Item, GC, IdSet, YType, // eslint-disable-line
|
||||
diffIdSet
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time'
|
||||
@@ -18,12 +19,12 @@ import { ObservableV2 } from 'lib0/observable'
|
||||
|
||||
export class StackItem {
|
||||
/**
|
||||
* @param {IdSet} deletions
|
||||
* @param {IdSet} insertions
|
||||
* @param {IdSet} deletions
|
||||
*/
|
||||
constructor (deletions, insertions) {
|
||||
this.insertions = insertions
|
||||
this.deletions = deletions
|
||||
constructor (insertions, deletions) {
|
||||
this.inserts = insertions
|
||||
this.deletes = deletions
|
||||
/**
|
||||
* Use this to save and restore metadata like selection range
|
||||
*/
|
||||
@@ -36,7 +37,7 @@ export class StackItem {
|
||||
* @param {StackItem} stackItem
|
||||
*/
|
||||
const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
||||
iterateStructsByIdSet(tr, stackItem.deletions, item => {
|
||||
iterateStructsByIdSet(tr, stackItem.deletes, item => {
|
||||
if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {YType} */ (type), item))) {
|
||||
keepItem(item, false)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
*/
|
||||
const itemsToDelete = []
|
||||
let performedChange = false
|
||||
iterateStructsByIdSet(transaction, stackItem.insertions, struct => {
|
||||
iterateStructsByIdSet(transaction, stackItem.inserts, struct => {
|
||||
if (struct instanceof Item) {
|
||||
if (struct.redone !== null) {
|
||||
let { item, diff } = followRedone(store, struct.id)
|
||||
@@ -84,18 +85,18 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
iterateStructsByIdSet(transaction, stackItem.deletions, struct => {
|
||||
iterateStructsByIdSet(transaction, stackItem.deletes, struct => {
|
||||
if (
|
||||
struct instanceof Item &&
|
||||
scope.some(type => type === transaction.doc || isParentOf(/** @type {YType} */ (type), struct)) &&
|
||||
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
||||
!stackItem.insertions.hasId(struct.id)
|
||||
!stackItem.inserts.hasId(struct.id)
|
||||
) {
|
||||
itemsToRedo.add(struct)
|
||||
}
|
||||
})
|
||||
itemsToRedo.forEach(struct => {
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.inserts, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange
|
||||
})
|
||||
// We want to delete in reverse order so that children are deleted before
|
||||
// parents, so we have more information available when items are filtered.
|
||||
@@ -230,11 +231,11 @@ export class UndoManager extends ObservableV2 {
|
||||
if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
// append change to last stack op
|
||||
const lastOp = stack[stack.length - 1]
|
||||
lastOp.deletions = mergeIdSets([lastOp.deletions, transaction.deleteSet])
|
||||
lastOp.insertions = mergeIdSets([lastOp.insertions, insertions])
|
||||
lastOp.deletes = mergeIdSets([lastOp.deletes, transaction.deleteSet])
|
||||
lastOp.inserts = mergeIdSets([lastOp.inserts, insertions])
|
||||
} else {
|
||||
// create a new stack op
|
||||
stack.push(new StackItem(transaction.deleteSet, insertions))
|
||||
stack.push(new StackItem(insertions, transaction.deleteSet))
|
||||
didAdd = true
|
||||
}
|
||||
if (!undoing && !redoing) {
|
||||
@@ -389,3 +390,17 @@ export class UndoManager extends ObservableV2 {
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*
|
||||
* This is not guaranteed to work on documents with gc enabled!
|
||||
*
|
||||
* @param {Doc} ydoc
|
||||
* @param {import('./meta.js').ContentIds} contentIds
|
||||
*/
|
||||
export const undoContentIds = (ydoc, contentIds) => {
|
||||
const um = new UndoManager(ydoc)
|
||||
um.undoStack.push(new StackItem(diffIdSet(contentIds.inserts, contentIds.deletes), diffIdSet(contentIds.deletes, contentIds.inserts)))
|
||||
um.undo()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ import { IdSetEncoderV2 } from './UpdateEncoder.js'
|
||||
*/
|
||||
export const createContentIds = (inserts, deletes) => ({ inserts, deletes })
|
||||
|
||||
/**
|
||||
* @param {ContentMap} contentMap
|
||||
*/
|
||||
export const createContentIdsFromContentMap = contentMap => createContentIds(
|
||||
idmap.createIdSetFromIdMap(contentMap.inserts),
|
||||
idmap.createIdSetFromIdMap(contentMap.deletes)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {import('./IdMap.js').IdMap<any>} inserts
|
||||
* @param {import('./IdMap.js').IdMap<any>} deletes
|
||||
|
||||
@@ -137,6 +137,7 @@ export const testChildListContent = () => {
|
||||
*/
|
||||
export const testAttributionSession1 = tc => {
|
||||
const { testConnector, users, text0, text1 } = init(tc, { users: 3 })
|
||||
users[0].gc = false
|
||||
const globalAttributions = new Y.Attributions()
|
||||
const v1 = Y.cloneDoc(users[0])
|
||||
users.forEach(user => user.on('update', (update, _, ydoc, tr) => {
|
||||
@@ -152,9 +153,21 @@ export const testAttributionSession1 = tc => {
|
||||
const d1 = text0.toDelta(Y.createAttributionManagerFromDiff(v1, users[0], { attrs: globalAttributions }))
|
||||
t.compare(d1, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { insert: ['1'] }))
|
||||
const v2 = Y.cloneDoc(users[0])
|
||||
text0.delete(0, 1)
|
||||
text1.insert(1, 'c')
|
||||
text0.delete(1, 1)
|
||||
text1.insert(2, 'c')
|
||||
testConnector.flushAllMessages()
|
||||
const d2 = text0.toDelta(Y.createAttributionManagerFromDiff(v2, users[0], { attrs: globalAttributions }))
|
||||
t.compare(d2, delta.create().insert('a', null, { delete: ['0'] }).insert('c', null, { insert: ['1'] }).insert('b'))
|
||||
t.compare(d2, delta.create().insert('a').insert('b', null, { delete: ['0'] }).insert('c', null, { insert: ['1'] }))
|
||||
|
||||
const onlyUser0ChangesAttributed = {
|
||||
inserts: Y.filterIdMap(globalAttributions.inserts, attr => attr.name === 'insert' && attr.val === '0'),
|
||||
deletes: Y.filterIdMap(globalAttributions.deletes, attr => attr.name === 'delete' && attr.val === '0')
|
||||
}
|
||||
const amUser0 = new Y.TwosetAttributionManager(onlyUser0ChangesAttributed.inserts, onlyUser0ChangesAttributed.deletes)
|
||||
const d3 = text0.toDelta(amUser0)
|
||||
t.compare(d3, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { delete: ['0'] }).insert('c'))
|
||||
Y.undoContentIds(users[0], Y.createContentIdsFromContentMap(onlyUser0ChangesAttributed))
|
||||
|
||||
const d4 = text0.toDelta()
|
||||
t.compare(d4, delta.create().insert('bc'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user