feat: F4.6f2 the text editor on the document's Y.Text, files both ways while the simulation runs

Oracle TextEditorWindowRun uses Java's text editor window and its file
dialog as a person does. The open file's text is its Y.Text in the
document (plan 7.3): typed is in the file at once, for everyone and for
the running engine. Core openText/saveTextAs without Java's mistakes
(lost saves after save-as onto a file or a removed file, files named
'ordner/d.txt' or ''); doc files.ts (changeFiles, fileText, nodeFiles,
readFilesInto); the room takes a person's files into the engine at once
and writes the engine's files back at the tick they changed; the editor's
window, Java's dialog, the text area following others' changes with the
caret in place, its own undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:50:27 +00:00
co-authored by Claude Opus 5.5
parent 3d6655e736
commit 2c7733b575
29 changed files with 3268 additions and 76 deletions
+56
View File
@@ -0,0 +1,56 @@
// Where the caret and the selection go when another changes the open file's
// text (textBinding.ts): moved by what was inserted or deleted before them,
// left before what another inserts just at them — the deltas as Yjs gives them.
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { mapIndex, type TextDelta } from '../src/desktop/textBinding.js';
/** The delta of a change to a text that held `before`. */
function deltaOf(before: string, change: (text: Y.Text) => void): TextDelta {
const doc = new Y.Doc();
const text = doc.getText('t');
text.insert(0, before);
let delta: TextDelta = [];
text.observe((event) => (delta = event.delta as TextDelta));
change(text);
return delta;
}
describe('a place in the text after another’s change', () => {
it('moves by what was inserted before it, not by what came after it or just at it', () => {
const insert = deltaOf('Hallo Welt', (text) => text.insert(6, 'schöne '));
expect([0, 5, 6, 7, 10].map((index) => mapIndex(index, insert))).toEqual([0, 5, 6, 14, 17]);
expect(
mapIndex(
3,
deltaOf('abc', (text) => text.insert(0, 'xy')),
),
).toBe(5);
expect(
mapIndex(
0,
deltaOf('abc', (text) => text.insert(0, 'xy')),
),
).toBe(0);
});
it('moves back by what was deleted before it, to where the deletion began when it lay inside', () => {
const removed = deltaOf('Hallo schöne Welt', (text) => text.delete(6, 7));
expect([0, 6, 9, 13, 17].map((index) => mapIndex(index, removed))).toEqual([0, 6, 6, 6, 10]);
});
it('takes a replacement as its deletion and then its insertion: a place at the end of what was replaced stays before the new text', () => {
const replaced = deltaOf('Erster Stand', (text) => {
text.doc!.transact(() => {
text.delete(0, 6);
text.insert(0, 'Zweiter');
});
});
expect(replaced).toEqual([{ delete: 6 }, { insert: 'Zweiter' }]);
expect([0, 3, 6, 7, 12].map((index) => mapIndex(index, replaced))).toEqual([0, 0, 0, 8, 13]);
// An embedded object counts as one place.
expect(mapIndex(2, [{ insert: { image: 'x' } }])).toBe(3);
expect(mapIndex(2, [{ retain: 2 }, { insert: { image: 'x' } }])).toBe(2);
});
});