1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 01:50:06 +02:00

Fix normalizing dirty paths (#2222)

#### Is this adding or improving a _feature_ or fixing a _bug_?

Fix.

#### What's the new behavior?

The dirty paths in a change are now transformed against incoming operations, such that they don't get out of sync as normalizations occur. This is a rough pass to get correctness and the bug fixed, and we can later optimize lots of the little details for performance.

#### Have you checked that...?

<!-- 
Please run through this checklist for your pull request: 
-->

* [x] The new code matches the existing patterns and styles.
* [x] The tests pass with `yarn test`.
* [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.)
* [x] The relevant examples still work. (Run examples with `yarn watch`.)

#### Does this fix any issues or need any specific reviewers?

Fixes: #2211
Fixes: #2215
Fixes: #2194
This commit is contained in:
Ian Storm Taylor
2018-10-02 15:57:48 -07:00
committed by GitHub
parent 9ed08c1544
commit d84f5072c1
3 changed files with 191 additions and 103 deletions

View File

@@ -39,7 +39,7 @@ export const output = {
nodes: [ nodes: [
{ {
object: 'text', object: 'text',
key: '11', key: '13',
leaves: [ leaves: [
{ {
object: 'leaf', object: 'leaf',
@@ -69,7 +69,7 @@ export const output = {
}, },
{ {
object: 'text', object: 'text',
key: '12', key: '14',
leaves: [ leaves: [
{ {
object: 'leaf', object: 'leaf',
@@ -88,7 +88,7 @@ export const output = {
nodes: [ nodes: [
{ {
object: 'text', object: 'text',
key: '13', key: '11',
leaves: [ leaves: [
{ {
object: 'leaf', object: 'leaf',
@@ -118,7 +118,7 @@ export const output = {
}, },
{ {
object: 'text', object: 'text',
key: '14', key: '12',
leaves: [ leaves: [
{ {
object: 'leaf', object: 'leaf',

View File

@@ -1,7 +1,7 @@
import Debug from 'debug' import Debug from 'debug'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import warning from 'slate-dev-warning' import warning from 'slate-dev-warning'
import { List, Map } from 'immutable' import { List } from 'immutable'
import Changes from '../changes' import Changes from '../changes'
import Operation from './operation' import Operation from './operation'
@@ -55,7 +55,6 @@ class Change {
const { operations } = this const { operations } = this
let { value } = this let { value } = this
let { history } = value let { history } = value
const oldValue = value
// Add in the current `value` in case the operation was serialized. // Add in the current `value` in case the operation was serialized.
if (isPlainObject(operation)) { if (isPlainObject(operation)) {
@@ -84,9 +83,16 @@ class Change {
value = value.set('history', history) value = value.set('history', history)
} }
// Get the keys of the affected nodes, and mark them as dirty. // Get the paths of the affected nodes, and mark them as dirty.
const keys = getDirtyKeys(operation, value, oldValue) const newDirtyPaths = getDirtyPaths(operation)
this.tmp.dirty = this.tmp.dirty.concat(keys) const dirty = this.tmp.dirty.reduce((memo, path) => {
path = PathUtils.create(path)
const transformed = PathUtils.transform(path, operation)
memo = memo.concat(transformed.toArray())
return memo
}, newDirtyPaths)
this.tmp.dirty = dirty
// Update the mutable change object. // Update the mutable change object.
this.value = value this.value = value
@@ -117,7 +123,7 @@ class Change {
call(fn, ...args) { call(fn, ...args) {
fn(this, ...args) fn(this, ...args)
this.normalizeDirtyOperations() this.normalizeDirtyPaths()
return this return this
} }
@@ -130,75 +136,29 @@ class Change {
normalize() { normalize() {
const { value } = this const { value } = this
const { document } = value const { document } = value
const keys = Object.keys(document.getKeysToPathsTable())
this.normalizeKeys(keys)
return this
}
/**
* Normalize any new "dirty" operations that have been added to the change.
*
* @return {Change}
*/
normalizeDirtyOperations() {
const { normalize, dirty } = this.tmp
if (!normalize) return this
if (!dirty.length) return this
this.tmp.dirty = []
this.normalizeKeys(dirty)
return this
}
/**
* Normalize a set of nodes by their `keys`.
*
* @param {Array} keys
* @return {Change}
*/
normalizeKeys(keys) {
const { value } = this
const { document } = value
// TODO: if we had an `Operations.tranform` method, we could optimize this
// to not use keys, and instead used transformed operation paths.
const table = document.getKeysToPathsTable() const table = document.getKeysToPathsTable()
let map = Map() const paths = Object.values(table).map(PathUtils.create)
this.tmp.dirty = this.tmp.dirty.concat(paths)
// TODO: this could be optimized to not need the nested map, and instead use this.normalizeDirtyPaths()
// clever sorting to arrive at the proper depth-first normalizing.
keys.forEach(key => {
const path = table[key]
if (!path) return
if (!path.length) return
if (!map.hasIn(path)) map = map.setIn(path, Map())
})
// To avoid infinite loops, we need to defer normalization until the end.
this.withoutNormalizing(() => {
this.normalizeMapAndPath(map)
})
return this return this
} }
/** /**
* Normalize all of the nodes in a normalization `map`, depth-first. An * Normalize any new "dirty" paths that have been added to the change.
* additional `path` argument specifics the current depth/location.
* *
* @param {Map} map
* @param {Array} path (optional)
* @return {Change} * @return {Change}
*/ */
normalizeMapAndPath(map, path = []) { normalizeDirtyPaths() {
map.forEach((m, k) => { if (!this.tmp.normalize) {
const p = [...path, k] return this
this.normalizeMapAndPath(m, p) }
})
while (this.tmp.dirty.length) {
const path = this.tmp.dirty.pop()
this.normalizeNodeByPath(path)
}
this.normalizePath(path)
return this return this
} }
@@ -210,7 +170,7 @@ class Change {
* @return {Change} * @return {Change}
*/ */
normalizePath(path) { normalizeNodeByPath(path) {
const { value } = this const { value } = this
let { document, schema } = value let { document, schema } = value
let node = document.assertNode(path) let node = document.assertNode(path)
@@ -264,7 +224,10 @@ class Change {
iterate() iterate()
} }
iterate() this.withoutNormalizing(() => {
iterate()
})
return this return this
} }
@@ -281,11 +244,7 @@ class Change {
this.tmp.normalize = false this.tmp.normalize = false
fn(this) fn(this)
this.tmp.normalize = value this.tmp.normalize = value
this.normalizeDirtyPaths()
if (this.tmp.normalize) {
this.normalizeDirtyOperations()
}
return this return this
} }
@@ -373,18 +332,14 @@ class Change {
} }
/** /**
* Get the "dirty" nodes's keys for a given `operation` and values. * Get the "dirty" paths for a given `operation`.
* *
* @param {Operation} operation * @param {Operation} operation
* @param {Value} newValue
* @param {Value} oldValue
* @return {Array} * @return {Array}
*/ */
function getDirtyKeys(operation, newValue, oldValue) { function getDirtyPaths(operation) {
const { type, node, path, newPath } = operation const { type, node, path, newPath } = operation
const newDocument = newValue.document
const oldDocument = oldValue.document
switch (type) { switch (type) {
case 'add_mark': case 'add_mark':
@@ -393,46 +348,50 @@ function getDirtyKeys(operation, newValue, oldValue) {
case 'remove_text': case 'remove_text':
case 'set_mark': case 'set_mark':
case 'set_node': { case 'set_node': {
const target = newDocument.assertNode(path) return [path]
const keys = [target.key]
return keys
} }
case 'insert_node': { case 'insert_node': {
const table = node.getKeysToPathsTable() const table = node.getKeysToPathsTable()
const keys = Object.keys(table) const paths = Object.values(table).map(p => path.concat(p))
return keys const parentPath = PathUtils.lift(path)
return [parentPath, path, ...paths]
} }
case 'split_node': { case 'split_node': {
const parentPath = PathUtils.lift(path)
const nextPath = PathUtils.increment(path) const nextPath = PathUtils.increment(path)
const target = newDocument.assertNode(path) return [parentPath, path, nextPath]
const split = newDocument.assertNode(nextPath)
const keys = [target.key, split.key]
return keys
} }
case 'merge_node': { case 'merge_node': {
const parentPath = PathUtils.lift(path)
const previousPath = PathUtils.decrement(path) const previousPath = PathUtils.decrement(path)
const merged = newDocument.assertNode(previousPath) return [parentPath, previousPath]
const keys = [merged.key]
return keys
} }
case 'move_node': { case 'move_node': {
const parentPath = PathUtils.lift(path) let parentPath = PathUtils.lift(path)
const newParentPath = PathUtils.lift(newPath) let newParentPath = PathUtils.lift(newPath)
const oldParent = oldDocument.assertNode(parentPath)
const newParent = oldDocument.assertNode(newParentPath) // HACK: this clause only exists because the `move_path` logic isn't
const keys = [oldParent.key, newParent.key] // consistent when it deals with siblings.
return keys if (!PathUtils.isSibling(path, newPath)) {
if (newParentPath.size && PathUtils.isYounger(path, newPath)) {
newParentPath = PathUtils.decrement(newParentPath, 1, path.size - 1)
}
if (parentPath.size && PathUtils.isYounger(newPath, path)) {
parentPath = PathUtils.increment(parentPath, 1, newPath.size - 1)
}
}
return [parentPath, newParentPath]
} }
case 'remove_node': { case 'remove_node': {
const parentPath = PathUtils.lift(path) const parentPath = PathUtils.lift(path)
const parent = newDocument.assertNode(parentPath) return [parentPath]
const keys = [parent.key]
return keys
} }
default: { default: {

View File

@@ -143,6 +143,23 @@ function isEqual(path, target) {
return path.equals(target) return path.equals(target)
} }
/**
* Is a `path` older than a `target` path? Meaning that it ends as an older
* sibling of one of the indexes in the target.
*
* @param {List} path
* @param {List} target
* @return {Boolean}
*/
function isOlder(path, target) {
const index = path.size - 1
const [p, t] = crop(path, target, index)
const pl = path.get(index)
const tl = target.get(index)
return isEqual(p, t) && pl > tl
}
/** /**
* Is a `path` a sibling of a `target` path? * Is a `path` a sibling of a `target` path?
* *
@@ -158,6 +175,23 @@ function isSibling(path, target) {
return p.equals(t) return p.equals(t)
} }
/**
* Is a `path` younger than a `target` path? Meaning that it ends as a younger
* sibling of one of the indexes in the target.
*
* @param {List} path
* @param {List} target
* @return {Boolean}
*/
function isYounger(path, target) {
const index = path.size - 1
const [p, t] = crop(path, target, index)
const pl = path.get(index)
const tl = target.get(index)
return isEqual(p, t) && pl < tl
}
/** /**
* Lift a `path` to refer to its parent. * Lift a `path` to refer to its parent.
* *
@@ -222,6 +256,98 @@ function relate(a, b) {
return path return path
} }
/**
* Transform a `path` by an `operation`, adjusting it to stay current.
*
* @param {List} path
* @param {Operation} operation
* @return {List<List>}
*/
function transform(path, operation) {
const { type, position, path: p } = operation
if (
type === 'add_mark' ||
type === 'insert_text' ||
type === 'remove_mark' ||
type === 'remove_text' ||
type === 'set_mark' ||
type === 'set_node' ||
type === 'set_selection' ||
type === 'set_value' ||
path.size === 0
) {
return List([path])
}
const pIndex = p.size - 1
const pEqual = isEqual(p, path)
const pYounger = isYounger(p, path)
const pAbove = isAbove(p, path)
if (type === 'insert_node') {
if (pEqual || pYounger || pAbove) {
path = increment(path, 1, pIndex)
}
}
if (type === 'remove_node') {
if (pYounger) {
path = decrement(path, 1, pIndex)
} else if (pEqual || pAbove) {
path = []
}
}
if (type === 'merge_node') {
if (pEqual || pYounger) {
path = decrement(path, 1, pIndex)
} else if (pAbove) {
path = decrement(path, 1, pIndex)
path = increment(path, position, pIndex + 1)
}
}
if (type === 'split_node') {
if (pEqual) {
path = [path, increment(path)]
} else if (pYounger) {
path = increment(path, 1, pIndex)
} else if (pAbove) {
if (path.get(pIndex + 1) >= position) {
path = increment(path, 1, pIndex)
path = decrement(path, position, pIndex + 1)
}
}
}
if (type === 'move_node') {
const { newPath: np } = operation
const npIndex = np.size - 1
const npEqual = isEqual(np, path)
const npYounger = isYounger(np, path)
const npAbove = isAbove(np, path)
if (pAbove) {
path = np.concat(path.slice(p.size))
} else {
if (pEqual) {
path = np
} else if (pYounger) {
path = decrement(path, 1, pIndex)
}
if (npEqual || npYounger || npAbove) {
path = increment(path, 1, npIndex)
}
}
}
const paths = Array.isArray(path) ? path : [path]
return List(paths)
}
/** /**
* Export. * Export.
* *
@@ -238,9 +364,12 @@ export default {
isAfter, isAfter,
isBefore, isBefore,
isEqual, isEqual,
isOlder,
isSibling, isSibling,
isYounger,
lift, lift,
max, max,
min, min,
relate, relate,
transform,
} }