You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2026/07/25 14:57:01 UTC

(struts-intellij-plugin) branch main updated: fix(diagram): restore blank Diagram tab on IntelliJ 2026.2 (#118)

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 <lu...@gmail.com>
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 <cu...@cursor.com>
    
    * docs: add implementation plan for blank Diagram tab on 262
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * fix(diagram): size EMPTY/UNAVAILABLE panels so placeholders are visible
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * fix(diagram): always apply built model and call super on tab select
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * docs: changelog and #117 link for Diagram blank-tab fix
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * 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 <cu...@cursor.com>
    
    ---------
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 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() {