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
+46 -5
View File
@@ -33,6 +33,13 @@
// as `origin: 'sim'`: at most once a second while it runs, after each action
// in a window and when it stops (writeRunState). So the next start, and every
// client, sees it — as Java's project saved after a run holds it.
//
// Files go both ways while it runs (plan §7.3: the text editor writes the
// document's Y.Text): what a person writes into the document goes into the
// engine's files at once (readFilesInto), and what the engine changed in its
// files goes into the document at the tick it changed, not a second later —
// so the document holds every change of the engine before a person's next
// one comes in, and a text two write at once keeps both.
import * as Y from 'yjs';
import {
@@ -50,7 +57,7 @@ import {
type PageLanguage,
type TopologySnapshot,
} from '@mdg-filius/core';
import { docSnapshot, emailServerAfterRun, fromY, installedSoftware, ROOTS, SIM_ORIGIN, writeRunState, type NodeRunState } from '@mdg-filius/doc';
import { docSnapshot, emailServerAfterRun, fromY, installedSoftware, readFilesInto, ROOTS, SIM_ORIGIN, writeRunState, type NodeRunState } from '@mdg-filius/doc';
import {
parseClientMessage,
SIM_PROTOCOL_VERSION,
@@ -136,6 +143,35 @@ export class SimulationRoom {
doc.on('update', () => {
if (this.mode === 'simulation') this.guarded(() => this.syncSoftware());
});
doc.getMap<Y.Map<unknown>>(ROOTS.nodes).observeDeep((events, transaction) => {
// The room's own writes hold what the engine has.
if (transaction.origin === SIM_ORIGIN) return;
const nodes = new Set<Id>();
for (const event of events) {
const [node, key] = event.path as string[];
// A device added or removed comes in at the next start.
if (node !== undefined && (key === 'files' || (key === undefined && event.keys.has('files')))) nodes.add(node);
}
if (nodes.size > 0) this.guarded(() => this.syncFiles(nodes));
});
}
/** The document's files of the nodes into the running engine's (a person wrote them). */
private syncFiles(nodes: ReadonlySet<Id>): void {
const fileSystems = this.engine.state.topologySnapshot.fileSystemsByNodeId;
for (const id of nodes) {
const files = fileSystems.get(id);
// A node whose programs use no files has none in the engine; it reads the document's at the next start.
if (files !== undefined && readFilesInto(this.doc, id, files)) this.writtenFiles.set(id, { files, revision: files.revision });
}
}
/** Whether a node's files changed since they were last written back; marked written if so. */
private takeChangedFiles(id: Id, files: { readonly revision: number } | undefined): boolean {
const written = this.writtenFiles.get(id);
const changed = files !== undefined && (written?.files !== files || written.revision !== files.revision);
if (files !== undefined && changed) this.writtenFiles.set(id, { files, revision: files.revision });
return changed;
}
/** The computers' programs in the document. */
@@ -206,10 +242,7 @@ export class SimulationRoom {
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
const files = snapshot.fileSystemsByNodeId.get(id);
const written = this.writtenFiles.get(id);
const changed = files !== undefined && (written?.files !== files || written.revision !== files.revision);
if (files !== undefined && changed) this.writtenFiles.set(id, { files, revision: files.revision });
states.set(id, { ...(changed ? { files } : {}), active, settings });
states.set(id, { ...(this.takeChangedFiles(id, files) && files !== undefined ? { files } : {}), active, settings });
}
return states;
}
@@ -221,6 +254,13 @@ export class SimulationRoom {
writeRunState(this.doc, this.runState(), SIM_ORIGIN);
}
/** Writes the files the engine changed since they were last written (origin 'sim'); nothing when none changed. */
private writeFilesBack(): void {
const states = new Map<Id, NodeRunState>();
for (const [id, files] of this.engine.state.topologySnapshot.fileSystemsByNodeId) if (this.takeChangedFiles(id, files)) states.set(id, { files });
if (states.size > 0) writeRunState(this.doc, states, SIM_ORIGIN);
}
/** The document as the engine takes it, in the language of the start. */
private snapshot(): TopologySnapshot {
const snapshot = docSnapshot(this.doc);
@@ -364,6 +404,7 @@ export class SimulationRoom {
if (!this.guarded(() => this.engine.handle({ type: 'advance', toMilliseconds: this.engine.state.currentTimeMilliseconds + elapsed }))) return;
this.flush([...this.clients.values()], false);
if (now - this.lastWriteBack >= (this.options.writeBackMilliseconds ?? WRITE_BACK_MILLISECONDS)) this.guarded(() => this.writeBack());
else this.guarded(() => this.writeFilesBack());
}
/**