Palimpsest
Four versions of one writing tool, to find out that undoing a text does not give you the same text back.
A text you undo does not quite come back as the text it was. It took me four versions of the same tool to put it that way.
The tool is called Palimpsest, and the name does the work: a scraped, rewritten parchment where the older text shows through the new one. Every layer of my text is a full editor in its own right, set down to the right of the previous one. A character’s colour is its layer — there is no “layer” field filed away somewhere, the colour is the data. And every character carries an identifier, eight base-36 signs, that follows it from one layer to the next. That is what lets me repaint the same character across every panel at once: you don’t repaint a position, you repaint an identity.
Then you undo.
CodeMirror keeps an honest history, in that it manages exactly what it says it manages: the document. It gives back the characters, not what I knew about them. Type, undo, redo — the text on screen is identical down to the sign, and every character in it is a stranger. The identifiers have been minted fresh, cross-panel tracking is dead, and nothing crashed. That is the worst kind of defect: the one where the screen agrees with you.
The repair runs to ten lines, and it has a name I didn’t know — invertedEffects:
const charsInverter = invertedEffects.of(tr => {
const charsTouched = tr.docChanged
|| tr.effects.some(eff => eff.is(setCharsEffect));
if (!charsTouched) return [];
return [setCharsEffect.of(tr.startState.field(charsField))];
});
For every transaction that touches the characters, you hand the history the inverse effect: one that carries the characters as they were, identifiers included. Undo replays it. The identity comes back verbatim.
What holds my attention there isn’t the line of code, it’s the question it ended up asking. What makes a text the same text? Sign-for-sign identity isn’t enough — that is precisely what the defect produced, an identical text made of different characters. Identity has to be carried separately and restored on purpose, or it evaporates on the first undo. A palimpsest says the same thing from the other end: erased text is not the absence of text.
The tally is not glorious. A tree-shaped v3, a v4 abandoned along the way because the seam between engine and editor refused to get simpler, a v5 archived, and a successor called git-nacre — git already knows how to say “same line, it moved”, and I had no reason to rewrite that. Four tools for one idea that fits in ten lines. That is the real ratio, and I would rather write it down than round it up.
P.S. This piece is the only thing coming out of that repository. The rest is archived, which is exactly what it deserved.