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);
+ }
+ }
}