mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 03:37:50 +01:00
[y.text] event returns delta - fix a bunch of bugs
This commit is contained in:
@@ -116,16 +116,6 @@ deletions and insertions only, without Attributions).
|
|||||||
`AttributionManager` is an abstract class for mapping attributions. It is
|
`AttributionManager` is an abstract class for mapping attributions. It is
|
||||||
possible to highlight arbitrary content with this approach.
|
possible to highlight arbitrary content with this approach.
|
||||||
|
|
||||||
The next steps are to:
|
|
||||||
|
|
||||||
- finish the implementation for Y.Map and Y.Xml* (which should be easy, compared
|
|
||||||
to Y.Map).
|
|
||||||
- Implement an AttributionManager-CRDT for the backend that sits there and
|
|
||||||
associates changes with users.
|
|
||||||
- use `getContent(attributionManager)` instead of `toDelta` in y-prosemirror.
|
|
||||||
Would like to make the attribution part of y-prosemirror, however Nick can also
|
|
||||||
use this approach to customly render the changes in ProseMirror.
|
|
||||||
|
|
||||||
The AttributionManager is encodes very efficiently. The ids are encoded using
|
The AttributionManager is encodes very efficiently. The ids are encoded using
|
||||||
run-length encoding and the Attributes are de-duplicated and only encoded once.
|
run-length encoding and the Attributes are de-duplicated and only encoded once.
|
||||||
The above example encodes in 20 bytes.
|
The above example encodes in 20 bytes.
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ const cleanupContextlessFormattingGap = (transaction, item) => {
|
|||||||
*
|
*
|
||||||
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
|
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
|
||||||
*
|
*
|
||||||
* @param {YText} type
|
* @param {YText<any>} type
|
||||||
* @return {number} How many formatting attributes have been cleaned up.
|
* @return {number} How many formatting attributes have been cleaned up.
|
||||||
*/
|
*/
|
||||||
export const cleanupYTextFormatting = type => {
|
export const cleanupYTextFormatting = type => {
|
||||||
@@ -485,7 +485,7 @@ export const cleanupYTextFormatting = type => {
|
|||||||
*/
|
*/
|
||||||
export const cleanupYTextAfterTransaction = transaction => {
|
export const cleanupYTextAfterTransaction = transaction => {
|
||||||
/**
|
/**
|
||||||
* @type {Set<YText>}
|
* @type {Set<YText<any>>}
|
||||||
*/
|
*/
|
||||||
const needFullCleanup = new Set()
|
const needFullCleanup = new Set()
|
||||||
// check if another formatting item was inserted
|
// check if another formatting item was inserted
|
||||||
@@ -500,10 +500,10 @@ export const cleanupYTextAfterTransaction = transaction => {
|
|||||||
// cleanup in a new transaction
|
// cleanup in a new transaction
|
||||||
transact(doc, (t) => {
|
transact(doc, (t) => {
|
||||||
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
|
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
|
||||||
if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
|
if (item instanceof GC || !(/** @type {YText<any>} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText<any>} */ (item.parent))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const parent = /** @type {YText} */ (item.parent)
|
const parent = /** @type {YText<any>} */ (item.parent)
|
||||||
if (item.content.constructor === ContentFormat) {
|
if (item.content.constructor === ContentFormat) {
|
||||||
needFullCleanup.add(parent)
|
needFullCleanup.add(parent)
|
||||||
} else {
|
} else {
|
||||||
@@ -588,12 +588,13 @@ const deleteText = (transaction, currPos, length) => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends YEvent<YText>
|
* @template {{ [key:string]: any } | AbstractType<any> } TextEmbeds
|
||||||
|
* @extends YEvent<YText<any>>
|
||||||
* Event that describes the changes on a YText type.
|
* Event that describes the changes on a YText type.
|
||||||
*/
|
*/
|
||||||
export class YTextEvent extends YEvent {
|
export class YTextEvent extends YEvent {
|
||||||
/**
|
/**
|
||||||
* @param {YText} ytext
|
* @param {YText<TextEmbeds>} ytext
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {Set<any>} subs The keys that changed
|
* @param {Set<any>} subs The keys that changed
|
||||||
*/
|
*/
|
||||||
@@ -620,12 +621,12 @@ export class YTextEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta}}
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta<TextEmbeds>}}
|
||||||
*/
|
*/
|
||||||
get changes () {
|
get changes () {
|
||||||
if (this._changes === null) {
|
if (this._changes === null) {
|
||||||
/**
|
/**
|
||||||
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta}}
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta<TextEmbeds>}}
|
||||||
*/
|
*/
|
||||||
const changes = {
|
const changes = {
|
||||||
keys: this.keys,
|
keys: this.keys,
|
||||||
@@ -642,7 +643,7 @@ export class YTextEvent extends YEvent {
|
|||||||
* 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 {delta.TextDelta}
|
* @type {delta.TextDelta<TextEmbeds>}
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@@ -754,6 +755,7 @@ export class YTextEvent extends YEvent {
|
|||||||
* block formats (format information on a paragraph), embeds (complex elements
|
* block formats (format information on a paragraph), embeds (complex elements
|
||||||
* like pictures and videos), and text formats (**bold**, *italic*).
|
* like pictures and videos), and text formats (**bold**, *italic*).
|
||||||
*
|
*
|
||||||
|
* @template {any} Embeds
|
||||||
* @extends AbstractType<YTextEvent>
|
* @extends AbstractType<YTextEvent>
|
||||||
*/
|
*/
|
||||||
export class YText extends AbstractType {
|
export class YText extends AbstractType {
|
||||||
@@ -811,7 +813,7 @@ export class YText extends AbstractType {
|
|||||||
*
|
*
|
||||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||||
*
|
*
|
||||||
* @return {YText}
|
* @return {YText<Embeds>}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
const text = new YText()
|
const text = new YText()
|
||||||
@@ -916,14 +918,14 @@ export class YText extends AbstractType {
|
|||||||
* attribution `{ isDeleted: true, .. }`.
|
* attribution `{ isDeleted: true, .. }`.
|
||||||
*
|
*
|
||||||
* @param {AbstractAttributionManager} am
|
* @param {AbstractAttributionManager} am
|
||||||
* @return {import('../utils/Delta.js').TextDelta<string | import('./AbstractType.js').DeepContent >} The Delta representation of this type.
|
* @return {import('../utils/Delta.js').TextDelta< Embeds extends import('./AbstractType.js').AbstractType<any> ? import('./AbstractType.js').DeepContent : Embeds >} The Delta representation of this type.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getContentDeep (am = noAttributionsManager) {
|
getContentDeep (am = noAttributionsManager) {
|
||||||
return this.getContent(am).map(d =>
|
return this.getContent(am).map(d =>
|
||||||
d instanceof delta.InsertStringOp && d.insert instanceof AbstractType
|
d instanceof delta.InsertEmbedOp && d.insert instanceof AbstractType
|
||||||
? new delta.InsertStringOp(d.insert.getContent(am), d.attributes, d.attribution)
|
? new delta.InsertEmbedOp(d.insert.getContent(am), d.attributes, d.attribution)
|
||||||
: d
|
: d
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -936,7 +938,7 @@ export class YText extends AbstractType {
|
|||||||
* attribution `{ isDeleted: true, .. }`.
|
* attribution `{ isDeleted: true, .. }`.
|
||||||
*
|
*
|
||||||
* @param {AbstractAttributionManager} am
|
* @param {AbstractAttributionManager} am
|
||||||
* @return {import('../utils/Delta.js').TextDelta} The Delta representation of this type.
|
* @return {import('../utils/Delta.js').TextDelta<Embeds>} The Delta representation of this type.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -271,8 +271,9 @@ export class SnapshotAttributionManager {
|
|||||||
const inserts = createIdMap()
|
const inserts = createIdMap()
|
||||||
const deletes = createIdMapFromIdSet(diffIdSet(nextSnapshot.ds, prevSnapshot.ds), [createAttributionItem('change', '')])
|
const deletes = createIdMapFromIdSet(diffIdSet(nextSnapshot.ds, prevSnapshot.ds), [createAttributionItem('change', '')])
|
||||||
nextSnapshot.sv.forEach((clock, client) => {
|
nextSnapshot.sv.forEach((clock, client) => {
|
||||||
inserts.add(client, 0, prevSnapshot.sv.get(client) || 0, [])
|
const prevClock = prevSnapshot.sv.get(client) || 0
|
||||||
inserts.add(client, prevSnapshot.sv.get(client) || 0, clock, [createAttributionItem('change', '')])
|
inserts.add(client, 0, prevClock, []) // content is included in prevSnapshot is rendered without attributes
|
||||||
|
inserts.add(client, prevClock, clock - prevClock, [createAttributionItem('change', '')]) // content is rendered as "inserted"
|
||||||
})
|
})
|
||||||
this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes])
|
this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes])
|
||||||
}
|
}
|
||||||
@@ -289,10 +290,12 @@ export class SnapshotAttributionManager {
|
|||||||
let content = slice.length === 1 ? item.content : item.content.copy()
|
let content = slice.length === 1 ? item.content : item.content.copy()
|
||||||
slice.forEach(s => {
|
slice.forEach(s => {
|
||||||
const deleted = this.nextSnapshot.ds.has(item.id.client, s.clock)
|
const deleted = this.nextSnapshot.ds.has(item.id.client, s.clock)
|
||||||
|
const nonExistend = (this.nextSnapshot.sv.get(item.id.client) ?? 0) <= s.clock
|
||||||
const c = content
|
const c = content
|
||||||
if (s.len < c.getLength()) {
|
if (s.len < c.getLength()) {
|
||||||
content = c.splice(s.len)
|
content = c.splice(s.len)
|
||||||
}
|
}
|
||||||
|
if (nonExistend) return
|
||||||
if (!deleted || (s.attrs != null && s.attrs.length > 0)) {
|
if (!deleted || (s.attrs != null && s.attrs.length > 0)) {
|
||||||
let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null
|
let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null
|
||||||
if (s.attrs?.length === 0) {
|
if (s.attrs?.length === 0) {
|
||||||
|
|||||||
@@ -192,10 +192,6 @@ export class RetainOp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {string | { [key: string]: any }} TextDeltaContent
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {(TextDelta<any> | ArrayDelta<any>)} Delta
|
* @typedef {(TextDelta<any> | ArrayDelta<any>)} Delta
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ export const compare = users => {
|
|||||||
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
||||||
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
||||||
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
|
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
|
||||||
const userTextValues = users.map(u => u.getText('text').getContent())
|
const userTextValues = users.map(u => u.getText('text').getContentDeep())
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
t.assert(u.store.pendingDs === null)
|
t.assert(u.store.pendingDs === null)
|
||||||
t.assert(u.store.pendingStructs === null)
|
t.assert(u.store.pendingStructs === null)
|
||||||
|
|||||||
@@ -1872,7 +1872,7 @@ export const testSnapshotDeleteAfter = tc => {
|
|||||||
}, {
|
}, {
|
||||||
insert: 'e'
|
insert: 'e'
|
||||||
}])
|
}])
|
||||||
const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot1))
|
const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1))
|
||||||
t.compare(state1, delta.fromJSON([{ insert: 'abcd' }]))
|
t.compare(state1, delta.fromJSON([{ insert: 'abcd' }]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user