Skip to content

Commit

Permalink
Generate more accurate deltas from typing (slab#2252)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgreensp authored and jhchen committed Aug 20, 2018
1 parent d54d2d0 commit 5258163
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 27 deletions.
91 changes: 88 additions & 3 deletions core/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import extend from 'extend';
import Delta from 'quill-delta';
import DeltaOp from 'quill-delta/lib/op';
import { LeafBlot } from 'parchment';
import { Range } from './selection';
import CursorBlot from '../blots/cursor';
import Block, { bubbleFormats } from '../blots/block';
import Break from '../blots/break';
Expand Down Expand Up @@ -188,7 +189,7 @@ class Editor {
return this.applyDelta(delta);
}

update(change, mutations = [], cursorIndex = undefined) {
update(change, mutations = [], selectionInfo = undefined) {
const oldDelta = this.delta;
if (
mutations.length === 1 &&
Expand All @@ -203,9 +204,13 @@ class Editor {
const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
const oldText = new Delta().insert(oldValue);
const newText = new Delta().insert(textBlot.value());
const relativeSelectionInfo = selectionInfo && [
new Range(selectionInfo[0].index - index, selectionInfo[0].length),
new Range(selectionInfo[1].index - index, selectionInfo[1].length),
];
const diffDelta = new Delta()
.retain(index)
.concat(oldText.diff(newText, cursorIndex));
.concat(diffDeltas(oldText, newText, relativeSelectionInfo));
change = diffDelta.reduce((delta, op) => {
if (op.insert) {
return delta.insert(op.insert, formats);
Expand All @@ -216,7 +221,7 @@ class Editor {
} else {
this.delta = this.getDelta();
if (!change || !equal(oldDelta.compose(change), this.delta)) {
change = oldDelta.diff(this.delta, cursorIndex);
change = diffDeltas(oldDelta, this.delta, selectionInfo);
}
}
return change;
Expand Down Expand Up @@ -328,4 +333,84 @@ function normalizeDelta(delta) {
}, new Delta());
}

function splitDelta(delta, index) {
return [delta.slice(0, index), delta.slice(index)];
}

function diffDeltas(oldDelta, newDelta, selectionInfo = undefined) {
if (selectionInfo == null) {
return oldDelta.diff(newDelta);
}

// generate better diffs than Delta#diff by taking into account the
// old and new selection. for example, a text change from "xxx" to "xx"
// could be a delete or forwards-delete of any one of the x's, or the
// result of selecting two of the x's and typing "x".
const [oldSelection, newSelection] = selectionInfo;
const oldDeltaLength = oldDelta.length();
const newDeltaLength = newDelta.length();
if (oldSelection.length === 0 && newSelection.length === 0) {
// see if we have an insert or delete before or after cursor
const oldCursor = oldSelection.index;
const newCursor = newSelection.index;
const [oldBefore, oldAfter] = splitDelta(oldDelta, oldCursor);
const [newBefore, newAfter] = splitDelta(newDelta, newCursor);
if (equal(oldAfter, newAfter)) {
const prefixLength = Math.min(oldCursor, newCursor);
const [oldPrefix, oldMiddle] = splitDelta(oldBefore, prefixLength);
const [newPrefix, newMiddle] = splitDelta(newBefore, prefixLength);
if (equal(oldPrefix, newPrefix)) {
// insert or delete right before cursor
return new Delta()
.retain(prefixLength)
.concat(oldMiddle.diff(newMiddle));
}
} else if (equal(oldBefore, newBefore)) {
const suffixLength = Math.min(
oldDeltaLength - oldCursor,
newDeltaLength - newCursor,
);
const [oldMiddle, oldSuffix] = splitDelta(
oldAfter,
oldDeltaLength - oldCursor - suffixLength,
);
const [newMiddle, newSuffix] = splitDelta(
newAfter,
newDeltaLength - newCursor - suffixLength,
);
if (equal(oldSuffix, newSuffix)) {
// insert or delete right after cursor
return new Delta().retain(oldCursor).concat(oldMiddle.diff(newMiddle));
}
}
}
if (oldSelection.length > 0 && newSelection.length === 0) {
// see if diff could be a splice of the old selection range
const oldPrefix = oldDelta.slice(0, oldSelection.index);
const oldSuffix = oldDelta.slice(oldSelection.index + oldSelection.length);
const prefixLength = oldPrefix.length();
const suffixLength = oldSuffix.length();
if (newDeltaLength >= prefixLength + suffixLength) {
const newPrefix = newDelta.slice(0, prefixLength);
const newSuffix = newDelta.slice(newDeltaLength - suffixLength);
if (equal(oldPrefix, newPrefix) && equal(oldSuffix, newSuffix)) {
const oldMiddle = oldDelta.slice(
prefixLength,
oldDeltaLength - suffixLength,
);
const newMiddle = newDelta.slice(
prefixLength,
newDeltaLength - suffixLength,
);
return new Delta()
.retain(prefixLength)
.concat(newMiddle)
.delete(oldMiddle.length());
}
}
}

return oldDelta.diff(newDelta);
}

export default Editor;
8 changes: 5 additions & 3 deletions core/quill.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,13 @@ class Quill {
}
});
this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
const range = this.selection.lastRange;
const index = range && range.length === 0 ? range.index : undefined;
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo =
oldRange && newRange ? [oldRange, newRange] : undefined;
modify.call(
this,
() => this.editor.update(null, mutations, index),
() => this.editor.update(null, mutations, selectionInfo),
source,
);
});
Expand Down
137 changes: 116 additions & 21 deletions test/unit/core/quill.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,28 +234,123 @@ describe('Quill', function() {
}, 1);
});

