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/06/25 18:45:45 UTC

(struts-intellij-plugin) branch main updated: feat: auto-refresh Diagram tab on struts.xml DOM changes (#101)

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 <lu...@gmail.com>
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 <cu...@cursor.com>
    
    * test: cover diagram editor select/deselect notify lifecycle
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * 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 <cu...@cursor.com>
    
    * docs: note Diagram tab auto-refresh in CHANGELOG
    
    Co-authored-by: Cursor <cu...@cursor.com>
    
    * 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 <cu...@cursor.com>
    
    ---------
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 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);
+        }
+    }
 }