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() {