it('insert same character', function(done) {
this.quill.setText('aaaa\n');
this.quill.setSelection(2);
this.quill.update();
const old = this.quill.getContents();
const textNode = this.container.firstChild.firstChild.firstChild;
textNode.data = 'aaaaa';
this.quill.selection.setNativeRange(textNode.data, 3);
const delta = new Delta().retain(2).insert('a');
setTimeout(() => {
const calls = this.quill.emitter.emit.calls.all();
calls.pop();
const { args } = calls.pop();
expect(args).toEqual([
Emitter.events.TEXT_CHANGE,
delta,
old,
Emitter.sources.USER,
]);
done();
}, 1);
function editTest(
oldText,
oldSelection,
newText,
newSelection,
expectedDelta,
) {
return function(done) {
this.quill.setText(`${oldText}\n`);
this.quill.setSelection(oldSelection); // number or Range
this.quill.update();
const oldContents = this.quill.getContents();
const textNode = this.container.firstChild.firstChild.firstChild;
textNode.data = newText;
if (typeof newSelection === 'number') {
this.quill.selection.setNativeRange(textNode, newSelection);
} else {
this.quill.selection.setNativeRange(
textNode,
newSelection.index,
textNode,
newSelection.index + newSelection.length,
);
}
setTimeout(() => {
const calls = this.quill.emitter.emit.calls.all();
if (
calls[calls.length - 1].args[1] === Emitter.events.SELECTION_CHANGE
) {
calls.pop();
}
const { args } = calls.pop();
expect(args).toEqual([
Emitter.events.TEXT_CHANGE,
expectedDelta,
oldContents,
Emitter.sources.USER,
]);
done();
}, 1);
};
}

describe('insert a in aaaa', function() {
it(
'at index 0',
editTest('aaaa', 0, 'aaaaa', 1, new Delta().insert('a')),
);
it(
'at index 1',
editTest('aaaa', 1, 'aaaaa', 2, new Delta().retain(1).insert('a')),
);
it(
'at index 2',
editTest('aaaa', 2, 'aaaaa', 3, new Delta().retain(2).insert('a')),
);
it(
'at index 3',
editTest('aaaa', 3, 'aaaaa', 4, new Delta().retain(3).insert('a')),
);
});

describe('insert a in xaa', function() {
it(
'at index 1',
editTest('xaa', 1, 'xaaa', 2, new Delta().retain(1).insert('a')),
);
it(
'at index 2',
editTest('xaa', 2, 'xaaa', 3, new Delta().retain(2).insert('a')),
);
it(
'at index 3',
editTest('xaa', 3, 'xaaa', 4, new Delta().retain(3).insert('a')),
);
});

describe('insert aa in ax', function() {
it('at index 0', editTest('ax', 0, 'aaax', 2, new Delta().insert('aa')));
it(
'at index 1',
editTest('ax', 1, 'aaax', 3, new Delta().retain(1).insert('aa')),
);
});

describe('delete a in xaa', function() {
it(
'at index 1',
editTest('xaa', 2, 'xa', 1, new Delta().retain(1).delete(1)),
);
it(
'at index 2',
editTest('xaa', 3, 'xa', 2, new Delta().retain(2).delete(1)),
);
});

describe('forward-delete a in xaa', function() {
it(
'at index 1',
editTest('xaa', 1, 'xa', 1, new Delta().retain(1).delete(1)),
);
it(
'at index 2',
editTest('xaa', 2, 'xa', 2, new Delta().retain(2).delete(1)),
);
});

it(
'replace yay with y',
editTest(
'yay',
new Range(0, 3),
'y',
1,
new Delta().insert('y').delete(3),
),
);
});

describe('setContents()', function() {
Expand Down

0 comments on commit 5258163

Please sign in to comment.