added utilities to make and recover snapshots

This commit is contained in:
Kevin Jahns
2019-01-29 00:54:58 +01:00
parent 77e479c03b
commit 3a0694c35c
23 changed files with 337 additions and 109 deletions

View File

@@ -11,6 +11,8 @@ import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state
import * as math from '../lib/math.js'
import * as object from '../lib/object.js'
import * as YPos from '../utils/relativePosition.js'
import { isVisible } from '../utils/snapshot.js'
import { simpleDiff } from '../lib/diff.js'
/**
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
@@ -33,6 +35,9 @@ export const prosemirrorPluginKey = new PluginKey('yjs')
export const prosemirrorPlugin = yXmlFragment => {
let changedInitialContent = false
const plugin = new Plugin({
props: {
editable: (state) => prosemirrorPluginKey.getState(state).snapshot == null
},
key: prosemirrorPluginKey,
state: {
init: (initargs, state) => {
@@ -55,7 +60,16 @@ export const prosemirrorPlugin = yXmlFragment => {
if (change !== undefined && change.snapshot !== undefined) {
// snapshot changed, rerender next
setTimeout(() => {
pluginState.binding._renderSnapshot(change.snapshot)
if (change.restore == null) {
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot)
} else {
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot)
// reset to current prosemirror state
delete pluginState.restore
delete pluginState.snapshot
delete pluginState.prevSnapshot
pluginState.binding._prosemirrorChanged(pluginState.binding.prosemirrorView.state.doc)
}
}, 0)
} else if (pluginState.snapshot == null) {
// only apply if no snapshot active
@@ -112,7 +126,14 @@ export const cursorPlugin = new Plugin({
const y = ystate.y
const awareness = y.getAwarenessInfo()
const decorations = []
if (ystate.snapshot != null) {
// do not render cursors while snapshot is active
return
}
awareness.forEach((aw, userID) => {
if (userID === y.userID) {
return
}
if (aw.cursor != null) {
let user = aw.user || {}
if (user.color == null) {
@@ -154,7 +175,7 @@ export const cursorPlugin = new Plugin({
}
const updateCursorInfo = () => {
const current = y.getLocalAwarenessInfo()
if (view.hasFocus()) {
if (view.hasFocus() && ystate.binding !== null) {
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
@@ -321,11 +342,24 @@ export class ProsemirrorBinding {
})
yXmlFragment.observeDeep(this._observeFunction)
}
_renderSnapshot (snapshot) {
_forceRerender () {
this.mapping = new Map()
this.mux(() => {
const fragmentContent = this.type.toArray().map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
this.prosemirrorView.dispatch(tr)
})
}
/**
*
* @param {*} snapshot
* @param {*} prevSnapshot
*/
_renderSnapshot (snapshot, prevSnapshot) {
// clear mapping because we are going to rerender
this.mapping = new Map()
this.mux(() => {
const fragmentContent = this.type.toArray(snapshot).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot)).filter(n => n !== null)
const fragmentContent = this.type.toArray({ sm: snapshot.sm, ds: prevSnapshot.ds}).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot, prevSnapshot)).filter(n => n !== null)
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
this.prosemirrorView.dispatch(tr)
})
@@ -370,12 +404,13 @@ export class ProsemirrorBinding {
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {HistorySnapshot} [snapshot]
* @param {HistorySnapshot} [prevSnapshot]
* @return {PModel.Node}
*/
export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => {
const node = mapping.get(el)
if (node === undefined) {
return createNodeFromYElement(el, schema, mapping, snapshot)
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot)
}
return node
}
@@ -385,19 +420,31 @@ export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
* @param {YXmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {import('../protocols/history.js').HistorySnapshot} snapshot
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
* @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot]
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
*/
export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
export const createNodeFromYElement = (el, schema, mapping, snapshot, prevSnapshot) => {
let _snapshot = snapshot
let _prevSnapshot = prevSnapshot
if (snapshot !== undefined && prevSnapshot !== undefined) {
if (!isVisible(el, snapshot)) {
// if this element is already rendered as deleted (ychange), then do not render children as deleted
_snapshot = {sm: snapshot.sm, ds: prevSnapshot.ds}
_prevSnapshot = _snapshot
} else if (!isVisible(el, prevSnapshot)) {
_prevSnapshot = _snapshot
}
}
const children = []
el.toArray(snapshot).forEach(type => {
const createChildren = type => {
if (type.constructor === YXmlElement) {
const n = createNodeIfNotExists(type, schema, mapping, snapshot)
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot)
if (n !== null) {
children.push(n)
}
} else {
const ns = createTextNodesFromYText(type, schema, mapping, snapshot)
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot)
if (ns !== null) {
ns.forEach(textchild => {
if (textchild !== null) {
@@ -406,16 +453,31 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
})
}
}
})
}
if (snapshot === undefined || prevSnapshot === undefined) {
el.toArray().forEach(createChildren)
} else {
el.toArray({sm: snapshot.sm, ds: prevSnapshot.ds}).forEach(createChildren)
}
let node
try {
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(snapshot), children)
const attrs = el.getAttributes(_snapshot)
if (snapshot !== undefined) {
if (!isVisible(el, snapshot)) {
attrs.ychange = { user: el._id.user, state: 'removed' }
} else if (!isVisible(el, prevSnapshot)) {
attrs.ychange = { user: el._id.user, state: 'added' }
}
}
node = schema.node(el.nodeName.toLowerCase(), attrs, children)
} catch (e) {
// an error occured while creating the node. This is probably a result because of a concurrent action.
// delete the node and do not push to children
// ignore the node while rendering
/* do not delete anymore
el._y.transact(() => {
el._delete(el._y, true)
})
*/
return null
}
mapping.set(el, node)
@@ -427,12 +489,13 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
* @param {YText} text
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {HistorySnapshot} [snapshot]
* @param {HistorySnapshot} [snapshot]
* @param {HistorySnapshot} [prevSnapshot]
* @return {Array<PModel.Node>}
*/
export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => {
const nodes = []
const deltas = text.toDelta(snapshot)
const deltas = text.toDelta(snapshot, prevSnapshot)
try {
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i]
@@ -446,9 +509,11 @@ export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
}
} catch (e) {
/*
text._y.transact(() => {
text._delete(text._y, true)
})
*/
return null
}
return nodes
@@ -465,13 +530,17 @@ export const createTypeFromNode = (node, mapping) => {
if (node.isText) {
type = new YText()
const attrs = {}
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
node.marks.forEach(mark => {
if (mark.type.name !== 'ychange') {
attrs[mark.type.name] = mark.attrs
}
})
type.insert(0, node.text, attrs)
} else {
type = new YXmlElement(node.type.name)
for (let key in node.attrs) {
const val = node.attrs[key]
if (val !== null) {
if (val !== null && key !== 'ychange') {
type.setAttribute(key, val)
}
}
@@ -490,7 +559,9 @@ const equalAttrs = (pattrs, yattrs) => {
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
for (let i = 0; i < keys.length && eq; i++) {
const key = keys[i]
eq = pattrs[key] === yattrs[key]
const l = pattrs[key]
const r = yattrs[key]
eq = key === 'ychange' || l === r || (typeof l === 'object' && typeof r === 'object' && equalAttrs(l, r))
}
return eq
}
@@ -554,7 +625,7 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
const pAttrs = pContent.attrs
for (let key in pAttrs) {
if (pAttrs[key] !== null) {
if (yDomAttrs[key] !== pAttrs[key]) {
if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
yDomFragment.setAttribute(key, pAttrs[key])
}
} else {
@@ -610,8 +681,23 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
const rightP = pContent.child(pChildCnt - right - 1)
if (leftY.constructor === YText && leftP.isText) {
if (!equalYTextPText(leftY, leftP)) {
yDomFragment.delete(left, 1)
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
// try to apply diff. Only if attrs don't match, delete insert
// TODO: use a single ytext to hold all following Prosemirror Text nodes
const pattrs = {}
leftP.marks.forEach(mark => {
if (mark.type.name !== 'ychange') {
pattrs[mark.type.name] = mark.attrs
}
})
const delta = leftY.toDelta()
if (delta.length === 1 && delta[0].insert && equalAttrs(pattrs, delta[0].attributes || {})) {
const diff = simpleDiff(delta[0].insert, leftP.text)
leftY.delete(diff.pos, diff.remove)
leftY.insert(diff.pos, diff.insert)
} else {
yDomFragment.delete(left, 1)
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
}
}
left += 1
} else {