This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts-intellij-plugin.git


The following commit(s) were added to refs/heads/main by this push:
     new cee696e  fix(diagram): restore blank Diagram tab on IntelliJ 2026.2 
(#118)
cee696e is described below

commit cee696e56eda9c38e979b55f28fad3374634c66a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Jul 25 16:56:57 2026 +0200

    fix(diagram): restore blank Diagram tab on IntelliJ 2026.2 (#118)
    
    * docs: add design for blank Diagram tab on 262
    
    Capture the agreed Approach 1 fix: harden PerspectiveFileEditor lifecycle
    and placeholder sizing; keep Diagrams API migration as future work.
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: add implementation plan for blank Diagram tab on 262
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): size EMPTY/UNAVAILABLE panels so placeholders are visible
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): always apply built model and call super on tab select
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: changelog and #117 link for Diagram blank-tab fix
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): address final blank-tab review findings
    
    Add lifecycle regression coverage and coalesce duplicate model builds while 
clarifying the defensive sizing behavior.
    
    Co-authored-by: Cursor <[email protected]>
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 CHANGELOG.md                                       |   4 +
 .../plans/2026-07-25-diagram-blank-262.md          | 396 +++++++++++++++++++++
 .../specs/2026-07-25-diagram-blank-262-design.md   | 129 +++++++
 .../fileEditor/Struts2DiagramFileEditor.java       |  11 +-
 .../diagram/ui/Struts2DiagramComponent.java        |   5 +
 .../Struts2DiagramFileEditorProviderTest.java      |  49 +++
 .../diagram/StrutsConfigDiagramModelTest.java      |  42 +++
 7 files changed, 631 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2818269..d80a47e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@
 - Dependencies - upgrade Qodana linter image to 
`jetbrains/qodana-jvm:2026.2-eap`
 - Dependencies - declare IntelliJ Platform 2026.2 v2 content modules 
