Publish shared FXML diagnostics and integrate IntelliJ Problems
Navigate authoritative FXML locations, withdraw stale results and preserve host Undo/Redo. Include editor ownership in native problem identity so equal diagnostics from separate owners survive independent cleanup. Qualify both hosts automatically.
This commit is contained in:
@@ -18,6 +18,7 @@ dependencies {
|
||||
intellijPlatform {
|
||||
local(providers.gradleProperty("ideaHome").orElse(providers.environmentVariable("IDEA_HOME")))
|
||||
testFramework(TestFrameworkType.Platform)
|
||||
bundledPlugin("com.intellij.problemsView")
|
||||
}
|
||||
// Maven platform profiles are not selected by Gradle: include the qualified macOS natives explicitly.
|
||||
listOf("base", "controls", "fxml", "graphics", "media", "swing", "web").forEach {
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.analysis.problemsView.*;
|
||||
import com.intellij.openapi.application.*;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.*;
|
||||
import de.neitzel.fxdesigner.contracts.DocumentSnapshot;
|
||||
import de.neitzel.fxdesigner.host.HostServices;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/** Publishes only current FXML evidence to the IDE's existing Problems collector. */
|
||||
final class IntellijDiagnostics
|
||||
implements HostServices.Diagnostics, ProblemsProvider, AutoCloseable {
|
||||
private final Project project;
|
||||
private final HostServices.Diagnostics output;
|
||||
private final Map<String, List<Problem>> owned = new HashMap<>(); // EDT only
|
||||
private volatile boolean closed;
|
||||
|
||||
IntellijDiagnostics(Project project, HostServices.Diagnostics output) {
|
||||
this.project = project;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Project getProject() {
|
||||
return project;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void report(HostServices.Diagnostic d) {
|
||||
output.report(d);
|
||||
}
|
||||
|
||||
private void edt(Runnable action) {
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(
|
||||
() -> {
|
||||
if (!project.isDisposed()) WriteIntentReadAction.run(action);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(
|
||||
String owner,
|
||||
DocumentSnapshot source,
|
||||
String context,
|
||||
List<HostServices.Diagnostic> diagnostics) {
|
||||
var captured = List.copyOf(diagnostics);
|
||||
edt(
|
||||
() -> {
|
||||
remove(owner);
|
||||
if (closed) return;
|
||||
var file =
|
||||
LocalFileSystem.getInstance().refreshAndFindFileByNioFile(Path.of(source.document()));
|
||||
if (file == null) return;
|
||||
var document = FileDocumentManager.getInstance().getDocument(file);
|
||||
if (document == null || !document.getText().equals(source.text())) return;
|
||||
var entries = new ArrayList<Problem>();
|
||||
for (var d : captured) {
|
||||
if (d.location().isEmpty() || !d.location().get().document().equals(source.document()))
|
||||
continue;
|
||||
int offset = Math.min(d.location().get().offset(), document.getTextLength());
|
||||
int line = document.getLineNumber(offset),
|
||||
column = offset - document.getLineStartOffset(line);
|
||||
var p = new Entry(this, owner, file, line, column, d, context);
|
||||
entries.add(p);
|
||||
ProblemsCollector.getInstance(project).problemAppeared(p);
|
||||
}
|
||||
owned.put(owner, entries);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear(String owner) {
|
||||
edt(() -> remove(owner));
|
||||
}
|
||||
|
||||
private void remove(String owner) {
|
||||
var old = owned.remove(owner);
|
||||
if (old != null) old.forEach(ProblemsCollector.getInstance(project)::problemDisappeared);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
edt(() -> new ArrayList<>(owned.keySet()).forEach(this::remove));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
close();
|
||||
}
|
||||
|
||||
private record Entry(
|
||||
ProblemsProvider provider,
|
||||
String owner,
|
||||
VirtualFile file,
|
||||
int line,
|
||||
int column,
|
||||
HostServices.Diagnostic diagnostic,
|
||||
String context)
|
||||
implements FileProblem {
|
||||
@Override
|
||||
public ProblemsProvider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VirtualFile getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLine() {
|
||||
return line;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumn() {
|
||||
return column;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return diagnostic.severity() + ": " + diagnostic.message();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return "Neitzel FX Designer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return diagnostic.operation() + " · " + context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public javax.swing.Icon getIcon() {
|
||||
return switch (diagnostic.severity()) {
|
||||
case ERROR -> com.intellij.icons.AllIcons.General.Error;
|
||||
case WARNING -> com.intellij.icons.AllIcons.General.Warning;
|
||||
case INFO -> com.intellij.icons.AllIcons.General.Information;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public final class IntellijHost implements AutoCloseable {
|
||||
Project project, HostServices.Runtimes runtimes, HostServices.Diagnostics diagnostics) {
|
||||
var docs = scope.own(new IntellijDocuments(project));
|
||||
var builds = new ProcessBuilds();
|
||||
var problems = scope.own(new IntellijDiagnostics(project, diagnostics));
|
||||
services =
|
||||
new HostServices(
|
||||
docs,
|
||||
@@ -62,6 +63,33 @@ public final class IntellijHost implements AutoCloseable {
|
||||
}));
|
||||
}
|
||||
|
||||
public CompletionStage<Void> openSource(HostServices.Location location) {
|
||||
var result = new CompletableFuture<Void>();
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(
|
||||
() ->
|
||||
WriteIntentReadAction.run(
|
||||
() -> {
|
||||
try {
|
||||
if (project.isDisposed())
|
||||
throw new IllegalStateException("Project disposed");
|
||||
var file =
|
||||
LocalFileSystem.getInstance()
|
||||
.refreshAndFindFileByNioFile(
|
||||
Path.of(location.document()));
|
||||
if (file == null)
|
||||
throw new IllegalArgumentException(
|
||||
"Navigation file unavailable");
|
||||
new OpenFileDescriptor(project, file, location.offset())
|
||||
.navigate(true);
|
||||
result.complete(null);
|
||||
} catch (Throwable e) {
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
|
||||
public CompletionStage<Void> open(HostServices.Location location) {
|
||||
var result = new CompletableFuture<Void>();
|
||||
ApplicationManager.getApplication()
|
||||
@@ -107,7 +135,7 @@ public final class IntellijHost implements AutoCloseable {
|
||||
return result;
|
||||
}
|
||||
},
|
||||
diagnostics,
|
||||
problems,
|
||||
scope);
|
||||
Disposer.register(project, this::close);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<id>de.neitzel.fxdesigner</id><name>Neitzel FX Designer</name><vendor>Neitzel</vendor>
|
||||
<description>Shared Kit designer with isolated target previews and IDE-owned FXML documents.</description>
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<depends>com.intellij.problemsView</depends>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<fileEditorProvider implementation="de.neitzel.fxdesigner.intellij.DesignerEditorProvider"/>
|
||||
</extensions>
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.analysis.problemsView.*;
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
|
||||
import de.neitzel.fxdesigner.contracts.DocumentSnapshot;
|
||||
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public final class IntellijDiagnosticsTest extends BasePlatformTestCase {
|
||||
private void until(java.util.function.BooleanSupplier ready) throws Exception {
|
||||
long end = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10);
|
||||
while (!ready.getAsBoolean() && System.nanoTime() < end) {
|
||||
com.intellij.testFramework.PlatformTestUtil.dispatchAllEventsInIdeEventQueue();
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertTrue(ready.getAsBoolean());
|
||||
}
|
||||
|
||||
public void testReplacementIsolationStaleRejectionAndClose() throws Exception {
|
||||
Path path = Files.createTempFile("designer-problems-", ".fxml");
|
||||
Files.writeString(path, "<VBox>\n<Label/>\n</VBox>");
|
||||
com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(
|
||||
getTestRootDisposable(), path.toRealPath().toString());
|
||||
var file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path);
|
||||
var collector = ProblemsCollector.getInstance(getProject());
|
||||
var publisher = new IntellijDiagnostics(getProject(), d -> {});
|
||||
try {
|
||||
var source = new DocumentSnapshot(path.toUri(), 1, Files.readString(path), true, false);
|
||||
var error =
|
||||
new Diagnostic(
|
||||
"Bindings", Severity.ERROR, "test error", Optional.of(new Location(path.toUri(), 7)));
|
||||
publisher.replace("one", source, "current", List.of(error));
|
||||
until(() -> collector.getFileProblemCount(file) == 1);
|
||||
var problem = (FileProblem) collector.getFileProblems(file).iterator().next();
|
||||
assertEquals(1, problem.getLine());
|
||||
assertEquals(0, problem.getColumn());
|
||||
publisher.replace("one", source, "current", List.of(error));
|
||||
until(() -> collector.getFileProblemCount(file) == 1);
|
||||
publisher.replace("two", source, "current", List.of(error));
|
||||
until(() -> collector.getFileProblemCount(file) == 2);
|
||||
publisher.clear("one");
|
||||
until(() -> collector.getFileProblemCount(file) == 1);
|
||||
publisher.replace(
|
||||
"one",
|
||||
new DocumentSnapshot(path.toUri(), 0, "old source", true, false),
|
||||
"stale",
|
||||
List.of(error));
|
||||
com.intellij.testFramework.PlatformTestUtil.dispatchAllEventsInIdeEventQueue();
|
||||
assertEquals(1, collector.getFileProblemCount(file));
|
||||
publisher.close();
|
||||
until(() -> collector.getFileProblemCount(file) == 0);
|
||||
publisher.replace("one", source, "late result", List.of(error));
|
||||
com.intellij.testFramework.PlatformTestUtil.dispatchAllEventsInIdeEventQueue();
|
||||
assertEquals(0, collector.getFileProblemCount(file));
|
||||
} finally {
|
||||
publisher.close();
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user