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 45db4e4  feat: auto-refresh Diagram tab on struts.xml DOM changes 
(#101)
45db4e4 is described below

commit 45db4e41490f05ac623e88045da8ad5ca99b75f2
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Jun 25 20:45:40 2026 +0200

    feat: auto-refresh Diagram tab on struts.xml DOM changes (#101)
    
    * test: add DOM file-filter helpers for diagram auto-refresh
    
    Extract isDomElementInFile for unit testing before wiring the listener.
    
    Co-authored-by: Cursor <[email protected]>
    
    * test: cover diagram editor select/deselect notify lifecycle
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat: auto-refresh Diagram tab on struts.xml DOM changes
    
    Register a debounced DomEventListener while the Diagram tab is active
    and rebuild immediately on tab selection. Closes #97.
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: note Diagram tab auto-refresh in CHANGELOG
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: add design spec and implementation plan for diagram auto-refresh
    
    Document the brainstorming output and task plan for issue #97.
    
    Co-authored-by: Cursor <[email protected]>
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 CHANGELOG.md                                       |   4 +
 .../plans/2026-06-25-diagram-auto-refresh.md       | 392 +++++++++++++++++++++
 .../2026-06-25-diagram-auto-refresh-design.md      | 129 +++++++
 .../fileEditor/Struts2DiagramFileEditor.java       |  72 +++-
 .../Struts2DiagramFileEditorDomFilterTest.java     |  64 ++++
 .../Struts2DiagramFileEditorProviderTest.java      |  16 +
 6 files changed, 675 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44d3519..1882c56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- Diagram tab auto-refreshes when `struts.xml` is edited (same file, active 
tab) and on tab activation after Text edits 
([#97](https://github.com/apache/struts-intellij-plugin/issues/97))
+
 ### Removed
 
 - Remove deprecated Graph editor tab (`com.intellij.struts2.graph`) and 
`com.intellij.struts2.enableGraphEditor` JVM property; use the Diagram tab 
instead
diff --git a/docs/superpowers/plans/2026-06-25-diagram-auto-refresh.md 
b/docs/superpowers/plans/2026-06-25-diagram-auto-refresh.md
new file mode 100644
index 0000000..11ecead
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-25-diagram-auto-refresh.md
@@ -0,0 +1,392 @@
+# Diagram Tab Auto-Refresh 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:** Rebuild the Struts Diagram tab automatically when the underlying 
`struts.xml` DOM changes (same file, active tab) and when the user switches to 
the Diagram tab after editing elsewhere.
+
+**Architecture:** Extend `Struts2DiagramFileEditor` with a project-wide 
`DomEventListener` (disposed with the editor), a 300 ms debounce `Alarm`, and 
`selectNotify`/`deselectNotify` visibility gating. Reuse existing 
`ReadAction.nonBlocking` → `StrutsConfigDiagramModel.build()` → 
`myComponent.rebuild()` pipeline.
+
+**Tech Stack:** IntelliJ Platform (`DomEventListener`, `Alarm`, 
`ReadAction.nonBlocking`, `PerspectiveFileEditor`), Apache Struts 2 DOM model, 
JUnit 4 light tests.
+
+**Spec:** `docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md`
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|---|---|---|
+| 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
 | Modify | DOM listener, debounce, tab visibility, guarded rebuild |
+| 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
 | Create | Unit tests for `isDomElementInFile` / `isEventForMyFile` |
+| 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
 | Modify | Add `selectNotify`/`deselectNotify` lifecycle test |
+| `CHANGELOG.md` | Modify | Unreleased entry for auto-refresh feature |
+
+No other files change.
+
+---
+
+### Task 1: File-filter helper tests
+
+**Files:**
+- Create: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
+- Modify: (none yet — tests reference methods that do not exist)
+
+- [ ] **Step 1: Write the failing test class**
+
+Create `Struts2DiagramFileEditorDomFilterTest.java`:
+
+```java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditor;
+import com.intellij.struts2.dom.struts.StrutsRoot;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+
+public class Struts2DiagramFileEditorDomFilterTest extends 
BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testIsDomElementInFileMatchesSameVirtualFile() {
+        createStrutsFileSet("struts-local-a.xml", "struts-local-b.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        VirtualFile vfB = myFixture.findFileInTempDir("struts-local-b.xml");
+        assertNotNull(vfA);
+        assertNotNull(vfB);
+
+        ReadAction.run(() -> {
+            XmlFile fileA = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vfA);
+            assertNotNull(fileA);
+            DomFileElement<StrutsRoot> root =
+                    
DomManager.getDomManager(getProject()).getFileElement(fileA, StrutsRoot.class);
+            assertNotNull(root);
+            DomElement pkg = root.getRootElement().getPackages().getFirst();
+            assertTrue(Struts2DiagramFileEditor.isDomElementInFile(pkg, vfA));
+            assertFalse(Struts2DiagramFileEditor.isDomElementInFile(pkg, vfB));
+        });
+    }
+
+    public void testIsDomElementInFileRejectsNullElement() {
+        createStrutsFileSet("struts-local-a.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        assertNotNull(vfA);
+        assertFalse(Struts2DiagramFileEditor.isDomElementInFile(null, vfA));
+    }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.Struts2DiagramFileEditorDomFilterTest"
+```
+
+Expected: FAIL — `isDomElementInFile` method not found on 
`Struts2DiagramFileEditor`.
+
+- [ ] **Step 3: Add package-private static helpers to 
`Struts2DiagramFileEditor`**
+
+Add these methods (keep existing code intact for now):
+
+```java
+import com.intellij.util.xml.events.DomEvent;
+
+static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull VirtualFile 
file) {
+    return isDomElementInFile(event.getContextElement(), file);
+}
+
+static boolean isDomElementInFile(@Nullable DomElement element, @NotNull 
VirtualFile file) {
+    if (element == null) {
+        return false;
+    }
+    VirtualFile elementFile = element.getOriginalFile().getVirtualFile();
+    return file.equals(elementFile);
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.Struts2DiagramFileEditorDomFilterTest"
+```
+
+Expected: BUILD SUCCESSFUL, 2 tests passed.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add 
src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java
 \
+        
src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
+git commit -m "$(cat <<'EOF'
+test: add DOM file-filter helpers for diagram auto-refresh
+
+Extract isDomElementInFile for unit testing before wiring the listener.
+EOF
+)"
+```
+
+---
+
+### Task 2: Tab lifecycle test
+
+**Files:**
+- Modify: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
+
+- [ ] **Step 1: Add lifecycle test**
+
+Append to `Struts2DiagramFileEditorProviderTest`:
+
+```java
+public void testSelectAndDeselectNotifyDoNotThrow() {
+    createStrutsFileSet("struts-diagram.xml");
+    VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+    assertNotNull(file);
+
+    Struts2DiagramFileEditor editor =
+            (Struts2DiagramFileEditor) myProvider.createEditor(getProject(), 
file);
+    try {
+        editor.selectNotify();
+        editor.deselectNotify();
+        editor.selectNotify();
+    } finally {
+        Disposer.dispose(editor);
+    }
+}
+```
+
+- [ ] **Step 2: Run test — expect compile error or no-op pass**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.Struts2DiagramFileEditorProviderTest.testSelectAndDeselectNotifyDoNotThrow"
+```
+
+Expected before Task 3: PASS (default `FileEditor` no-op methods) or compile 
error if overrides missing — either way, test is in place.
+
+- [ ] **Step 3: Commit test only**
+
+```bash
+git add 
src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
+git commit -m "$(cat <<'EOF'
+test: cover diagram editor select/deselect notify lifecycle
+EOF
+)"
+```
+
+---
+
+### Task 3: Wire DOM listener, debounce, and visibility gating
+
+**Files:**
+- Modify: 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
+
+- [ ] **Step 1: Add fields and constants**
+
+Inside `Struts2DiagramFileEditor`, add:
+
+```java
+private static final int DOM_UPDATE_DELAY_MS = 300;
+
+private final VirtualFile myVirtualFile;
+private final Alarm myUpdateAlarm;
+private boolean myDiagramSelected;
+```
+
+In constructor, after `myXmlFile = (XmlFile) psiFile;`:
+
+```java
+myVirtualFile = file;
+myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
+registerDomChangeListener();
+```
+
+Add imports:
+
+```java
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.AppExecutorUtil;
+import com.intellij.util.xml.DomEventListener;
+import com.intellij.util.xml.DomManager;
+import com.intellij.util.xml.events.DomEvent;
+```
+
+- [ ] **Step 2: Add listener, debounce, and tab overrides**
+
+Add methods:
+
+```java
+@Override
+public void selectNotify() {
+    myDiagramSelected = true;
+    myUpdateAlarm.cancelAllRequests();
+    scheduleModelBuild();
+}
+
+@Override
+public void deselectNotify() {
+    myDiagramSelected = false;
+    myUpdateAlarm.cancelAllRequests();
+}
+
+private void registerDomChangeListener() {
+    DomManager.getDomManager(getProject()).addDomEventListener(new 
DomEventListener() {
+        @Override
+        public void eventOccured(@NotNull DomEvent event) {
+            if (!myDiagramSelected) {
+                return;
+            }
+            if (!isEventForMyFile(event, myVirtualFile)) {
+                return;
+            }
+            queueDebouncedModelBuild();
+        }
+    }, this);
+}
+
+private void queueDebouncedModelBuild() {
+    myUpdateAlarm.cancelAllRequests();
+    myUpdateAlarm.addRequest(this::scheduleModelBuild, DOM_UPDATE_DELAY_MS);
+}
+```
+
+- [ ] **Step 3: Guard UI-thread rebuild callback**
+
+Replace `scheduleModelBuild()` with:
+
+```java
+private void scheduleModelBuild() {
+    ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile))
+            .expireWith(this)
+            
.finishOnUiThread(com.intellij.openapi.application.ModalityState.defaultModalityState(),
+                    model -> {
+                        if (myDiagramSelected) {
+                            myComponent.rebuild(model);
+                        }
+                    })
+            .submit(AppExecutorUtil.getAppExecutorService());
+}
+```
+
+Remove the old 
`.submit(com.intellij.util.concurrency.AppExecutorUtil.getAppExecutorService())`
 duplicate import if present — use `AppExecutorUtil` consistently.
+
+- [ ] **Step 4: Update class Javadoc**
+
+Extend the class Javadoc to mention DOM-driven auto-refresh while the tab is 
selected.
+
+- [ ] **Step 5: Run all diagram tests**
+
+Run:
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: BUILD SUCCESSFUL — all diagram tests pass including new lifecycle 
and filter tests.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add 
src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
+git commit -m "$(cat <<'EOF'
+feat: auto-refresh Diagram tab on struts.xml DOM changes
+
+Register a debounced DomEventListener while the Diagram tab is active
+and rebuild immediately on tab selection. Closes #97.
+EOF
+)"
+```
+
+---
+
+### Task 4: CHANGELOG and full regression
+
+**Files:**
+- Modify: `CHANGELOG.md`
+
+- [ ] **Step 1: Add Unreleased entry**
+
+Under `## [Unreleased]` → `### Added`:
+
+```markdown
+- Diagram tab auto-refreshes when `struts.xml` is edited (same file, active 
tab) and on tab activation after Text edits 
([#97](https://github.com/apache/struts-intellij-plugin/issues/97))
+```
+
+- [ ] **Step 2: Run full test suite (excluding RAT)**
+
+Run:
+
+```bash
+./gradlew test -x rat
+```
+
+Expected: BUILD SUCCESSFUL.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add CHANGELOG.md
+git commit -m "$(cat <<'EOF'
+docs: note Diagram tab auto-refresh in CHANGELOG
+EOF
+)"
+```
+
+---
+
+## Manual Smoke Test (post-implementation)
+
+1. `./gradlew runIde`
+2. Open a project with a registered `struts.xml`; open the file.
+3. Switch to **Diagram** tab — packages/actions/results render.
+4. Switch to **Text** tab; add or rename an `<action>`; switch back to 
**Diagram** — change appears immediately.
+5. Stay on **Diagram** tab; edit XML in a split editor — diagram updates after 
~300 ms pause.
+6. Open a large config; type continuously — no EDT freeze.
+
+---
+
+## Plan Self-Review
+
+| Spec requirement | Task |
+|---|---|
+| Live update while Diagram tab active | Task 3 — `DomEventListener` + 
`myDiagramSelected` |
+| Refresh on tab activation | Task 3 — `selectNotify()` |
+| Debounced, non-blocking EDT | Task 3 — `Alarm` 300 ms + 
`ReadAction.nonBlocking` |
+| Background read action | Task 3 — unchanged `scheduleModelBuild()` pipeline |
+| Same-file filtering | Task 1 helpers + Task 3 listener guard |
+| Skip rebuild when tab inactive | Task 3 — `deselectNotify` + UI callback 
guard |
+| Unit tests for filtering | Task 1 |
+| Lifecycle tests | Task 2 |
+| No cross-file refresh | Task 1 + Task 3 filtering |
+
+No placeholders. All code shown is complete for each step.
diff --git a/docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md 
b/docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md
new file mode 100644
index 0000000..4f520a7
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md
@@ -0,0 +1,129 @@
+# Diagram Tab Auto-Refresh on DOM Changes
+
+**Issue:** [#97](https://github.com/apache/struts-intellij-plugin/issues/97)  
+**Date:** 2026-06-25  
+**Status:** Approved for implementation planning
+
+## Problem
+
+The Diagram tab (`Struts2DiagramFileEditor`) rebuilds its model only on editor 
creation and `reset()`. Edits in the Text/XML perspective are not reflected 
until the tab is reopened or reset.
+
+The removed Graph tab solved this with a project-wide `DomEventListener` that 
called `queueUpdate()` when the component was visible (`isShowing()`).
+
+## Goals
+
+1. Diagram updates when the user edits `struts.xml` in the same file while the 
Diagram tab is the **active** editor tab.
+2. Diagram refreshes immediately when the user **switches to** the Diagram tab 
after editing on the Text tab.
+3. Rebuild is debounced and does not block the EDT.
+4. Model build stays on a background read action (existing 
`scheduleModelBuild()` pattern).
+5. No noticeable UI freezes on large configs.
+
+## Non-Goals
+
+- Refresh when Diagram tab is open but not selected (user chose option A).
+- Refresh on DOM changes in other files in the file set.
+- Loading indicator or incremental/diff-based diagram updates.
+- UI robot / end-to-end IDE tests in v1.
+
+## Decisions
+
+| Question | Decision |
+|---|---|
+| When to rebuild on live edit? | Only while Diagram is the active tab 
(`selectNotify` / `deselectNotify`) |
+| Catch-up after Text tab edits? | Yes — immediate rebuild on `selectNotify` |
+| Change detection mechanism | `DomEventListener` (Approach 1; matches legacy 
Graph tab, aligned with DOM-based model build) |
+| Debounce delay | 300 ms via `Alarm` (SWING_THREAD) |
+| Scope of file changes | Same `VirtualFile` as the editor only |
+
+## Architecture
+
+```
+Editor created
+  → register DomEventListener (disposed with editor)
+  → scheduleModelBuild()                    // initial load
+
+selectNotify (Diagram tab activated)
+  → myDiagramSelected = true
+  → cancel pending alarm
+  → scheduleModelBuild()                    // immediate, no debounce
+
+deselectNotify (Diagram tab deactivated)
+  → myDiagramSelected = false
+  → cancel pending alarm
+
+DomEvent (project-wide)
+  → if !myDiagramSelected → ignore
+  → if !isEventForMyFile(event) → ignore
+  → queueDebouncedModelBuild()              // 300 ms Alarm
+
+queueDebouncedModelBuild()
+  → cancel/reschedule alarm
+  → on fire: scheduleModelBuild()
+
+scheduleModelBuild()                        // unchanged pipeline
+  → ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile))
+      .expireWith(this)
+      .finishOnUiThread(..., model -> {
+          if (myDiagramSelected) myComponent.rebuild(model);
+      })
+      .submit(AppExecutorUtil.getAppExecutorService())
+```
+
+## Components
+
+All changes are confined to `Struts2DiagramFileEditor`. No new top-level 
classes.
+
+### New fields
+
+- `boolean myDiagramSelected` — set in `selectNotify` / `deselectNotify`.
+- `Alarm myUpdateAlarm` — `new Alarm(Alarm.ThreadToUse.SWING_THREAD, this)`.
+
+### New / modified methods
+
+| Method | Visibility | Description |
+|---|---|---|
+| `registerDomChangeListener()` | private | Registers `DomEventListener` via 
`DomManager.addDomEventListener(..., this)` |
+| `isEventForMyFile(DomEvent)` | package-private static | Returns true when 
event context resolves to editor's `VirtualFile` |
+| `queueDebouncedModelBuild()` | private | Schedules alarm-fired call to 
`scheduleModelBuild()` |
+| `selectNotify()` | public override | Visibility on + immediate rebuild |
+| `deselectNotify()` | public override | Visibility off + cancel alarm |
+| `scheduleModelBuild()` | private | Add visibility guard in UI-thread 
callback |
+
+`Struts2DiagramComponent` and `StrutsConfigDiagramModel` are unchanged.
+
+## Error Handling & Edge Cases
+
+| Scenario | Behavior |
+|---|---|
+| Rapid typing while on Diagram tab | Debounced — one build after 300 ms pause 
|
+| Switch to Diagram during debounce | Alarm cancelled; immediate rebuild on 
`selectNotify` |
+| In-flight build completes after tab switch | UI callback skips `rebuild()` 
when `!myDiagramSelected` |
+| Editor disposed | `expireWith(this)` cancels read action; alarm disposed 
with editor |
+| DOM event from another file | Ignored by `isEventForMyFile` |
+| Build returns null / empty | Existing `UNAVAILABLE` / `EMPTY` component 
states |
+
+## File Filtering
+
+`isEventForMyFile` resolves the event's context element (via 
`DomEvent.getContextElement()`) to its containing `VirtualFile` and compares 
with `myXmlFile.getVirtualFile()`. Returns false for null context or mismatched 
files.
+
+Extracted as package-private static for unit testing.
+
+## Testing
+
+### Automated
+
+1. **`isEventForMyFile` unit tests** — same-file event accepted; other-file 
event rejected; null context rejected.
+2. **`selectNotify` / `deselectNotify` lifecycle test** — editor survives tab 
activation cycle without exception (extends 
`Struts2DiagramFileEditorProviderTest` pattern).
+3. **Regression** — existing `testResetDoesNotThrow` continues to pass.
+
+### Manual smoke test
+
+1. Open `struts.xml`, switch to Diagram tab.
+2. Add/remove an action in Text tab, switch back to Diagram — diagram reflects 
change.
+3. Stay on Diagram tab, edit XML in split or via structure — diagram updates 
after brief pause.
+4. Open a large config — no EDT freeze during typing.
+
+## References
+
+- `Struts2DiagramFileEditor.scheduleModelBuild()` — current rebuild pipeline
+- Removed `Struts2GraphComponent` (commit `9394888^`) — legacy 
`DomEventListener` + visibility gating pattern
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 36449e1..78b2256 100644
--- 
a/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
+++ 
b/src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java
@@ -23,7 +23,13 @@ import com.intellij.psi.PsiFile;
 import com.intellij.psi.xml.XmlFile;
 import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
 import com.intellij.struts2.diagram.ui.Struts2DiagramComponent;
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.AppExecutorUtil;
 import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomEventListener;
+import com.intellij.util.xml.DomManager;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.events.DomEvent;
 import com.intellij.util.xml.ui.PerspectiveFileEditor;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -39,11 +45,21 @@ import javax.swing.*;
  * and applied asynchronously; both initial creation and {@link #reset()}
  * go through the same path so the component always reflects the current
  * model state — including explicit empty and unavailable fallbacks.
+ * <p>
+ * While the Diagram tab is the active editor tab, a debounced
+ * {@link DomEventListener} triggers model rebuilds on struts.xml DOM changes.
+ * Switching to the Diagram tab ({@link #selectNotify()}) performs an immediate
+ * refresh so edits made on the Text tab are reflected without reopening the 
file.
  */
 public class Struts2DiagramFileEditor extends PerspectiveFileEditor {
 
+    private static final int DOM_UPDATE_DELAY_MS = 300;
+
     private final XmlFile myXmlFile;
+    private final VirtualFile myVirtualFile;
     private final Struts2DiagramComponent myComponent;
+    private final Alarm myUpdateAlarm;
+    private boolean myDiagramSelected;
 
     public Struts2DiagramFileEditor(final Project project, final VirtualFile 
file) {
         super(project, file);
@@ -51,10 +67,26 @@ public class Struts2DiagramFileEditor extends 
PerspectiveFileEditor {
         final PsiFile psiFile = getPsiFile();
         assert psiFile instanceof XmlFile;
         myXmlFile = (XmlFile) psiFile;
+        myVirtualFile = file;
+        myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
+        registerDomChangeListener();
         myComponent = new Struts2DiagramComponent(null);
         scheduleModelBuild();
     }
 
+    @Override
+    public void selectNotify() {
+        myDiagramSelected = true;
+        myUpdateAlarm.cancelAllRequests();
+        scheduleModelBuild();
+    }
+
+    @Override
+    public void deselectNotify() {
+        myDiagramSelected = false;
+        myUpdateAlarm.cancelAllRequests();
+    }
+
     @Override
     @Nullable
     protected DomElement getSelectedDomElement() {
@@ -92,11 +124,47 @@ public class Struts2DiagramFileEditor extends 
PerspectiveFileEditor {
         return "Diagram";
     }
 
+    private void registerDomChangeListener() {
+        DomManager.getDomManager(getProject()).addDomEventListener(new 
DomEventListener() {
+            @Override
+            public void eventOccured(@NotNull DomEvent event) {
+                if (!myDiagramSelected) {
+                    return;
+                }
+                if (!isEventForMyFile(event, myVirtualFile)) {
+                    return;
+                }
+                queueDebouncedModelBuild();
+            }
+        }, this);
+    }
+
+    private void queueDebouncedModelBuild() {
+        myUpdateAlarm.cancelAllRequests();
+        myUpdateAlarm.addRequest(this::scheduleModelBuild, 
DOM_UPDATE_DELAY_MS);
+    }
+
     private void scheduleModelBuild() {
         ReadAction.nonBlocking(() -> StrutsConfigDiagramModel.build(myXmlFile))
                 .expireWith(this)
                 
.finishOnUiThread(com.intellij.openapi.application.ModalityState.defaultModalityState(),
-                        myComponent::rebuild)
-                
.submit(com.intellij.util.concurrency.AppExecutorUtil.getAppExecutorService());
+                        model -> {
+                            if (myDiagramSelected) {
+                                myComponent.rebuild(model);
+                            }
+                        })
+                .submit(AppExecutorUtil.getAppExecutorService());
+    }
+
+    public static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull 
VirtualFile file) {
+        return isDomElementInFile(event.getElement(), file);
+    }
+
+    public static boolean isDomElementInFile(@Nullable DomElement element, 
@NotNull VirtualFile file) {
+        if (element == null) {
+            return false;
+        }
+        VirtualFile elementFile = 
DomUtil.getFile(element).getOriginalFile().getVirtualFile();
+        return file.equals(elementFile);
     }
 }
diff --git 
a/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java
 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java
new file mode 100644
index 0000000..a5114b5
--- /dev/null
+++ 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.struts2.diagram;
+
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditor;
+import com.intellij.struts2.dom.struts.StrutsRoot;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomFileElement;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+
+public class Struts2DiagramFileEditorDomFilterTest extends 
BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testIsDomElementInFileMatchesSameVirtualFile() {
+        createStrutsFileSet("struts-local-a.xml", "struts-local-b.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        VirtualFile vfB = myFixture.findFileInTempDir("struts-local-b.xml");
+        assertNotNull(vfA);
+        assertNotNull(vfB);
+
+        ReadAction.run(() -> {
+            XmlFile fileA = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vfA);
+            assertNotNull(fileA);
+            DomFileElement<StrutsRoot> root =
+                    
DomManager.getDomManager(getProject()).getFileElement(fileA, StrutsRoot.class);
+            assertNotNull(root);
+            DomElement pkg = root.getRootElement().getPackages().getFirst();
+            assertTrue(Struts2DiagramFileEditor.isDomElementInFile(pkg, vfA));
+            assertFalse(Struts2DiagramFileEditor.isDomElementInFile(pkg, vfB));
+        });
+    }
+
+    public void testIsDomElementInFileRejectsNullElement() {
+        createStrutsFileSet("struts-local-a.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        assertNotNull(vfA);
+        assertFalse(Struts2DiagramFileEditor.isDomElementInFile(null, vfA));
+    }
+}
diff --git 
a/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
index 664f590..0b34591 100644
--- 
a/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
+++ 
b/src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java
@@ -92,4 +92,20 @@ public class Struts2DiagramFileEditorProviderTest extends 
BasicLightHighlighting
             Disposer.dispose(editor);
         }
     }
+
+    public void testSelectAndDeselectNotifyDoNotThrow() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(file);
+
+        Struts2DiagramFileEditor editor =
+                (Struts2DiagramFileEditor) 
myProvider.createEditor(getProject(), file);
+        try {
+            editor.selectNotify();
+            editor.deselectNotify();
+            editor.selectNotify();
+        } finally {
+            Disposer.dispose(editor);
+        }
+    }
 }

Reply via email to