Add source highlighting and revision-checked diagnostic fixes in both hosts
This commit is contained in:
@@ -6,7 +6,7 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with minimal standalone and
|
|||||||
|
|
||||||
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
The first workflow is runnable in both hosts: Build and Refresh publishes immutable snapshots; authenticated target workers discover examples and render parent/Component FXML drafts into the shared Kit canvas. Source and supported canvas edits use host-owned documents and undo/redo. See [the connected workflow](docs/first-connected-workflow.md) for architecture, launch commands, evidence and limits.
|
||||||
|
|
||||||
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Broader discovery, generation and product packaging remain later work.
|
The repository builds six shared Maven modules, a Maven capture plugin and a separate IntelliJ adapter plugin. Application code runs only in target JVMs. The shared UI now applies [source-preserving edits](docs/source-preserving-projection.md), supports nested/anonymous Components and fx:root bodies, and provides Source/Design/Split modes. Retained [target sessions, target sizing, interactive preview and Component/Row navigation](docs/target-runtime-sessions.md) now extend that workflow. The shared [binding discovery browser](docs/binding-discovery.md) now lists declared Model paths and direction-aware target compatibility. The [DirectBinding inspector](docs/direct-binding-inspector.md) creates, edits and removes declarations through source-preserving host Undo/Redo. Shared [Converter discovery and binding validation](docs/converter-validation.md) now adds capability-based proposals, document diagnostics and preview gating. [Custom ViewModel discovery and Computed-binding validation](docs/viewmodel-computed-validation.md) now follow the explicitly selected Controller or Component in both hosts. The [Computed-binding inspector](docs/computed-binding-inspector.md) now creates, changes and removes Computed declarations with method suggestions and shared source-preserving Undo/Redo. The [Repeat inspector](docs/repeat-binding-inspector.md) adds source-preserving collection declarations with host Undo/Redo and [collection/Component compatibility discovery](docs/repeat-compatibility.md). The shared [Table/Column inspector](docs/table-column-inspector.md) adds source-preserving Table configuration, ordered Column authoring and explicit Row navigation, now with [Table-specific compatibility and declared Row alignment discovery](docs/table-compatibility.md). The shared [reference-safe Structure editor](docs/reference-safe-structure.md) now renames identifiers, copies subtrees with fresh identifiers and deletes associated bindings through source-preserving host Undo/Redo, including destination validation for cross-View paste. The [shared Problems panel and IntelliJ Problems integration](docs/shared-diagnostics.md) now expose current diagnostics with authoritative FXML navigation and editor-scoped cleanup. Shared [source highlighting and diagnostic quick fixes](docs/source-highlighting-fixes.md) now add explicit source-preserving corrections with host Undo/Redo and native IntelliJ Alt+Enter actions. Broader discovery, generation and product packaging remain later work.
|
||||||
|
|
||||||
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
Configuration 1.0 schemas, context selection, local JDK mappings and draft protocol validation are now executable. See [configuration and protocol](docs/configuration-and-protocol.md) for formats and validation rules.
|
||||||
|
|
||||||
|
|||||||
@@ -91,13 +91,46 @@ public record HostServices(
|
|||||||
ERROR
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Half-open UTF-16 range in the authoritative source. */
|
||||||
|
public record SourceRange(int start, int end) {
|
||||||
|
public SourceRange {
|
||||||
|
if (start < 0 || end < start) throw new IllegalArgumentException("Invalid diagnostic range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record QuickFix(
|
||||||
|
String label, List<de.neitzel.fxdesigner.contracts.EditBatch.Replacement> edits) {
|
||||||
|
public QuickFix {
|
||||||
|
if (label == null || label.isBlank()) throw new IllegalArgumentException("Missing fix label");
|
||||||
|
edits = List.copyOf(edits);
|
||||||
|
if (edits.isEmpty()) throw new IllegalArgumentException("Empty fix");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record Diagnostic(
|
public record Diagnostic(
|
||||||
|
String operation,
|
||||||
|
Severity severity,
|
||||||
|
String message,
|
||||||
|
Optional<Location> location,
|
||||||
|
Optional<SourceRange> range,
|
||||||
|
List<QuickFix> fixes) {
|
||||||
|
public Diagnostic(
|
||||||
String operation, Severity severity, String message, Optional<Location> location) {
|
String operation, Severity severity, String message, Optional<Location> location) {
|
||||||
|
this(operation, severity, message, location, Optional.empty(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
public Diagnostic {
|
public Diagnostic {
|
||||||
Objects.requireNonNull(operation);
|
Objects.requireNonNull(operation);
|
||||||
Objects.requireNonNull(severity);
|
Objects.requireNonNull(severity);
|
||||||
Objects.requireNonNull(message);
|
Objects.requireNonNull(message);
|
||||||
Objects.requireNonNull(location);
|
Objects.requireNonNull(location);
|
||||||
|
Objects.requireNonNull(range);
|
||||||
|
fixes = List.copyOf(fixes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +144,16 @@ public record HostServices(
|
|||||||
String context,
|
String context,
|
||||||
List<Diagnostic> diagnostics) {}
|
List<Diagnostic> diagnostics) {}
|
||||||
|
|
||||||
|
/** Fix execution stays with the shared editor's revision-checked host transaction. */
|
||||||
|
default void replace(
|
||||||
|
String owner,
|
||||||
|
de.neitzel.fxdesigner.contracts.DocumentSnapshot source,
|
||||||
|
String context,
|
||||||
|
List<Diagnostic> diagnostics,
|
||||||
|
Consumer<QuickFix> apply) {
|
||||||
|
replace(owner, source, context, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
/** Withdraw this editor's diagnostics without removing another editor's results. */
|
/** Withdraw this editor's diagnostics without removing another editor's results. */
|
||||||
default void clear(String owner) {}
|
default void clear(String owner) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+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,
|
DocumentSnapshot source,
|
||||||
String context,
|
String context,
|
||||||
List<HostServices.Diagnostic> diagnostics) {
|
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);
|
var captured = List.copyOf(diagnostics);
|
||||||
edt(
|
edt(
|
||||||
() -> {
|
() -> {
|
||||||
@@ -69,6 +79,7 @@ final class IntellijDiagnostics
|
|||||||
ProblemsCollector.getInstance(project).problemAppeared(p);
|
ProblemsCollector.getInstance(project).problemAppeared(p);
|
||||||
}
|
}
|
||||||
owned.put(owner, entries);
|
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) {
|
private void remove(String owner) {
|
||||||
|
DiagnosticAnnotations.get(project).clear(owner);
|
||||||
var old = owned.remove(owner);
|
var old = owned.remove(owner);
|
||||||
if (old != null) old.forEach(ProblemsCollector.getInstance(project)::problemDisappeared);
|
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>
|
<description>Shared Kit designer with isolated target previews and IDE-owned FXML documents.</description>
|
||||||
<depends>com.intellij.modules.platform</depends>
|
<depends>com.intellij.modules.platform</depends>
|
||||||
<depends>com.intellij.problemsView</depends>
|
<depends>com.intellij.problemsView</depends>
|
||||||
|
<depends>com.intellij.modules.lang</depends>
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
|
<annotator language="XML" implementationClass="de.neitzel.fxdesigner.intellij.DiagnosticAnnotator"/>
|
||||||
<fileEditorProvider implementation="de.neitzel.fxdesigner.intellij.DesignerEditorProvider"/>
|
<fileEditorProvider implementation="de.neitzel.fxdesigner.intellij.DesignerEditorProvider"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
</idea-plugin>
|
</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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
private final Path file;
|
private final Path file;
|
||||||
private final KitView kit;
|
private final KitView kit;
|
||||||
private final Label status = new Label("Opening document");
|
private final Label status = new Label("Opening document");
|
||||||
private final TextArea source = new TextArea(), output = new TextArea();
|
private final DiagnosticTextArea source = new DiagnosticTextArea();
|
||||||
|
private final TextArea output = new TextArea();
|
||||||
private final ComboBox<DesignerWorkflow.Example> examples = new ComboBox<>();
|
private final ComboBox<DesignerWorkflow.Example> examples = new ComboBox<>();
|
||||||
private final SplitPane design;
|
private final SplitPane design;
|
||||||
private final ContentPanelController content;
|
private final ContentPanelController content;
|
||||||
@@ -124,6 +125,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
private DiagnosticsPane diagnosticsPane;
|
private DiagnosticsPane diagnosticsPane;
|
||||||
private final String diagnosticsOwner = java.util.UUID.randomUUID().toString();
|
private final String diagnosticsOwner = java.util.UUID.randomUUID().toString();
|
||||||
private long diagnosticsRevision = -1;
|
private long diagnosticsRevision = -1;
|
||||||
|
private long diagnosticGeneration;
|
||||||
private SplitPane sourceSplit;
|
private SplitPane sourceSplit;
|
||||||
private Integer pendingSourceOffset;
|
private Integer pendingSourceOffset;
|
||||||
private String pendingReferenceValidation;
|
private String pendingReferenceValidation;
|
||||||
@@ -727,6 +729,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
host.diagnostics().clear(diagnosticsOwner);
|
host.diagnostics().clear(diagnosticsOwner);
|
||||||
if (diagnosticsPane != null) diagnosticsPane.show(java.util.List.of(), message);
|
if (diagnosticsPane != null) diagnosticsPane.show(java.util.List.of(), message);
|
||||||
diagnosticsRevision = -1;
|
diagnosticsRevision = -1;
|
||||||
|
diagnosticGeneration++;
|
||||||
|
source.diagnostics("", java.util.List.of());
|
||||||
validatedSource = null;
|
validatedSource = null;
|
||||||
validatingSource = null;
|
validatingSource = null;
|
||||||
validationPreview = false;
|
validationPreview = false;
|
||||||
@@ -743,8 +747,34 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
var diagnostics = java.util.List.copyOf(all);
|
var diagnostics = java.util.List.copyOf(all);
|
||||||
diagnosticsRevision = draft.revision();
|
diagnosticsRevision = draft.revision();
|
||||||
String label = context + " · draft revision " + draft.revision();
|
String label = context + " · draft revision " + draft.revision();
|
||||||
diagnosticsPane.show(diagnostics, label);
|
var captured = draft;
|
||||||
host.diagnostics().replace(diagnosticsOwner, draft, label, diagnostics);
|
long generation = ++diagnosticGeneration;
|
||||||
|
java.util.function.Consumer<HostServices.QuickFix> apply =
|
||||||
|
fix -> fx(() -> applyDiagnosticFix(captured, generation, diagnostics, fix));
|
||||||
|
diagnosticsPane.show(diagnostics, label, apply);
|
||||||
|
source.diagnostics(captured.text(), diagnostics);
|
||||||
|
host.diagnostics().replace(diagnosticsOwner, captured, label, diagnostics, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDiagnosticFix(
|
||||||
|
DocumentSnapshot captured,
|
||||||
|
long generation,
|
||||||
|
java.util.List<HostServices.Diagnostic> diagnostics,
|
||||||
|
HostServices.QuickFix fix) {
|
||||||
|
if (closed || generation != diagnosticGeneration) return;
|
||||||
|
withDraft(
|
||||||
|
() -> {
|
||||||
|
if (generation != diagnosticGeneration
|
||||||
|
|| draft.revision() != captured.revision()
|
||||||
|
|| !draft.text().equals(captured.text())
|
||||||
|
|| !draft.writable()
|
||||||
|
|| diagnostics.stream().noneMatch(d -> d.fixes().contains(fix))) {
|
||||||
|
status.setText("Diagnostic changed; validate again before applying a fix");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingReferenceValidation = apply(draft.text(), fix.edits());
|
||||||
|
submit(fix.edits(), fix.label());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validationUnavailable(Throwable error) {
|
private void validationUnavailable(Throwable error) {
|
||||||
@@ -1378,6 +1408,8 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
public void close() {
|
public void close() {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
|
diagnosticGeneration++;
|
||||||
|
source.diagnostics("", java.util.List.of());
|
||||||
host.diagnostics().clear(diagnosticsOwner);
|
host.diagnostics().clear(diagnosticsOwner);
|
||||||
try {
|
try {
|
||||||
changes.close();
|
changes.close();
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package de.neitzel.fxdesigner.kit;
|
||||||
|
|
||||||
|
import de.neitzel.fxdesigner.contracts.DocumentSnapshot;
|
||||||
|
import de.neitzel.fxdesigner.contracts.EditBatch.Replacement;
|
||||||
|
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||||
|
import java.util.*;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
|
/** Conservative, lexical fixes; no inference about application semantics. */
|
||||||
|
final class DiagnosticActions {
|
||||||
|
static List<Diagnostic> localBindings(DocumentSnapshot source, BindingDocument xml) {
|
||||||
|
var out = new ArrayList<Diagnostic>();
|
||||||
|
var occupied = new HashSet<String>();
|
||||||
|
for (int i = 0; i < xml.elements.size(); i++) {
|
||||||
|
Element e = xml.elements.get(i);
|
||||||
|
if (!binding(xml, e) || children(e)) continue;
|
||||||
|
if (xml.named(e, "DirectBinding")
|
||||||
|
&& !Set.of("view", "model", "both").contains(e.getAttribute("direction"))) {
|
||||||
|
var tag = xml.source.tags.get(i);
|
||||||
|
var attr = tag.attributes().get("direction");
|
||||||
|
var range =
|
||||||
|
attr == null
|
||||||
|
? new SourceRange(tag.start(), tag.openEnd())
|
||||||
|
: new SourceRange(
|
||||||
|
attr.valueStart() == attr.valueEnd() ? attr.start() : attr.valueStart(),
|
||||||
|
attr.valueStart() == attr.valueEnd() ? attr.end() : attr.valueEnd());
|
||||||
|
var choices = new ArrayList<QuickFix>();
|
||||||
|
if (source.writable())
|
||||||
|
for (String direction : List.of("view", "model", "both"))
|
||||||
|
choices.add(
|
||||||
|
new QuickFix(
|
||||||
|
"Set binding direction to " + direction,
|
||||||
|
xml.source.attributes(i, Map.of("direction", direction))));
|
||||||
|
out.add(
|
||||||
|
new Diagnostic(
|
||||||
|
"FXML",
|
||||||
|
Severity.ERROR,
|
||||||
|
"Invalid or missing direction: "
|
||||||
|
+ e.getAttribute("control")
|
||||||
|
+ "."
|
||||||
|
+ e.getAttribute("property"),
|
||||||
|
Optional.of(new Location(source.document(), range.start())),
|
||||||
|
Optional.of(range),
|
||||||
|
choices));
|
||||||
|
}
|
||||||
|
if (!xml.named(e, "Repeat")) {
|
||||||
|
String target = e.getAttribute("control") + "." + e.getAttribute("property");
|
||||||
|
if (!occupied.add(target))
|
||||||
|
out.add(
|
||||||
|
at(
|
||||||
|
source,
|
||||||
|
xml,
|
||||||
|
i,
|
||||||
|
"FXML",
|
||||||
|
Severity.ERROR,
|
||||||
|
"Duplicate binding target: " + target,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Diagnostic at(
|
||||||
|
DocumentSnapshot source,
|
||||||
|
BindingDocument xml,
|
||||||
|
int index,
|
||||||
|
String operation,
|
||||||
|
Severity severity,
|
||||||
|
String message,
|
||||||
|
boolean allowRemoval) {
|
||||||
|
var tag = xml.source.tags.get(index);
|
||||||
|
var e = xml.elements.get(index);
|
||||||
|
var fixes =
|
||||||
|
source.writable()
|
||||||
|
&& allowRemoval
|
||||||
|
&& severity == Severity.ERROR
|
||||||
|
&& binding(xml, e)
|
||||||
|
&& !children(e)
|
||||||
|
&& !e.hasAttributeNS(BindingDocument.FX, "id")
|
||||||
|
? List.of(
|
||||||
|
new QuickFix(
|
||||||
|
"Remove this binding declaration",
|
||||||
|
List.of(new Replacement(tag.start(), tag.end(), ""))))
|
||||||
|
: List.<QuickFix>of();
|
||||||
|
return new Diagnostic(
|
||||||
|
operation,
|
||||||
|
severity,
|
||||||
|
message,
|
||||||
|
Optional.of(new Location(source.document(), tag.start())),
|
||||||
|
Optional.of(new SourceRange(tag.start(), tag.openEnd())),
|
||||||
|
fixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean binding(BindingDocument xml, Element e) {
|
||||||
|
// Only declarations in the root BindingList are eligible, including its elements wrapper.
|
||||||
|
if (!(xml.named(e, "DirectBinding")
|
||||||
|
|| xml.named(e, "ComputedBinding")
|
||||||
|
|| xml.named(e, "Repeat"))) return false;
|
||||||
|
var parent = e.getParentNode();
|
||||||
|
if (parent instanceof Element p && p.getTagName().equals("elements"))
|
||||||
|
parent = p.getParentNode();
|
||||||
|
return parent instanceof Element list
|
||||||
|
&& xml.named(list, "BindingList")
|
||||||
|
&& list.getParentNode() instanceof Element define
|
||||||
|
&& BindingDocument.FX.equals(define.getNamespaceURI())
|
||||||
|
&& "define".equals(define.getLocalName())
|
||||||
|
&& define.getParentNode() == xml.document.getDocumentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean children(Element e) {
|
||||||
|
for (var n = e.getFirstChild(); n != null; n = n.getNextSibling())
|
||||||
|
if (n instanceof Element) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package de.neitzel.fxdesigner.kit;
|
||||||
|
|
||||||
|
import de.neitzel.fxdesigner.host.HostServices.Diagnostic;
|
||||||
|
import java.util.*;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.control.skin.TextAreaSkin;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Path;
|
||||||
|
|
||||||
|
/** Diagnostic underlines in the existing TextArea; input, selection and undo stay unchanged. */
|
||||||
|
final class DiagnosticTextArea extends TextArea {
|
||||||
|
private String evidence = "";
|
||||||
|
private List<Diagnostic> diagnostics = List.of();
|
||||||
|
|
||||||
|
DiagnosticTextArea() {
|
||||||
|
setId("fxml-source");
|
||||||
|
}
|
||||||
|
|
||||||
|
void diagnostics(String text, List<Diagnostic> values) {
|
||||||
|
evidence = text;
|
||||||
|
diagnostics = List.copyOf(values);
|
||||||
|
if (getSkin() instanceof HighlightSkin skin) skin.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
return new HighlightSkin(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HighlightSkin extends TextAreaSkin {
|
||||||
|
private final DiagnosticTextArea area;
|
||||||
|
private final List<Node> marks = new ArrayList<>();
|
||||||
|
private boolean dirty = true;
|
||||||
|
|
||||||
|
HighlightSkin(DiagnosticTextArea area) {
|
||||||
|
super(area);
|
||||||
|
this.area = area;
|
||||||
|
registerChangeListener(area.textProperty(), o -> refresh());
|
||||||
|
registerChangeListener(area.fontProperty(), o -> refresh());
|
||||||
|
registerChangeListener(area.widthProperty(), o -> refresh());
|
||||||
|
registerChangeListener(area.heightProperty(), o -> refresh());
|
||||||
|
registerChangeListener(area.wrapTextProperty(), o -> refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
dirty = true;
|
||||||
|
area.requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void layoutChildren(double x, double y, double w, double h) {
|
||||||
|
super.layoutChildren(x, y, w, h);
|
||||||
|
// TextAreaSkin only resizes its ScrollPane; lay out its content before reading glyphs.
|
||||||
|
for (var child : getChildren())
|
||||||
|
if (child instanceof javafx.scene.Parent parent) parent.layout();
|
||||||
|
if (!dirty) return;
|
||||||
|
dirty = false;
|
||||||
|
removeHighlight(marks);
|
||||||
|
marks.clear();
|
||||||
|
if (!area.getText().equals(area.evidence)) return;
|
||||||
|
for (var d : area.diagnostics)
|
||||||
|
d.range()
|
||||||
|
.ifPresent(
|
||||||
|
range -> {
|
||||||
|
if (range.end() > area.getLength() || range.start() == range.end()) return;
|
||||||
|
var path = new Path(getUnderlineShape(range.start(), range.end()));
|
||||||
|
path.setManaged(false);
|
||||||
|
path.setMouseTransparent(true);
|
||||||
|
path.setFill(null);
|
||||||
|
path.setStroke(
|
||||||
|
switch (d.severity()) {
|
||||||
|
case ERROR -> Color.FIREBRICK;
|
||||||
|
case WARNING -> Color.DARKORANGE;
|
||||||
|
case INFO -> Color.STEELBLUE;
|
||||||
|
});
|
||||||
|
path.setStrokeWidth(1.5);
|
||||||
|
path.getStyleClass().add("diagnostic-highlight");
|
||||||
|
path.getStyleClass()
|
||||||
|
.add("diagnostic-" + d.severity().name().toLowerCase(java.util.Locale.ROOT));
|
||||||
|
path.setUserData(d);
|
||||||
|
marks.add(path);
|
||||||
|
addHighlight(List.of(path), range.start());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
removeHighlight(marks);
|
||||||
|
marks.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.neitzel.fxdesigner.kit;
|
package de.neitzel.fxdesigner.kit;
|
||||||
|
|
||||||
import de.neitzel.fxdesigner.host.HostServices.Diagnostic;
|
import de.neitzel.fxdesigner.host.HostServices.Diagnostic;
|
||||||
|
import de.neitzel.fxdesigner.host.HostServices.QuickFix;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
@@ -10,6 +11,8 @@ import javafx.scene.layout.VBox;
|
|||||||
/** One shared, navigable diagnostic list for both hosts. */
|
/** One shared, navigable diagnostic list for both hosts. */
|
||||||
final class DiagnosticsPane extends VBox {
|
final class DiagnosticsPane extends VBox {
|
||||||
private final ListView<Diagnostic> problems = new ListView<>();
|
private final ListView<Diagnostic> problems = new ListView<>();
|
||||||
|
private final ComboBox<QuickFix> fixes = new ComboBox<>();
|
||||||
|
private Consumer<QuickFix> apply = f -> {};
|
||||||
private final Label state = new Label("Target validation not run");
|
private final Label state = new Label("Target validation not run");
|
||||||
|
|
||||||
DiagnosticsPane(Runnable validate, Consumer<Diagnostic> navigate) {
|
DiagnosticsPane(Runnable validate, Consumer<Diagnostic> navigate) {
|
||||||
@@ -46,15 +49,40 @@ final class DiagnosticsPane extends VBox {
|
|||||||
e.consume();
|
e.consume();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
fixes.setId("diagnostic-fixes");
|
||||||
|
fixes.setPromptText("Choose a fix");
|
||||||
|
fixes.setPrefWidth(280);
|
||||||
|
problems
|
||||||
|
.getSelectionModel()
|
||||||
|
.selectedItemProperty()
|
||||||
|
.addListener(
|
||||||
|
(o, a, b) -> {
|
||||||
|
fixes.getSelectionModel().clearSelection();
|
||||||
|
fixes.getItems().setAll(b == null ? List.of() : b.fixes());
|
||||||
|
});
|
||||||
|
var fix = new Button("Apply diagnostic fix");
|
||||||
|
fix.disableProperty().bind(fixes.getSelectionModel().selectedItemProperty().isNull());
|
||||||
|
fix.setOnAction(
|
||||||
|
e -> {
|
||||||
|
var selected = fixes.getValue();
|
||||||
|
if (selected != null) apply.accept(selected);
|
||||||
|
});
|
||||||
var check = new Button("Validate View");
|
var check = new Button("Validate View");
|
||||||
check.setOnAction(e -> validate.run());
|
check.setOnAction(e -> validate.run());
|
||||||
var go = new Button("Go to FXML");
|
var go = new Button("Go to FXML");
|
||||||
go.setOnAction(e -> open.run());
|
go.setOnAction(e -> open.run());
|
||||||
go.disableProperty().bind(problems.getSelectionModel().selectedItemProperty().isNull());
|
go.disableProperty().bind(problems.getSelectionModel().selectedItemProperty().isNull());
|
||||||
getChildren().addAll(new javafx.scene.layout.HBox(6, check, go), state, problems);
|
getChildren().addAll(new javafx.scene.layout.HBox(6, check, go, fixes, fix), state, problems);
|
||||||
}
|
}
|
||||||
|
|
||||||
void show(List<Diagnostic> diagnostics, String context) {
|
void show(List<Diagnostic> diagnostics, String context) {
|
||||||
|
show(diagnostics, context, f -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void show(List<Diagnostic> diagnostics, String context, Consumer<QuickFix> action) {
|
||||||
|
apply = action;
|
||||||
|
fixes.getSelectionModel().clearSelection();
|
||||||
|
fixes.getItems().clear();
|
||||||
problems.getItems().setAll(diagnostics);
|
problems.getItems().setAll(diagnostics);
|
||||||
state.setText(context + " · " + diagnostics.size() + " diagnostics");
|
state.setText(context + " · " + diagnostics.size() + " diagnostics");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ final class FxmlDiagnostics {
|
|||||||
static List<Diagnostic> local(DocumentSnapshot source) {
|
static List<Diagnostic> local(DocumentSnapshot source) {
|
||||||
var messages = new LinkedHashSet<String>();
|
var messages = new LinkedHashSet<String>();
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
var located = new ArrayList<Diagnostic>();
|
||||||
try {
|
try {
|
||||||
var xml = new BindingDocument(source.text());
|
var xml = new BindingDocument(source.text());
|
||||||
offset = xml.source.tags.getFirst().start();
|
offset = xml.source.tags.getFirst().start();
|
||||||
messages.addAll(xml.errors());
|
located.addAll(DiagnosticActions.localBindings(source, xml));
|
||||||
|
var detailed =
|
||||||
|
located.stream().map(Diagnostic::message).collect(java.util.stream.Collectors.toSet());
|
||||||
|
xml.errors().stream().filter(m -> !detailed.contains(m)).forEach(messages::add);
|
||||||
messages.addAll(new TableDocument(source.text()).errors());
|
messages.addAll(new TableDocument(source.text()).errors());
|
||||||
messages.addAll(new ReferenceDocument(source.text()).referenceErrors());
|
messages.addAll(new ReferenceDocument(source.text()).referenceErrors());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -24,7 +28,9 @@ final class FxmlDiagnostics {
|
|||||||
messages.add(String.valueOf(e.getMessage()));
|
messages.add(String.valueOf(e.getMessage()));
|
||||||
}
|
}
|
||||||
int origin = offset;
|
int origin = offset;
|
||||||
return messages.stream().map(m -> diagnostic(source, "FXML", "ERROR", m, origin)).toList();
|
located.addAll(
|
||||||
|
messages.stream().map(m -> diagnostic(source, "FXML", "ERROR", m, origin)).toList());
|
||||||
|
return List.copyOf(located);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Diagnostic> target(DocumentSnapshot source, List<Problem> problems) {
|
static List<Diagnostic> target(DocumentSnapshot source, List<Problem> problems) {
|
||||||
@@ -55,8 +61,8 @@ final class FxmlDiagnostics {
|
|||||||
if (matches.isEmpty()) matches.add(0);
|
if (matches.isEmpty()) matches.add(0);
|
||||||
for (int i : matches)
|
for (int i : matches)
|
||||||
result.add(
|
result.add(
|
||||||
diagnostic(
|
DiagnosticActions.at(
|
||||||
source, "Bindings", p.severity(), p.message(), xml.source.tags.get(i).start()));
|
source, xml, i, "Bindings", Severity.valueOf(p.severity()), p.message(), true));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return local(source);
|
return local(source);
|
||||||
@@ -78,7 +84,9 @@ final class FxmlDiagnostics {
|
|||||||
operation,
|
operation,
|
||||||
Severity.valueOf(severity),
|
Severity.valueOf(severity),
|
||||||
message,
|
message,
|
||||||
Optional.of(new Location(source.document(), offset)));
|
Optional.of(new Location(source.document(), offset)),
|
||||||
|
Optional.of(new SourceRange(offset, Math.min(source.text().length(), offset + 1))),
|
||||||
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
static int offset(String text, int line, int column) {
|
static int offset(String text, int line, int column) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package de.neitzel.fxdesigner.kit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import de.neitzel.fxdesigner.contracts.*;
|
||||||
|
import de.neitzel.fxdesigner.host.HostServices.*;
|
||||||
|
import de.neitzel.fxdesigner.protocol.BindingMetadata.Problem;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class DiagnosticActionsTest {
|
||||||
|
private static final String XML =
|
||||||
|
"<?neitzel-model app.Person?>\r\n"
|
||||||
|
+ "<?import de.neitzel.fx.view.*?><VBox"
|
||||||
|
+ " xmlns:fx='http://javafx.com/fxml/1'><fx:define><BindingList><!-- before"
|
||||||
|
+ " --><DirectBinding control='name' property='text' path='name' direction = 'bad'"
|
||||||
|
+ " extra='keep'/><!-- after --></BindingList></fx:define><Label fx:id='name'"
|
||||||
|
+ " text='bad'/></VBox>";
|
||||||
|
|
||||||
|
private DocumentSnapshot snapshot(String text, boolean writable) {
|
||||||
|
return new DocumentSnapshot(URI.create("file:/View.fxml"), 7, text, writable, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Diagnostic direction(DocumentSnapshot d) {
|
||||||
|
return FxmlDiagnostics.local(d).stream()
|
||||||
|
.filter(p -> p.message().startsWith("Invalid or missing direction:"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String apply(DocumentSnapshot d, QuickFix fix) {
|
||||||
|
return new EditBatch(d.revision(), fix.label(), fix.edits()).applyTo(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void directionChoicesAreExplicitAndChangeOnlyTheAttributeValue() {
|
||||||
|
var d = snapshot(XML, true);
|
||||||
|
var problem = direction(d);
|
||||||
|
assertEquals(
|
||||||
|
"bad",
|
||||||
|
XML.substring(problem.range().orElseThrow().start(), problem.range().orElseThrow().end()));
|
||||||
|
assertEquals(3, problem.fixes().size());
|
||||||
|
for (var fix : problem.fixes()) {
|
||||||
|
String value = fix.label().substring(fix.label().lastIndexOf(' ') + 1);
|
||||||
|
assertEquals(XML.replace("direction = 'bad'", "direction = '" + value + "'"), apply(d, fix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingAndEmptyDirectionKeepSourceAndHighlightAnActualRange() {
|
||||||
|
for (String text :
|
||||||
|
List.of(XML.replace(" direction = 'bad'", ""), XML.replace("'bad' extra", "'' extra"))) {
|
||||||
|
var d = snapshot(text, true);
|
||||||
|
var p = direction(d);
|
||||||
|
assertTrue(p.range().orElseThrow().end() > p.range().orElseThrow().start());
|
||||||
|
String fixed = apply(d, p.fixes().getFirst());
|
||||||
|
assertTrue(fixed.contains("<!-- before -->"));
|
||||||
|
assertTrue(fixed.contains("extra='keep'"));
|
||||||
|
assertFalse(
|
||||||
|
FxmlDiagnostics.local(snapshot(fixed, true)).stream()
|
||||||
|
.anyMatch(x -> x.message().startsWith("Invalid or missing direction:")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void duplicateRemovalPreservesFirstDeclarationAndSurroundingComments() {
|
||||||
|
String declaration = "<ComputedBinding control='name' property='text' method='#name'/>";
|
||||||
|
String text =
|
||||||
|
XML.replace("'bad' extra", "'view' extra")
|
||||||
|
.replace("<!-- after -->", "<!-- after -->" + declaration + "<!-- end -->");
|
||||||
|
var d = snapshot(text, true);
|
||||||
|
var p =
|
||||||
|
FxmlDiagnostics.local(d).stream()
|
||||||
|
.filter(x -> x.message().startsWith("Duplicate binding target:"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
assertEquals(text.replace(declaration, ""), apply(d, p.fixes().getFirst()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void targetFixesRequireKnownSimpleDeclarationsAndNeverRemoveControls() {
|
||||||
|
var p = new Problem("name", "text", "ERROR", "unknown path");
|
||||||
|
var d = snapshot(XML, true);
|
||||||
|
assertEquals(1, FxmlDiagnostics.target(d, List.of(p)).getFirst().fixes().size());
|
||||||
|
for (String text :
|
||||||
|
List.of(
|
||||||
|
XML.replace("extra='keep'", "fx:id='binding'"),
|
||||||
|
XML.replace(
|
||||||
|
"extra='keep'/>", "extra='keep'><converter>app.C</converter></DirectBinding>"),
|
||||||
|
XML.replace("DirectBinding", "app.UnknownBinding")))
|
||||||
|
assertTrue(
|
||||||
|
FxmlDiagnostics.target(snapshot(text, true), List.of(p)).getFirst().fixes().isEmpty());
|
||||||
|
assertTrue(
|
||||||
|
FxmlDiagnostics.target(d, List.of(new Problem("name", "text", "WARNING", "unverified")))
|
||||||
|
.getFirst()
|
||||||
|
.fixes()
|
||||||
|
.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readOnlyAndStaleSourceCannotApplyFixes() {
|
||||||
|
assertTrue(direction(snapshot(XML, false)).fixes().isEmpty());
|
||||||
|
var d = snapshot(XML, true);
|
||||||
|
var fix = direction(d).fixes().getFirst();
|
||||||
|
var edit = new EditBatch(d.revision(), fix.label(), fix.edits());
|
||||||
|
assertThrows(
|
||||||
|
ConcurrentModificationException.class,
|
||||||
|
() -> edit.applyTo(new DocumentSnapshot(d.document(), 8, XML, true, true)));
|
||||||
|
assertThrows(IllegalStateException.class, () -> edit.applyTo(snapshot(XML, false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -760,6 +760,137 @@ public final class ProjectionHostChecks {
|
|||||||
+ " withdrawal, correction, exact undo/redo and malformed FXML recovery");
|
+ " withdrawal, correction, exact undo/redo and malformed FXML recovery");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void diagnosticFixes(DesignerPane pane, DocumentSession document, String full)
|
||||||
|
throws Exception {
|
||||||
|
String bad = full.replaceFirst("direction=\"both\"", "direction = 'bad'");
|
||||||
|
set(document, bad);
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(bad)));
|
||||||
|
bindingTab(pane, "Problems");
|
||||||
|
var issue =
|
||||||
|
diagnostics(pane).stream()
|
||||||
|
.filter(d -> d.message().startsWith("Invalid or missing direction:"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
((ListView<de.neitzel.fxdesigner.host.HostServices.Diagnostic>)
|
||||||
|
pane.lookup("#diagnostics-list"))
|
||||||
|
.getSelectionModel()
|
||||||
|
.select(issue);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
click(pane, "Go to FXML");
|
||||||
|
await(() -> fx(() -> !pane.lookupAll(".diagnostic-highlight").isEmpty()));
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
var area = source(pane);
|
||||||
|
int caret = area.getCaretPosition();
|
||||||
|
area.setWrapText(true);
|
||||||
|
area.setScrollTop(100);
|
||||||
|
area.setScrollLeft(20);
|
||||||
|
pane.applyCss();
|
||||||
|
pane.layout();
|
||||||
|
check(area.getCaretPosition() == caret, "Highlighting changed caret");
|
||||||
|
var combo =
|
||||||
|
(ComboBox<de.neitzel.fxdesigner.host.HostServices.QuickFix>)
|
||||||
|
pane.lookup("#diagnostic-fixes");
|
||||||
|
combo
|
||||||
|
.getSelectionModel()
|
||||||
|
.select(
|
||||||
|
combo.getItems().stream()
|
||||||
|
.filter(f -> f.label().equals("Set binding direction to both"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
pane.applyCss();
|
||||||
|
pane.layout();
|
||||||
|
var area = source(pane);
|
||||||
|
var text =
|
||||||
|
area.lookupAll(".text").stream()
|
||||||
|
.filter(
|
||||||
|
n ->
|
||||||
|
n instanceof javafx.scene.text.Text t
|
||||||
|
&& t.getText().equals(area.getText()))
|
||||||
|
.map(n -> (javafx.scene.text.Text) n)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
for (var mark : area.lookupAll(".diagnostic-highlight")) {
|
||||||
|
var d = (de.neitzel.fxdesigner.host.HostServices.Diagnostic) mark.getUserData();
|
||||||
|
var range = d.range().orElseThrow();
|
||||||
|
var shape =
|
||||||
|
new javafx.scene.shape.Path(text.underlineShape(range.start(), range.end()));
|
||||||
|
shape.setStrokeWidth(1.5);
|
||||||
|
var expected = text.localToScene(shape.getBoundsInLocal());
|
||||||
|
var actual = mark.localToScene(mark.getBoundsInLocal());
|
||||||
|
check(
|
||||||
|
Math.abs(expected.getMinX() - actual.getMinX()) < .5
|
||||||
|
&& Math.abs(expected.getMinY() - actual.getMinY()) < .5
|
||||||
|
&& Math.abs(expected.getMaxX() - actual.getMaxX()) < .5
|
||||||
|
&& Math.abs(expected.getMaxY() - actual.getMaxY()) < .5,
|
||||||
|
"Underline is not aligned with wrapped source glyphs: "
|
||||||
|
+ expected
|
||||||
|
+ " / "
|
||||||
|
+ actual);
|
||||||
|
}
|
||||||
|
var image = java.nio.file.Files.createTempFile("designer-source-highlights-", ".png");
|
||||||
|
javax.imageio.ImageIO.write(
|
||||||
|
javafx.embed.swing.SwingFXUtils.fromFXImage(pane.snapshot(null, null), null),
|
||||||
|
"png",
|
||||||
|
image.toFile());
|
||||||
|
System.out.println("Diagnostic source image: " + image);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
click(pane, "Apply diagnostic fix");
|
||||||
|
String fixed = bad.replace("direction = 'bad'", "direction = 'both'");
|
||||||
|
await(() -> documentText(document).equals(fixed));
|
||||||
|
repeatHistory(pane, document, bad, fixed);
|
||||||
|
await(
|
||||||
|
() ->
|
||||||
|
diagnostics(pane).stream()
|
||||||
|
.noneMatch(d -> d.message().startsWith("Invalid or missing direction:")));
|
||||||
|
String duplicate =
|
||||||
|
"<de.neitzel.fx.view.DirectBinding control=\"rankInput\" property=\"text\" path=\"rank\""
|
||||||
|
+ " direction=\"view\"/>";
|
||||||
|
String duplicated =
|
||||||
|
full.replace("</BindingList>", "<!--keep-->" + duplicate + "</BindingList>");
|
||||||
|
set(document, duplicated);
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(duplicated)));
|
||||||
|
bindingTab(pane, "Problems");
|
||||||
|
var conflict =
|
||||||
|
diagnostics(pane).stream()
|
||||||
|
.filter(d -> d.message().startsWith("Duplicate binding target:"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
((ListView<de.neitzel.fxdesigner.host.HostServices.Diagnostic>)
|
||||||
|
pane.lookup("#diagnostics-list"))
|
||||||
|
.getSelectionModel()
|
||||||
|
.select(conflict);
|
||||||
|
((ComboBox<?>) pane.lookup("#diagnostic-fixes")).getSelectionModel().selectFirst();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
click(pane, "Apply diagnostic fix");
|
||||||
|
String removed = duplicated.replace(duplicate, "");
|
||||||
|
await(() -> documentText(document).equals(removed));
|
||||||
|
repeatHistory(pane, document, duplicated, removed);
|
||||||
|
set(document, full);
|
||||||
|
await(() -> fx(() -> source(pane).getText().equals(full)));
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
source(pane).setWrapText(false);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
click(pane, "Split");
|
||||||
|
System.out.println(
|
||||||
|
"PASS diagnostic fixes: source underlines, wrap/scroll without caret edits, explicit"
|
||||||
|
+ " direction and duplicate removal, lexical preservation and single-step Undo/Redo");
|
||||||
|
}
|
||||||
|
|
||||||
private static void structureEdits(DesignerPane pane, DocumentSession document, String full)
|
private static void structureEdits(DesignerPane pane, DocumentSession document, String full)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String base =
|
String base =
|
||||||
@@ -1778,6 +1909,7 @@ public final class ProjectionHostChecks {
|
|||||||
tableEdits(pane, document, full);
|
tableEdits(pane, document, full);
|
||||||
structureEdits(pane, document, full);
|
structureEdits(pane, document, full);
|
||||||
diagnosticsEdits(pane, document, full);
|
diagnosticsEdits(pane, document, full);
|
||||||
|
diagnosticFixes(pane, document, full);
|
||||||
document
|
document
|
||||||
.save(document.snapshot().toCompletableFuture().get().revision())
|
.save(document.snapshot().toCompletableFuture().get().revision())
|
||||||
.toCompletableFuture()
|
.toCompletableFuture()
|
||||||
|
|||||||
@@ -141,3 +141,7 @@ The [shared Structure editor](reference-safe-structure.md) adds source-preservin
|
|||||||
## Shared diagnostics and IntelliJ Problems
|
## Shared diagnostics and IntelliJ Problems
|
||||||
|
|
||||||
The [shared diagnostics slice](shared-diagnostics.md) adds a View-wide Problems panel, source locations, stale-result withdrawal and native IntelliJ Problems publication/navigation. Validation stays shared and target-isolated; both hosts retain ordinary document Undo/Redo and invalid-draft saving. Generated metadata, richer Java/Row origins, source highlighting and quick fixes remain open.
|
The [shared diagnostics slice](shared-diagnostics.md) adds a View-wide Problems panel, source locations, stale-result withdrawal and native IntelliJ Problems publication/navigation. Validation stays shared and target-isolated; both hosts retain ordinary document Undo/Redo and invalid-draft saving. Generated metadata, richer Java/Row origins, source highlighting and quick fixes remain open.
|
||||||
|
|
||||||
|
## Source highlighting and diagnostic quick fixes
|
||||||
|
|
||||||
|
The [shared highlighting/fixes slice](source-highlighting-fixes.md) adds current source underlines, explicit direction corrections and conservative declaration removal. Both hosts apply lexical edits through their authoritative Undo/Redo transaction. IntelliJ adds native XML annotations and Alt+Enter actions, with revision and publication guards. Generated metadata, richer Java/Column/Row origins, further semantic fixes and project-wide diagnostics remain open.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Both hosts now provide **Bindings → Problems**. The shared list combines immediate FXML/structural checks with target-side Direct, Computed, Converter, Repeat and Table validation. **Validate View** validates the current draft against the selected successful build. Results include severity and a build/draft qualifier. **Go to FXML**, double-click or Enter opens the authoritative source at the affected declaration without editing the document or adding an undo command.
|
Both hosts now provide **Bindings → Problems**. The shared list combines immediate FXML/structural checks with target-side Direct, Computed, Converter, Repeat and Table validation. **Validate View** validates the current draft against the selected successful build. Results include severity and a build/draft qualifier. **Go to FXML**, double-click or Enter opens the authoritative source at the affected declaration without editing the document or adding an undo command.
|
||||||
|
|
||||||
Target diagnostics match control/property pairs to binding declarations, Repeat panes to Repeat declarations and Table identities to Tables. Duplicate matching bindings expose each declaration. Unknown/aggregate origins fall back to the document root; malformed XML uses the parser's line/column. This does not guess a Java origin or a particular Column/Row field when metadata supplies only a Table-level location. Local structural checks currently expose document-level locations except XML syntax errors. No protocol change or application execution in the hosts is needed.
|
Target diagnostics match control/property pairs to binding declarations, Repeat panes to Repeat declarations and Table identities to Tables. Duplicate matching bindings expose each declaration. Unknown/aggregate origins fall back to the document root; malformed XML uses the parser's line/column. This does not guess a Java origin or a particular Column/Row field when metadata supplies only a Table-level location. Direction and duplicate-target checks now expose precise ranges through the [source highlighting and fixes slice](source-highlighting-fixes.md); other structural checks retain document-level origins except XML syntax errors. No protocol change or application execution in the hosts is needed.
|
||||||
|
|
||||||
## Freshness and lifetime
|
## Freshness and lifetime
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Existing request tickets, source revisions and captured related-draft checks rej
|
|||||||
|
|
||||||
## IntelliJ adapter
|
## IntelliJ adapter
|
||||||
|
|
||||||
The IntelliJ adapter publishes the same locations and messages through the native `ProblemsCollector` as `FileProblem` entries grouped under **Neitzel FX Designer**. Severity is exposed in the text and icon. The collector feeds the Problems tool window's Project Errors view; this slice does not install a PSI annotator or source squiggles in Current File. Native file/line/column navigation opens the FXML text editor. The shared panel still uses the shared Source mode in either host.
|
The IntelliJ adapter publishes the same locations and messages through the native `ProblemsCollector` as `FileProblem` entries grouped under **Neitzel FX Designer**. Severity is exposed in the text and icon. The collector feeds the Problems tool window's Project Errors view; the subsequent [highlighting slice](source-highlighting-fixes.md) also installs a native XML annotator and explicit quick fixes. Native file/line/column navigation opens the FXML text editor. The shared panel still uses the shared Source mode in either host.
|
||||||
|
|
||||||
The adapter checks the current IDE Document text before publishing queued results, removes superseded entries rather than appending duplicates, isolates editor ownership and withdraws entries on close/project disposal. IDE operations use the EDT with read access; target validation remains outside the IDE process. Host navigation now distinguishes source navigation from Component designer navigation, and standalone source navigation honors offsets in existing or newly opened editors.
|
The adapter checks the current IDE Document text before publishing queued results, removes superseded entries rather than appending duplicates, isolates editor ownership and withdraws entries on close/project disposal. IDE operations use the EDT with read access; target validation remains outside the IDE process. Host navigation now distinguishes source navigation from Component designer navigation, and standalone source navigation honors offsets in existing or newly opened editors.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ The pinned IntelliJ build is **263.7307.200**. Its bundled `com.intellij.problem
|
|||||||
|
|
||||||
Unit tests cover FXML origins, nested bindings, duplicate matching declarations, severity preservation, root fallbacks and malformed XML. The IDE adapter regression checks replacement, independent owners, source mismatch rejection and close/late-result cleanup through the real collector. The shared live scenario checks validation, exact source navigation, stale withdrawal, correction with Undo/Redo and invalid-source recovery in both native standalone and registered IntelliJ launch modes. The registered companion additionally checks native Problems publication, source navigation using collector file/line/column, Undo/Redo invalidation and registered-editor close cleanup.
|
Unit tests cover FXML origins, nested bindings, duplicate matching declarations, severity preservation, root fallbacks and malformed XML. The IDE adapter regression checks replacement, independent owners, source mismatch rejection and close/late-result cleanup through the real collector. The shared live scenario checks validation, exact source navigation, stale withdrawal, correction with Undo/Redo and invalid-source recovery in both native standalone and registered IntelliJ launch modes. The registered companion additionally checks native Problems publication, source navigation using collector file/line/column, Undo/Redo invalidation and registered-editor close cleanup.
|
||||||
|
|
||||||
Run the bootstrap and registered IDE checks in the [README](../README.md). No manual tests are required for this slice. Previously skipped manual checks remain skipped. Generated metadata, precise Java/Row source origins, source highlighting, quick fixes and broader project-wide diagnostics remain subsequent work.
|
Run the bootstrap and registered IDE checks in the [README](../README.md). No manual tests are required for this slice. Previously skipped manual checks remain skipped. Source highlighting and bounded quick fixes are covered by the subsequent slice. Generated metadata, precise Java/Row source origins and broader project-wide diagnostics remain subsequent work.
|
||||||
|
|
||||||
### Qualification — 2026-11-13
|
### Qualification — 2026-11-13
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Source highlighting and diagnostic fixes
|
||||||
|
|
||||||
|
Both hosts underline current diagnostics in the shared Source editor. **Bindings → Problems** offers a list of explicit fixes for the selected diagnostic and an **Apply diagnostic fix** button. No fix is selected automatically. IntelliJ also publishes native XML annotations with the same ranges and exposes the same actions through **Alt+Enter** in its text editor. Existing Problems navigation remains available.
|
||||||
|
|
||||||
|
## Supported actions
|
||||||
|
|
||||||
|
- An invalid or missing DirectBinding direction offers `view`, `model` and `both` as separate choices. Only the selected attribute value changes; a missing attribute is inserted.
|
||||||
|
- A duplicate Direct/Computed target identifies the later declaration and offers removal of that declaration.
|
||||||
|
- Target validation errors on simple root BindingList DirectBinding, ComputedBinding and Repeat declarations offer explicit removal. Warnings, unknown declarations, nested element payloads, declarations with `fx:id` and Tables do not offer removal.
|
||||||
|
|
||||||
|
These actions do not infer Model paths, Controller methods, Converter choices or Column/Row repairs. Removal deletes the chosen declaration; surrounding comments, unrelated declarations, whitespace and quote style remain unchanged. Direction errors highlight the value (or opening tag if missing); other located binding errors highlight the declaration opening tag. Aggregate errors retain their document fallback origin.
|
||||||
|
|
||||||
|
## Transactions and freshness
|
||||||
|
|
||||||
|
A fix carries lexical replacement ranges tied to the published document snapshot. The shared editor checks the captured revision, exact source, diagnostic generation, offered action and writable state before submitting the existing host edit transaction. Each fix is one normal Undo/Redo command in standalone and IntelliJ. Successful fixes request fresh validation. Edits, related-draft changes, build changes and editor closure invalidate prior evidence and actions.
|
||||||
|
|
||||||
|
The IntelliJ annotator reads only current published evidence, never application classes. Its intentions additionally check the publication identity, file, document modification stamp and writable state. The shared callback remains authoritative if an asynchronous action becomes obsolete. The annotator does not write PSI or open its own nested undo command.
|
||||||
|
|
||||||
|
The shared editor uses the protected highlight APIs of [JavaFX TextAreaSkin](https://openjfx.io/javadoc/26/javafx.controls/javafx/scene/control/skin/TextAreaSkin.html). Underlines live in the existing scrolling content and do not intercept mouse input or alter selection. Text, font, wrapping and size changes refresh their geometry; source text that no longer matches the evidence has no stale underlines. Native integration follows IntelliJ's [annotator and quick-fix model](https://plugins.jetbrains.com/docs/intellij/syntax-highlighting-and-error-highlighting.html), qualified against the pinned SDK rather than all IDE versions.
|
||||||
|
|
||||||
|
## Automated acceptance
|
||||||
|
|
||||||
|
Unit checks cover exact lexical changes, missing/empty direction values, duplicate removal, restricted removal eligibility, read-only/stale edits, native range/action availability and independent editor ownership. The shared native-host scenario checks rendered underlines with exact glyph alignment, wrapping/scrolling without caret changes, direction and duplicate fixes, exact source and single-step Undo/Redo. The registered IntelliJ companion additionally checks actual daemon annotations, native intention invocation, exact source ranges, stale-action rejection and highlight withdrawal.
|
||||||
|
|
||||||
|
Run the bootstrap and registered IDE checks documented in the [README](../README.md). No manual testing is required. Previously skipped manual gates remain skipped. Project-wide scanning, generated metadata, precise Java/Column/Row origins and additional semantic fixes remain future work.
|
||||||
|
|
||||||
|
### Qualification — 2026-11-13
|
||||||
|
|
||||||
|
The full bootstrap passed with 138 unit/adapter tests (128 Maven, ten IntelliJ), 36 target processes with 512 fixture checks, 48 lifecycle cycles, connected adapters and packaging/documentation checks. Screenshot review exposed underline geometry being captured before wrapped text layout. The skin now lays out the scrolling content before querying glyph geometry; an exact geometry assertion covers this regression. The final shared build, plugin packaging and connected-host checks passed again after that correction.
|
||||||
|
|
||||||
|
The registered native test invokes Undo/Redo through the selected IntelliJ text editor, while the shared checks exercise host document history. Its initial synthetic-editor Undo assertion stalled after the native action had succeeded; using the actual selected editor corrected that test boundary. Other attempts encountered the already recorded initial-preview timeout and a Table-save external-change conflict before the new native check. The startup/persistence reliability follow-ups remain open; source and history assertions were not weakened.
|
||||||
|
|
||||||
|
The final registered IntelliJ classpath and module-path runs passed native daemon ranges, Alt+Enter corrections, native Undo/Redo, stale-action rejection, highlight withdrawal, the shared regression suite, Problems cleanup, exclusive JavaFX 26 natives and worker shutdown. No manual testing was used. Final registered evidence: `/tmp/fixes-registered-ide4.log`.
|
||||||
|
|
||||||
|
Local evidence: `/tmp/fixes-bootstrap.log`, `/tmp/fixes-layout-build.log`, `/tmp/fixes-layout-plugin.log`, `/tmp/fixes-connected-final.log`, `target/connected-workflow/` and `target/ide-workflow/`. Earlier registered attempts are retained in `/tmp/fixes-registered-ide.log`, `/tmp/fixes-registered-ide2.log` and `/tmp/fixes-registered-ide3.log`. Generated evidence is not a repository input.
|
||||||
@@ -82,6 +82,175 @@ public final class WorkflowProbe implements StartupActivity.DumbAware {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
private static void checkProblems(
|
||||||
Project project,
|
Project project,
|
||||||
VirtualFile file,
|
VirtualFile file,
|
||||||
@@ -386,6 +555,13 @@ public final class WorkflowProbe implements StartupActivity.DumbAware {
|
|||||||
});
|
});
|
||||||
try (var document =
|
try (var document =
|
||||||
pane.kit().host().documents().open(path.toUri()).toCompletableFuture().get()) {
|
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);
|
de.neitzel.fxdesigner.kit.ProjectionHostChecks.run(pane, document);
|
||||||
checkProblems(project, file, pane, document);
|
checkProblems(project, file, pane, document);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user