`intellij.javascript.parser`, `intellij.javascript.backend`, 
`intellij.javaee.jsp.shared`, `intellij.css`, and `intellij.css.backend` 
([#103](https://github.com/apache/struts-intellij-plugin/issues/103))
 
+### Fixed
+
+- Fix blank Diagram tab on IntelliJ 2026.2 by invoking the 
`PerspectiveFileEditor` select/deselect hooks so the custom component is 
installed; placeholder sizing remains as a defensive fallback
+
 ## [261.19027.1] - 2026-07-13
 
 ### Added
diff --git a/docs/superpowers/plans/2026-07-25-diagram-blank-262.md 
b/docs/superpowers/plans/2026-07-25-diagram-blank-262.md
new file mode 100644
index 0000000..4ecf67f
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-25-diagram-blank-262.md
@@ -0,0 +1,396 @@
+# Fix Blank Diagram Tab on 262 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use 
superpowers:subagent-driven-development (recommended) or 
superpowers:executing-plans to implement this plan task-by-task. Steps use 
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Restore the struts.xml Diagram tab on IntelliJ 2026.2 so it shows 
packages/actions/results (or a visible placeholder), not a blank editor pane.
+
+**Architecture:** Keep the toolkit-neutral `StrutsConfigDiagramModel` and 
Swing `Struts2DiagramComponent`. Harden `Struts2DiagramFileEditor` so model 
apply is not gated on `myDiagramSelected`, call `super` on select/deselect, and 
size the component so EMPTY/UNAVAILABLE fill the editor area. DomEvent 
live-refresh stays selection-gated.
+
+**Tech Stack:** IntelliJ Platform (`PerspectiveFileEditor`, 
`ReadAction.nonBlocking`, `DomEventListener`), Swing, JUnit 4 light tests 
(`BasicLightHighlightingTestCase`).
+
+**Spec:** `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md`
+
+## Global Constraints
+
+- Target platform remains IntelliJ IDEA **2026.2** / build **262** only 
(`pluginSinceBuild=262`, `pluginUntilBuild=262.*`).
+- Do **not** migrate to `com.intellij.diagram.Provider` (tracked in 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117)).
+- Do not change `StrutsConfigDiagramModel` build semantics or 
`StrutsDiagramPresentation`.
+- Prefer editing existing diagram files; no new modules or extension points.
+- Tests: `./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"`.
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|---|---|---|
+| `src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java` 
| Modify | Preferred/minimum size for EMPTY/UNAVAILABLE so placeholders fill 
the editor |
+| 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
 | Modify | Always apply model on UI thread; call `super` in select/deselect; 
keep DomEvent selection gate |
+| 
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java` 
| Modify | Assert placeholder preferred/minimum size after `rebuild(null)` / 
empty model |
+| 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
 | Modify | Assert editor reaches `LOADED` after create + async pump without 
requiring DomEvent selection |
+| `CHANGELOG.md` | Modify | Unreleased Fixed entry |
+| `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md` | Modify | 
Link [#117](https://github.com/apache/struts-intellij-plugin/issues/117) under 
Future work; remove stray trailing `)` if still present |
+
+No other production files change.
+
+---
+
+### Task 1: Placeholder sizing on `Struts2DiagramComponent`
+
+**Files:**
+- Modify: 
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java`
+- Modify: 
`src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java`
+- Test: 
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java`
+
+**Interfaces:**
+- Consumes: `Struts2DiagramComponent.rebuild(@Nullable 
StrutsConfigDiagramModel)`, `getState()`, `getPreferredSize()`, 
`getMinimumSize()`
+- Produces: EMPTY/UNAVAILABLE states set preferred and minimum size to at 
least `400×300` (logical pixels via `JBUI.size` if already used elsewhere in 
the file; otherwise plain `Dimension`)
+
+- [ ] **Step 1: Write the failing size assertions**
+
+In `StrutsConfigDiagramModelTest.java`, add:
+
+```java
+public void testPlaceholderStatesHaveNonZeroPreferredSize() {
+    Struts2DiagramComponent unavailable = new Struts2DiagramComponent(null);
+    assertEquals(Struts2DiagramComponent.State.UNAVAILABLE, 
unavailable.getState());
+    assertTrue("UNAVAILABLE preferred width must fill a normal editor area, 
got "
+                    + unavailable.getPreferredSize(),
+            unavailable.getPreferredSize().width >= 400);
+    assertTrue("UNAVAILABLE preferred height must fill a normal editor area, 
got "
+                    + unavailable.getPreferredSize(),
+            unavailable.getPreferredSize().height >= 300);
+    assertTrue("UNAVAILABLE minimum width must be non-trivial, got "
+                    + unavailable.getMinimumSize(),
+            unavailable.getMinimumSize().width >= 400);
+    assertTrue("UNAVAILABLE minimum height must be non-trivial, got "
+                    + unavailable.getMinimumSize(),
+            unavailable.getMinimumSize().height >= 300);
+
+    createStrutsFileSet("struts-empty.xml");
+    VirtualFile vf = myFixture.findFileInTempDir("struts-empty.xml");
+    assertNotNull(vf);
+    PsiFile psi = PsiManager.getInstance(getProject()).findFile(vf);
+    assertInstanceOf(psi, XmlFile.class);
+    StrutsConfigDiagramModel emptyModel = ReadAction.nonBlocking(
+            () -> StrutsConfigDiagramModel.build((XmlFile) 
psi)).executeSynchronously();
+    assertNotNull(emptyModel);
+
+    Struts2DiagramComponent empty = new Struts2DiagramComponent(emptyModel);
+    assertEquals(Struts2DiagramComponent.State.EMPTY, empty.getState());
+    assertTrue("EMPTY preferred width must fill a normal editor area, got "
+                    + empty.getPreferredSize(),
+            empty.getPreferredSize().width >= 400);
+    assertTrue("EMPTY preferred height must fill a normal editor area, got "
+                    + empty.getPreferredSize(),
+            empty.getPreferredSize().height >= 300);
+
+    empty.rebuild(null);
+    assertEquals(Struts2DiagramComponent.State.UNAVAILABLE, empty.getState());
+    assertTrue("rebuild(null) must restore non-zero preferred size, got "
+                    + empty.getPreferredSize(),
+            empty.getPreferredSize().width >= 400
+                    && empty.getPreferredSize().height >= 300);
+}
+```
+
+Add imports only if missing (`VirtualFile`, `PsiFile`, `PsiManager`, 
`XmlFile`, `ReadAction`, `StrutsConfigDiagramModel` are already used in this 
class).
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsConfigDiagramModelTest.testPlaceholderStatesHaveNonZeroPreferredSize"
+```
+
+Expected: FAIL — preferred/minimum size width or height below 400/300 (default 
`JPanel` size).
+
+- [ ] **Step 3: Implement placeholder sizing**
+
+In `Struts2DiagramComponent.java`, add a constant near the other layout 
constants:
+
+```java
+private static final Dimension PLACEHOLDER_SIZE = new Dimension(400, 300);
+```
+
+Update `applyModel` so null/empty paths size the panel, and LOADED keeps using 
`layoutModel` (which already calls `setPreferredSize`):
+
+```java
+private void applyModel(@Nullable StrutsConfigDiagramModel model) {
+    if (model == null) {
+        state = State.UNAVAILABLE;
+        setPreferredSize(PLACEHOLDER_SIZE);
+        setMinimumSize(PLACEHOLDER_SIZE);
+        return;
+    }
+    if (model.getNodes().isEmpty()) {
+        state = State.EMPTY;
+        setPreferredSize(PLACEHOLDER_SIZE);
+        setMinimumSize(PLACEHOLDER_SIZE);
+        return;
+    }
+    state = State.LOADED;
+    setMinimumSize(null);
+    layoutModel(model);
+}
+```
+
+Do not change paint/placeholder message strings.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsConfigDiagramModelTest.testPlaceholderStatesHaveNonZeroPreferredSize"
+```
+
+Expected: BUILD SUCCESSFUL / test PASS.
+
+Also run existing component state tests:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsConfigDiagramModelTest.testComponentState*"
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add 
src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java \
+        
src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java
+git commit -m "$(cat <<'EOF'
+fix(diagram): size EMPTY/UNAVAILABLE panels so placeholders are visible
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Ungate model apply and harden editor lifecycle
+
+**Files:**
+- Modify: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
+- Modify: 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
+- Test: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
+
+**Interfaces:**
+- Consumes: `Struts2DiagramFileEditorProvider.createEditor`, 
`FileEditor.getPreferredFocusedComponent()`, 
`Struts2DiagramComponent.getState()`
+- Produces: `scheduleModelBuild()` UI callback always calls 
`myComponent.rebuild(model)`; `selectNotify`/`deselectNotify` call `super`; 
`myDiagramSelected` still gates DomEvent scheduling only
+
+- [ ] **Step 1: Write the failing editor load test**
+
+In `Struts2DiagramFileEditorProviderTest.java`, add imports:
+
+```java
+import com.intellij.struts2.diagram.ui.Struts2DiagramComponent;
+import com.intellij.testFramework.PlatformTestUtil;
+import com.intellij.util.ui.UIUtil;
+```
+
+Add test method (do **not** call `selectNotify` before asserting load — that 
is the regression):
+
+```java
+public void testEditorAppliesModelWithoutSelectNotify() {
+    createStrutsFileSet("struts-diagram.xml");
+    VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+    assertNotNull(file);
+
+    Struts2DiagramFileEditor editor =
+            (Struts2DiagramFileEditor) myProvider.createEditor(getProject(), 
file);
+    try {
+        PlatformTestUtil.waitForCondition(10_000, () -> {
+            UIUtil.dispatchAllInvocationEvents();
+            Struts2DiagramComponent component =
+                    (Struts2DiagramComponent) 
editor.getPreferredFocusedComponent();
+            return component != null
+                    && component.getState() == 
Struts2DiagramComponent.State.LOADED;
+        });
+
+        Struts2DiagramComponent component =
+                (Struts2DiagramComponent) 
editor.getPreferredFocusedComponent();
+        assertNotNull(component);
+        assertEquals("Constructor scheduleModelBuild must apply model without 
selectNotify",
+                Struts2DiagramComponent.State.LOADED, component.getState());
+    } finally {
+        Disposer.dispose(editor);
+    }
+}
+```
+
+If `PlatformTestUtil.waitForCondition(long, BooleanSupplier)` is unavailable 
or has a different signature on 262, use this equivalent wait loop instead 
(same semantics):
+
+```java
+long deadline = System.currentTimeMillis() + 10_000;
+Struts2DiagramComponent component = null;
+while (System.currentTimeMillis() < deadline) {
+    UIUtil.dispatchAllInvocationEvents();
+    component = (Struts2DiagramComponent) 
editor.getPreferredFocusedComponent();
+    if (component != null && component.getState() == 
Struts2DiagramComponent.State.LOADED) {
+        break;
+    }
+    Thread.sleep(50);
+}
+assertNotNull(component);
+assertEquals(Struts2DiagramComponent.State.LOADED, component.getState());
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.Struts2DiagramFileEditorProviderTest.testEditorAppliesModelWithoutSelectNotify"
+```
+
+Expected: FAIL or timeout — `myDiagramSelected` is false, so 
`finishOnUiThread` skips `rebuild()` and state stays `UNAVAILABLE`.
+
+- [ ] **Step 3: Implement editor lifecycle fixes**
+
+In `Struts2DiagramFileEditor.java`, replace `selectNotify` / `deselectNotify` 
/ `scheduleModelBuild` as follows:
+
+```java
+@Override
+public void selectNotify() {
+    super.selectNotify();
+    myDiagramSelected = true;
+    myUpdateAlarm.cancelAllRequests();
+    scheduleModelBuild();
+}
+
+@Override
+public void deselectNotify() {
+    myDiagramSelected = false;
+    myUpdateAlarm.cancelAllRequests();
+    super.deselectNotify();
+}
+
+private void scheduleModelBuild() {
+    ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile))
+            .expireWith(this)
+            
.finishOnUiThread(com.intellij.openapi.application.ModalityState.defaultModalityState(),
+                    model -> myComponent.rebuild(model))
+            .submit(AppExecutorUtil.getAppExecutorService());
+}
+```
+
+Leave `registerDomChangeListener()` unchanged — it must still return early 
when `!myDiagramSelected`.
+
+Update the class Javadoc bullet that says the UI callback is visibility-gated 
for rebuild; state that DomEvents remain gated, but completed builds always 
call `rebuild`.
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.Struts2DiagramFileEditorProviderTest"
+```
+
+Expected: BUILD SUCCESSFUL — including 
`testEditorAppliesModelWithoutSelectNotify` and existing select/deselect / 
reset tests.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add 
src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
 \
