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
+3 -56
View File
@@ -9,8 +9,9 @@
// the second time and a text edited meanwhile keeps what it did not touch.
import * as Y from 'yjs';
import type { Id, InstallableProgram, VirtualFileContent, VirtualFileSystem } from '@mdg-filius/core';
import { fileEntry, NODE_SETTINGS, ROOTS } from './projectDoc.js';
import type { Id, InstallableProgram, VirtualFileSystem } from '@mdg-filius/core';
import { sameBytes, writeFiles } from './files.js';
import { NODE_SETTINGS, ROOTS } from './projectDoc.js';
export { emailServerAfterRun } from '@mdg-filius/fls';
import { fromY, toY, type PlainValue } from './values.js';
@@ -43,60 +44,6 @@ function same(left: unknown, right: unknown): boolean {
return left === right;
}
function sameBytes(left: Uint8Array, right: Uint8Array): boolean {
return left.length === right.length && left.every((byte, index) => byte === right[index]);
}
/** A text changed where it changed: the common start and end stay (and with them what another person typed there). */
function replaceText(text: Y.Text, next: string): void {
const current = text.toString();
if (current === next) return;
let start = 0;
while (start < current.length && start < next.length && current[start] === next[start]) start += 1;
let end = 0;
while (end < current.length - start && end < next.length - start && current[current.length - 1 - end] === next[next.length - 1 - end]) end += 1;
if (current.length - start - end > 0) text.delete(start, current.length - start - end);
if (next.length - start - end > 0) text.insert(start, next.slice(start, next.length - end));
}
/** Whether a document entry holds the content. */
function holds(entry: Y.Map<unknown>, content: VirtualFileContent): boolean {
if (entry.get('kind') !== content.kind) return false;
if (content.kind === 'directory' || content.kind === 'text') return true;
return sameBytes(entry.get('data') as Uint8Array, content.data) && entry.get('mediaType') === content.mediaType;
}
/** A node's files as the run left them: new ones after the others, changed ones in their place, gone ones removed. */
function writeFiles(node: Y.Map<unknown>, fileSystem: VirtualFileSystem): void {
let files = node.get('files') as Y.Map<Y.Map<unknown>> | undefined;
const wanted: [string, VirtualFileContent][] = [];
const walk = (folder: string): void => {
for (const entry of fileSystem.entriesInCreationOrder(folder)) {
wanted.push([entry.path, entry.content]);
if (entry.content.kind === 'directory') walk(entry.path);
}
};
walk('/');
if (files === undefined) {
if (wanted.length === 0) return;
files = new Y.Map<Y.Map<unknown>>();
node.set('files', files);
}
const paths = new Set(wanted.map(([path]) => path));
for (const path of [...files.keys()]) if (!paths.has(path)) files.delete(path);
let order = Math.max(-1, ...[...files.values()].map((entry) => entry.get('order') as number)) + 1;
for (const [path, content] of wanted) {
const entry = files.get(path);
if (entry === undefined) {
files.set(path, fileEntry(order++, content));
} else if (!holds(entry, content)) {
files.set(path, fileEntry(entry.get('order') as number, content));
} else if (content.kind === 'text') {
replaceText(entry.get('text') as Y.Text, content.text);
}
}
}
/** Writes what the runs left of the nodes, what differs only, in one transaction; nodes the document no longer has are left out. */
export function writeRunState(doc: Y.Doc, states: ReadonlyMap<Id, NodeRunState>, origin: unknown = SIM_ORIGIN): void {
const nodes = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);