Preserve FXML source ranges and extend shared Component projection

This commit is contained in:
2026-11-12 11:52:43 +01:00
parent 7db7218a6e
commit dfa2dd1508
19 changed files with 1145 additions and 49 deletions
@@ -64,7 +64,11 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
}
public CompletionStage<List<DocumentSnapshot>> snapshots() {
return submit(() -> documents.values().stream().map(FileDocuments::snapshot).toList());
return submit(
() -> {
for (State s : documents.values()) refresh(s);
return documents.values().stream().map(FileDocuments::snapshot).toList();
});
}
public CompletionStage<List<DocumentSnapshot>> unsaved() {
@@ -76,6 +80,18 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
.toList());
}
private void refresh(State s) throws java.io.IOException {
if (!s.text.equals(s.saved)) return; // Dirty draft belongs to the user; save detects conflict.
String disk = Files.readString(s.file);
if (!disk.equals(s.saved)) {
s.text = s.saved = disk;
s.undo.clear();
s.redo.clear();
s.revision++;
changes.forEach(listener -> listener.accept(s.file.toUri()));
}
}
private static DocumentSnapshot snapshot(State s) {
return new DocumentSnapshot(
s.file.toUri(), s.revision, s.text, Files.isWritable(s.file), !s.text.equals(s.saved));
@@ -98,12 +114,17 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
}
public CompletionStage<DocumentSnapshot> snapshot() {
return run(() -> FileDocuments.snapshot(s));
return run(
() -> {
refresh(s);
return FileDocuments.snapshot(s);
});
}
public CompletionStage<DocumentSnapshot> apply(EditBatch edits) {
return run(
() -> {
refresh(s);
String updated = edits.applyTo(FileDocuments.snapshot(s));
if (!updated.equals(s.text)) {
s.undo.push(s.text);
@@ -154,6 +175,8 @@ public final class FileDocuments implements HostServices.Documents, AutoCloseabl
!= null) {
Files.setPosixFilePermissions(temp, Files.getPosixFilePermissions(s.file));
}
if (!Files.readString(s.file).equals(s.saved))
throw new ConcurrentModificationException("External change; draft retained");
Files.move(
temp,
s.file,
@@ -67,4 +67,32 @@ class FileDocumentsTest {
assertEquals("draft", d.snapshot().toCompletableFuture().get().text());
}
}
@Test
void cleanExternalChangesReloadAndInvalidateOldTransactions() throws Exception {
Path file = folder.resolve("External.fxml");
Files.writeString(file, "old");
try (var docs = new FileDocuments()) {
var first = docs.open(file.toUri()).toCompletableFuture().get();
var second = docs.open(file.toUri()).toCompletableFuture().get();
var initial = first.snapshot().toCompletableFuture().get();
Files.writeString(file, "external");
var reloaded = second.snapshot().toCompletableFuture().get();
assertEquals("external", reloaded.text());
assertFalse(reloaded.dirty());
assertNotEquals(initial.revision(), reloaded.revision());
assertThrows(
ExecutionException.class,
() ->
first
.apply(
new EditBatch(
initial.revision(),
"old edit",
List.of(new EditBatch.Replacement(0, 1, "X"))))
.toCompletableFuture()
.get());
assertEquals("external", first.undo().toCompletableFuture().get().text());
}
}
}
@@ -127,15 +127,30 @@ public final class KitWorkflowSmoke {
var field =
(TextField)
pane.lookupAll(".text-field").stream()
.filter(n -> n instanceof TextField t && t.getText().equals(caption))
.filter(
n ->
n instanceof TextField t
&& t.getText().equals(caption)
&& (t.getId() == null
|| !t.getId().startsWith("component-property-")))
.findFirst()
.orElseThrow();
field.setText("alpha bravo charlie");
pane.applyCss();
pane.layout();
var result = new StringBuilder();
TextSelectionChecks.run(field, result);
System.out.print(result);
try {
TextSelectionChecks.run(field, result);
} finally {
System.out.print(result);
System.out.println(
"Caption field: "
+ field.getId()
+ " width="
+ field.getWidth()
+ " text="
+ field.getText());
}
field.setText(caption);
field.fireEvent(new javafx.event.ActionEvent());
return null;
@@ -223,6 +238,10 @@ public final class KitWorkflowSmoke {
if (!Files.readString(Path.of(args[1])).contains("Native canvas edit"))
throw new AssertionError("Host save missing");
}
try (var document =
host.services().documents().open(Path.of(args[1]).toUri()).toCompletableFuture().get()) {
ProjectionHostChecks.run(pane, document);
}
fx(
() -> {
pane.close();