+        
src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
+git commit -m "$(cat <<'EOF'
+fix(diagram): always apply built model and call super on tab select
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Changelog, spec cross-link, full diagram suite
+
+**Files:**
+- Modify: `CHANGELOG.md`
+- Modify: `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md`
+- Test: full `com.intellij.struts2.diagram.*` suite
+
+**Interfaces:**
+- Consumes: Tasks 1–2 behavior
+- Produces: Documented fix under Unreleased Fixed; Future work links 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117)
+
+- [ ] **Step 1: Update CHANGELOG**
+
+Under `## [Unreleased]`, add a `### Fixed` section if missing, with:
+
+```markdown
+### Fixed
+
+- Fix blank Diagram tab on IntelliJ 2026.2: always apply the built model, 
invoke `PerspectiveFileEditor` select/deselect hooks, and size 
EMPTY/UNAVAILABLE panels so placeholders are visible
+```
+
+Keep existing `### Changed` entries intact.
+
+- [ ] **Step 2: Cross-link Diagrams API issue in the design spec**
+
+In `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md`:
+
+1. In **Future work (out of scope)**, mention tracking issue 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117).
+2. If the file still ends with a stray `)` after the last line, delete that 
character.
+
+Example Future work opener:
+
+```markdown
+## Future work (out of scope)
+
+Migrate rendering/editor to `com.intellij.diagram.Provider` (tracked in 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117)) per 
`com.intellij.struts2.diagram.model` package-info: ...
+```
+
+- [ ] **Step 3: Run full diagram test suite**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: BUILD SUCCESSFUL — all diagram tests green.
+
+- [ ] **Step 4: Manual smoke (optional but recommended before PR)**
+
+```bash
+./gradlew runIde
+```
+
+1. Open a file-set `struts.xml` → Diagram shows packages/actions/results (not 
blank).
+2. Edit on Text, switch to Diagram → catch-up refresh.
+3. Stay on Diagram, edit XML → debounced refresh.
+4. Empty/unavailable case shows placeholder text, not a blank pane.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add CHANGELOG.md 
docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md
+git commit -m "$(cat <<'EOF'
+docs: changelog and #117 link for Diagram blank-tab fix
+
+EOF
+)"
+```
+
+---
+
+## Spec coverage checklist
+
+| Spec requirement | Task |
+|---|---|
+| Always apply model in `finishOnUiThread` | Task 2 |
+| `super.selectNotify` / `super.deselectNotify` | Task 2 |
+| DomEvent scheduling still gated by `myDiagramSelected` | Task 2 (leave 
listener unchanged) |
+| EMPTY/UNAVAILABLE fill editor / non-zero size | Task 1 |
+| Regression: editor reaches `LOADED` without selection gate blocking first 
paint | Task 2 |
+| Placeholder size test | Task 1 |
+| Keep existing select/deselect + Dom filter tests | Task 2 step 4 / Task 3 
step 3 |
+| CHANGELOG | Task 3 |
+| No Diagrams API migration | Global constraint; Future work → #117 in Task 3 |
+
+## Manual verification (PR description)
+
+Copy the four `runIde` checks from Task 3 Step 4 into the PR test plan.
diff --git a/docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md 
b/docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md
new file mode 100644
index 0000000..1a63e9a
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md
@@ -0,0 +1,129 @@
+# Fix Blank Diagram Tab on IntelliJ 2026.2 (262)
+
+**Date:** 2026-07-25  
+**Status:** Approved for implementation planning  
+**Related:** [#97](https://github.com/apache/struts-intellij-plugin/issues/97) 
/ [#101](https://github.com/apache/struts-intellij-plugin/pull/101) 
auto-refresh (prior); platform bump 
[#115](https://github.com/apache/struts-intellij-plugin/pull/115)
+
+## Problem
+
+After targeting IntelliJ IDEA **2026.2** (build branch **262**), the read-only 
**Diagram** tab for `struts.xml` shows a completely empty canvas: editor 
background only — no package/action/result nodes and no centered placeholder 
message.
+
+The 262 upgrade PR did not change diagram sources. The blank symptom matches a 
lifecycle/layout failure in the existing lightweight Swing host rather than a 
broken `StrutsConfigDiagramModel` snapshot.
+
+### Working hypothesis
+
+1. `Struts2DiagramComponent` starts with a `null` model (`UNAVAILABLE`) and 
does not set a preferred/minimum size large enough to fill the editor area, so 
placeholder text (if painted) is not visible against the parent chrome.
+2. `scheduleModelBuild()`’s UI callback applies the model only when 
`myDiagramSelected` is true. That flag is set solely in `selectNotify()`.
+3. `selectNotify()` / `deselectNotify()` overrides do not call `super`, which 
can skip `PerspectiveFileEditor` wiring that 262 relies on more strictly.
+4. Combined, the child panel can remain zero-sized / never rebuilt → full 
blank tab.
+
+## Goals
+
+1. Diagram tab on 262 shows packages, actions, and results for file-set 
`struts.xml` files (not a blank pane).
+2. Null/empty models show the existing centered placeholder text in a filled 
editor area (never blank).
+3. Live DomEvent debounce and Text→Diagram catch-up refresh keep working.
+4. Regression tests catch “editor created but never reaches `LOADED` / never 
sized for placeholders”.
+5. Harden lifecycle so rendering does not depend only on an ungated 
`selectNotify` apply path.
+
+## Non-Goals
+
+- Migrating to `com.intellij.diagram.Provider` / Diagrams API (documented 
future work; see below).
+- Loading indicators, incremental/diff updates, or robot/UI e2e tests.
+- Changing model/DOM traversal or presentation tooltip/navigation logic.
+- Re-enabling unrelated disabled platform tests.
+
+## Decision
+
+**Approach 1 — Harden the existing `PerspectiveFileEditor` + Swing panel.**
+
+Keep the toolkit-neutral model. Fix editor lifecycle and component sizing so 
the canvas always receives a model (or a visible fallback) and fills the 
viewport.
+
+### Alternatives considered
+
+| Approach | Verdict |
+|---|---|
+| Minimal ungating only (drop `myDiagramSelected` in UI callback) | Rejected 
as sole fix — does not address zero-size placeholder / missing `super` |
+| Migrate to Diagrams API now | Rejected for this bug — larger undocumented 
Ultimate API rewrite; blank fix would be coupled to migration risk |
+| Two-phase (harden then migrate) in one delivery | Rejected for this ticket — 
migration remains a separate design |
+
+## Architecture & data flow
+
+```
+Editor created
+  → create Struts2DiagramComponent(null)
+  → scheduleModelBuild()                 // initial load
+
+ReadAction completes → finishOnUiThread
+  → ALWAYS myComponent.rebuild(model)    // no myDiagramSelected gate
+
+selectNotify
+  → super.selectNotify()
+  → myDiagramSelected = true
+  → cancel alarm → scheduleModelBuild()  // catch-up after Text edits
+
+deselectNotify
+  → myDiagramSelected = false
+  → cancel alarm
+  → super.deselectNotify()
+
+DomEvent (live edit)
+  → only if myDiagramSelected && same file
+  → debounced scheduleModelBuild()       // unchanged intent
+```
+
+`myDiagramSelected` remains a **DOM live-refresh gate only**, not a gate on 
whether the canvas may show a model.
+
+This revises the auto-refresh design 
(`2026-06-25-diagram-auto-refresh-design.md`): that spec gated `rebuild()` on 
selection to skip stale applies after tab switch. Skipping rebuild after 
deselect is an optimization; applying a completed snapshot is cheap and avoids 
the blank-tab failure mode when selection timing differs across platform 
versions. Live **scheduling** of builds from DomEvents stays selection-gated.
+
+## Components
+
+| Unit | Change |
+|---|---|
+| `Struts2DiagramFileEditor` | Call `super` in select/deselect; remove 
`myDiagramSelected` check from the UI apply callback; keep the flag for 
DomEvent filtering only |
+| `Struts2DiagramComponent` | Ensure `EMPTY` / `UNAVAILABLE` fill the editor 
area (preferred and/or minimum size, or equivalent layout expansion) so 
placeholders are visible |
+| `StrutsConfigDiagramModel` / presentation | Unchanged |
+| Provider / `plugin.xml` | Unchanged |
+
+No new modules or extension points.
+
+## Error handling & edge cases
+
+| Scenario | Behavior |
+|---|---|
+| Model build returns `null` | `UNAVAILABLE` + centered placeholder, filling 
the editor area |
+| Model has no nodes | `EMPTY` + placeholder, same fill behavior |
+| In-flight build finishes after tab switch away | Still apply `rebuild()`; 
DomEvents stay gated so extra builds are not scheduled while deselected |
+| Rapid Text edits while on Diagram | Unchanged 300 ms debounce |
+| `selectNotify` / `deselectNotify` | Always invoke `super` so platform 
perspective wiring stays intact |
+| Exceptions during build | Existing `ReadAction.nonBlocking` + 
`expireWith(this)`; blank must not be the failure mode for null/empty |
+| Editor disposed | Alarm disposed with editor; in-flight read actions expire |
+
+## Testing
+
+### Automated
+
+1. **Model applies without DomEvent selection semantics blocking first paint** 
— create editor (and pump non-blocking read actions / UI as needed); component 
reaches `LOADED` with expected nodes from `struts-diagram.xml`. Use a 
package-visible test accessor or `getPreferredFocusedComponent()` cast if that 
stays clean.
+2. **Placeholder not zero-sized** — `rebuild(null)` / empty model → state 
`UNAVAILABLE`/`EMPTY` and preferred (or minimum) size is large enough to paint 
the message in a normal editor area.
+3. Keep existing select/deselect and Dom filter tests; adjust only if 
lifecycle changes require it.
+
+### Manual (`./gradlew runIde`)
+
+1. Open a file-set `struts.xml` → Diagram shows packages/actions/results (not 
blank).
+2. Text → Diagram after an edit → catch-up refresh still works.
+3. Stay on Diagram, edit XML → debounced refresh still works.
+4. Unavailable/empty cases show placeholder text, not a blank pane.
+
+### Gate
+
+`./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"` green; 
`CHANGELOG.md` notes the fix.
+
+## Future work (out of scope)
+
+Migrate rendering/editor to `com.intellij.diagram.Provider` (tracked in 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117)) per 
`com.intellij.struts2.diagram.model` package-info: keep 
`StrutsConfigDiagramModel` + `StrutsDiagramPresentation`, replace only 
`diagram.ui` and `diagram.fileEditor`. Requires optional/bundled dependency on 
`com.intellij.diagram` (Ultimate Diagrams). Track as a separate design when 
product UX (zoom, IDE diagram chrome) justifies the undocumented  [...]
+
+## References
+
+- `Struts2DiagramFileEditor` — lifecycle and `scheduleModelBuild()`
+- `Struts2DiagramComponent` — paint / placeholder / preferred size
+- `docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md` — prior 
selection-gating decision (partially revised here)
+- `docs/superpowers/specs/2026-07-24-intellij-2026-2-compatibility-design.md` 
— 262 upgrade (no diagram code changes)
diff --git 
a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
index 78b2256..1deaf12 100644
--- 
a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
+++ 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
@@ -48,6 +48,8 @@ import javax.swing.*;
  * <p>
  * While the Diagram tab is the active editor tab, a debounced
  * {@link DomEventListener} triggers model rebuilds on struts.xml DOM changes.
