// Minimal reproduction: LoroTree undo/redo loses text content when
// splitBlock-like operations span multiple undo steps.
//
// We are building a high-performance Block-Native rich text editor on top
// of Loro — similar in ambition to AFFiNE but with ProseMirror/CodeMirror 6
// level performance. We use LoroTree for document block structure and
// doc.getText('text_${treeNodeId}') for per-block text containers.
//
// A block editor's "splitBlock" is: deleteText + createNode + insertText,
// committed as one undo item. Subsequent typing is a separate undo item.
//
// Bug: redo of createNode generates NEW TreeID, but redo of insertText
// still targets OLD TreeID's text container. container_remap does not
// persist across separate redo() calls.
//
// Environment: loro-crdt ^1.10.6
// To run: npx vitest run undo-redo-tree-text-repro.test.ts
import { describe, it, expect } from 'vitest';
import { LoroDoc, UndoManager } from 'loro-crdt';
describe('LoroTree undo/redo: text lost after splitBlock + type + undo + redo', () => {
it('two separate undo items: redo loses text on second item', () => {
const doc = new LoroDoc();
const undoMgr = new UndoManager(doc, { mergeInterval: 0 });
const tree = doc.getTree('tree');
// Setup: root + paragraph A with "Hello World" (excluded from undo)
undoMgr.addExcludeOriginPrefix('setup');
const root = tree.createNode();
const paraA = root.createNode();
const textA = doc.getText(`text_${paraA.id}`);
textA.insert(0, 'Hello World');
doc.commit({ origin: 'setup' });
// --- Undo item 1: splitBlock at offset 5 ---
textA.delete(5, 6); // delete " World"
const paraB = root.createNode(); // create new node
const paraBId = paraB.id;
const textB = doc.getText(`text_${paraBId}`);
textB.insert(0, ' World'); // move tail to new node
doc.commit();
expect(textA.toString()).toBe('Hello');
expect(textB.toString()).toBe(' World');
// --- Undo item 2: user types "你好" at start of paraB ---
textB.insert(0, '你好');
doc.commit();
expect(textB.toString()).toBe('你好 World');
// --- Undo x2 ---
expect(undoMgr.undo()).toBe(true); // undo "你好"
expect(undoMgr.undo()).toBe(true); // undo splitBlock
expect(textA.toString()).toBe('Hello World');
expect(paraB.isDeleted()).toBe(true);
// --- Redo step 1: redo splitBlock ---
// Creates a NEW TreeID (different from original paraBId)
expect(undoMgr.redo()).toBe(true);
const children = root.children()!;
expect(children.length).toBe(2);
const redoParaB = children[1];
const redoParaBId = redoParaB.id;
const sameId = JSON.stringify(paraBId) === JSON.stringify(redoParaBId);
// --- Redo step 2: redo insertText ---
expect(undoMgr.redo()).toBe(true);
// --- Verify ---
const visibleText = doc.getText(`text_${redoParaBId}`);
if (!sameId) {
const ghostText = doc.getText(`text_${paraBId}`);
console.log('REPRODUCTION CONFIRMED:');
console.log(' Original paraB TreeID:', JSON.stringify(paraBId));
console.log(' Redo paraB TreeID: ', JSON.stringify(redoParaBId));
console.log(' Visible node text: ', JSON.stringify(visibleText.toString()));
console.log(' Ghost node text: ', JSON.stringify(ghostText.toString()));
console.log('');
console.log(' Expected: visible node has "你好 World"');
console.log(' Actual: visible node is empty, text went to ghost node');
}
// EXPECTED: "你好 World" on the visible (redo-created) node
// ACTUAL BUG: "" on visible node, "你好 World" on ghost (old TreeID) node
expect(visibleText.toString()).toBe('你好 World');
});
});
// Minimal reproduction: LoroTree undo/redo loses text content when
// splitBlock-like operations span multiple undo steps.
//
// We are building a high-performance Block-Native rich text editor on top
// of Loro — similar in ambition to AFFiNE but with ProseMirror/CodeMirror 6
// level performance. We use LoroTree for document block structure and
// doc.getText('text_${treeNodeId}') for per-block text containers.
//
// A block editor's "splitBlock" is: deleteText + createNode + insertText,
// committed as one undo item. Subsequent typing is a separate undo item.
//
// Bug: redo of createNode generates NEW TreeID, but redo of insertText
// still targets OLD TreeID's text container. container_remap does not
// persist across separate redo() calls.
//
// Environment: loro-crdt ^1.10.6
// To run: npx vitest run undo-redo-tree-text-repro.test.ts