docs/solutions/logic-errors/2026-05-09-slate-v2-character-delete-must-share-text-unit-boundaries.md
Slate's text-unit helper knew that complex Unicode sequences should be handled
as text units, but reverse unit: "character" deletion had a separate complex
script restoration path. That split let package distance tests pass while
destructive delete still corrupted the behavior.
getCharacterDistance("\u0BA8\u0BBF", true) returned 2, but reverse delete
removed only the trailing Tamil vowel mark.getCharacterDistance rows alone was too weak; the delete
transform could still diverge.Editor.before(..., { unit: "character" }).Make unit: "character" deletion obey the same boundary owner everywhere.
The fix removed the complex-script reverse-delete reinsertion path from
.tmp/slate-v2/packages/slate/src/transforms-text/delete-text.ts, then updated
the Thai fixtures to expect a whole text-unit deletion.
The regression lock lives in
.tmp/slate-v2/packages/slate/test/text-units-contract.ts:
const assertUnitCharacterDeletion = (
testCase: LexicalGraphemeCase,
reverse: boolean,
) => {
const editor = createTextEditor(
testCase.text,
reverse ? testCase.text.length : 0,
);
for (const distance of distances) {
const before = getEditorText(editor);
const expected = reverse
? before.slice(0, before.length - distance)
: before.slice(distance);
editor.update((tx) => {
tx.text.delete({ reverse, unit: "character" });
});
assert.equal(getEditorText(editor), expected, testCase.description);
}
};
Editor.before and Editor.after already derive character stops from
getCharacterDistance. The delete transform should remove the range those APIs
select. Reinserting part of the removed text after the fact created a second
definition of "character" for reverse deletion only.
Once the restoration branch is gone, forward delete, reverse delete, and text-unit measurement all share one boundary source.