253 lines
11 KiB
Java
253 lines
11 KiB
Java
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<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");
|
|
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);
|
|
}
|
|
}
|
|
}
|