+ * DomEvents remain gated by tab selection, but completed model builds always
+ * call {@link Struts2DiagramComponent#rebuild(StrutsConfigDiagramModel)}.
  * Switching to the Diagram tab ({@link #selectNotify()}) performs an immediate
  * refresh so edits made on the Text tab are reflected without reopening the 
file.
  */
@@ -76,6 +78,7 @@ public class Struts2DiagramFileEditor extends 
PerspectiveFileEditor {
 
     @Override
     public void selectNotify() {
+        super.selectNotify();
         myDiagramSelected = true;
         myUpdateAlarm.cancelAllRequests();
         scheduleModelBuild();
@@ -85,6 +88,7 @@ public class Struts2DiagramFileEditor extends 
PerspectiveFileEditor {
     public void deselectNotify() {
         myDiagramSelected = false;
         myUpdateAlarm.cancelAllRequests();
+        super.deselectNotify();
     }
 
     @Override
@@ -147,12 +151,9 @@ public class Struts2DiagramFileEditor extends 
PerspectiveFileEditor {
     private void scheduleModelBuild() {
         ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile))
                 .expireWith(this)
+                .coalesceBy(this, myXmlFile)
                 
.finishOnUiThread(com.intellij.openapi.application.ModalityState.defaultModalityState(),
-                        model -> {
-                            if (myDiagramSelected) {
-                                myComponent.rebuild(model);
-                            }
-                        })
+                        model -> myComponent.rebuild(model))
                 .submit(AppExecutorUtil.getAppExecutorService());
     }
 
diff --git 
a/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java 
b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java
index 6650747..f2a71b8 100644
--- a/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java
+++ b/src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java
@@ -102,13 +102,18 @@ public final class Struts2DiagramComponent extends JPanel 
{
     private void applyModel(@Nullable StrutsConfigDiagramModel model) {
         if (model == null) {
             state = State.UNAVAILABLE;
+            setPreferredSize(new Dimension(400, 300));
+            setMinimumSize(new Dimension(400, 300));
             return;
         }
         if (model.getNodes().isEmpty()) {
             state = State.EMPTY;
+            setPreferredSize(new Dimension(400, 300));
+            setMinimumSize(new Dimension(400, 300));
             return;
         }
         state = State.LOADED;
+        setMinimumSize(null);
         layoutModel(model);
     }
 
diff --git 
a/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
index 0b34591..17b558a 100644
--- 
a/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
+++ 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
@@ -22,8 +22,12 @@ import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.struts2.BasicLightHighlightingTestCase;
 import com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditor;
 import 
com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider;
+import com.intellij.struts2.diagram.ui.Struts2DiagramComponent;
+import com.intellij.util.ui.UIUtil;
 import org.jetbrains.annotations.NotNull;
 
+import javax.swing.*;
+
 /**
  * Tests for {@link Struts2DiagramFileEditorProvider} covering both acceptance
  * gating and basic editor lifecycle (creation, name, reset).
@@ -79,6 +83,34 @@ public class Struts2DiagramFileEditorProviderTest extends 
BasicLightHighlighting
         }
     }
 
+    public void testEditorAppliesModelWithoutSelectNotify() throws 
InterruptedException {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(file);
+
+        Struts2DiagramFileEditor editor =
+                (Struts2DiagramFileEditor) 
myProvider.createEditor(getProject(), file);
+        try {
+            long deadline = System.currentTimeMillis() + 10_000;
+            Struts2DiagramComponent component = null;
+            while (System.currentTimeMillis() < deadline) {
+                UIUtil.dispatchAllInvocationEvents();
+                component =
+                        (Struts2DiagramComponent) 
editor.getPreferredFocusedComponent();
+                if (component != null
+                        && component.getState() == 
Struts2DiagramComponent.State.LOADED) {
+                    break;
+                }
+                Thread.sleep(50);
+            }
+            assertNotNull(component);
+            assertEquals("Constructor scheduleModelBuild must apply model 
without selectNotify",
+                    Struts2DiagramComponent.State.LOADED, 
component.getState());
+        } finally {
+            Disposer.dispose(editor);
+        }
+    }
+
     public void testResetDoesNotThrow() {
         createStrutsFileSet("struts-diagram.xml");
         VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
@@ -108,4 +140,21 @@ public class Struts2DiagramFileEditorProviderTest extends 
BasicLightHighlighting
             Disposer.dispose(editor);
         }
     }
+
+    public void testSelectNotifyInstallsComponentIntoEditorWrapper() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(file);
+
+        Struts2DiagramFileEditor editor =
+                (Struts2DiagramFileEditor) 
myProvider.createEditor(getProject(), file);
+        try {
+            editor.selectNotify();
+            assertTrue("selectNotify must call super so PerspectiveFileEditor 
installs the custom component",
+                    SwingUtilities.isDescendingFrom(
+                            editor.getPreferredFocusedComponent(), 
editor.getComponent()));
+        } finally {
+            Disposer.dispose(editor);
+        }
+    }
 }
diff --git 
a/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java 
b/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java
index 22781d3..083ea41 100644
--- 
a/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java
+++ 
b/src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java
@@ -208,6 +208,48 @@ public class StrutsConfigDiagramModelTest extends 
BasicLightHighlightingTestCase
                 Struts2DiagramComponent.State.UNAVAILABLE, 
component.getState());
     }
 
+    public void testPlaceholderStatesHaveNonZeroPreferredSize() {
+        Struts2DiagramComponent unavailable = new 
Struts2DiagramComponent(null);
+        assertEquals(Struts2DiagramComponent.State.UNAVAILABLE, 
unavailable.getState());
+        assertTrue("UNAVAILABLE preferred width must fill a normal editor 
area, got "
+                        + unavailable.getPreferredSize(),
+                unavailable.getPreferredSize().width >= 400);
+        assertTrue("UNAVAILABLE preferred height must fill a normal editor 
area, got "
+                        + unavailable.getPreferredSize(),
+                unavailable.getPreferredSize().height >= 300);
+        assertTrue("UNAVAILABLE minimum width must be non-trivial, got "
+                        + unavailable.getMinimumSize(),
+                unavailable.getMinimumSize().width >= 400);
+        assertTrue("UNAVAILABLE minimum height must be non-trivial, got "
+                        + unavailable.getMinimumSize(),
+                unavailable.getMinimumSize().height >= 300);
+
+        createStrutsFileSet("struts-empty.xml");
+        VirtualFile vf = myFixture.findFileInTempDir("struts-empty.xml");
+        assertNotNull(vf);
+        PsiFile psi = PsiManager.getInstance(getProject()).findFile(vf);
+        assertInstanceOf(psi, XmlFile.class);
+        StrutsConfigDiagramModel emptyModel = ReadAction.nonBlocking(
+                () -> StrutsConfigDiagramModel.build((XmlFile) 
psi)).executeSynchronously();
+        assertNotNull(emptyModel);
+
+        Struts2DiagramComponent empty = new 
Struts2DiagramComponent(emptyModel);
+        assertEquals(Struts2DiagramComponent.State.EMPTY, empty.getState());
+        assertTrue("EMPTY preferred width must fill a normal editor area, got "
+                        + empty.getPreferredSize(),
+                empty.getPreferredSize().width >= 400);
+        assertTrue("EMPTY preferred height must fill a normal editor area, got 
"
+                        + empty.getPreferredSize(),
+                empty.getPreferredSize().height >= 300);
+
+        empty.rebuild(null);
+        assertEquals(Struts2DiagramComponent.State.UNAVAILABLE, 
empty.getState());
+        assertTrue("rebuild(null) must restore non-zero preferred size, got "
+                        + empty.getPreferredSize(),
+                empty.getPreferredSize().width >= 400
+                        && empty.getPreferredSize().height >= 300);
+    }
+
     // --- Unresolved result label tests ---
 
     public void testUnresolvedResultUsesDescriptiveLabel() {


Reply via email to