Flush queued IDE writes and restart validation after host revision changes

This commit is contained in:
2026-11-13 20:53:22 +01:00
parent 8be0aec287
commit 1856f69637
11 changed files with 284 additions and 2 deletions
@@ -554,6 +554,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
private void load(DocumentSnapshot value) {
if (closed) return;
// A newer host revision can have identical text (edit/revert or native reload).
// Its predecessor's validation reply will be rejected by revision, so it must
// not leave an in-flight marker that absorbs every subsequent render request.
if (draft == null || draft.revision() != value.revision() || !draft.text().equals(value.text()))
invalidateBindings("Document synchronized; target validation outdated.");
loading = true;
layoutPasses = 0;
canvasCommitQueued = false;
@@ -635,6 +640,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
report(error);
} finally {
loading = false;
// The change debounce may already have fired against the previous draft.
// Render only after the authoritative revision and its projection agree.
if (projection != null && examples.getValue() != null) resize.playFromStart();
if (pendingSourceOffset != null) {
int offset = pendingSourceOffset;
pendingSourceOffset = null;
@@ -1604,7 +1604,72 @@ public final class ProjectionHostChecks {
await(() -> fx(() -> source(pane).getText().equals(original)));
}
private static Object field(DesignerPane pane, String name) throws Exception {
var field = DesignerPane.class.getDeclaredField(name);
field.setAccessible(true);
return field.get(pane);
}
/** Force a host revision to arrive after validation starts, with identical final text. */
public static void renderRevisionRace(DesignerPane pane, DocumentSession document)
throws Exception {
String original = document.snapshot().toCompletableFuture().get().text();
var poll = (javafx.animation.Timeline) field(pane, "poll");
var resize = (javafx.animation.PauseTransition) field(pane, "resize");
fx(
() -> {
poll.stop();
resize.stop();
return null;
});
try {
set(document, original + "<!-- transient external edit -->");
set(document, original);
long revision = document.snapshot().toCompletableFuture().get().revision();
fx(
() -> {
resize.stop();
pane.render();
check(original.equals(field(pane, "validatingSource")), "Validation was not in flight");
// Deliver the real host-poll callback before the worker validation reply.
poll.getKeyFrames().getFirst().getOnFinished().handle(new javafx.event.ActionEvent());
return null;
});
await(() -> fx(() -> ((DocumentSnapshot) field(pane, "draft")).revision() == revision));
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
boolean rendered = false;
while (System.nanoTime() < deadline) {
rendered =
fx(
() ->
List.of("card", "second").stream()
.allMatch(
id -> {
var region = (SnapshotRegion) object(pane, id).getSceneGraphObject();
return region.image() != null
&& region.statusText().equals("Target runtime");
}));
if (rendered) break;
Thread.sleep(25);
}
check(rendered, "Host revision stranded target validation/rendering");
check(
document.snapshot().toCompletableFuture().get().text().equals(original),
"Render edited source");
System.out.println(
"PASS host revision during validation: identical text, fresh target pixels, no extra"
+ " action");
} finally {
fx(
() -> {
poll.playFromStart();
return null;
});
}
}
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
renderRevisionRace(pane, document);
String original = document.snapshot().toCompletableFuture().get().text();
// The same real application, now with nested, fully-qualified Component declarations.
String nested =