import { html, render } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import type { VirgoLine } from '../index.js';
import type { DeltaInsert } from '../types.js';
import type { DeltaEntry, VRange } from '../types.js';
import type { BaseTextAttributes } from '../utils/index.js';
import { deltaInsertsToChunks, renderElement } from '../utils/index.js';
import type { VEditor } from '../virgo.js';
export class VirgoDeltaService {
constructor(public readonly editor: VEditor) {}
get deltas() {
return this.editor.yText.toDelta() as DeltaInsert[];
}
get normalizedDeltas() {
// According to our regulations, the length of each "embed" node should only be 1.
// Therefore, if the length of an "embed" type node is greater than 1,
// we will divide it into multiple parts.
const result: DeltaInsert[] = [];
for (const delta of this.deltas) {
if (this.editor.isEmbed(delta)) {
const dividedDeltas = [...delta.insert].map(subInsert => ({
insert: subInsert,
attributes: delta.attributes,
}));
result.push(...dividedDeltas);
} else {
result.push(delta);
}
}
return result;
}
mapDeltasInVRange = (
vRange: VRange,
callback: (
delta: DeltaInsert,
rangeIndex: number,
deltaIndex: number
) => Result,
normalize = false
) => {
const deltas = normalize ? this.normalizedDeltas : this.deltas;
const result: Result[] = [];
deltas.reduce((rangeIndex, delta, deltaIndex) => {
const length = delta.insert.length;
const from = vRange.index - length;
const to = vRange.index + vRange.length;
const deltaInRange =
rangeIndex >= from &&
(rangeIndex < to ||
(vRange.length === 0 && rangeIndex === vRange.index));
if (deltaInRange) {
const value = callback(delta, rangeIndex, deltaIndex);
result.push(value);
}
return rangeIndex + length;
}, 0);
return result;
};
isNormalizedDeltaSelected(
normalizedDeltaIndex: number,
vRange: VRange
): boolean {
let result = false;
if (vRange.length >= 1) {
this.editor.mapDeltasInVRange(
vRange,
(_, rangeIndex, deltaIndex) => {
if (
deltaIndex === normalizedDeltaIndex &&
rangeIndex >= vRange.index
) {
result = true;
}
},
// we need to normalize the delta here,
true
);
}
return result;
}
/**
* Here are examples of how this function computes and gets the delta.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* ]
* ```
*
* `getDeltaByRangeIndex(0)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(1)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(3)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(4)` returns `{ insert: 'bbb', attributes: { italic: true } }`.
*/
getDeltaByRangeIndex = (rangeIndex: number) => {
const deltas = this.deltas;
let index = 0;
for (const delta of deltas) {
if (index + delta.insert.length >= rangeIndex) {
return delta;
}
index += delta.insert.length;
}
return null;
};
/**
* Here are examples of how this function computes and gets the deltas.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* {
* insert: 'ccc',
* attributes: { underline: true },
* },
* ]
* ```
*
* `getDeltasByVRange({ index: 0, length: 0 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 3 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }],
* [{ insert: 'ccc', attributes: { underline: true }, }, { index: 6, length: 3, }]]
* ```
*/
getDeltasByVRange = (vRange: VRange): DeltaEntry[] => {
return this.mapDeltasInVRange(
vRange,
(delta, index): DeltaEntry => [
delta,
{ index, length: delta.insert.length },
]
);
};
// render current deltas to VLines
render = async (syncVRange = true) => {
if (!this.editor.mounted) return;
const rootElement = this.editor.rootElement;
const normalizedDeltas = this.normalizedDeltas;
const chunks = deltaInsertsToChunks(normalizedDeltas);
let normalizedDeltaIndex = 0;
// every chunk is a line
const lines = chunks.map(chunk => {
if (chunk.length > 0) {
const lineDeltas: [DeltaInsert, number][] = [];
chunk.forEach(delta => {
lineDeltas.push([delta, normalizedDeltaIndex]);
normalizedDeltaIndex++;
});
const elements: VirgoLine['elements'] = lineDeltas.map(
([delta, normalizedDeltaIndex]) => {
let selected = false;
const vRange = this.editor.getVRange();
if (vRange) {
selected = this.isNormalizedDeltaSelected(
normalizedDeltaIndex,
vRange
);
}
return [
renderElement(
delta,
this.editor.attributeService.normalizeAttributes,
selected
),
delta,
];
}
);
return html``;
} else {
return html``;
}
});
try {
render(
repeat(
lines.map((line, i) => ({ line, index: i })),
entry => entry.index,
entry => entry.line
),
rootElement
);
} catch (error) {
// Lit may be crashed by IME input and we need to rerender whole editor for it
this.editor.rerenderWholeEditor();
await this.editor.waitForUpdate();
}
await this.editor.waitForUpdate();
if (syncVRange) {
// We need to synchronize the selection immediately after rendering is completed,
// otherwise there is a possibility of an error in the cursor position
this.editor.rangeService.syncVRange();
}
this.editor.slots.updated.emit();
};
}