Skip to content

Commit

Permalink
Improve inserting performance
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Jun 30, 2023
1 parent a6b7b02 commit 99bf79f
Show file tree
Hide file tree
Showing 5 changed files with 472 additions and 98 deletions.
44 changes: 5 additions & 39 deletions blots/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
LeafBlot,
Scope,
} from 'parchment';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Delta from 'quill-delta';
import Break from './break';
import Inline from './inline';
import TextBlot from './text';
Expand All @@ -28,28 +28,17 @@ class Block extends BlockBlot {
this.cache = {};
}

formatAt(index, length, format, value?) {
formatAt(index, length, name, value) {
if (length <= 0) return;
if (typeof format != 'string') {
const order = Inline.order.slice().reverse();
Object.keys(format)
.sort((a, b) => {
const aOrder = order.indexOf(a);
const bOrder = order.indexOf(b);
return aOrder - bOrder;
})
.forEach(key => {
this.formatAt(index, length, key, format[key]);
});
} else if (this.scroll.query(format, Scope.BLOCK)) {
if (this.scroll.query(name, Scope.BLOCK)) {
if (index + length === this.length()) {
this.format(format, value);
this.format(name, value);
}
} else {
super.formatAt(
index,
Math.min(length, this.length() - index - 1),
format,
name,
value,
);
}
Expand Down Expand Up @@ -93,29 +82,6 @@ class Block extends BlockBlot {
this.cache = {};
}

// TODO: Either handle \n in deltas or move insertAt splitting responsibility to scroll
insertContents(index: number, delta: Delta) {
delta.reduce((index: number, op: Op) => {
const length = Op.length(op);
let attributes = op.attributes || {};
if (op.insert != null) {
if (typeof op.insert === 'string') {
const text = op.insert;
this.insertAt(index, text);
const [leaf] = this.descendant(LeafBlot, index);
const formats = bubbleFormats(leaf);
attributes = AttributeMap.diff(formats, attributes) || {};
} else if (typeof op.insert === 'object') {
const key = Object.keys(op.insert)[0]; // There should only be one key
if (key == null) return index;
this.insertAt(index, key, op.insert[key]);
}
}
this.formatAt(index, length, attributes);
return index + length;
}, index);
}

length() {
if (this.cache.length == null) {
this.cache.length = super.length() + NEWLINE_LENGTH;
Expand Down
243 changes: 187 additions & 56 deletions blots/scroll.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import {
Blot,
ContainerBlot,
EmbedBlot,
LeafBlot,
Parent,
ParentBlot,
Registry,
Scope,
ScrollBlot,
} from 'parchment';
import Delta, { AttributeMap } from 'quill-delta';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Emitter, { EmitterSource } from '../core/emitter';
import Block, { BlockEmbed } from './block';
import Break from './break';
import Container from './container';
import { bubbleFormats } from './block';

type RenderBlock =
| {
type: 'blockEmbed';
attributes: AttributeMap;
key: string;
value: unknown;
}
| { type: 'block'; attributes: AttributeMap; delta: Delta };

function isLine(blot: unknown): blot is Block | BlockEmbed {
return blot instanceof Block || blot instanceof BlockEmbed;
Expand All @@ -26,20 +37,6 @@ function isUpdatable(blot: Blot): blot is Blot & UpdatableEmbed {
return typeof (blot as unknown as any).updateContent === 'function';
}

function deltaToLines(delta: Delta) {
const lines: { delta: Delta; attributes: AttributeMap }[] = [];
// eachLine can't tell if we end in newline or not
// add trailing newline to differentiate
// 'hello\nworld' -> ['hello', 'world']
// 'hello\nworld\n' -> ['hello', 'world', '']

// TODO: insert() here modifies original delta
delta.insert('\n').eachLine((lineDelta, attributes) => {
lines.push({ delta: lineDelta, attributes });
});
return lines;
}

class Scroll extends ScrollBlot {
static blotName = 'scroll';
static className = 'ql-editor';
Expand Down Expand Up @@ -153,53 +150,70 @@ class Scroll extends ScrollBlot {
}

insertContents(index: number, delta: Delta) {
const [child, offset] = this.children.find(index);
if (child == null) return;
const lines = deltaToLines(delta);
const first = lines.shift();
if (first == null) return;
const renderBlocks = this.deltaToRenderBlocks(
delta.concat(new Delta().insert('\n')),
);
const last = renderBlocks.pop();
if (last == null) return;

this.batchStart();
// @ts-ignore
child.insertContents(offset, first.delta);
const last = lines.pop();
let after;
if (last != null) {
after = child.split(offset + first.delta.length(), true);
Object.keys(first.attributes).forEach(name => {
// @ts-ignore
after.prev.format(name, first.attributes[name]);

const first = renderBlocks.shift();
if (first) {
const shouldInsertNewlineChar =
first.type === 'block' &&
(first.delta.length() === 0 ||
(!this.descendant(BlockEmbed, index)[0] && index < this.length()));
const delta =
first.type === 'block'
? first.delta
: new Delta().insert({ [first.key]: first.value });
insertInlineContents(this, index, delta);
const newlineCharIndex = index + delta.length();
if (shouldInsertNewlineChar) {
this.insertAt(newlineCharIndex, '\n');
}

const formats = bubbleFormats(this.line(index)[0]);
const attributes = AttributeMap.diff(formats, first.attributes) || {};
Object.keys(attributes).forEach(name => {
this.formatAt(newlineCharIndex, 1, name, attributes[name]);
});
after.insertContents(0, last.delta);

index = newlineCharIndex + 1;
}

lines.forEach(({ delta: lineDelta, attributes }) => {
const blockAttribute = Object.keys(attributes).find(
key =>
this.query(
key,
// eslint-disable-next-line no-bitwise
Scope.BLOCK & Scope.BLOT,
) != null,
);
const block = this.create(
blockAttribute || this.statics.defaultChild.blotName,
blockAttribute ? attributes[blockAttribute] : undefined,
);
this.insertBefore(block, after);
// @ts-ignore
block.insertContents(0, lineDelta);
Object.keys(attributes).forEach(key => {
if (
this.query(
key,
// eslint-disable-next-line no-bitwise
Scope.BLOCK & Scope.BLOT,
) == null
) {
block.formatAt(0, block.length(), key, attributes[key]);
let [refBlot, refBlotOffset] = this.children.find(index);
if (renderBlocks.length) {
if (refBlot) {
refBlot = refBlot.split(refBlotOffset);
refBlotOffset = 0;
}

renderBlocks.forEach(renderBlock => {
if (renderBlock.type === 'block') {
const block = this.createBlock(renderBlock.attributes);
this.insertBefore(block, refBlot || undefined);
insertInlineContents(block, 0, renderBlock.delta);
} else {
const blockEmbed = this.create(
renderBlock.key,
renderBlock.value,
) as EmbedBlot;
Object.keys(renderBlock.attributes).forEach(name => {
blockEmbed.format(name, renderBlock.attributes[name]);
});
this.insertBefore(blockEmbed, refBlot || undefined);
}
});
});
}

if (last.type === 'block' && last.delta.length()) {
const offset = refBlot
? refBlot.offset(refBlot.scroll) + refBlotOffset
: this.length();
insertInlineContents(this, offset, last.delta);
}

this.batchEnd();
this.optimize();
Expand Down Expand Up @@ -310,6 +324,123 @@ class Scroll extends ScrollBlot {
blot.updateContent(change);
}
}

private deltaToRenderBlocks(delta: Delta) {
const renderBlocks: RenderBlock[] = [];

let currentBlockDelta = new Delta();
delta.forEach(op => {
const insert = op?.insert;
if (!insert) return;
if (typeof insert === 'string') {
const splitted = insert.split('\n');
splitted.slice(0, -1).forEach(text => {
currentBlockDelta.insert(text, op.attributes);
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: op.attributes ?? {},
});
currentBlockDelta = new Delta();
});
const last = splitted[splitted.length - 1];
if (last) {
currentBlockDelta.insert(last, op.attributes);
}
} else {
const key = Object.keys(insert)[0];
if (!key) return;
if (this.query(key, Scope.INLINE)) {
currentBlockDelta.push(op);
} else {
if (currentBlockDelta.length()) {
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: {},
});
}
currentBlockDelta = new Delta();
renderBlocks.push({
type: 'blockEmbed',
key,
value: insert[key],
attributes: op.attributes ?? {},
});
}
}
});

if (currentBlockDelta.length()) {
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: {},
});
}

return renderBlocks;
}

private createBlock(attributes: AttributeMap) {
let blotName: string | undefined;
const formats: AttributeMap = {};

Object.entries(attributes).forEach(([key, value]) => {
const isBlockBlot = this.query(key, Scope.BLOCK & Scope.BLOT) != null;
if (isBlockBlot) {
blotName = key;
} else {
formats[key] = value;
}
});

const block = this.create(
blotName || this.statics.defaultChild.blotName,
blotName ? attributes[blotName] : undefined,
) as ParentBlot;

const length = block.length();
Object.entries(formats).forEach(([key, value]) => {
block.formatAt(0, length, key, value);
});

return block;
}
}

function insertInlineContents(
parent: ParentBlot,
index: number,
inlineContents: Delta,
) {
inlineContents.reduce((index, op) => {
const length = Op.length(op);
let attributes = op.attributes || {};
if (op.insert != null) {
if (typeof op.insert === 'string') {
const text = op.insert;
parent.insertAt(index, text);
const [leaf] = parent.descendant(LeafBlot, index);
const formats = bubbleFormats(leaf);
attributes = AttributeMap.diff(formats, attributes) || {};
} else if (typeof op.insert === 'object') {
const key = Object.keys(op.insert)[0]; // There should only be one key
if (key == null) return index;
parent.insertAt(index, key, op.insert[key]);
const isInlineEmbed = parent.scroll.query(key, Scope.INLINE) != null;
if (isInlineEmbed) {
const [leaf] = parent.descendant(LeafBlot, index);
const formats = bubbleFormats(leaf);
attributes = AttributeMap.diff(formats, attributes) || {};
}
}
}
Object.keys(attributes).forEach(key => {
parent.formatAt(index, length, key, attributes[key]);
});
return index + length;
}, index);
}

export interface ScrollConstructor {
Expand Down
Loading

0 comments on commit 99bf79f

Please sign in to comment.