Flush queued IDE writes and restart validation after host revision changes
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
+90
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user