package de.neitzel.fxdesigner.intellij; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import de.neitzel.fxdesigner.contracts.*; 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 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"); com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess( getTestRootDisposable(), file.toRealPath().toString()); try (var docs = new IntellijDocuments(getProject())) { var a = docs.open(file.toUri()).toCompletableFuture().get(); var b = docs.open(file.toUri()).toCompletableFuture().get(); var initial = a.snapshot().toCompletableFuture().get(); var changed = a.apply( new EditBatch( initial.revision(), "Component properties", List.of( new EditBatch.Replacement(0, 1, "A"), new EditBatch.Replacement(5, 6, "F")))) .toCompletableFuture() .get(); assertEquals("AbcdeF", b.snapshot().toCompletableFuture().get().text()); a.close(); var undone = b.undo().toCompletableFuture().get(); assertEquals("abcdef", undone.text()); assertTrue(undone.revision() > changed.revision()); try { b.apply(new EditBatch(initial.revision(), "stale", List.of())).toCompletableFuture().get(); fail("Stale edit accepted"); } catch (java.util.concurrent.ExecutionException expected) { assertInstanceOf(expected.getCause(), ConcurrentModificationException.class); } var redone = b.redo().toCompletableFuture().get(); assertEquals("AbcdeF", redone.text()); b.save(redone.revision()).toCompletableFuture().get(); assertEquals("AbcdeF", Files.readString(file)); b.close(); } finally { Files.deleteIfExists(file); } } public void testExternalChangesDoNotLoseDraft() throws Exception { Path file = Files.createTempFile("designer-conflict-", ".fxml"); Files.writeString(file, "old"); 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(); var changed = d.apply(new EditBatch(0, "edit", List.of(new EditBatch.Replacement(0, 3, "draft")))) .toCompletableFuture() .get(); Files.writeString(file, "external"); try { d.save(changed.revision()).toCompletableFuture().get(); fail("Conflict accepted"); } catch (java.util.concurrent.ExecutionException expected) { assertInstanceOf(expected.getCause(), ConcurrentModificationException.class); } assertEquals("draft", d.snapshot().toCompletableFuture().get().text()); assertEquals("external", Files.readString(file)); d.close(); } finally { Files.deleteIfExists(file); } } public void testKeepsNativeDocumentOpenedThroughAlias() throws Exception { Path directory = Files.createTempDirectory("designer-alias-"); Path real = directory.resolve("real"); Files.createDirectory(real); Path alias = directory.resolve("alias"); Files.createSymbolicLink(alias, real); Path file = real.resolve("View.fxml"); Files.writeString(file, "before"); com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess( getTestRootDisposable(), directory.toRealPath().toString(), directory.toString()); var virtual = com.intellij.openapi.vfs.LocalFileSystem.getInstance() .refreshAndFindFileByNioFile(alias.resolve("View.fxml")); var nativeDocument = com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().getDocument(virtual); try (var docs = new IntellijDocuments(getProject()); var handle = docs.open(alias.resolve("View.fxml").toUri()).toCompletableFuture().get()) { var before = handle.snapshot().toCompletableFuture().get(); var after = handle .apply( new EditBatch( before.revision(), "Alias edit", List.of(new EditBatch.Replacement(0, before.text().length(), "after")))) .toCompletableFuture() .get(); assertEquals("after", nativeDocument.getText()); assertEquals(file.toRealPath().toUri(), after.document()); handle.save(after.revision()).toCompletableFuture().get(); } finally { Files.deleteIfExists(alias); Files.deleteIfExists(file); Files.deleteIfExists(real); Files.deleteIfExists(directory); } } public void testCleanExternalReloadAndRangeCaretPreservation() throws Exception { Path file = Files.createTempFile("designer-reload-", ".fxml"); Files.writeString(file, "prefix value suffix"); com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess( getTestRootDisposable(), file.toRealPath().toString()); try (var docs = new IntellijDocuments(getProject()); var handle = docs.open(file.toUri()).toCompletableFuture().get()) { var initial = handle.snapshot().toCompletableFuture().get(); Files.writeString(file, "prefix external suffix"); var current = handle.snapshot().toCompletableFuture().get(); assertEquals("prefix external suffix", current.text()); assertFalse(current.dirty()); assertTrue(initial.revision() != current.revision()); var vf = com.intellij.openapi.vfs.LocalFileSystem.getInstance().refreshAndFindFileByNioFile(file); var document = com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().getDocument(vf); var editor = com.intellij.openapi.editor.EditorFactory.getInstance() .createEditor(document, getProject()); try { editor.getCaretModel().moveToOffset(current.text().length()); editor .getSelectionModel() .setSelection(current.text().indexOf("suffix"), current.text().length()); var updated = handle .apply( new EditBatch( current.revision(), "Range edit", List.of(new EditBatch.Replacement(7, 15, "X")))) .toCompletableFuture() .get(); assertEquals("prefix X suffix", updated.text()); assertEquals("suffix", editor.getSelectionModel().getSelectedText()); assertEquals(updated.text().length(), editor.getCaretModel().getOffset()); com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().saveDocument(document); var saved = handle.snapshot().toCompletableFuture().get(); assertEquals(updated.revision(), saved.revision()); assertFalse(saved.dirty()); handle.save(updated.revision()).toCompletableFuture().get(); } finally { com.intellij.openapi.editor.EditorFactory.getInstance().releaseEditor(editor); } } finally { Files.deleteIfExists(file); } } }