609 lines
26 KiB
Java
609 lines
26 KiB
Java
package qualification;
|
|
|
|
import com.intellij.openapi.application.*;
|
|
import com.intellij.openapi.fileEditor.*;
|
|
import com.intellij.openapi.project.Project;
|
|
import com.intellij.openapi.startup.StartupActivity;
|
|
import com.intellij.openapi.vfs.*;
|
|
import com.oracle.javafx.scenebuilder.kit.fxom.*;
|
|
import de.neitzel.fxdesigner.kit.*;
|
|
import java.nio.file.*;
|
|
import java.util.concurrent.*;
|
|
import javafx.application.Platform;
|
|
import javafx.embed.swing.JFXPanel;
|
|
import javafx.scene.control.Button;
|
|
|
|
/** Disposable companion plugin, never included in the delivered designer plugin. */
|
|
public final class WorkflowProbe implements StartupActivity.DumbAware {
|
|
static final Path output = Path.of(System.getProperty("designer.check.output"));
|
|
static DesignerPane active;
|
|
static final java.util.concurrent.atomic.AtomicBoolean started =
|
|
new java.util.concurrent.atomic.AtomicBoolean();
|
|
|
|
static <T> T edt(Callable<T> action) throws Exception {
|
|
var f = new CompletableFuture<T>();
|
|
ApplicationManager.getApplication()
|
|
.invokeLater(
|
|
() ->
|
|
WriteIntentReadAction.run(
|
|
() -> {
|
|
try {
|
|
f.complete(action.call());
|
|
} catch (Throwable t) {
|
|
f.completeExceptionally(t);
|
|
}
|
|
}),
|
|
ModalityState.nonModal());
|
|
return f.get(20, TimeUnit.SECONDS);
|
|
}
|
|
|
|
static <T> T fx(Callable<T> action) throws Exception {
|
|
var f = new CompletableFuture<T>();
|
|
Platform.runLater(
|
|
() -> {
|
|
try {
|
|
f.complete(action.call());
|
|
} catch (Throwable t) {
|
|
f.completeExceptionally(t);
|
|
}
|
|
});
|
|
return f.get(20, TimeUnit.SECONDS);
|
|
}
|
|
|
|
static JFXPanel bridge(java.awt.Component c) {
|
|
if (c instanceof JFXPanel p) return p;
|
|
if (c instanceof java.awt.Container parent)
|
|
for (var child : parent.getComponents()) {
|
|
var p = bridge(child);
|
|
if (p != null) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void await(Callable<Boolean> condition) throws Exception {
|
|
long until = System.nanoTime() + TimeUnit.SECONDS.toNanos(120);
|
|
while (System.nanoTime() < until) {
|
|
if (condition.call()) return;
|
|
Thread.sleep(100);
|
|
}
|
|
throw new AssertionError("IDE workflow timed out");
|
|
}
|
|
|
|
static void click(DesignerPane pane, String text) throws Exception {
|
|
fx(
|
|
() -> {
|
|
((Button)
|
|
pane.lookupAll(".button").stream()
|
|
.filter(n -> n instanceof Button b && b.getText().equals(text))
|
|
.findFirst()
|
|
.orElseThrow())
|
|
.fire();
|
|
return null;
|
|
});
|
|
}
|
|
|
|
private static com.intellij.codeInsight.daemon.impl.HighlightInfo directionHighlight(
|
|
Project project, VirtualFile file) {
|
|
var document = FileDocumentManager.getInstance().getDocument(file);
|
|
var result = new java.util.ArrayList<com.intellij.codeInsight.daemon.impl.HighlightInfo>();
|
|
com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx.processHighlights(
|
|
document,
|
|
project,
|
|
com.intellij.lang.annotation.HighlightSeverity.ERROR,
|
|
0,
|
|
document.getTextLength(),
|
|
info -> {
|
|
if (info.getDescription() != null
|
|
&& info.getDescription().startsWith("Invalid or missing direction:"))
|
|
result.add(info);
|
|
return true;
|
|
});
|
|
return result.isEmpty() ? null : result.getFirst();
|
|
}
|
|
|
|
private static void checkNativeFixes(
|
|
Project project,
|
|
VirtualFile file,
|
|
DesignerPane pane,
|
|
de.neitzel.fxdesigner.host.DocumentSession document)
|
|
throws Exception {
|
|
var before = document.snapshot().toCompletableFuture().get();
|
|
String bad = before.text().replaceFirst("direction=\"both\"", "direction = 'bad'");
|
|
if (bad.equals(before.text())) throw new AssertionError("Fixture has no direction");
|
|
document
|
|
.apply(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch(
|
|
before.revision(),
|
|
"Native fix fixture",
|
|
java.util.List.of(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
|
|
0, before.text().length(), bad))))
|
|
.toCompletableFuture()
|
|
.get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
|
|
.getText()
|
|
.equals(bad)));
|
|
int at = bad.indexOf("'bad'") + 1;
|
|
edt(
|
|
() -> {
|
|
new OpenFileDescriptor(project, file, at).navigate(true);
|
|
return null;
|
|
});
|
|
await(() -> edt(() -> directionHighlight(project, file) != null));
|
|
var info = edt(() -> directionHighlight(project, file));
|
|
if (info.getStartOffset() != at || info.getEndOffset() != at + 3)
|
|
throw new AssertionError("Wrong native diagnostic range");
|
|
var action =
|
|
edt(
|
|
() ->
|
|
info.findRegisteredQuickFix(
|
|
(descriptor, range) ->
|
|
"Set binding direction to both".equals(descriptor.getAction().getText())
|
|
? descriptor.getAction()
|
|
: null));
|
|
if (action == null) throw new AssertionError("Native Alt+Enter fix not registered");
|
|
edt(
|
|
() -> {
|
|
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
var psi =
|
|
com.intellij.psi.PsiDocumentManager.getInstance(project)
|
|
.getPsiFile(editor.getDocument());
|
|
if (!action.isAvailable(project, editor, psi))
|
|
throw new AssertionError("Native fix unavailable");
|
|
action.invoke(project, editor, psi);
|
|
return null;
|
|
});
|
|
String fixed = bad.replace("direction = 'bad'", "direction = 'both'");
|
|
await(() -> document.snapshot().toCompletableFuture().get().text().equals(fixed));
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
|
|
.getText()
|
|
.equals(fixed)));
|
|
await(() -> edt(() -> directionHighlight(project, file) == null));
|
|
edt(
|
|
() -> {
|
|
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
var psi =
|
|
com.intellij.psi.PsiDocumentManager.getInstance(project)
|
|
.getPsiFile(editor.getDocument());
|
|
if (action.isAvailable(project, editor, psi))
|
|
throw new AssertionError("Old fix survived source change");
|
|
action.invoke(project, editor, psi);
|
|
return null;
|
|
});
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(fixed))
|
|
throw new AssertionError("Stale native fix changed source");
|
|
edt(
|
|
() -> {
|
|
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
com.intellij.openapi.command.undo.UndoManager.getInstance(project)
|
|
.undo(
|
|
com.intellij
|
|
.openapi
|
|
.fileEditor
|
|
.impl
|
|
.text
|
|
.TextEditorProvider
|
|
.getInstance()
|
|
.getTextEditor(editor));
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
|
|
.getText()
|
|
.equals(bad)));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(bad))
|
|
throw new AssertionError("Native fix Undo not exact");
|
|
edt(
|
|
() -> {
|
|
var editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
com.intellij.openapi.command.undo.UndoManager.getInstance(project)
|
|
.redo(
|
|
com.intellij
|
|
.openapi
|
|
.fileEditor
|
|
.impl
|
|
.text
|
|
.TextEditorProvider
|
|
.getInstance()
|
|
.getTextEditor(editor));
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
|
|
.getText()
|
|
.equals(fixed)));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(fixed))
|
|
throw new AssertionError("Native fix Redo not exact");
|
|
var current = document.snapshot().toCompletableFuture().get();
|
|
document
|
|
.apply(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch(
|
|
current.revision(),
|
|
"Restore native fixture",
|
|
java.util.List.of(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
|
|
0, current.text().length(), before.text()))))
|
|
.toCompletableFuture()
|
|
.get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
((javafx.scene.control.TextArea) pane.lookup("#fxml-source"))
|
|
.getText()
|
|
.equals(before.text())));
|
|
System.out.println(
|
|
"PASS native source highlighting and Alt+Enter fix: exact ranges, source-preserving"
|
|
+ " transaction, Undo/Redo, stale action rejection and highlight withdrawal");
|
|
}
|
|
|
|
private static void checkProblems(
|
|
Project project,
|
|
VirtualFile file,
|
|
DesignerPane pane,
|
|
de.neitzel.fxdesigner.host.DocumentSession document)
|
|
throws Exception {
|
|
var original = document.snapshot().toCompletableFuture().get();
|
|
String invalid = original.text().replace("path=\"home\"", "path=\"problemMissing\"");
|
|
if (invalid.equals(original.text())) throw new AssertionError("Missing fixture binding");
|
|
document
|
|
.apply(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch(
|
|
original.revision(),
|
|
"Problem fixture",
|
|
java.util.List.of(
|
|
new de.neitzel.fxdesigner.contracts.EditBatch.Replacement(
|
|
0, original.text().length(), invalid))))
|
|
.toCompletableFuture()
|
|
.get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(invalid))));
|
|
fx(
|
|
() -> {
|
|
pane.lookupAll(".titled-pane").stream()
|
|
.filter(
|
|
n ->
|
|
n instanceof javafx.scene.control.TitledPane t
|
|
&& "Bindings".equals(t.getText()))
|
|
.forEach(n -> ((javafx.scene.control.TitledPane) n).setExpanded(true));
|
|
pane.lookupAll(".tab-pane").stream()
|
|
.filter(n -> n instanceof javafx.scene.control.TabPane)
|
|
.map(n -> (javafx.scene.control.TabPane) n)
|
|
.forEach(
|
|
t ->
|
|
t.getTabs().stream()
|
|
.filter(tab -> "Problems".equals(tab.getText()))
|
|
.findFirst()
|
|
.ifPresent(tab -> t.getSelectionModel().select(tab)));
|
|
return null;
|
|
});
|
|
click(pane, "Validate View");
|
|
var collector = com.intellij.analysis.problemsView.ProblemsCollector.getInstance(project);
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.anyMatch(p -> p.getText().contains("problemMissing"))));
|
|
var problem =
|
|
edt(
|
|
() ->
|
|
(com.intellij.analysis.problemsView.FileProblem)
|
|
collector.getFileProblems(file).stream()
|
|
.filter(p -> p.getText().contains("problemMissing"))
|
|
.findFirst()
|
|
.orElseThrow());
|
|
int expectedOffset =
|
|
edt(
|
|
() -> {
|
|
var d = FileDocumentManager.getInstance().getDocument(file);
|
|
return d.getLineStartOffset(problem.getLine()) + problem.getColumn();
|
|
});
|
|
String declaration =
|
|
invalid.substring(expectedOffset, invalid.indexOf('>', expectedOffset) + 1);
|
|
if (!declaration.startsWith("<DirectBinding")
|
|
|| !declaration.contains("path=\"problemMissing\""))
|
|
throw new AssertionError(
|
|
"Problems origin did not locate an affected binding: " + declaration);
|
|
edt(
|
|
() -> {
|
|
var window =
|
|
com.intellij.openapi.wm.ToolWindowManager.getInstance(project)
|
|
.getToolWindow("Problems View");
|
|
if (window == null) throw new AssertionError("Native Problems tool window missing");
|
|
window.show();
|
|
new OpenFileDescriptor(project, problem.getFile(), problem.getLine(), problem.getColumn())
|
|
.navigate(true);
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() -> {
|
|
var textEditor = FileEditorManager.getInstance(project).getSelectedTextEditor();
|
|
return textEditor != null
|
|
&& textEditor.getCaretModel().getOffset() == expectedOffset;
|
|
}));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(invalid))
|
|
throw new AssertionError("Problems navigation mutated source");
|
|
document.undo().toCompletableFuture().get();
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.noneMatch(p -> p.getText().contains("problemMissing"))));
|
|
if (!document.snapshot().toCompletableFuture().get().text().equals(original.text()))
|
|
throw new AssertionError("Problems navigation changed undo history");
|
|
// Observe the shared editor's Undo revision before issuing Redo; its previous invalid
|
|
// text must not satisfy the Redo wait while the Undo notification is still queued.
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(original.text()))));
|
|
document.redo().toCompletableFuture().get();
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
pane.lookupAll(".text-area").stream()
|
|
.anyMatch(
|
|
n ->
|
|
n instanceof javafx.scene.control.TextArea t
|
|
&& t.getText().equals(invalid))));
|
|
click(pane, "Validate View");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.anyMatch(p -> p.getText().contains("problemMissing"))));
|
|
edt(
|
|
() -> {
|
|
FileEditorManager.getInstance(project).closeFile(file);
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
collector.getFileProblems(file).stream()
|
|
.noneMatch(p -> "Neitzel FX Designer".equals(p.getGroup()))));
|
|
System.out.println(
|
|
"PASS native Problems: current validation, FXML origin/navigation, correction via Undo,"
|
|
+ " redo validation and editor-close cleanup");
|
|
}
|
|
|
|
public void runActivity(Project project) {
|
|
if (!started.compareAndSet(false, true)) return;
|
|
ApplicationManager.getApplication()
|
|
.executeOnPooledThread(
|
|
() -> {
|
|
try {
|
|
Path path =
|
|
Path.of(
|
|
project.getBasePath(),
|
|
"app/src/main/resources/fixture/components/Person.fxml");
|
|
VirtualFile file =
|
|
edt(() -> LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path));
|
|
FileEditor editor =
|
|
edt(
|
|
() -> {
|
|
var manager = FileEditorManager.getInstance(project);
|
|
var all = manager.openFile(file, true);
|
|
manager.setSelectedEditor(file, "neitzel-fx-designer");
|
|
return java.util.Arrays.stream(all)
|
|
.filter(e -> e.getName().equals("Neitzel Designer"))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
});
|
|
await(() -> edt(() -> bridge(editor.getComponent()) != null));
|
|
JFXPanel bridge = edt(() -> bridge(editor.getComponent()));
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
bridge.getScene() != null
|
|
&& bridge.getScene().getRoot() instanceof DesignerPane));
|
|
DesignerPane pane = fx(() -> (DesignerPane) bridge.getScene().getRoot());
|
|
active = pane;
|
|
await(() -> fx(() -> pane.kit().editor().getFxomDocument() != null));
|
|
click(pane, "Build and Refresh");
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
java.util.List.of("card", "second").stream()
|
|
.allMatch(
|
|
id -> {
|
|
var item =
|
|
new FXOMFxIdIndex(
|
|
pane.kit().editor().getFxomDocument())
|
|
.lookup(id);
|
|
return item instanceof FXOMInstance instance
|
|
&& instance.getSceneGraphObject()
|
|
instanceof SnapshotRegion region
|
|
&& region.image() != null
|
|
&& region.statusText().equals("Target runtime");
|
|
})));
|
|
fx(
|
|
() -> {
|
|
var kit = pane.kit().editor();
|
|
var card =
|
|
(FXOMInstance) new FXOMFxIdIndex(kit.getFxomDocument()).lookup("card");
|
|
kit.getSelection().select(card);
|
|
var property =
|
|
(FXOMPropertyT)
|
|
card.getProperties()
|
|
.get(
|
|
new com.oracle.javafx.scenebuilder.kit.metadata.util
|
|
.PropertyName("caption"));
|
|
String old = property.getValue();
|
|
kit.getJobManager()
|
|
.push(
|
|
new com.oracle.javafx.scenebuilder.kit.editor.job.Job(kit) {
|
|
public boolean isExecutable() {
|
|
return true;
|
|
}
|
|
|
|
public String getDescription() {
|
|
return "IDE caption check";
|
|
}
|
|
|
|
public void execute() {
|
|
set("IDE canvas draft");
|
|
}
|
|
|
|
public void undo() {
|
|
set(old);
|
|
}
|
|
|
|
public void redo() {
|
|
execute();
|
|
}
|
|
|
|
void set(String text) {
|
|
var doc = kit.getFxomDocument();
|
|
doc.beginUpdate();
|
|
try {
|
|
property.setValue(text);
|
|
} finally {
|
|
doc.endUpdate();
|
|
}
|
|
}
|
|
});
|
|
return null;
|
|
});
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("IDE canvas draft")));
|
|
click(pane, "Undo");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("FXML caption")));
|
|
click(pane, "Redo");
|
|
await(
|
|
() ->
|
|
edt(
|
|
() ->
|
|
FileDocumentManager.getInstance()
|
|
.getDocument(file)
|
|
.getText()
|
|
.contains("IDE canvas draft")));
|
|
click(pane, "Render drafts");
|
|
await(
|
|
() ->
|
|
fx(
|
|
() ->
|
|
java.util.List.of("card", "second").stream()
|
|
.allMatch(
|
|
id -> {
|
|
var item =
|
|
new FXOMFxIdIndex(
|
|
pane.kit().editor().getFxomDocument())
|
|
.lookup(id);
|
|
return item instanceof FXOMInstance instance
|
|
&& instance.getSceneGraphObject()
|
|
instanceof SnapshotRegion region
|
|
&& region.image() != null
|
|
&& region.statusText().equals("Target runtime");
|
|
})));
|
|
fx(
|
|
() -> {
|
|
javax.imageio.ImageIO.write(
|
|
javafx.embed.swing.SwingFXUtils.fromFXImage(
|
|
pane.snapshot(null, null), null),
|
|
"png",
|
|
output.resolve("intellij.png").toFile());
|
|
return null;
|
|
});
|
|
try (var document =
|
|
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
|
|
checkNativeFixes(project, file, pane, document);
|
|
edt(
|
|
() -> {
|
|
FileEditorManager.getInstance(project)
|
|
.setSelectedEditor(file, "neitzel-fx-designer");
|
|
return null;
|
|
});
|
|
de.neitzel.fxdesigner.kit.ProjectionHostChecks.run(pane, document);
|
|
checkProblems(project, file, pane, document);
|
|
}
|
|
String version = fx(() -> System.getProperty("javafx.runtime.version"));
|
|
if (!version.startsWith("26"))
|
|
throw new AssertionError("Wrong host JavaFX: " + version);
|
|
Files.writeString(
|
|
output.resolve("SUCCESS.txt"),
|
|
"PASS registered IntelliJ editor, shared Kit, real Build and Refresh, target"
|
|
+ " pixels, native document edits, undo/redo, draft render,"
|
|
+ " source-preserving projection and extended Component scenarios; JavaFX "
|
|
+ version
|
|
+ "\n");
|
|
} catch (Throwable e) {
|
|
try {
|
|
if (active != null)
|
|
fx(
|
|
() -> {
|
|
Files.writeString(
|
|
output.resolve("ui-state.txt"),
|
|
active.lookupAll(".label").stream()
|
|
.filter(n -> n instanceof javafx.scene.control.Label)
|
|
.map(
|
|
n ->
|
|
String.valueOf(
|
|
((javafx.scene.control.Label) n).getText()))
|
|
.toList()
|
|
.toString());
|
|
javax.imageio.ImageIO.write(
|
|
javafx.embed.swing.SwingFXUtils.fromFXImage(
|
|
active.snapshot(null, null), null),
|
|
"png",
|
|
output.resolve("failed.png").toFile());
|
|
return null;
|
|
});
|
|
var w = new java.io.StringWriter();
|
|
e.printStackTrace(new java.io.PrintWriter(w));
|
|
Files.writeString(output.resolve("FAILURE.txt"), w.toString());
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|