mirror of
https://github.com/yjs/yjs.git
synced 2026-02-23 19:49:59 +01:00
suggestion fixes
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
createAttributionFromAttributionItems,
|
||||
mergeIdSets,
|
||||
diffIdSet,
|
||||
createIdSet,
|
||||
ContentDeleted
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -230,7 +231,7 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
||||
// check if we really need to remove attributes
|
||||
while (
|
||||
currPos.right !== null && (
|
||||
currPos.right.deleted === true || (
|
||||
(currPos.right.deleted && (currPos.am == noAttributionsManager || currPos.am.contentLength(currPos.right) === 0)) || (
|
||||
currPos.right.content.constructor === ContentFormat &&
|
||||
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
|
||||
)
|
||||
@@ -281,7 +282,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
|
||||
while (true) {
|
||||
if (currPos.right === null) {
|
||||
break
|
||||
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
||||
} else if (currPos.right.deleted ? (currPos.am.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
||||
//
|
||||
} else {
|
||||
break
|
||||
@@ -539,7 +540,7 @@ const deleteText = (transaction, currPos, length) => {
|
||||
const startAttrs = map.copy(currPos.currentAttributes)
|
||||
const start = currPos.right
|
||||
while (length > 0 && currPos.right !== null) {
|
||||
if (currPos.right.deleted === false) {
|
||||
if (!currPos.right.deleted) {
|
||||
switch (currPos.right.content.constructor) {
|
||||
case ContentType:
|
||||
case ContentEmbed:
|
||||
@@ -551,6 +552,19 @@ const deleteText = (transaction, currPos, length) => {
|
||||
currPos.right.delete(transaction)
|
||||
break
|
||||
}
|
||||
} else if (currPos.am !== noAttributionsManager) {
|
||||
const item = currPos.right
|
||||
let d = currPos.am.contentLength(item)
|
||||
if (d > 0) {
|
||||
if (length < d) {
|
||||
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
|
||||
d = length
|
||||
}
|
||||
// deleting already deleted content. store that information in a meta property, but do
|
||||
// nothing
|
||||
map.setIfUndefined(transaction.meta, 'attributedDeletes', createIdSet).add(item.id.client, item.id.clock, d)
|
||||
length -= d
|
||||
}
|
||||
}
|
||||
currPos.forward()
|
||||
}
|
||||
@@ -954,7 +968,7 @@ export class YText extends AbstractType {
|
||||
d.usedAttributes = currentAttributes
|
||||
usingCurrentAttributes = true
|
||||
if (c.deleted ? retainDeletes : retainInserts) {
|
||||
d.retain(/** @type {ContentString} */ (c.content).str.length, null, attribution ?? null)
|
||||
d.retain(/** @type {ContentString} */ (c.content).str.length, null, attribution ?? {})
|
||||
} else {
|
||||
d.insert(/** @type {ContentString} */ (c.content).str, null, attribution)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,15 @@ import {
|
||||
createID,
|
||||
mergeIdSets,
|
||||
IdSet, Item, Snapshot, Doc, AbstractContent, IdMap, // eslint-disable-line
|
||||
intersectSets
|
||||
applyUpdate,
|
||||
writeIdSet,
|
||||
UpdateEncoderV1,
|
||||
transact
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
/**
|
||||
* @todo rename this to `insertBy`, `insertAt`, ..
|
||||
@@ -48,35 +52,33 @@ export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
* @type {Attribution}
|
||||
*/
|
||||
const attribution = {}
|
||||
if (attrs != null) {
|
||||
if (deleted) {
|
||||
attribution.delete = []
|
||||
} else {
|
||||
attribution.insert = []
|
||||
}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
case 'acceptDelete':
|
||||
delete attribution.delete
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'acceptInsert':
|
||||
delete attribution.insert
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'insert':
|
||||
case 'delete': {
|
||||
const as = /** @type {import('../utils/Delta.js').Attribution} */ (attribution)
|
||||
const ls = as[attr.name] = as[attr.name] ?? []
|
||||
ls.push(attr.val)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (attr.name[0] !== '_') {
|
||||
/** @type {any} */ (attribution)[attr.name] = attr.val
|
||||
}
|
||||
if (deleted) {
|
||||
attribution.delete = []
|
||||
} else {
|
||||
attribution.insert = []
|
||||
}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
case 'acceptDelete':
|
||||
delete attribution.delete
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'acceptInsert':
|
||||
delete attribution.insert
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'insert':
|
||||
case 'delete': {
|
||||
const as = /** @type {import('../utils/Delta.js').Attribution} */ (attribution)
|
||||
const ls = as[attr.name] = as[attr.name] ?? []
|
||||
ls.push(attr.val)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (attr.name[0] !== '_') {
|
||||
/** @type {any} */ (attribution)[attr.name] = attr.val
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return attribution
|
||||
}
|
||||
|
||||
@@ -123,6 +125,8 @@ export class AbstractAttributionManager extends ObservableV2 {
|
||||
* Calculate the length of the attributed content. This is used by iterators that walk through the
|
||||
* content.
|
||||
*
|
||||
* If the content is not countable, it should return 0.
|
||||
*
|
||||
* @param {Item} _item
|
||||
* @return {number}
|
||||
*/
|
||||
@@ -174,7 +178,9 @@ export class TwosetAttributionManager extends ObservableV2 {
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (item) {
|
||||
if (!item.deleted) {
|
||||
if (!item.content.isCountable()) {
|
||||
return 0
|
||||
} else if (!item.deleted) {
|
||||
return item.length
|
||||
} else {
|
||||
return this.deletes.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
|
||||
@@ -209,7 +215,7 @@ export class NoAttributionsManager extends ObservableV2 {
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (item) {
|
||||
return item.deleted ? 0 : item.length
|
||||
return (item.deleted || !item.content.isCountable()) ? 0 : item.length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +255,14 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
this._prevBOH = prevDoc.on('beforeObserverCalls', tr => {
|
||||
insertIntoIdSet(_prevDocInserts, tr.insertSet)
|
||||
insertIntoIdSet(prevDocDeletes, tr.deleteSet)
|
||||
insertIntoIdMap(this.inserts, createIdMapFromIdSet(intersectSets(tr.insertSet, this.inserts), [createAttributionItem('acceptInsert', 'unknown')]))
|
||||
// insertIntoIdMap(this.inserts, createIdMapFromIdSet(intersectSets(tr.insertSet, this.inserts), [createAttributionItem('acceptInsert', 'unknown')]))
|
||||
if (tr.insertSet.clients.size < 2) {
|
||||
tr.insertSet.forEach((attrRange, client) => {
|
||||
this.inserts.delete(client, attrRange.clock, attrRange.len)
|
||||
})
|
||||
} else {
|
||||
this.inserts = diffIdMap(this.inserts, tr.insertSet)
|
||||
}
|
||||
// insertIntoIdMap(this.deletes, createIdMapFromIdSet(intersectSets(tr.deleteSet, this.deletes), [createAttributionItem('acceptDelete', 'unknown')]))
|
||||
if (tr.deleteSet.clients.size < 2) {
|
||||
tr.deleteSet.forEach((attrRange, client) => {
|
||||
@@ -261,6 +274,41 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
// @todo fire update ranges on `tr.insertSet` and `tr.deleteSet`
|
||||
this.emit('change', [mergeIdSets([tr.insertSet, tr.deleteSet]), tr.origin, tr.local])
|
||||
})
|
||||
// changes from prevDoc should always flow into suggestionDoc
|
||||
// changes from suggestionDoc only flow into ydoc if suggestion-mode is disabled
|
||||
this._prevUpdateListener = prevDoc.on('update', (update, origin) => {
|
||||
origin !== this && applyUpdate(nextDoc, update)
|
||||
})
|
||||
this._ndUpdateListener = nextDoc.on('update', (update, origin, _doc, tr) => {
|
||||
// only if event is local and suggestion mode is enabled
|
||||
if (!this.suggestionMode && tr.local && (this.suggestionOrigins == null || this.suggestionOrigins.some(o => o === origin))) {
|
||||
applyUpdate(prevDoc, update, this)
|
||||
}
|
||||
})
|
||||
this._afterTrListener = nextDoc.on('afterTransaction', (tr) => {
|
||||
// apply deletes on attributed deletes (content that is already deleted, but is rendered by
|
||||
// the attribution manager)
|
||||
if (!this.suggestionMode && tr.local && (this.suggestionOrigins == null || this.suggestionOrigins.some(o => o === origin))) {
|
||||
const attributedDeletes = tr.meta.get('attributedDeletes')
|
||||
if (attributedDeletes != null) {
|
||||
transact(prevDoc, () => {
|
||||
// apply attributed deletes if there are any
|
||||
const ds = new UpdateEncoderV1()
|
||||
encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
|
||||
writeIdSet(ds, attributedDeletes)
|
||||
applyUpdate(prevDoc, ds.toUint8Array())
|
||||
}, this)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.suggestionMode = true
|
||||
/**
|
||||
* Optionally limit origins that may sync changes to the main doc if suggestion-mode is
|
||||
* disabled.
|
||||
*
|
||||
* @type {Array<any>?}
|
||||
*/
|
||||
this.suggestionOrigins = null
|
||||
this._destroyHandler = nextDoc.on('destroy', this.destroy.bind(this))
|
||||
prevDoc.on('destroy', this._destroyHandler)
|
||||
}
|
||||
@@ -271,6 +319,9 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
this._prevDoc.off('destroy', this._destroyHandler)
|
||||
this._nextDoc.off('beforeObserverCalls', this._nextBOH)
|
||||
this._prevDoc.off('beforeObserverCalls', this._prevBOH)
|
||||
this._prevDoc.off('update', this._prevUpdateListener)
|
||||
this._nextDoc.off('update', this._ndUpdateListener)
|
||||
this._nextDoc.off('afterTransaction', this._afterTrListener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,10 +367,18 @@ export class DiffAttributionManager extends ObservableV2 {
|
||||
*/
|
||||
contentLength (item) {
|
||||
if (!item.deleted) {
|
||||
return item.length
|
||||
} else {
|
||||
return this.deletes.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
|
||||
return item.content.isCountable() ? item.length : 0
|
||||
}
|
||||
const slice = this.deletes.sliceId(item.id, item.length)
|
||||
let content = item.content
|
||||
if (content instanceof ContentDeleted && slice[0].attrs != null && !this.inserts.hasId(item.id)) {
|
||||
const prevItem = getItem(this._prevDocStore, item.id)
|
||||
content = prevItem.content
|
||||
}
|
||||
if (!content.isCountable()) {
|
||||
return 0
|
||||
}
|
||||
return slice.reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +452,12 @@ export class SnapshotAttributionManager extends ObservableV2 {
|
||||
* @return {number}
|
||||
*/
|
||||
contentLength (item) {
|
||||
return this.attrs.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
|
||||
return item.content.isCountable()
|
||||
? (item.deleted
|
||||
? this.attrs.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
|
||||
: item.length
|
||||
)
|
||||
: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -268,10 +268,7 @@ export class AbstractDelta {
|
||||
* @param {T | null} a
|
||||
* @param {T | null} b
|
||||
*/
|
||||
const mergeAttrs = (a, b) => {
|
||||
const merged = object.isEmpty(a) ? b : (object.isEmpty(b) ? a : object.assign({}, a, b))
|
||||
return object.isEmpty(merged) ? null : merged
|
||||
}
|
||||
const mergeAttrs = (a, b) => object.isEmpty(a) ? b : (object.isEmpty(b) ? a : object.assign({}, a, b))
|
||||
|
||||
/**
|
||||
* @template {'array' | 'text' | 'custom'} Type
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ContentType,
|
||||
followRedone,
|
||||
getItem,
|
||||
StructStore, ID, Doc, AbstractType, // eslint-disable-line
|
||||
StructStore, ID, Doc, AbstractType,
|
||||
noAttributionsManager, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as encoding from 'lib0/encoding'
|
||||
@@ -72,6 +73,7 @@ export class RelativePosition {
|
||||
* @type {number}
|
||||
*/
|
||||
this.assoc = assoc
|
||||
this.item && console.log('created relpos', this.item) // @todo remove
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,11 +158,12 @@ export const createRelativePosition = (type, item, assoc) => {
|
||||
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||
* @param {number} index The absolute position.
|
||||
* @param {number} [assoc]
|
||||
* @param {import('../utils/AttributionManager.js').AbstractAttributionManager} attributionManager
|
||||
* @return {RelativePosition}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
|
||||
export const createRelativePositionFromTypeIndex = (type, index, assoc = 0, attributionManager = noAttributionsManager) => {
|
||||
let t = type._start
|
||||
if (assoc < 0) {
|
||||
// associated to the left character or the beginning of a type, increment index if possible.
|
||||
@@ -170,13 +173,12 @@ export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
|
||||
index--
|
||||
}
|
||||
while (t !== null) {
|
||||
if (!t.deleted && t.countable) {
|
||||
if (t.length > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc)
|
||||
}
|
||||
index -= t.length
|
||||
const len = attributionManager.contentLength(t)
|
||||
if (len > index) {
|
||||
// case 1: found position somewhere in the linked list
|
||||
return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc)
|
||||
}
|
||||
index -= len
|
||||
if (t.right === null && assoc < 0) {
|
||||
// left-associated position, return last available id
|
||||
return createRelativePosition(type, t.lastId, assoc)
|
||||
@@ -282,11 +284,12 @@ const getItemWithOffset = (store, id) => {
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {Doc} doc
|
||||
* @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
|
||||
* @param {import('../utils/AttributionManager.js').AbstractAttributionManager} attributionManager
|
||||
* @return {AbsolutePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true, attributionManager = noAttributionsManager) => {
|
||||
const store = doc.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
@@ -305,12 +308,10 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo
|
||||
}
|
||||
type = /** @type {AbstractType<any>} */ (right.parent)
|
||||
if (type._item === null || !type._item.deleted) {
|
||||
index = (right.deleted || !right.countable) ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary
|
||||
index = attributionManager.contentLength(right) === 0 ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary
|
||||
let n = right.left
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable) {
|
||||
index += n.length
|
||||
}
|
||||
index += attributionManager.contentLength(n)
|
||||
n = n.left
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@ import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
import * as delta from '../src/utils/Delta.js'
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testRelativePositions = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
ytext.insert(0, 'hello world')
|
||||
const v1 = Y.cloneDoc(ydoc)
|
||||
ytext.delete(1, 6)
|
||||
ytext.insert(1, 'x', )
|
||||
const am = Y.createAttributionManagerFromDiff(v1, ydoc)
|
||||
const rel = Y.createRelativePositionFromTypeIndex(ytext, 9, 1, am) // pos after "hello wo"
|
||||
const abs1 = Y.createAbsolutePositionFromRelativePosition(rel, ydoc, true, am)
|
||||
const abs2 = Y.createAbsolutePositionFromRelativePosition(rel, ydoc, true)
|
||||
t.assert(abs1?.index === 9)
|
||||
t.assert(abs2?.index === 3)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user