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
@@ -63,6 +63,10 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
com.intellij.openapi.fileEditor.FileDocumentManagerListener.TOPIC,
new com.intellij.openapi.fileEditor.FileDocumentManagerListener() {
public void afterDocumentSaved(Document document) {
// A native save can queue physical I/O. Record the accepted version now;
// subsequent NIO reads must flush that file before comparing disk contents.
for (var state : states.values())
if (state.document == document) state.disk = document.getText();
var file = FileDocumentManager.getInstance().getFile(document);
if (file != null && file.isInLocalFileSystem())
saves.forEach(listener -> listener.accept(canonicalUri(file)));
@@ -112,6 +116,8 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
if (state.file == null) throw new IllegalArgumentException("Missing file: " + path);
state.document = FileDocumentManager.getInstance().getDocument(state.file);
if (state.document == null) throw new IllegalArgumentException("Not a text document");
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance()
.flushPendingUpdates(state.file);
state.disk = Files.readString(path);
var observed = state;
state.document.addDocumentListener(
@@ -172,6 +178,7 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
private void refreshClean(State s) throws java.io.IOException {
var manager = FileDocumentManager.getInstance();
if (manager.isDocumentUnsaved(s.document)) return;
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
String disk = Files.readString(Path.of(s.file.getPath()));
if (disk.equals(s.document.getText())) {
s.disk = disk; // Native autosave already persisted this revision; do not reload it.
@@ -270,6 +277,7 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
if (!s.document.isWritable() || !s.file.isWritable())
throw new IllegalStateException("Read-only document");
Path file = Path.of(s.file.getPath());
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
String onDisk = Files.readString(file);
if (!FileDocumentManager.getInstance().isDocumentUnsaved(s.document)
&& onDisk.equals(s.document.getText())) {
@@ -281,6 +289,9 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
FileDocumentManager.getInstance().saveDocument(s.document);
if (FileDocumentManager.getInstance().isDocumentUnsaved(s.document))
throw new IllegalStateException("IDE did not save document");
// FileDocumentManager may mark the Document clean before its asynchronous
// VFS write reaches disk. Our completion promises persistence to NIO consumers.
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
s.disk = Files.readString(file);
return IntellijDocuments.this.snapshot(s);
});
@@ -6,6 +6,96 @@ import java.nio.file.*;
import java.util.*;
public final class IntellijDocumentsTest extends BasePlatformTestCase {
/** Keeps real VFS writes queued until its per-file flush executes them. */
public void testSaveWaitsForQueuedVfsWrites() throws Exception {
var fs = com.intellij.openapi.vfs.LocalFileSystem.getInstance();
var executorField = fs.getClass().getDeclaredField("ioTasksExecutor");
executorField.setAccessible(true);
var executor = executorField.get(fs);
var workerField = executor.getClass().getDeclaredField("pendingTasksExecutor");
workerField.setAccessible(true);
var originalWorker = workerField.get(executor);
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates();
var queuedTasks = new java.util.concurrent.atomic.AtomicInteger();
var queued =
new java.util.concurrent.AbstractExecutorService() {
public void execute(Runnable task) {
queuedTasks.incrementAndGet();
}
public void shutdown() {}
public java.util.List<Runnable> shutdownNow() {
return java.util.List.of();
}
public boolean isShutdown() {
return false;
}
public boolean isTerminated() {
return false;
}
public boolean awaitTermination(long time, java.util.concurrent.TimeUnit unit) {
return false;
}
};
Path file = Files.createTempFile("designer-queued-save-", ".fxml");
Files.writeString(file, "before");
com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(
getTestRootDisposable(), file.toRealPath().toString());
try (var docs = new IntellijDocuments(getProject());
var d = docs.open(file.toUri()).toCompletableFuture().get()) {
workerField.set(executor, queued);
var changed =
d.apply(new EditBatch(0, "edit", List.of(new EditBatch.Replacement(0, 6, "after"))))
.toCompletableFuture()
.get();
var saved = d.save(changed.revision()).toCompletableFuture().get();
assertFalse(saved.dirty());
assertEquals(
"A completed save must be visible to NIO readers", "after", Files.readString(file));
assertEquals("after", d.snapshot().toCompletableFuture().get().text());
assertTrue("The test must exercise asynchronous VFS writes", queuedTasks.get() > 0);
var nativeEdit =
d.apply(
new EditBatch(
saved.revision(),
"native save",
List.of(new EditBatch.Replacement(0, 5, "native"))))
.toCompletableFuture()
.get();
var virtual = com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByNioFile(file);
var manager = com.intellij.openapi.fileEditor.FileDocumentManager.getInstance();
manager.saveDocument(manager.getDocument(virtual));
assertEquals("Native autosave is queued", "after", Files.readString(file));
// Simulate editing in IntelliJ's text editor: no adapter apply/refresh in between.
com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction(
getProject(), () -> manager.getDocument(virtual).setText("followup"));
// A second designer host can open after native editing has already begun.
// Its initial disk baseline must include the queued native save as well.
try (var late = new IntellijDocuments(getProject());
var opened = late.open(file.toUri()).toCompletableFuture().get()) {
var current = opened.snapshot().toCompletableFuture().get();
opened.save(current.revision()).toCompletableFuture().get();
assertEquals("followup", Files.readString(file));
}
var followup = d.snapshot().toCompletableFuture().get();
assertTrue(followup.revision() > nativeEdit.revision());
d.save(followup.revision()).toCompletableFuture().get();
assertEquals("followup", Files.readString(file));
assertEquals("followup", d.snapshot().toCompletableFuture().get().text());
} finally {
try {
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates();
} finally {
workerField.set(executor, originalWorker);
Files.deleteIfExists(file);
}
}
}
public void testNativeDocumentTransactionUndoAndIndependentHandles() throws Exception {
Path file = Files.createTempFile("designer-document-", ".fxml");
Files.writeString(file, "abcdef");