Add source highlighting and revision-checked diagnostic fixes in both hosts
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
|
||||
import com.intellij.openapi.components.Service;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.*;
|
||||
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Current published evidence only; annotators never build or execute target application code. */
|
||||
@Service(Service.Level.PROJECT)
|
||||
public final class DiagnosticAnnotations {
|
||||
record Publication(
|
||||
String owner,
|
||||
VirtualFile file,
|
||||
Document document,
|
||||
long stamp,
|
||||
List<Diagnostic> diagnostics,
|
||||
Consumer<QuickFix> apply) {}
|
||||
|
||||
private final Map<String, Publication> owners = new ConcurrentHashMap<>();
|
||||
private final Project project;
|
||||
|
||||
public DiagnosticAnnotations(Project project) {
|
||||
this.project = project;
|
||||
}
|
||||
|
||||
static DiagnosticAnnotations get(Project project) {
|
||||
return project.getService(DiagnosticAnnotations.class);
|
||||
}
|
||||
|
||||
void replace(
|
||||
String owner,
|
||||
VirtualFile file,
|
||||
Document document,
|
||||
List<Diagnostic> diagnostics,
|
||||
Consumer<QuickFix> apply) {
|
||||
clear(owner);
|
||||
owners.put(
|
||||
owner,
|
||||
new Publication(
|
||||
owner,
|
||||
file,
|
||||
document,
|
||||
document.getModificationStamp(),
|
||||
List.copyOf(diagnostics),
|
||||
apply));
|
||||
restart(file);
|
||||
}
|
||||
|
||||
void clear(String owner) {
|
||||
var old = owners.remove(owner);
|
||||
if (old != null) restart(old.file());
|
||||
}
|
||||
|
||||
private void restart(VirtualFile file) {
|
||||
if (project.isDisposed() || !file.isValid()) return;
|
||||
var psi = PsiManager.getInstance(project).findFile(file);
|
||||
if (psi != null) DaemonCodeAnalyzer.getInstance(project).restart(psi);
|
||||
}
|
||||
|
||||
List<Publication> current(PsiFile file) {
|
||||
return owners.values().stream()
|
||||
.filter(p -> p.file().equals(file.getVirtualFile()) && current(p))
|
||||
.toList();
|
||||
}
|
||||
|
||||
boolean current(Publication p) {
|
||||
return owners.get(p.owner()) == p
|
||||
&& p.file().isValid()
|
||||
&& p.document().getModificationStamp() == p.stamp();
|
||||
}
|
||||
|
||||
boolean available(Publication p, QuickFix fix) {
|
||||
return current(p)
|
||||
&& p.document().isWritable()
|
||||
&& p.file().isWritable()
|
||||
&& p.diagnostics().stream().anyMatch(d -> d.fixes().contains(fix));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction;
|
||||
import com.intellij.lang.annotation.*;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.*;
|
||||
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||
|
||||
/** Native XML error highlighting and Alt+Enter actions from shared diagnostics. */
|
||||
public final class DiagnosticAnnotator implements Annotator {
|
||||
@Override
|
||||
public void annotate(PsiElement element, AnnotationHolder holder) {
|
||||
if (!(element instanceof PsiFile file) || !file.getName().endsWith(".fxml")) return;
|
||||
var store = DiagnosticAnnotations.get(file.getProject());
|
||||
for (var publication : store.current(file))
|
||||
for (var d : publication.diagnostics()) {
|
||||
if (d.range().isEmpty() || d.location().isEmpty()) continue;
|
||||
var r = d.range().get();
|
||||
if (r.end() > file.getTextLength()) continue;
|
||||
var severity =
|
||||
switch (d.severity()) {
|
||||
case ERROR -> HighlightSeverity.ERROR;
|
||||
case WARNING -> HighlightSeverity.WARNING;
|
||||
case INFO -> HighlightSeverity.INFORMATION;
|
||||
};
|
||||
var builder =
|
||||
holder.newAnnotation(severity, d.message()).range(new TextRange(r.start(), r.end()));
|
||||
for (var fix : d.fixes()) builder.withFix(new Fix(store, publication, fix));
|
||||
builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
private record Fix(
|
||||
DiagnosticAnnotations store, DiagnosticAnnotations.Publication publication, QuickFix fix)
|
||||
implements IntentionAction {
|
||||
@Override
|
||||
public String getText() {
|
||||
return fix.label();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return "Neitzel FX Designer diagnostic fixes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(Project project, Editor editor, PsiFile file) {
|
||||
return !project.isDisposed()
|
||||
&& file != null
|
||||
&& publication.file().equals(file.getVirtualFile())
|
||||
&& store.available(publication, fix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(Project project, Editor editor, PsiFile file) {
|
||||
if (isAvailable(project, editor, file)) publication.apply().accept(fix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startInWriteAction() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -47,6 +47,16 @@ final class IntellijDiagnostics
|
||||
DocumentSnapshot source,
|
||||
String context,
|
||||
List<HostServices.Diagnostic> diagnostics) {
|
||||
replace(owner, source, context, diagnostics, fix -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(
|
||||
String owner,
|
||||
DocumentSnapshot source,
|
||||
String context,
|
||||
List<HostServices.Diagnostic> diagnostics,
|
||||
java.util.function.Consumer<HostServices.QuickFix> apply) {
|
||||
var captured = List.copyOf(diagnostics);
|
||||
edt(
|
||||
() -> {
|
||||
@@ -69,6 +79,7 @@ final class IntellijDiagnostics
|
||||
ProblemsCollector.getInstance(project).problemAppeared(p);
|
||||
}
|
||||
owned.put(owner, entries);
|
||||
DiagnosticAnnotations.get(project).replace(owner, file, document, captured, apply);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,6 +89,7 @@ final class IntellijDiagnostics
|
||||
}
|
||||
|
||||
private void remove(String owner) {
|
||||
DiagnosticAnnotations.get(project).clear(owner);
|
||||
var old = owned.remove(owner);
|
||||
if (old != null) old.forEach(ProblemsCollector.getInstance(project)::problemDisappeared);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<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>
|
||||
<depends>com.intellij.modules.lang</depends>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<annotator language="XML" implementationClass="de.neitzel.fxdesigner.intellij.DiagnosticAnnotator"/>
|
||||
<fileEditorProvider implementation="de.neitzel.fxdesigner.intellij.DesignerEditorProvider"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.codeInsight.daemon.impl.AnnotationHolderImpl;
|
||||
import com.intellij.codeInsight.intention.IntentionAction;
|
||||
import com.intellij.lang.annotation.AnnotationSession;
|
||||
import com.intellij.openapi.command.WriteCommandAction;
|
||||
import com.intellij.psi.PsiDocumentManager;
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
|
||||
import de.neitzel.fxdesigner.contracts.EditBatch.Replacement;
|
||||
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||
import java.util.*;
|
||||
|
||||
public final class DiagnosticAnnotatorTest extends BasePlatformTestCase {
|
||||
public void testRangesActionsAndStaleOrClosedEvidence() {
|
||||
var file = myFixture.configureByText("View.fxml", "<VBox direction='bad'/>");
|
||||
var document = myFixture.getEditor().getDocument();
|
||||
var store = DiagnosticAnnotations.get(getProject());
|
||||
int at = document.getText().indexOf("bad");
|
||||
var fix = new QuickFix("Set direction", List.of(new Replacement(at, at + 3, "view")));
|
||||
var d =
|
||||
new Diagnostic(
|
||||
"FXML",
|
||||
Severity.ERROR,
|
||||
"Invalid direction",
|
||||
Optional.of(new Location(java.net.URI.create(file.getVirtualFile().getUrl()), at)),
|
||||
Optional.of(new SourceRange(at, at + 3)),
|
||||
List.of(fix));
|
||||
var invoked = new java.util.concurrent.atomic.AtomicInteger();
|
||||
store.replace(
|
||||
"one", file.getVirtualFile(), document, List.of(d), f -> invoked.incrementAndGet());
|
||||
var holder = new AnnotationHolderImpl(new AnnotationSession(file), false);
|
||||
holder.runAnnotatorWithContext(file, new DiagnosticAnnotator());
|
||||
assertEquals(1, holder.size());
|
||||
assertEquals(at, holder.getFirst().getStartOffset());
|
||||
assertEquals(at + 3, holder.getFirst().getEndOffset());
|
||||
IntentionAction action = holder.getFirst().getQuickFixes().getFirst().quickFix;
|
||||
assertTrue(action.isAvailable(getProject(), myFixture.getEditor(), file));
|
||||
action.invoke(getProject(), myFixture.getEditor(), file);
|
||||
assertEquals(1, invoked.get());
|
||||
WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0, " "));
|
||||
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
|
||||
assertFalse(action.isAvailable(getProject(), myFixture.getEditor(), file));
|
||||
action.invoke(getProject(), myFixture.getEditor(), file);
|
||||
assertEquals(1, invoked.get());
|
||||
holder = new AnnotationHolderImpl(new AnnotationSession(file), false);
|
||||
holder.runAnnotatorWithContext(file, new DiagnosticAnnotator());
|
||||
assertTrue(holder.isEmpty());
|
||||
store.clear("one");
|
||||
assertFalse(action.isAvailable(getProject(), myFixture.getEditor(), file));
|
||||
}
|
||||
|
||||
public void testIdenticalOwnersAndReadOnlyActionsAreIndependent() {
|
||||
var file = myFixture.configureByText("Other.fxml", "<VBox/>");
|
||||
var document = myFixture.getEditor().getDocument();
|
||||
var store = DiagnosticAnnotations.get(getProject());
|
||||
var fix = new QuickFix("Remove", List.of(new Replacement(0, 7, "")));
|
||||
var d =
|
||||
new Diagnostic(
|
||||
"FXML",
|
||||
Severity.WARNING,
|
||||
"warning",
|
||||
Optional.of(new Location(java.net.URI.create(file.getVirtualFile().getUrl()), 0)),
|
||||
Optional.of(new SourceRange(0, 7)),
|
||||
List.of(fix));
|
||||
store.replace("one", file.getVirtualFile(), document, List.of(d), f -> {});
|
||||
store.replace("two", file.getVirtualFile(), document, List.of(d), f -> {});
|
||||
store.clear("one");
|
||||
assertEquals(1, store.current(file).size());
|
||||
var p = store.current(file).getFirst();
|
||||
document.setReadOnly(true);
|
||||
assertFalse(store.available(p, fix));
|
||||
document.setReadOnly(false);
|
||||
store.clear("two");
|
||||
assertTrue(store.current(file).isEmpty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user