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 fc7862c  Migrate Diagram tab to IntelliJ Show Diagram API (#117) (#119)
fc7862c is described below

commit fc7862c79816c876cd565cfb6d5dd4bbc25d61b0
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Jul 25 21:34:45 2026 +0200

    Migrate Diagram tab to IntelliJ Show Diagram API (#117) (#119)
    
    * docs: add design for Diagrams API migration (#117)
    
    Capture the approved Show Diagram host swap: hard com.intellij.diagram
    dependency, keep toolkit-neutral model/presentation, remove the Swing tab.
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: add implementation plan for Diagrams API migration (#117)
    
    Co-authored-by: Cursor <[email protected]>
    
    * build: hard-depend on IntelliJ Diagrams plugin for #117
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat(diagram): extract DomEvent same-file helpers for Diagrams host
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat(diagram): register Show Diagram provider for Struts configs
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat(diagram): map Struts snapshot into API nodes
    
    Co-authored-by: Cursor <[email protected]>
    
    * test(diagram): assert snapshot-to-API edge triple mapping
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat(diagram): refresh model on same-file DomEvents
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): refresh presentation after DomEvents
    
    Build diagram API objects under the non-blocking read action so the EDT 
only swaps collections and requests a builder refresh.
    
    Co-authored-by: Cursor <[email protected]>
    
    * refactor(diagram): remove Swing Diagram tab in favor of Show Diagram
    
    Co-authored-by: Cursor <[email protected]>
    
    * docs: changelog and package-info for Diagrams API migration
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): preserve node identity across VFS resolution
    
    Resolve snapshot node IDs from diagram FQNs and prevent callers from 
mutating the live diagram collections.
    
    Co-authored-by: Cursor <[email protected]>
    
    * feat(diagram): keep Swing Diagram tab alongside Show Diagram
    
    Restore the PerspectiveFileEditor host so both UIs can be compared
    while the Diagrams API presentation is polished.
    
    Co-authored-by: Cursor <[email protected]>
    
    * fix(diagram): navigate to XML on Show Diagram double-click
    
    Editor-mode double-click uses EditNodeHandler, not DiagramNode.navigate().
    Wire Extras + PSI data context so Jump to Source works as well.
    
    Co-authored-by: Cursor <[email protected]>
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 CHANGELOG.md                                       |    6 +-
 build.gradle.kts                                   |    2 +
 .../plans/2026-07-25-diagrams-api-migration.md     | 1200 ++++++++++++++++++++
 .../2026-07-25-diagrams-api-migration-design.md    |  182 +++
 .../struts2/diagram/model/package-info.java        |   22 +-
 .../diagram/provider/StrutsDiagramApiEdge.java     |   43 +
 .../diagram/provider/StrutsDiagramApiNode.java     |   68 ++
 .../diagram/provider/StrutsDiagramDataModel.java   |  173 +++
 .../diagram/provider/StrutsDiagramDomRefresh.java  |   44 +
 .../provider/StrutsDiagramElementManager.java      |  100 ++
 .../diagram/provider/StrutsDiagramExtras.java      |  100 ++
 .../diagram/provider/StrutsDiagramItem.java        |   88 ++
 .../diagram/provider/StrutsDiagramProvider.java    |   79 ++
 .../diagram/provider/StrutsDiagramVfsResolver.java |   71 ++
 src/main/resources/META-INF/plugin.xml             |    2 +
 .../diagram/StrutsDiagramDataModelMappingTest.java |  210 ++++
 .../diagram/StrutsDiagramDomRefreshTest.java       |   64 ++
 .../struts2/diagram/StrutsDiagramProviderTest.java |  139 +++
 18 files changed, 2576 insertions(+), 17 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d80a47e..e88f23d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,10 +13,8 @@
 - Dependencies - upgrade `org.jetbrains.kotlin.jvm` to `2.3.0`
 - 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
+- Add IntelliJ **Show Diagram** support for Struts 2 configs via 
`com.intellij.diagram`, alongside the existing Diagram editor tab for 
comparison ([#117](https://github.com/apache/struts-intellij-plugin/issues/117))
+- Dependencies - hard-depend on `com.intellij.diagram` (Ultimate Diagrams)
 
 ## [261.19027.1] - 2026-07-13
 
diff --git a/build.gradle.kts b/build.gradle.kts
index a3cc4aa..b0524bf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -59,6 +59,7 @@ dependencies {
         bundledModule("intellij.javaee.jsp.shared")
         bundledPlugin("com.intellij.spring")
         bundledPlugin("com.intellij.java-i18n")
+        bundledPlugin("com.intellij.diagram")
         bundledPlugin("com.intellij.freemarker")
         bundledPlugin("com.intellij.velocity")
         bundledPlugin("org.intellij.groovy")
@@ -75,6 +76,7 @@ dependencies {
 
         testBundledPlugins(
             "com.intellij.java",
+            "com.intellij.diagram",
             "intellij.structureView.plugin",
             "intellij.todo.plugin",
             "intellij.structuralSearch.plugin",
diff --git a/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md 
b/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md
new file mode 100644
index 0000000..38ff162
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-25-diagrams-api-migration.md
@@ -0,0 +1,1200 @@
+# Migrate Struts Diagram to Diagrams API 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:** Replace the custom Swing Diagram editor tab with an IntelliJ **Show 
Diagram** provider that consumes the existing toolkit-neutral Struts config 
snapshot.
+
+**Architecture:** Keep `StrutsConfigDiagramModel` + 
`StrutsDiagramPresentation`. Add a `diagram.provider` package implementing 
`BaseDiagramProvider` / `DiagramDataModel` that maps snapshot nodes/edges into 
Diagrams API types, registers `com.intellij.diagram.Provider`, hard-depends on 
`com.intellij.diagram`, and removes `diagram.ui` + `diagram.fileEditor`.
+
+**Tech Stack:** IntelliJ IDEA Ultimate 2026.2 (262), `com.intellij.diagram` 
(`BaseDiagramProvider`, `DiagramDataModel`, `DiagramNodeBase`, 
`DiagramEdgeBase`), DomEventListener, JUnit 4 light tests 
(`BasicLightHighlightingTestCase`).
+
+**Spec:** `docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md`
+
+## Global Constraints
+
+- Target platform remains IntelliJ IDEA **2026.2** / build **262** only 
(`pluginSinceBuild=262`, `pluginUntilBuild=262.*`).
+- Hard dependency on plugin id **`com.intellij.diagram`** (bundled 
UML/Diagrams).
+- Entry point is **Show Diagram only** — remove the Diagram 
`PerspectiveFileEditor` tab.
+- Show Diagram accepts any Struts 2 config 
(`StrutsManager.isStruts2ConfigFile`), not only file-set members.
+- Do **not** change `StrutsConfigDiagramModel` build semantics or 
`StrutsDiagramPresentation` tooltip/navigation logic.
+- Do **not** implement #96–#100 in this plan.
+- Dom live refresh: while the data model is alive, same-file DomEvents 
debounce **300 ms**, then `refreshDataModel()`.
+- Tests gate: `./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"`.
+- Learn API details from local IU SDK jars under Gradle caches 
(`plugins/uml/lib/uml-support.jar`) and Spring Integration’s 
`SpringIntegrationDiagramProvider` pattern when signatures drift.
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|---|---|---|
+| `build.gradle.kts` | Modify | `bundledPlugin("com.intellij.diagram")` (+ 
test bundled if needed) |
+| `src/main/resources/META-INF/plugin.xml` | Modify | Hard `<plugin 
id="com.intellij.diagram"/>`; register `diagram.Provider`; remove 
`fileEditorProvider` |
+| `src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java` 
| Create | Identifying element `T` for Diagrams API (file root + snapshot node) 
|
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java`
 | Create | Pure DomEvent same-file helpers (migrated from 
`Struts2DiagramFileEditor`) |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java`
 | Create | `findInDataContext` / `isAcceptableAsNode` / titles / tooltips |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java`
 | Create | Persist/resolve root `XmlFile` by URL |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java` 
| Create | `DiagramNodeBase` wrapping `StrutsDiagramNode` |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java` 
| Create | `DiagramEdgeBase` wrapping snapshot edges |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
 | Create | Snapshot → nodes/edges; DomEvent debounce refresh |
+| 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java`
 | Create | `BaseDiagramProvider` + EP implementation |
+| `src/main/java/com/intellij/struts2/diagram/model/package-info.java` | 
Modify | Document Show Diagram host; drop Swing references |
+| `src/main/java/com/intellij/struts2/diagram/ui/*` | Delete | Custom Swing 
renderer |
+| `src/main/java/com/intellij/struts2/diagram/fileEditor/*` | Delete | 
`PerspectiveFileEditor` host |
+| `src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java` 
| Create | Accept / reject / EP presence (replaces editor provider tests) |
+| 
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java` | 
Create | Renamed/moved Dom filter tests |
+| 
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java`
 | Create | Snapshot → API node/edge mapping |
+| 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
 | Delete | Tab host tests |
+| 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
 | Delete | Replaced by Dom refresh test |
+| 
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java` 
| Keep | Semantic model tests (remove any Swing-only assertions if present) |
+| `CHANGELOG.md` | Modify | Unreleased Changed/Removed for Show Diagram 
migration |
+
+No new public plugin extension points.
+
+---
+
+### Task 1: Hard-depend on `com.intellij.diagram`
+
+**Files:**
+- Modify: `build.gradle.kts`
+- Modify: `src/main/resources/META-INF/plugin.xml`
+- Test: compile/plugin classpath (no unit test yet)
+
+**Interfaces:**
+- Consumes: IntelliJ Platform Gradle `bundledPlugin`
+- Produces: compile-time access to `com.intellij.diagram.*`; runtime hard 
dependency on plugin id `com.intellij.diagram`
+
+- [ ] **Step 1: Add Gradle bundled plugin**
+
+In `build.gradle.kts` inside `dependencies { intellijPlatform { ... } }`, 
after the existing bundled plugins (near `com.intellij.java-i18n`), add:
+
+```kotlin
+bundledPlugin("com.intellij.diagram")
+```
+
+Also add `"com.intellij.diagram"` to `testBundledPlugins(...)` so light tests 
can load the EP.
+
+- [ ] **Step 2: Declare hard plugin dependency in `plugin.xml`**
+
+Inside the existing `<dependencies>` block, add:
+
+```xml
+<plugin id="com.intellij.diagram"/>
+```
+
+Do **not** register `diagram.Provider` yet (next tasks).
+
+- [ ] **Step 3: Verify the dependency resolves**
+
+Run:
+
+```bash
+./gradlew compileJava -q
+```
+
+Expected: BUILD SUCCESSFUL (no missing `com.intellij.diagram` types yet 
because no new sources).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add build.gradle.kts src/main/resources/META-INF/plugin.xml
+git commit -m "$(cat <<'EOF'
+build: hard-depend on IntelliJ Diagrams plugin for #117
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Dom refresh helpers (pure, testable)
+
+**Files:**
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java`
+- Create: 
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java`
+- Delete (later Task 6, not now): old Dom filter test still references file 
editor — leave it until migration completes, **or** rewrite now and leave a 
temporary compile dependency. Prefer: rewrite test now against the new helper; 
keep old file editor until Task 6.
+
+**Interfaces:**
+- Consumes: `DomEvent`, `DomElement`, `DomUtil`, `VirtualFile`
+- Produces:
+  - `static boolean isEventForMyFile(@NotNull DomEvent event, @NotNull 
VirtualFile file)`
+  - `static boolean isDomElementInFile(@Nullable DomElement element, @NotNull 
VirtualFile file)`
+
+- [ ] **Step 1: Write failing Dom filter tests**
+
+Create `StrutsDiagramDomRefreshTest.java` (copy logic from 
`Struts2DiagramFileEditorDomFilterTest`, change call sites to 
`StrutsDiagramDomRefresh`):
+
+```java
+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.provider.StrutsDiagramDomRefresh;
+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 StrutsDiagramDomRefreshTest 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(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfA));
+            assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfB));
+        });
+    }
+
+    public void testIsDomElementInFileRejectsNullElement() {
+        createStrutsFileSet("struts-local-a.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        assertNotNull(vfA);
+        assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(null, vfA));
+    }
+}
+```
+
+Use the same Apache license header as sibling test files.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramDomRefreshTest" 
+```
+
+Expected: FAIL (class `StrutsDiagramDomRefresh` not found).
+
+- [ ] **Step 3: Implement helper**
+
+Create `StrutsDiagramDomRefresh.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.events.DomEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Pure helpers for same-file DomEvent filtering used by {@link 
StrutsDiagramDataModel}.
+ */
+public final class StrutsDiagramDomRefresh {
+
+    private StrutsDiagramDomRefresh() {}
+
+    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);
+    }
+}
+```
+
+- [ ] **Step 4: Run tests to verify they pass**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramDomRefreshTest"
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add 
src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
 \
+        
src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): extract DomEvent same-file helpers for Diagrams host
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Identifying item + Provider accept path
+
+**Files:**
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java`
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java`
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java`
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java`
 (skeleton; data model stub OK temporarily)
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
 (minimal stub compiling)
+- Modify: `src/main/resources/META-INF/plugin.xml` — register provider
+- Create: 
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java`
+
+**Interfaces:**
+- Consumes: `StrutsManager.isStruts2ConfigFile(XmlFile)`, 
`BaseDiagramProvider`, `AbstractDiagramElementManager`
+- Produces:
+  - `StrutsDiagramItem.forFile(XmlFile)` / `forNode(XmlFile, 
StrutsDiagramNode)` / `getXmlFile()` / `getSnapshotNode()`
+  - Provider id constant `StrutsDiagramProvider.ID = "ApacheStrutsConfig"`
+  - `StrutsDiagramElementManager.findInDataContext` → root item when PSI file 
is Struts config
+  - `canBeBuiltFrom(Object)` / accept semantics: Struts config **without** 
requiring file-set membership
+
+- [ ] **Step 1: Write failing accept tests**
+
+Create `StrutsDiagramProviderTest.java`:
+
+```java
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
+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.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramProviderTest extends BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testProviderExtensionIsRegistered() {
+        DiagramProvider<?> provider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull("Struts DiagramProvider must be registered via 
com.intellij.diagram.Provider",
+                provider);
+        assertInstanceOf(provider, StrutsDiagramProvider.class);
+    }
+
+    public void testAcceptsStrutsConfigWithoutFileSet() {
+        VirtualFile file = myFixture.copyFileToProject("struts-diagram.xml");
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(file);
+        assertNotNull(xml);
+        StrutsDiagramProvider provider = (StrutsDiagramProvider) 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull(provider);
+        DataContext ctx = SimpleDataContext.builder()
+                .add(CommonDataKeys.PSI_FILE, xml)
+                .add(CommonDataKeys.PROJECT, getProject())
+                .build();
+        StrutsDiagramItem item = 
provider.getElementManager().findInDataContext(ctx);
+        assertNotNull("Show Diagram must accept Struts config outside a file 
set", item);
+        assertEquals(xml, item.getXmlFile());
+        assertNull(item.getSnapshotNode());
+    }
+
+    public void testRejectsPlainXml() {
+        XmlFile xml = (XmlFile) myFixture.configureByText("plain.xml", 
"<root/>");
+        StrutsDiagramProvider provider = (StrutsDiagramProvider) 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull(provider);
+        DataContext ctx = SimpleDataContext.builder()
+                .add(CommonDataKeys.PSI_FILE, xml)
+                .add(CommonDataKeys.PROJECT, getProject())
+                .build();
+        assertNull(provider.getElementManager().findInDataContext(ctx));
+        assertFalse(provider.getElementManager().canBeBuiltFrom(xml));
+    }
+}
+```
+
+If `SimpleDataContext.builder()` API differs on 262, use the project’s 
existing DataContext construction pattern or `MapDataContext` — resolve against 
the IU SDK; keep the same assertions.
+
+- [ ] **Step 2: Run tests to verify they fail**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramProviderTest"
+```
+
+Expected: FAIL (provider not registered / classes missing).
+
+- [ ] **Step 3: Implement item + managers + stub provider/data model**
+
+`StrutsDiagramItem.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Identifying element for Struts Diagrams API nodes.
+ * Root items (snapshotNode == null) seed Show Diagram from an XmlFile.
+ */
+public final class StrutsDiagramItem {
+
+    private final @NotNull SmartPsiElementPointer<XmlFile> filePointer;
+    private final @Nullable StrutsDiagramNode snapshotNode;
+
+    private StrutsDiagramItem(@NotNull SmartPsiElementPointer<XmlFile> 
filePointer,
+                              @Nullable StrutsDiagramNode snapshotNode) {
+        this.filePointer = filePointer;
+        this.snapshotNode = snapshotNode;
+    }
+
+    public static @NotNull StrutsDiagramItem forFile(@NotNull XmlFile file) {
+        return new StrutsDiagramItem(
+                
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+                null);
+    }
+
+    public static @NotNull StrutsDiagramItem forNode(@NotNull XmlFile file, 
@NotNull StrutsDiagramNode node) {
+        return new StrutsDiagramItem(
+                
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+                node);
+    }
+
+    public @Nullable XmlFile getXmlFile() {
+        return filePointer.getElement();
+    }
+
+    public @Nullable StrutsDiagramNode getSnapshotNode() {
+        return snapshotNode;
+    }
+
+    public boolean isRoot() {
+        return snapshotNode == null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof StrutsDiagramItem that)) return false;
+        if (snapshotNode == null || that.snapshotNode == null) {
+            return snapshotNode == that.snapshotNode
+                    && Objects.equals(fileUrl(), that.fileUrl());
+        }
+        return snapshotNode.equals(that.snapshotNode);
+    }
+
+    @Override
+    public int hashCode() {
+        return snapshotNode != null ? snapshotNode.hashCode() : 
Objects.hash(fileUrl());
+    }
+
+    private @Nullable String fileUrl() {
+        PsiFile file = filePointer.getElement();
+        return file != null && file.getVirtualFile() != null ? 
file.getVirtualFile().getUrl() : null;
+    }
+}
+```
+
+`StrutsDiagramVfsResolver.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.diagram.DiagramVfsResolver;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramVfsResolver implements 
DiagramVfsResolver<StrutsDiagramItem> {
+
+    @Override
+    public @Nullable String getQualifiedName(StrutsDiagramItem item) {
+        if (item == null) return null;
+        XmlFile file = item.getXmlFile();
+        if (file == null || file.getVirtualFile() == null) return null;
+        if (item.getSnapshotNode() != null) {
+            return file.getVirtualFile().getUrl() + "#" + 
item.getSnapshotNode().getId();
+        }
+        return file.getVirtualFile().getUrl();
+    }
+
+    @Override
+    public @Nullable StrutsDiagramItem resolveElementByFQN(@NotNull String 
fqn, @NotNull Project project) {
+        String url = fqn.contains("#") ? fqn.substring(0, fqn.indexOf('#')) : 
fqn;
+        VirtualFile vf = VirtualFileManager.getInstance().findFileByUrl(url);
+        if (vf == null) return null;
+        PsiFile psi = PsiManager.getInstance(project).findFile(vf);
+        if (!(psi instanceof XmlFile xmlFile)) return null;
+        return StrutsDiagramItem.forFile(xmlFile);
+    }
+}
+```
+
+`StrutsDiagramElementManager.java` (extend `AbstractDiagramElementManager`):
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.AbstractDiagramElementManager;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import com.intellij.ui.SimpleColoredText;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramElementManager extends 
AbstractDiagramElementManager<StrutsDiagramItem> {
+
+    @Override
+    public @Nullable StrutsDiagramItem findInDataContext(@NotNull DataContext 
context) {
+        PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(context);
+        if (!(psiFile instanceof XmlFile xmlFile)) {
+            return null;
+        }
+        if 
(!StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile)) 
{
+            return null;
+        }
+        return StrutsDiagramItem.forFile(xmlFile);
+    }
+
+    @Override
+    public boolean canBeBuiltFrom(Object element) {
+        if (element instanceof StrutsDiagramItem item) {
+            XmlFile file = item.getXmlFile();
+            return file != null
+                    && 
StrutsManager.getInstance(file.getProject()).isStruts2ConfigFile(file);
+        }
+        if (element instanceof XmlFile xmlFile) {
+            return 
StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isAcceptableAsNode(Object element) {
+        return element instanceof StrutsDiagramItem item && !item.isRoot();
+    }
+
+    @Override
+    public @Nullable @Nls String getElementTitle(StrutsDiagramItem item) {
+        if (item == null) return null;
+        StrutsDiagramNode node = item.getSnapshotNode();
+        if (node != null) return node.getName();
+        XmlFile file = item.getXmlFile();
+        return file != null ? file.getName() : null;
+    }
+
+    @Override
+    public @Nullable String getNodeTooltip(StrutsDiagramItem item) {
+        if (item == null || item.getSnapshotNode() == null) return null;
+        return item.getSnapshotNode().getTooltipHtml();
+    }
+
+    @Override
+    public Object[] getNodeItems(StrutsDiagramItem element) {
+        return EMPTY_ARRAY;
+    }
+
+    @Override
+    public boolean canCollapse(StrutsDiagramItem element) {
+        return false;
+    }
+
+    @Override
+    public boolean isContainerFor(StrutsDiagramItem parent, StrutsDiagramItem 
child) {
+        return false;
+    }
+}
+```
+
+Remove unused imports (`SimpleColoredText`, `SimpleTextAttributes`, `Icon`) if 
the compiler flags them. After adding the class, compile once and fix any 
remaining 262 abstract-method signatures / `@NotNull` annotations; keep the 
accept/tooltip/title behavior above.
+
+Minimal compiling `StrutsDiagramDataModel` stub (full implementation in Task 
4):
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.psi.PsiManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+
+public final class StrutsDiagramDataModel extends 
DiagramDataModel<StrutsDiagramItem> {
+
+    public StrutsDiagramDataModel(@NotNull Project project,
+                                  @NotNull StrutsDiagramProvider provider,
+                                  @Nullable StrutsDiagramItem seed) {
+        super(project, provider);
+        if (seed != null) {
+            setOriginalElement(seed);
+        }
+    }
+
+    @Override
+    public @NotNull ModificationTracker getModificationTracker() {
+        return PsiManager.getInstance(getProject()).getModificationTracker();
+    }
+
+    @Override
+    public @NotNull Collection<? extends DiagramNode<StrutsDiagramItem>> 
getNodes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public @NotNull Collection<? extends DiagramEdge<StrutsDiagramItem>> 
getEdges() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public @NotNull String getNodeName(@NotNull DiagramNode<StrutsDiagramItem> 
node) {
+        StrutsDiagramItem item = node.getIdentifyingElement();
+        String title = getProvider().getElementManager().getElementTitle(item);
+        return title != null ? title : "";
+    }
+
+    @Override
+    public @Nullable DiagramNode<StrutsDiagramItem> addElement(@Nullable 
StrutsDiagramItem element) {
+        return null;
+    }
+
+    @Override
+    public void refreshDataModel() {
+        // Task 4
+    }
+}
+```
+
+`StrutsDiagramProvider.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.BaseDiagramProvider;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramElementManager;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.struts2.Struts2Icons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramProvider extends 
BaseDiagramProvider<StrutsDiagramItem> {
+
+    public static final String ID = "ApacheStrutsConfig";
+
+    private final DiagramElementManager<StrutsDiagramItem> elementManager = 
new StrutsDiagramElementManager();
+    private final DiagramVfsResolver<StrutsDiagramItem> vfsResolver = new 
StrutsDiagramVfsResolver();
+
+    @Override
+    public @NotNull String getID() {
+        return ID;
+    }
+
+    @Override
+    public @NotNull @Nls String getPresentableName() {
+        return "Struts Configuration";
+    }
+
+    @Override
+    public @Nullable Icon getActionIcon(boolean isPopup) {
+        return Struts2Icons.Action;
+    }
+
+    @Override
+    public @NotNull DiagramElementManager<StrutsDiagramItem> 
getElementManager() {
+        return elementManager;
+    }
+
+    @Override
+    public @NotNull DiagramVfsResolver<StrutsDiagramItem> getVfsResolver() {
+        return vfsResolver;
+    }
+
+    @Override
+    public @NotNull DiagramDataModel<StrutsDiagramItem> 
createDataModel(@NotNull Project project,
+                                                                       
@Nullable StrutsDiagramItem element,
+                                                                       
@Nullable VirtualFile file,
+                                                                       
@NotNull DiagramPresentationModel presentationModel) {
+        return new StrutsDiagramDataModel(project, this, element);
+    }
+}
+```
+
+Register in `plugin.xml` under `<extensions 
defaultExtensionNs="com.intellij">`:
+
+```xml
+<diagram.Provider 
implementation="com.intellij.struts2.diagram.provider.StrutsDiagramProvider"/>
+```
+
+Keep the old `fileEditorProvider` until Task 6 so the IDE still has a diagram 
tab during incremental work.
+
+- [ ] **Step 4: Compile and fix API mismatches**
+
+```bash
+./gradlew compileJava compileTestJava
+```
+
+Fix any 262 signature mismatches (imports, `@NotNull`, removed abstract 
methods). Re-run until green compile.
+
+- [ ] **Step 5: Run provider tests**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramProviderTest"
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/provider \
+        src/main/resources/META-INF/plugin.xml \
+        
src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): register Show Diagram provider for Struts configs
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Snapshot → Diagrams nodes/edges mapping
+
+**Files:**
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java`
+- Create: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java`
+- Modify: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
 (full refresh)
+- Create: 
`src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java`
+
+**Interfaces:**
+- Consumes: `StrutsConfigDiagramModel.build(XmlFile)`, `StrutsDiagramNode`, 
`StrutsDiagramEdge`, `DiagramRelationships.DEPENDENCY`, 
`DiagramRelationshipInfoAdapter` for labeled edges
+- Produces: After `refreshDataModel()`, `getNodes()`/`getEdges()` mirror the 
snapshot (same counts, ids, edge endpoints/labels)
+
+- [ ] **Step 1: Write failing mapping test**
+
+```java
+package com.intellij.struts2.diagram;
+
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramProvider;
+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.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDataModel;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StrutsDiagramDataModelMappingTest extends 
BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testRefreshMapsSnapshotNodesAndEdges() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(vf);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vf);
+        assertNotNull(xml);
+
+        StrutsConfigDiagramModel snapshot = ReadAction.nonBlocking(
+                () -> 
StrutsConfigDiagramModel.build(xml)).executeSynchronously();
+        assertNotNull(snapshot);
+        assertFalse(snapshot.getNodes().isEmpty());
+
+        StrutsDiagramProvider provider =
+                (StrutsDiagramProvider) 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull(provider);
+        StrutsDiagramDataModel dataModel =
+                new StrutsDiagramDataModel(getProject(), provider, 
StrutsDiagramItem.forFile(xml));
+        try {
+            ReadAction.run(dataModel::refreshDataModel);
+
+            assertEquals(snapshot.getNodes().size(), 
dataModel.getNodes().size());
+            assertEquals(snapshot.getEdges().size(), 
dataModel.getEdges().size());
+
+            Set<String> snapshotIds = snapshot.getNodes().stream()
+                    .map(StrutsDiagramNode::getId).collect(Collectors.toSet());
+            Set<String> apiIds = dataModel.getNodes().stream()
+                    .map(DiagramNode::getIdentifyingElement)
+                    .map(StrutsDiagramItem::getSnapshotNode)
+                    .map(StrutsDiagramNode::getId)
+                    .collect(Collectors.toSet());
+            assertEquals(snapshotIds, apiIds);
+
+            for (DiagramEdge<StrutsDiagramItem> edge : dataModel.getEdges()) {
+                
assertNotNull(edge.getSource().getIdentifyingElement().getSnapshotNode());
+                
assertNotNull(edge.getTarget().getIdentifyingElement().getSnapshotNode());
+            }
+        } finally {
+            dataModel.dispose();
+        }
+    }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramDataModelMappingTest"
+```
+
+Expected: FAIL (empty nodes from stub refresh).
+
+- [ ] **Step 3: Implement API node/edge + full `refreshDataModel`**
+
+`StrutsDiagramApiNode.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramNodeBase;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramApiNode extends 
DiagramNodeBase<StrutsDiagramItem> {
+
+    private final @NotNull StrutsDiagramItem item;
+
+    public StrutsDiagramApiNode(@NotNull DiagramProvider<StrutsDiagramItem> 
provider,
+                                @NotNull StrutsDiagramItem item) {
+        super(provider);
+        this.item = item;
+    }
+
+    @Override
+    public @NotNull StrutsDiagramItem getIdentifyingElement() {
+        return item;
+    }
+
+    @Override
+    public @Nullable String getTooltip() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null ? node.getTooltipHtml() : null;
+    }
+
+    @Override
+    public @Nullable Icon getIcon() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null ? node.getIcon() : null;
+    }
+
+    @Override
+    public void navigate(boolean requestFocus) {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        if (node != null) {
+            StrutsDiagramPresentation.navigateToElement(node);
+        }
+    }
+
+    @Override
+    public boolean canNavigate() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null && node.getNavigationPointer() != null;
+    }
+}
+```
+
+`StrutsDiagramApiEdge.java`:
+
+```java
+package com.intellij.struts2.diagram.provider;
+
+import com.intellij.diagram.DiagramEdgeBase;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationshipInfoAdapter;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.diagram.presentation.DiagramLineType;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import org.jetbrains.annotations.NotNull;
+
+public final class StrutsDiagramApiEdge extends 
DiagramEdgeBase<StrutsDiagramItem> {
+
+    public StrutsDiagramApiEdge(@NotNull DiagramNode<StrutsDiagramItem> source,
+                                @NotNull DiagramNode<StrutsDiagramItem> target,
+                                @NotNull StrutsDiagramEdge snapshotEdge) {
+        super(source, target, relationshipFor(snapshotEdge));
+    }
+
+    private static @NotNull DiagramRelationshipInfo relationshipFor(@NotNull 
StrutsDiagramEdge edge) {
+        String label = edge.getLabel();
+        if (label == null || label.isEmpty()) {
+            return DiagramRelationships.DEPENDENCY;
+        }
+        return new DiagramRelationshipInfoAdapter(label, 
DiagramLineType.SOLID, label);
+    }
+}
+```
+
+If `DiagramRelationshipInfoAdapter` / `DiagramLineType` constructors differ on 
262, adjust to the closest public constructor that accepts a center/name label; 
unlabeled edges must still use `DiagramRelationships.DEPENDENCY`.
+
+Replace `refreshDataModel` / collections in `StrutsDiagramDataModel`:
+
+```java
+private final List<DiagramNode<StrutsDiagramItem>> nodes = new ArrayList<>();
+private final List<DiagramEdge<StrutsDiagramItem>> edges = new ArrayList<>();
+
+@Override
+public void refreshDataModel() {
+    nodes.clear();
+    edges.clear();
+    StrutsDiagramItem seed = getOriginalElement();
+    XmlFile xmlFile = seed != null ? seed.getXmlFile() : null;
+    if (xmlFile == null) {
+        return;
+    }
+    StrutsConfigDiagramModel snapshot = 
StrutsConfigDiagramModel.build(xmlFile);
+    if (snapshot == null) {
+        return;
+    }
+    Map<StrutsDiagramNode, DiagramNode<StrutsDiagramItem>> map = new 
IdentityHashMap<>();
+    for (StrutsDiagramNode snapshotNode : snapshot.getNodes()) {
+        StrutsDiagramItem item = StrutsDiagramItem.forNode(xmlFile, 
snapshotNode);
+        StrutsDiagramApiNode apiNode = new StrutsDiagramApiNode(getProvider(), 
item);
+        nodes.add(apiNode);
+        map.put(snapshotNode, apiNode);
+    }
+    for (StrutsDiagramEdge snapshotEdge : snapshot.getEdges()) {
+        DiagramNode<StrutsDiagramItem> source = 
map.get(snapshotEdge.getSource());
+        DiagramNode<StrutsDiagramItem> target = 
map.get(snapshotEdge.getTarget());
+        if (source != null && target != null) {
+            edges.add(new StrutsDiagramApiEdge(source, target, snapshotEdge));
+        }
+    }
+}
+```
+
+Return `nodes` / `edges` from `getNodes()` / `getEdges()`. Document that 
`refreshDataModel()` must run under a read action (callers: initial Show 
Diagram path and Dom refresh bridge).
+
+- [ ] **Step 4: Run mapping test**
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsDiagramDataModelMappingTest"
+```
+
+Expected: PASS. Also run model tests:
+
+```bash
+./gradlew test -x rat --tests 
"com.intellij.struts2.diagram.StrutsConfigDiagramModelTest"
+```
+
+Expected: PASS (unchanged semantics).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/provider \
+        
src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): map Struts snapshot model into Diagrams API nodes
+
+EOF
+)"
+```
+
+---
+
+### Task 5: DomEvent live refresh while data model is alive
+
+**Files:**
+- Modify: 
`src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java`
+- Test: extend `StrutsDiagramDomRefreshTest` or mapping test with a small unit 
assertion that the listener uses `StrutsDiagramDomRefresh` (full DomEvent UI 
pump optional)
+
+**Interfaces:**
+- Consumes: `DomManager.addDomEventListener`, `Alarm` 300 ms, 
`StrutsDiagramDomRefresh`, `ReadAction.nonBlocking`
+- Produces: While model not disposed, same-file DomEvents debounce then 
rebuild snapshot on background read + apply on EDT via Diagrams refresh 
(`refreshDataModel` + builder update if required by 262)
+
+- [ ] **Step 1: Implement listener in data model constructor**
+
+Fields:
+
+```java
+private static final int DOM_UPDATE_DELAY_MS = 300;
+private final Alarm updateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, 
this);
+private final VirtualFile virtualFile; // from seed XmlFile; may be null
+```
+
+In constructor, after `setOriginalElement`:
+
+```java
+XmlFile xml = seed != null ? seed.getXmlFile() : null;
+this.virtualFile = xml != null ? xml.getVirtualFile() : null;
+if (virtualFile != null) {
+    DomManager.getDomManager(project).addDomEventListener(event -> {
+        if (!StrutsDiagramDomRefresh.isEventForMyFile(event, virtualFile)) {
+            return;
+        }
+        queueDebouncedRefresh();
+    }, this);
+}
+```
+
+```java
+private void queueDebouncedRefresh() {
+    updateAlarm.cancelAllRequests();
+    updateAlarm.addRequest(this::scheduleRefresh, DOM_UPDATE_DELAY_MS);
+}
+
+private void scheduleRefresh() {
+    ReadAction.nonBlocking(() -> {
+                refreshDataModel();
+                return null;
+            })
+            .expireWith(this)
+            .coalesceBy(this)
+            .finishOnUiThread(ModalityState.defaultModalityState(), ignored -> 
{
+                // If 262 requires an explicit diagram builder update after 
mutating
+                // getNodes()/getEdges(), call 
DiagramDataModel.refreshDataModelInSmartMode(getBuilder())
+                // or getBuilder().queryUpdate()... per SDK — only when 
getBuilder() != null.
+            })
+            .submit(AppExecutorUtil.getAppExecutorService());
+}
+```
+
+**Threading note:** Prefer building the snapshot inside 
`ReadAction.nonBlocking` and assigning node lists on the EDT. If 
`refreshDataModel()` both reads PSI and mutates lists, split into 
`buildSnapshot()` (read) + `applySnapshot()` (EDT) to match the old editor’s 
pattern and avoid PSI from EDT.
+
+Recommended split:
+
+```java
+private @Nullable StrutsConfigDiagramModel buildSnapshot() { /* 
StrutsConfigDiagramModel.build */ }
+private void applySnapshot(@Nullable StrutsConfigDiagramModel snapshot) { /* 
clear/fill nodes+edges */ }
+
+public void refreshDataModel() {
+    // Called under read action by platform OR by scheduleRefresh apply path:
+    applySnapshot(buildSnapshot());
+}
+```
+
+Use the split that compiles cleanly and keeps PSI off the EDT for 
Dom-triggered updates.
+
+- [ ] **Step 2: Smoke-test Dom helpers still pass; run diagram suite**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: all current diagram tests PASS (old editor tests still present until 
Task 6).
+
+- [ ] **Step 3: Manual checkpoint (optional but recommended)**
+
+```bash
+./gradlew runIde
+```
+
+Open a Struts config → Show Diagram → edit XML → diagram updates after ~300 
ms. If refresh does not repaint, fix the builder update call from Step 1 before 
committing.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add 
src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
+git commit -m "$(cat <<'EOF'
+feat(diagram): refresh Show Diagram model on same-file DomEvents
+
+EOF
+)"
+```
+
+---
+
+### Task 6: Remove Swing Diagram tab host
+
+**Files:**
+- Delete: 
`src/main/java/com/intellij/struts2/diagram/ui/Struts2DiagramComponent.java`
+- Delete: 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditor.java`
+- Delete: 
`src/main/java/com/intellij/struts2/diagram/fileEditor/Struts2DiagramFileEditorProvider.java`
+- Delete: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorProviderTest.java`
+- Delete: 
`src/test/java/com/intellij/struts2/diagram/Struts2DiagramFileEditorDomFilterTest.java`
+- Modify: `src/main/resources/META-INF/plugin.xml` — remove 
`fileEditorProvider` line
+- Modify: 
`src/test/java/com/intellij/struts2/diagram/StrutsConfigDiagramModelTest.java` 
— remove any imports/assertions against `Struts2DiagramComponent` if present 
after #118
+
+**Interfaces:**
+- Consumes: provider path from Tasks 3–5
+- Produces: no Diagram editor tab; Show Diagram is the only host
+
+- [ ] **Step 1: Remove `fileEditorProvider` registration**
+
+Delete this line from `plugin.xml`:
+
+```xml
+<fileEditorProvider 
implementation="com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider"/>
+```
+
+- [ ] **Step 2: Delete UI/fileEditor sources and obsolete tests**
+
+Delete the five files listed above. Grep for leftover references:
+
+```bash
+rg -n 
"Struts2DiagramComponent|Struts2DiagramFileEditor|diagram\\.ui|diagram\\.fileEditor"
 src || true
+```
+
+Expected: no matches (except historical docs/specs).
+
+- [ ] **Step 3: Fix `StrutsConfigDiagramModelTest` if it still touches Swing**
+
+Remove placeholder-size / component tests that import 
`Struts2DiagramComponent`. Keep pure model assertions.
+
+- [ ] **Step 4: Run full diagram test suite**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: PASS. No references to deleted classes.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add -A src/main/java/com/intellij/struts2/diagram \
+        src/test/java/com/intellij/struts2/diagram \
+        src/main/resources/META-INF/plugin.xml
+git commit -m "$(cat <<'EOF'
+refactor(diagram): remove Swing Diagram tab in favor of Show Diagram
+
+EOF
+)"
+```
+
+---
+
+### Task 7: Docs, package-info, changelog, final gate
+
+**Files:**
+- Modify: `src/main/java/com/intellij/struts2/diagram/model/package-info.java`
+- Modify: `CHANGELOG.md`
+- Modify (optional clarity): 
`docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md` status 
line only if needed
+
+**Interfaces:**
+- Produces: accurate migration boundary docs + user-facing changelog
+
+- [ ] **Step 1: Update `package-info.java`**
+
+Replace the “future migration” wording so it states the Diagrams API host 
lives in `com.intellij.struts2.diagram.provider`, model/presentation remain 
toolkit-neutral, and the Swing/`PerspectiveFileEditor` host has been removed.
+
+- [ ] **Step 2: Update `CHANGELOG.md` under `[Unreleased]`**
+
+Add under **Changed** (and **Removed** as appropriate):
+
+```markdown
+### Changed
+
+- Migrate struts.xml diagram visualization from the custom Diagram editor tab 
to the IntelliJ **Show Diagram** action (`com.intellij.diagram`) 
([#117](https://github.com/apache/struts-intellij-plugin/issues/117))
+- Dependencies - hard-depend on `com.intellij.diagram` (Ultimate Diagrams)
+
+### Removed
+
+- Remove custom Swing Diagram editor tab (`Struts2DiagramFileEditor` / 
`Struts2DiagramComponent`); use **Show Diagram** on a Struts 2 config file 
instead
+```
+
+Adjust section placement to match existing Unreleased structure.
+
+- [ ] **Step 3: Final automated gate**
+
+```bash
+./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"
+```
+
+Expected: BUILD SUCCESSFUL, all diagram tests green.
+
+- [ ] **Step 4: Manual verification checklist** (`./gradlew runIde`)
+
+1. Open a Struts 2 config **not** in a file set → Show Diagram available → 
packages/actions/results render  
+2. Chain/redirect action→action edges still appear (`struts-redirect-*.xml` 
fixtures)  
+3. Edit XML while diagram open → debounced refresh  
+4. Double-click node → navigates to XML  
+5. Hover → tooltip HTML  
+6. Confirm **Diagram** editor tab is gone  
+7. Zoom/pan via platform chrome works at smoke level  
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/main/java/com/intellij/struts2/diagram/model/package-info.java 
CHANGELOG.md
+git commit -m "$(cat <<'EOF'
+docs: changelog and package-info for Diagrams API migration
+
+EOF
+)"
+```
+
+---
+
+## Spec coverage (self-review)
+
+| Spec requirement | Task |
+|---|---|
+| Show Diagram entry (no tab) | 3, 6 |
+| Hard `com.intellij.diagram` dependency | 1 |
+| Any Struts config (not only file set) | 3 |
+| Keep model + presentation | 4 (consume only) |
+| Tooltips + navigate | 4 (`StrutsDiagramApiNode`) |
+| Dom debounce refresh while model alive | 5 |
+| Remove Swing / PerspectiveFileEditor | 6 |
+| Changelog | 7 |
+| Automated diagram tests + manual checklist | 3–7 |
+| Out of scope #96–#100 | Not scheduled |
+
+## Placeholder / consistency notes
+
+- Exact 262 method signatures for `AbstractDiagramElementManager` / 
relationship adapters may need compile-driven tweaks; behavior in this plan is 
normative.
+- Provider id is fixed: `ApacheStrutsConfig`.
+- Dom helpers live only in `StrutsDiagramDomRefresh` after Task 6.
diff --git a/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md 
b/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md
new file mode 100644
index 0000000..e4460ea
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-25-diagrams-api-migration-design.md
@@ -0,0 +1,182 @@
+# Migrate Struts Diagram to IntelliJ Diagrams API
+
+**Date:** 2026-07-25  
+**Status:** Approved for implementation planning  
+**Related:** 
[#117](https://github.com/apache/struts-intellij-plugin/issues/117); prior 
blank-tab host fix 
[#118](https://github.com/apache/struts-intellij-plugin/pull/118) / 
`2026-07-25-diagram-blank-262-design.md`; auto-refresh 
`2026-06-25-diagram-auto-refresh-design.md`; model boundary in 
`com.intellij.struts2.diagram.model` package-info
+
+## Problem
+
+The struts.xml **Diagram** tab is a custom Swing panel 
(`Struts2DiagramComponent`) hosted by `PerspectiveFileEditor`. That host owns 
layout, zoom, and lifecycle, which has already caused platform-version 
regressions (blank tab on 262). The model layer (`StrutsConfigDiagramModel`, 
nodes/edges) and presentation helpers were intentionally kept toolkit-neutral 
so rendering can move to the platform Diagrams API.
+
+Related UX follow-ups (#96 multi-file, #98 selection sync, #99 Structure tool 
window, #100 zoom/pan) would largely come for free—or be simpler—on the IDE 
diagram chrome, but are **not** in scope for this migration.
+
+## Goals
+
+1. Provide a Struts config diagram via the IntelliJ **Show Diagram** entry 
(Diagrams API), not a custom editor tab.
+2. Packages, actions, results, and edges match current model semantics 
(including chain/redirect action→action edges).
+3. Tooltips and double-click / navigate-to-XML still work via 
`StrutsDiagramPresentation`.
+4. Auto-refresh on DOM changes (or equivalent Diagrams refresh) while the 
diagram is open/visible.
+5. Plugin declares a **hard** dependency on `com.intellij.diagram`; builds and 
tests against IU.
+6. Custom Swing diagram UI and `PerspectiveFileEditor` path are removed.
+7. Changelog documents the migration and UX change (tab removed → Show 
Diagram).
+
+## Non-Goals
+
+- Fixing blank Diagram tab lifecycle of the current Swing host (already 
tracked/fixed separately).
+- Rewriting model building or Struts DOM converters.
+- Merged multi-file view (#96), selection sync (#98), Structure tool window 
(#99), or custom zoom/pan beyond platform chrome (#100).
+- Optional dependency / Community Edition fallback / keeping a Swing tab as 
backup.
+- Robot / UI e2e tests or layout pixel assertions.
+- Experimental Diagrams “v2” Builder API.
+
+## Decisions
+
+| Question | Decision |
+|---|---|
+| Entry point | **Show Diagram only** — remove the Diagram editor tab |
+| `com.intellij.diagram` dependency | **Hard** (plugin already targets IU 
features such as JavaEE/JSP) |
+| When Show Diagram is available | Any file recognized as a Struts 2 config 
(`StrutsManager.isStruts2ConfigFile`), **not** limited to file-set membership |
+| Scope | Pure host swap; #96–#100 remain separate issues |
+| Implementation approach | Classic `DiagramProvider` + snapshot 
`DiagramDataModel` consuming `StrutsConfigDiagramModel.build()` |
+
+### Alternatives considered
+
+| Approach | Verdict |
+|---|---|
+| Keep Diagram editor tab backed by Diagrams API | Rejected — retains custom 
editor lifecycle |
+| Show Diagram only (this design) | **Chosen** — matches Ultimate diagram UX; 
escapes `PerspectiveFileEditor` |
+| Both tab + Show Diagram | Rejected for v1 — two hosts to maintain |
+| Optional `com.intellij.diagram` + Swing fallback | Rejected — unnecessary 
given IU-only surface |
+| Experimental Diagrams v2 Builder API | Rejected — stability risk for Apache 
plugin on 262 |
+
+## Architecture
+
+```
+struts.xml (Psi/XmlFile)
+        │
+        ▼
+StrutsConfigDiagramModel.build()     ← unchanged (toolkit-neutral snapshot)
+        │
+        ▼
+StrutsDiagramDataModel                 ← NEW: DiagramDataModel adapter
+  maps StrutsDiagramNode/Edge
+  → Diagrams API nodes/edges
+        │
+        ▼
+StrutsDiagramProvider                  ← NEW: DiagramProvider + EP
+  Show Diagram action / dedicated diagram editor
+        │
+        ├── tooltips / navigate  → StrutsDiagramPresentation (unchanged)
+        └── live refresh         → DomEvent (while diagram open) → rebuild 
snapshot → refreshDataModel
+```
+
+**Kept:** `diagram.model`, `diagram.presentation`  
+**Removed:** `diagram.ui`, `diagram.fileEditor`, 
`PerspectiveFileEditorProvider` registration  
+**Added:** adapter package (e.g. `diagram.provider`) + hard 
`com.intellij.diagram` dependency  
+**Semantics:** Still file-local packages/actions/results + chain/redirect 
action→action edges — no multi-file merge
+
+## Components
+
+| Unit | Role |
+|---|---|
+| `StrutsConfigDiagramModel` / `StrutsDiagramNode` / `StrutsDiagramEdge` | 
Unchanged snapshot builder and graph types |
+| `StrutsDiagramPresentation` | Unchanged tooltips + navigate-to-XML |
+| `StrutsDiagramProvider` | `DiagramProvider` for Struts config; accept ≈ 
`isStruts2ConfigFile`; wires element manager, VFS resolver, data model factory |
+| `StrutsDiagramDataModel` | Holds current snapshot; `refreshDataModel()` 
rebuilds via `StrutsConfigDiagramModel.build(xmlFile)` and republishes 
nodes/edges |
+| Node/edge adapters | Thin wrappers: id, kind, name, icon, tooltip from 
snapshot; double-click → `navigateToElement` |
+| Dom refresh bridge | While the data model for file F is alive, DomEvents for 
F debounce (~300 ms) and call `refreshDataModel()` |
+| `plugin.xml` / Gradle | Hard depend on `com.intellij.diagram`; register 
`com.intellij.diagram.Provider`; remove `Struts2DiagramFileEditorProvider` |
+| Deleted | `Struts2DiagramComponent`, `Struts2DiagramFileEditor`, 
`Struts2DiagramFileEditorProvider` |
+
+No new public plugin extension points. Follow-ups #96–#100 stay out of this 
package set.
+
+Exact Diagrams API type names (`DiagramProvider` vs historical `Provider`, 
element manager interfaces, etc.) are resolved against the 262 IU SDK during 
implementation; the responsibilities above are fixed.
+
+## Data flow
+
+### Open
+
+1. User invokes Show Diagram on a Struts 2 config `XmlFile`.
+2. `StrutsDiagramProvider` accepts the element.
+3. Platform creates `StrutsDiagramDataModel` for that file.
+4. Initial `refreshDataModel()` runs under a read action → 
`StrutsConfigDiagramModel.build(xmlFile)` → adapters publish nodes/edges.
+5. Diagrams UI renders with platform layout/chrome (zoom/pan/toolbar).
+
+### Interact
+
+- Hover → precomputed tooltip HTML from the snapshot (via presentation 
helpers).
+- Double-click / navigate → `StrutsDiagramPresentation.navigateToElement`.
+
+### Live refresh
+
+Replaces tab `selectNotify` / `deselectNotify` gating from the auto-refresh 
design:
+
+- While the Struts diagram data model for file F is alive (diagram 
editor/popup still open): DomEvents for F debounce 300 ms → rebuild snapshot → 
`refreshDataModel()`.
+- No Diagram tab → no `selectNotify` catch-up; an already-open diagram 
refreshes on DomEvents; after dispose, nothing until Show Diagram again.
+- Other files’ DomEvents ignored; multi-file set merge remains out of scope.
+
+### Close / dispose
+
+- Dom listener and alarms dispose with the diagram/data-model lifecycle so 
closed diagrams do not refresh.
+
+## Error handling & edge cases
+
+| Scenario | Behavior |
+|---|---|
+| File not a Struts 2 config | Provider rejects; Show Diagram not offered |
+| `build()` returns `null` | Empty diagram + platform empty state (or provider 
message); no crash |
+| Model with no nodes | Empty diagram; same intent as today’s empty 
placeholder |
+| Unresolved / external chain-redirect targets | Keep current model semantics 
(labeled RESULT fallback) |
+| DomEvent after diagram closed | No-op — listener disposed / gated on open 
model |
+| Rapid edits while diagram open | 300 ms debounce; coalesce rebuilds |
+| Invalid / deleted file while diagram open | Refresh yields 
empty/unavailable; no PSI access on EDT outside read actions |
+| Navigation pointer stale | `navigateToElement` no-ops safely (existing 
behavior) |
+| Exceptions during build | Caught/logged by read-action path; diagram stays 
on last good snapshot or empty |
+
+## Testing
+
+### Automated
+
+- Keep `StrutsConfigDiagramModelTest` as the semantic source of truth 
(nodes/edges, chain/redirect).
+- Replace editor-provider / `PerspectiveFileEditor` lifecycle tests with 
provider-focused tests:
+  - Accept: Struts config yes; non-Struts XML no; **file-set membership not 
required**
+  - Adapter mapping: snapshot nodes/edges → Diagrams nodes/edges (ids, kinds, 
edge labels) without full UI paint
+  - Dom-refresh helper: same-file vs other-file filtering (pure static 
helpers, as today)
+
+### Out of scope for v1
+
+- Robot / UI e2e for Show Diagram chrome
+- Layout pixel assertions
+- Tests for #96–#100
+
+### Manual (`./gradlew runIde` on IU)
+
+1. Show Diagram on a Struts config → packages/actions/results + chain/redirect 
edges
+2. Edit XML while diagram open → debounced refresh
+3. Double-click node → navigates to XML
+4. Hover → tooltips
+5. Confirm Diagram **tab** is gone
+6. Zoom/pan via platform chrome works at a smoke level
+
+### Gate
+
+`./gradlew test -x rat --tests "com.intellij.struts2.diagram.*"` green; 
`CHANGELOG.md` notes migration, hard `com.intellij.diagram` dependency, and 
removal of the Diagram tab.
+
+## UX / changelog note
+
+This is a **breaking UX change** for users who relied on the Diagram editor 
tab: the tab is removed; diagrams are opened via the IDE **Show Diagram** 
action on a Struts 2 config file. Document that clearly in `CHANGELOG.md`.
+
+## Future work (out of scope)
+
+- [#96](https://github.com/apache/struts-intellij-plugin/issues/96) merged 
multi-file view across Struts file set
+- [#98](https://github.com/apache/struts-intellij-plugin/issues/98) sync 
selection with XML perspective
+- [#99](https://github.com/apache/struts-intellij-plugin/issues/99) Structure 
tool window integration
+- [#100](https://github.com/apache/struts-intellij-plugin/issues/100) 
zoom/pan/fit beyond what platform chrome already provides
+
+## References
+
+- Issue [#117](https://github.com/apache/struts-intellij-plugin/issues/117)
+- `com.intellij.struts2.diagram.model` package-info — migration boundary
+- `docs/superpowers/specs/2026-07-25-diagram-blank-262-design.md` — Swing host 
fix; migration deferred here
+- `docs/superpowers/specs/2026-06-25-diagram-auto-refresh-design.md` — 
DomEvent debounce semantics (adapted to open diagram, not tab selection)
+- Platform Explorer / OSS examples of `com.intellij.diagram.Provider` (e.g. 
JHipster) — learn-by-examples for undocumented Ultimate API
diff --git a/src/main/java/com/intellij/struts2/diagram/model/package-info.java 
b/src/main/java/com/intellij/struts2/diagram/model/package-info.java
index 635f83c..2e32711 100644
--- a/src/main/java/com/intellij/struts2/diagram/model/package-info.java
+++ b/src/main/java/com/intellij/struts2/diagram/model/package-info.java
@@ -18,20 +18,16 @@
 /**
  * Toolkit-neutral Struts configuration diagram model.
  *
- * <h2>Migration boundary</h2>
  * <p>The types in this package ({@link 
com.intellij.struts2.diagram.model.StrutsConfigDiagramModel},
  * {@link com.intellij.struts2.diagram.model.StrutsDiagramNode},
- * {@link com.intellij.struts2.diagram.model.StrutsDiagramEdge}) are 
intentionally independent of
- * the {@code com.intellij.diagram.Provider} (Diagrams API) that may be 
adopted in the future.</p>
- * <p>This isolation means that the rendering/editor layer (currently a 
lightweight Swing panel in
- * {@code com.intellij.struts2.diagram.ui}) can be replaced without touching 
the DOM traversal or
- * presentation logic. A future migration to {@code 
com.intellij.diagram.Provider} should:</p>
- * <ol>
- *   <li>Implement {@code DiagramProvider} / {@code DiagramDataModel} 
consuming the snapshot produced
- *       by {@link 
com.intellij.struts2.diagram.model.StrutsConfigDiagramModel#build}.</li>
- *   <li>Reuse {@link 
com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation} for
- *       tooltips and navigation.</li>
- *   <li>Replace only the {@code diagram.ui} and {@code diagram.fileEditor} 
packages.</li>
- * </ol>
+ * {@link com.intellij.struts2.diagram.model.StrutsDiagramEdge}) and the 
presentation helpers in
+ * {@code com.intellij.struts2.diagram.presentation} are intentionally 
independent of any UI toolkit.</p>
+ *
+ * <p>Two hosts currently consume this snapshot for comparison:</p>
+ * <ul>
+ *   <li>Custom Swing Diagram editor tab ({@code 
com.intellij.struts2.diagram.ui} /
+ *       {@code com.intellij.struts2.diagram.fileEditor})</li>
+ *   <li>IntelliJ Diagrams API Show Diagram ({@code 
com.intellij.struts2.diagram.provider})</li>
+ * </ul>
  */
 package com.intellij.struts2.diagram.model;
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java
new file mode 100644
index 0000000..4a905ce
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiEdge.java
@@ -0,0 +1,43 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.DiagramEdgeBase;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationshipInfoAdapter;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.diagram.presentation.DiagramLineType;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import org.jetbrains.annotations.NotNull;
+
+public final class StrutsDiagramApiEdge extends 
DiagramEdgeBase<StrutsDiagramItem> {
+
+    public StrutsDiagramApiEdge(@NotNull DiagramNode<StrutsDiagramItem> source,
+                                @NotNull DiagramNode<StrutsDiagramItem> target,
+                                @NotNull StrutsDiagramEdge snapshotEdge) {
+        super(source, target, relationshipFor(snapshotEdge));
+    }
+
+    private static @NotNull DiagramRelationshipInfo relationshipFor(@NotNull 
StrutsDiagramEdge edge) {
+        String label = edge.getLabel();
+        if (label.isEmpty()) {
+            return DiagramRelationships.DEPENDENCY;
+        }
+        return new DiagramRelationshipInfoAdapter(label, 
DiagramLineType.SOLID, label);
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java
new file mode 100644
index 0000000..ebdb04f
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramApiNode.java
@@ -0,0 +1,68 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.DiagramNodeBase;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramApiNode extends 
DiagramNodeBase<StrutsDiagramItem> {
+
+    private final @NotNull StrutsDiagramItem item;
+
+    public StrutsDiagramApiNode(@NotNull DiagramProvider<StrutsDiagramItem> 
provider,
+                                @NotNull StrutsDiagramItem item) {
+        super(provider);
+        this.item = item;
+    }
+
+    @Override
+    public @NotNull StrutsDiagramItem getIdentifyingElement() {
+        return item;
+    }
+
+    @Override
+    public @Nullable String getTooltip() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null ? node.getTooltipHtml() : null;
+    }
+
+    @Override
+    public @Nullable Icon getIcon() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null ? node.getIcon() : null;
+    }
+
+    @Override
+    public void navigate(boolean requestFocus) {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        if (node != null) {
+            StrutsDiagramPresentation.navigateToElement(node);
+        }
+    }
+
+    @Override
+    public boolean canNavigate() {
+        StrutsDiagramNode node = item.getSnapshotNode();
+        return node != null && node.getNavigationPointer() != null;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
new file mode 100644
index 0000000..ea10c83
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDataModel.java
@@ -0,0 +1,173 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.diagram.DiagramDataKeys;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.ModificationTracker;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.util.Alarm;
+import com.intellij.util.concurrency.AppExecutorUtil;
+import com.intellij.util.xml.DomManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class StrutsDiagramDataModel extends 
DiagramDataModel<StrutsDiagramItem> {
+
+    private static final int DOM_UPDATE_DELAY_MS = 300;
+
+    private final List<DiagramNode<StrutsDiagramItem>> nodes = new 
ArrayList<>();
+    private final List<DiagramEdge<StrutsDiagramItem>> edges = new 
ArrayList<>();
+    private final Alarm updateAlarm = new 
Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
+    private final @Nullable XmlFile xmlFile;
+    private final @Nullable VirtualFile virtualFile;
+
+    public StrutsDiagramDataModel(@NotNull Project project,
+                                  @NotNull StrutsDiagramProvider provider,
+                                  @Nullable StrutsDiagramItem seed) {
+        super(project, provider);
+        xmlFile = seed != null ? seed.getXmlFile() : null;
+        virtualFile = xmlFile != null ? xmlFile.getVirtualFile() : null;
+        if (seed != null) {
+            setOriginalElement(seed);
+        }
+        if (virtualFile != null) {
+            DomManager.getDomManager(project).addDomEventListener(event -> {
+                if (StrutsDiagramDomRefresh.isEventForMyFile(event, 
virtualFile)) {
+                    queueDebouncedRefresh();
+                }
+            }, this);
+        }
+    }
+
+    @Override
+    public @NotNull ModificationTracker getModificationTracker() {
+        return PsiManager.getInstance(getProject()).getModificationTracker();
+    }
+
+    @Override
+    public @NotNull Collection<? extends DiagramNode<StrutsDiagramItem>> 
getNodes() {
+        return Collections.unmodifiableList(nodes);
+    }
+
+    @Override
+    public @NotNull Collection<? extends DiagramEdge<StrutsDiagramItem>> 
getEdges() {
+        return Collections.unmodifiableList(edges);
+    }
+
+    @Override
+    public @NotNull String getNodeName(@NotNull DiagramNode<StrutsDiagramItem> 
node) {
+        StrutsDiagramItem item = node.getIdentifyingElement();
+        String title = getProvider().getElementManager().getElementTitle(item);
+        return title != null ? title : "";
+    }
+
+    @Override
+    public @Nullable DiagramNode<StrutsDiagramItem> addElement(@Nullable 
StrutsDiagramItem element) {
+        return null;
+    }
+
+    /**
+     * Rebuilds the API model from the current Struts configuration snapshot.
+     * Must be invoked under a read action because model construction accesses 
PSI/DOM.
+     */
+    @Override
+    public void refreshDataModel() {
+        applyApiModel(buildApiModel());
+    }
+
+    private @NotNull ApiModel buildApiModel() {
+        StrutsConfigDiagramModel snapshot =
+                xmlFile != null ? StrutsConfigDiagramModel.build(xmlFile) : 
null;
+        if (xmlFile == null || snapshot == null) {
+            return ApiModel.EMPTY;
+        }
+        List<DiagramNode<StrutsDiagramItem>> newNodes = new ArrayList<>();
+        List<DiagramEdge<StrutsDiagramItem>> newEdges = new ArrayList<>();
+        Map<StrutsDiagramNode, DiagramNode<StrutsDiagramItem>> map = new 
IdentityHashMap<>();
+        for (StrutsDiagramNode snapshotNode : snapshot.getNodes()) {
+            StrutsDiagramItem item = StrutsDiagramItem.forNode(xmlFile, 
snapshotNode);
+            StrutsDiagramApiNode apiNode = new 
StrutsDiagramApiNode(getProvider(), item);
+            newNodes.add(apiNode);
+            map.put(snapshotNode, apiNode);
+        }
+        for (StrutsDiagramEdge snapshotEdge : snapshot.getEdges()) {
+            DiagramNode<StrutsDiagramItem> source = 
map.get(snapshotEdge.getSource());
+            DiagramNode<StrutsDiagramItem> target = 
map.get(snapshotEdge.getTarget());
+            if (source != null && target != null) {
+                newEdges.add(new StrutsDiagramApiEdge(source, target, 
snapshotEdge));
+            }
+        }
+        return new ApiModel(newNodes, newEdges);
+    }
+
+    private void applyApiModel(@NotNull ApiModel model) {
+        nodes.clear();
+        nodes.addAll(model.nodes());
+        edges.clear();
+        edges.addAll(model.edges());
+    }
+
+    private void applyLiveUpdate(@NotNull ApiModel model) {
+        applyApiModel(model);
+        DiagramBuilder builder = getUserData(DiagramDataKeys.GRAPH_BUILDER);
+        if (builder != null) {
+            DiagramDataModel.refreshDataModelInSmartMode(builder);
+        }
+    }
+
+    private void queueDebouncedRefresh() {
+        updateAlarm.cancelAllRequests();
+        updateAlarm.addRequest(this::scheduleRefresh, DOM_UPDATE_DELAY_MS);
+    }
+
+    private void scheduleRefresh() {
+        ReadAction.nonBlocking(this::buildApiModel)
+                .expireWith(this)
+                .coalesceBy(this)
+                .finishOnUiThread(ModalityState.defaultModalityState(), 
this::applyLiveUpdate)
+                .submit(AppExecutorUtil.getAppExecutorService());
+    }
+
+    private record ApiModel(@NotNull List<DiagramNode<StrutsDiagramItem>> 
nodes,
+                            @NotNull List<DiagramEdge<StrutsDiagramItem>> 
edges) {
+
+        private static final ApiModel EMPTY = new ApiModel(List.of(), 
List.of());
+    }
+
+    @Override
+    public void dispose() {
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
new file mode 100644
index 0000000..3c7dbd0
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramDomRefresh.java
@@ -0,0 +1,44 @@
+/*
+ * 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.provider;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xml.DomElement;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.events.DomEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Pure helpers for same-file DomEvent filtering used by {@link 
StrutsDiagramDataModel}.
+ */
+public final class StrutsDiagramDomRefresh {
+
+    private StrutsDiagramDomRefresh() {}
+
+    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/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
new file mode 100644
index 0000000..4763a42
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramElementManager.java
@@ -0,0 +1,100 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.AbstractDiagramElementManager;
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramElementManager extends 
AbstractDiagramElementManager<StrutsDiagramItem> {
+
+    @Override
+    public @Nullable StrutsDiagramItem findInDataContext(@NotNull DataContext 
context) {
+        PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(context);
+        if (!(psiFile instanceof XmlFile xmlFile)) {
+            return null;
+        }
+        if 
(!StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile)) 
{
+            return null;
+        }
+        return StrutsDiagramItem.forFile(xmlFile);
+    }
+
+    @Override
+    public boolean canBeBuiltFrom(Object element) {
+        if (element instanceof StrutsDiagramItem item) {
+            XmlFile file = item.getXmlFile();
+            return file != null
+                    && 
StrutsManager.getInstance(file.getProject()).isStruts2ConfigFile(file);
+        }
+        if (element instanceof XmlFile xmlFile) {
+            return 
StrutsManager.getInstance(xmlFile.getProject()).isStruts2ConfigFile(xmlFile);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isAcceptableAsNode(Object element) {
+        return element instanceof StrutsDiagramItem item && !item.isRoot();
+    }
+
+    @Override
+    public @Nullable @Nls String getElementTitle(StrutsDiagramItem item) {
+        if (item == null) return null;
+        StrutsDiagramNode node = item.getSnapshotNode();
+        if (node != null) return node.getName();
+        XmlFile file = item.getXmlFile();
+        return file != null ? file.getName() : null;
+    }
+
+    @Override
+    public @Nullable String getNodeTooltip(StrutsDiagramItem item) {
+        if (item == null || item.getSnapshotNode() == null) return null;
+        return item.getSnapshotNode().getTooltipHtml();
+    }
+
+    @Override
+    public @Nullable PsiElement getItemDocOwner(Object element, DiagramBuilder 
builder) {
+        return element instanceof StrutsDiagramItem item
+                ? StrutsDiagramExtras.resolvePsiElement(item)
+                : null;
+    }
+
+    @Override
+    public Object[] getNodeItems(StrutsDiagramItem element) {
+        return EMPTY_ARRAY;
+    }
+
+    @Override
+    public boolean canCollapse(StrutsDiagramItem element) {
+        return false;
+    }
+
+    @Override
+    public boolean isContainerFor(StrutsDiagramItem parent, StrutsDiagramItem 
child) {
+        return false;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java
new file mode 100644
index 0000000..0ee8f34
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramExtras.java
@@ -0,0 +1,100 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.DiagramBuilder;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.extras.DiagramExtras;
+import com.intellij.diagram.extras.EditNodeHandler;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataSink;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.pom.Navigatable;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlElement;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.presentation.StrutsDiagramPresentation;
+import com.intellij.util.containers.ContainerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Diagrams host hooks for Struts config diagrams.
+ * <p>
+ * Double-click in the diagram <em>editor</em> (non-popup) goes through
+ * {@link EditNodeHandler}, not {@link DiagramNode}'s {@link Navigatable} API.
+ * Popup mode and Jump to Source use {@link #uiDataSnapshot}.
+ */
+public final class StrutsDiagramExtras extends 
DiagramExtras<StrutsDiagramItem> {
+
+    private final EditNodeHandler<StrutsDiagramItem> editNodeHandler = 
this::navigateNode;
+
+    @Override
+    public @NotNull EditNodeHandler<StrutsDiagramItem> getEditNodeHandler() {
+        return editNodeHandler;
+    }
+
+    @Override
+    public void uiDataSnapshot(@NotNull DataSink sink,
+                               @NotNull List<DiagramNode<StrutsDiagramItem>> 
nodes,
+                               @NotNull DiagramBuilder builder) {
+        DiagramNode<StrutsDiagramItem> only = ContainerUtil.getOnlyItem(nodes);
+        if (only == null) {
+            return;
+        }
+        sink.lazy(CommonDataKeys.PSI_ELEMENT, () -> 
resolvePsiElement(only.getIdentifyingElement()));
+        sink.lazy(CommonDataKeys.NAVIGATABLE, () -> {
+            if (only instanceof Navigatable navigatable && 
navigatable.canNavigate()) {
+                return navigatable;
+            }
+            PsiElement psi = resolvePsiElement(only.getIdentifyingElement());
+            return psi instanceof Navigatable ? (Navigatable) psi : null;
+        });
+    }
+
+    private void navigateNode(@NotNull DiagramNode<StrutsDiagramItem> node,
+                              DiagramPresentationModel presentationModel) {
+        StrutsDiagramItem item = node.getIdentifyingElement();
+        if (item == null) {
+            return;
+        }
+        StrutsDiagramNode snapshotNode = item.getSnapshotNode();
+        if (snapshotNode != null) {
+            StrutsDiagramPresentation.navigateToElement(snapshotNode);
+        }
+    }
+
+    public static @Nullable PsiElement resolvePsiElement(@Nullable 
StrutsDiagramItem item) {
+        if (item == null) {
+            return null;
+        }
+        StrutsDiagramNode snapshotNode = item.getSnapshotNode();
+        if (snapshotNode == null) {
+            return item.getXmlFile();
+        }
+        SmartPsiElementPointer<XmlElement> pointer = 
snapshotNode.getNavigationPointer();
+        if (pointer == null) {
+            return null;
+        }
+        return ApplicationManager.getApplication().runReadAction(
+                (com.intellij.openapi.util.Computable<PsiElement>) 
pointer::getElement);
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java
new file mode 100644
index 0000000..0c50767
--- /dev/null
+++ b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramItem.java
@@ -0,0 +1,88 @@
+/*
+ * 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.provider;
+
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.SmartPointerManager;
+import com.intellij.psi.SmartPsiElementPointer;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Identifying element for Struts Diagrams API nodes.
+ * Root items ({@code snapshotNode == null}) seed Show Diagram from an XML 
file.
+ */
+public final class StrutsDiagramItem {
+
+    private final @NotNull SmartPsiElementPointer<XmlFile> filePointer;
+    private final @Nullable StrutsDiagramNode snapshotNode;
+
+    private StrutsDiagramItem(@NotNull SmartPsiElementPointer<XmlFile> 
filePointer,
+                              @Nullable StrutsDiagramNode snapshotNode) {
+        this.filePointer = filePointer;
+        this.snapshotNode = snapshotNode;
+    }
+
+    public static @NotNull StrutsDiagramItem forFile(@NotNull XmlFile file) {
+        return new StrutsDiagramItem(
+                
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+                null);
+    }
+
+    public static @NotNull StrutsDiagramItem forNode(@NotNull XmlFile file, 
@NotNull StrutsDiagramNode node) {
+        return new StrutsDiagramItem(
+                
SmartPointerManager.getInstance(file.getProject()).createSmartPsiElementPointer(file),
+                node);
+    }
+
+    public @Nullable XmlFile getXmlFile() {
+        return filePointer.getElement();
+    }
+
+    public @Nullable StrutsDiagramNode getSnapshotNode() {
+        return snapshotNode;
+    }
+
+    public boolean isRoot() {
+        return snapshotNode == null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof StrutsDiagramItem that)) return false;
+        if (snapshotNode == null || that.snapshotNode == null) {
+            return snapshotNode == that.snapshotNode
+                    && Objects.equals(fileUrl(), that.fileUrl());
+        }
+        return snapshotNode.equals(that.snapshotNode);
+    }
+
+    @Override
+    public int hashCode() {
+        return snapshotNode != null ? snapshotNode.hashCode() : 
Objects.hash(fileUrl());
+    }
+
+    private @Nullable String fileUrl() {
+        PsiFile file = filePointer.getElement();
+        return file != null && file.getVirtualFile() != null ? 
file.getVirtualFile().getUrl() : null;
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
new file mode 100644
index 0000000..be9334c
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.BaseDiagramProvider;
+import com.intellij.diagram.DiagramDataModel;
+import com.intellij.diagram.DiagramElementManager;
+import com.intellij.diagram.DiagramPresentationModel;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.diagram.extras.DiagramExtras;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.struts2.Struts2Icons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public final class StrutsDiagramProvider extends 
BaseDiagramProvider<StrutsDiagramItem> {
+
+    public static final String ID = "ApacheStrutsConfig";
+
+    private final DiagramElementManager<StrutsDiagramItem> elementManager = 
new StrutsDiagramElementManager();
+    private final DiagramVfsResolver<StrutsDiagramItem> vfsResolver = new 
StrutsDiagramVfsResolver();
+    private final DiagramExtras<StrutsDiagramItem> extras = new 
StrutsDiagramExtras();
+
+    @Override
+    public @NotNull String getID() {
+        return ID;
+    }
+
+    @Override
+    public @NotNull @Nls String getPresentableName() {
+        return "Struts Configuration";
+    }
+
+    @Override
+    public @Nullable Icon getActionIcon(boolean isPopup) {
+        return Struts2Icons.Action;
+    }
+
+    @Override
+    public @NotNull DiagramElementManager<StrutsDiagramItem> 
getElementManager() {
+        return elementManager;
+    }
+
+    @Override
+    public @NotNull DiagramVfsResolver<StrutsDiagramItem> getVfsResolver() {
+        return vfsResolver;
+    }
+
+    @Override
+    public @NotNull DiagramExtras<StrutsDiagramItem> getExtras() {
+        return extras;
+    }
+
+    @Override
+    public @NotNull DiagramDataModel<StrutsDiagramItem> 
createDataModel(@NotNull Project project,
+                                                                       
@Nullable StrutsDiagramItem element,
+                                                                       
@Nullable VirtualFile file,
+                                                                       
@NotNull DiagramPresentationModel presentationModel) {
+        return new StrutsDiagramDataModel(project, this, element);
+    }
+}
diff --git 
a/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
new file mode 100644
index 0000000..8001e33
--- /dev/null
+++ 
b/src/main/java/com/intellij/struts2/diagram/provider/StrutsDiagramVfsResolver.java
@@ -0,0 +1,71 @@
+/*
+ * 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.provider;
+
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class StrutsDiagramVfsResolver implements 
DiagramVfsResolver<StrutsDiagramItem> {
+
+    @Override
+    public @Nullable String getQualifiedName(StrutsDiagramItem item) {
+        if (item == null) return null;
+        XmlFile file = item.getXmlFile();
+        if (file == null || file.getVirtualFile() == null) return null;
+        if (item.getSnapshotNode() != null) {
+            return file.getVirtualFile().getUrl() + "#" + 
item.getSnapshotNode().getId();
+        }
+        return file.getVirtualFile().getUrl();
+    }
+
+    @Override
+    public @Nullable StrutsDiagramItem resolveElementByFQN(@NotNull String 
fqn, @NotNull Project project) {
+        int nodeSeparator = fqn.indexOf('#');
+        String url = nodeSeparator >= 0 ? fqn.substring(0, nodeSeparator) : 
fqn;
+        VirtualFile vf = VirtualFileManager.getInstance().findFileByUrl(url);
+        if (vf == null) return null;
+        PsiFile psi = PsiManager.getInstance(project).findFile(vf);
+        if (!(psi instanceof XmlFile xmlFile)) return null;
+        if (nodeSeparator >= 0) {
+            String nodeId = fqn.substring(nodeSeparator + 1);
+            StrutsConfigDiagramModel model = 
ApplicationManager.getApplication().isReadAccessAllowed()
+                    ? StrutsConfigDiagramModel.build(xmlFile)
+                    : ReadAction.compute(() -> 
StrutsConfigDiagramModel.build(xmlFile));
+            if (model != null) {
+                StrutsDiagramNode node = model.getNodes().stream()
+                        .filter(candidate -> candidate.getId().equals(nodeId))
+                        .findFirst()
+                        .orElse(null);
+                if (node != null) {
+                    return StrutsDiagramItem.forNode(xmlFile, node);
+                }
+            }
+        }
+        return StrutsDiagramItem.forFile(xmlFile);
+    }
+}
diff --git a/src/main/resources/META-INF/plugin.xml 
b/src/main/resources/META-INF/plugin.xml
index 27afd7d..681e2f5 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -35,6 +35,7 @@
         <module name="intellij.css.backend"/>
         <plugin id="intellij.structureView.plugin"/>
         <plugin id="com.intellij.java-i18n"/>
+        <plugin id="com.intellij.diagram"/>
     </dependencies>
 
     <!-- optional dependencies -->
@@ -181,6 +182,7 @@
                 
implementation="com.intellij.struts2.structure.StrutsStructureViewBuilderProvider"/>
 
         <fileEditorProvider 
implementation="com.intellij.struts2.diagram.fileEditor.Struts2DiagramFileEditorProvider"/>
+        <diagram.Provider 
implementation="com.intellij.struts2.diagram.provider.StrutsDiagramProvider"/>
 
         <struts2.resultContributor
                 
implementation="com.intellij.struts2.dom.struts.impl.path.DispatchPathResultContributor"/>
diff --git 
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
 
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
new file mode 100644
index 0000000..3d63f69
--- /dev/null
+++ 
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDataModelMappingTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.diagram.DiagramEdge;
+import com.intellij.diagram.DiagramNode;
+import com.intellij.diagram.DiagramProvider;
+import com.intellij.diagram.DiagramRelationshipInfo;
+import com.intellij.diagram.DiagramRelationships;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramEdge;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramDataModel;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StrutsDiagramDataModelMappingTest extends 
BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testRefreshMapsSnapshotNodesAndEdges() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(vf);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vf);
+        assertNotNull(xml);
+
+        StrutsConfigDiagramModel snapshot = ReadAction.nonBlocking(
+                () -> 
StrutsConfigDiagramModel.build(xml)).executeSynchronously();
+        assertNotNull(snapshot);
+        assertFalse(snapshot.getNodes().isEmpty());
+
+        DiagramProvider<?> diagramProvider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull(diagramProvider);
+        assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+        StrutsDiagramProvider provider = (StrutsDiagramProvider) 
diagramProvider;
+        StrutsDiagramDataModel dataModel =
+                new StrutsDiagramDataModel(getProject(), provider, 
StrutsDiagramItem.forFile(xml));
+        try {
+            ReadAction.run(dataModel::refreshDataModel);
+
+            assertEquals(snapshot.getNodes().size(), 
dataModel.getNodes().size());
+            assertEquals(snapshot.getEdges().size(), 
dataModel.getEdges().size());
+
+            Set<String> snapshotIds = snapshot.getNodes().stream()
+                    .map(StrutsDiagramNode::getId).collect(Collectors.toSet());
+            Set<String> apiIds = dataModel.getNodes().stream()
+                    .map(DiagramNode::getIdentifyingElement)
+                    .map(StrutsDiagramItem::getSnapshotNode)
+                    .map(StrutsDiagramNode::getId)
+                    .collect(Collectors.toSet());
+            assertEquals(snapshotIds, apiIds);
+
+            Set<EdgeTriple> snapshotEdgeTriples = snapshot.getEdges().stream()
+                    .map(StrutsDiagramDataModelMappingTest::snapshotEdgeTriple)
+                    .collect(Collectors.toSet());
+            Set<EdgeTriple> apiEdgeTriples = dataModel.getEdges().stream()
+                    .map(StrutsDiagramDataModelMappingTest::apiEdgeTriple)
+                    .collect(Collectors.toSet());
+            assertEquals("API edges must mirror snapshot endpoints and 
labels", snapshotEdgeTriples, apiEdgeTriples);
+
+            for (DiagramEdge<StrutsDiagramItem> edge : dataModel.getEdges()) {
+                
assertNotNull(edge.getSource().getIdentifyingElement().getSnapshotNode());
+                
assertNotNull(edge.getTarget().getIdentifyingElement().getSnapshotNode());
+                verifyRelationshipMapping(edge);
+            }
+        } finally {
+            Disposer.dispose(dataModel);
+        }
+    }
+
+    public void testExposedNodeAndEdgeCollectionsAreUnmodifiable() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(vf);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vf);
+        assertNotNull(xml);
+
+        DiagramProvider<?> diagramProvider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+        StrutsDiagramDataModel dataModel = new StrutsDiagramDataModel(
+                getProject(), (StrutsDiagramProvider) diagramProvider, 
StrutsDiagramItem.forFile(xml));
+        try {
+            ReadAction.run(dataModel::refreshDataModel);
+
+            assertThrows(UnsupportedOperationException.class, () -> 
dataModel.getNodes().clear());
+            assertThrows(UnsupportedOperationException.class, () -> 
dataModel.getEdges().clear());
+        } finally {
+            Disposer.dispose(dataModel);
+        }
+    }
+
+    public void testSameFileDomEventRefreshesLiveDataModel() throws 
InterruptedException {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile vf = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(vf);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(vf);
+        assertNotNull(xml);
+
+        DiagramProvider<?> diagramProvider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertInstanceOf(diagramProvider, StrutsDiagramProvider.class);
+        StrutsDiagramDataModel dataModel = new StrutsDiagramDataModel(
+                getProject(), (StrutsDiagramProvider) diagramProvider, 
StrutsDiagramItem.forFile(xml));
+        try {
+            ReadAction.run(dataModel::refreshDataModel);
+            int initialNodeCount = dataModel.getNodes().size();
+
+            Document document = 
PsiDocumentManager.getInstance(getProject()).getDocument(xml);
+            assertNotNull(document);
+            WriteCommandAction.runWriteCommandAction(getProject(), () -> {
+                String updated = document.getText().replace(
+                        "</package>",
+                        "<action name=\"liveRefresh\" 
class=\"MyClass\"/>\n</package>");
+                document.setText(updated);
+                
PsiDocumentManager.getInstance(getProject()).commitDocument(document);
+            });
+
+            long deadline = System.currentTimeMillis() + 10_000;
+            while (System.currentTimeMillis() < deadline
+                    && dataModel.getNodes().size() == initialNodeCount) {
+                UIUtil.dispatchAllInvocationEvents();
+                Thread.sleep(50);
+            }
+
+            assertTrue("Same-file DomEvent must refresh the live diagram data 
model",
+                    dataModel.getNodes().size() > initialNodeCount);
+        } finally {
+            Disposer.dispose(dataModel);
+        }
+    }
+
+    private record EdgeTriple(@NotNull String sourceId, @NotNull String 
targetId, @NotNull String label) {
+    }
+
+    private static @NotNull EdgeTriple snapshotEdgeTriple(@NotNull 
StrutsDiagramEdge edge) {
+        return new EdgeTriple(edge.getSource().getId(), 
edge.getTarget().getId(), edge.getLabel());
+    }
+
+    private static @NotNull EdgeTriple apiEdgeTriple(@NotNull 
DiagramEdge<StrutsDiagramItem> edge) {
+        StrutsDiagramNode source = 
edge.getSource().getIdentifyingElement().getSnapshotNode();
+        StrutsDiagramNode target = 
edge.getTarget().getIdentifyingElement().getSnapshotNode();
+        assertNotNull(source);
+        assertNotNull(target);
+        return new EdgeTriple(source.getId(), target.getId(), 
apiEdgeLabel(edge));
+    }
+
+    private static @NotNull String apiEdgeLabel(@NotNull 
DiagramEdge<StrutsDiagramItem> edge) {
+        DiagramRelationshipInfo relationship = edge.getRelationship();
+        if (relationship == DiagramRelationships.DEPENDENCY) {
+            return "";
+        }
+        String label = centerLabelText(relationship.getUpperCenterLabel());
+        if (label.isEmpty()) {
+            label = centerLabelText(relationship.getBottomCenterLabel());
+        }
+        return label;
+    }
+
+    private static @NotNull String centerLabelText(@Nullable 
DiagramRelationshipInfo.Label label) {
+        if (label == null) {
+            return "";
+        }
+        String text = label.getText();
+        return text != null ? text : "";
+    }
+
+    private static void verifyRelationshipMapping(@NotNull 
DiagramEdge<StrutsDiagramItem> edge) {
+        String label = apiEdgeLabel(edge);
+        if (label.isEmpty()) {
+            assertSame("Unlabeled edges must use DEPENDENCY", 
DiagramRelationships.DEPENDENCY, edge.getRelationship());
+        }
+        else {
+            assertNotSame("Labeled edges must not use DEPENDENCY", 
DiagramRelationships.DEPENDENCY, edge.getRelationship());
+            assertEquals(label, 
centerLabelText(edge.getRelationship().getUpperCenterLabel()));
+        }
+    }
+}
diff --git 
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java 
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.java
new file mode 100644
index 0000000..70241d8
--- /dev/null
+++ 
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramDomRefreshTest.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.provider.StrutsDiagramDomRefresh;
+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 StrutsDiagramDomRefreshTest 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(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfA));
+            assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(pkg, vfB));
+        });
+    }
+
+    public void testIsDomElementInFileRejectsNullElement() {
+        createStrutsFileSet("struts-local-a.xml");
+        VirtualFile vfA = myFixture.findFileInTempDir("struts-local-a.xml");
+        assertNotNull(vfA);
+        assertFalse(StrutsDiagramDomRefresh.isDomElementInFile(null, vfA));
+    }
+}
diff --git 
a/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java 
b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
new file mode 100644
index 0000000..5ca6956
--- /dev/null
+++ b/src/test/java/com/intellij/struts2/diagram/StrutsDiagramProviderTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.diagram.DiagramProvider;
+import com.intellij.diagram.DiagramVfsResolver;
+import com.intellij.diagram.extras.EditNodeHandler;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.struts2.BasicLightHighlightingTestCase;
+import com.intellij.struts2.diagram.model.StrutsConfigDiagramModel;
+import com.intellij.struts2.diagram.model.StrutsDiagramNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramApiNode;
+import com.intellij.struts2.diagram.provider.StrutsDiagramExtras;
+import com.intellij.struts2.diagram.provider.StrutsDiagramItem;
+import com.intellij.struts2.diagram.provider.StrutsDiagramProvider;
+import org.jetbrains.annotations.NotNull;
+
+public class StrutsDiagramProviderTest extends BasicLightHighlightingTestCase {
+
+    @Override
+    @NotNull
+    protected String getTestDataLocation() {
+        return "diagram";
+    }
+
+    public void testProviderExtensionIsRegistered() {
+        DiagramProvider<?> provider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull("Struts DiagramProvider must be registered via 
com.intellij.diagram.Provider",
+                provider);
+        assertInstanceOf(provider, StrutsDiagramProvider.class);
+    }
+
+    public void testAcceptsStrutsConfigWithoutFileSet() {
+        VirtualFile file = myFixture.copyFileToProject("struts-diagram.xml");
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(file);
+        assertNotNull(xml);
+        StrutsDiagramProvider provider = getProvider();
+        DataContext ctx = SimpleDataContext.builder()
+                .add(CommonDataKeys.PSI_FILE, xml)
+                .add(CommonDataKeys.PROJECT, getProject())
+                .build();
+        StrutsDiagramItem item = 
provider.getElementManager().findInDataContext(ctx);
+        assertNotNull("Show Diagram must accept Struts config outside a file 
set", item);
+        assertEquals(xml, item.getXmlFile());
+        assertNull(item.getSnapshotNode());
+    }
+
+    public void testRejectsPlainXml() {
+        XmlFile xml = (XmlFile) myFixture.configureByText("plain.xml", 
"<root/>");
+        StrutsDiagramProvider provider = getProvider();
+        DataContext ctx = SimpleDataContext.builder()
+                .add(CommonDataKeys.PSI_FILE, xml)
+                .add(CommonDataKeys.PROJECT, getProject())
+                .build();
+        assertNull(provider.getElementManager().findInDataContext(ctx));
+        assertFalse(provider.getElementManager().canBeBuiltFrom(xml));
+    }
+
+    public void testVfsResolverRoundTripsSnapshotNodeFqn() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(file);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(file);
+        assertNotNull(xml);
+
+        StrutsConfigDiagramModel model = ReadAction.compute(() -> 
StrutsConfigDiagramModel.build(xml));
+        assertNotNull(model);
+        StrutsDiagramNode node = model.getNodes().get(0);
+        DiagramVfsResolver<StrutsDiagramItem> resolver = 
getProvider().getVfsResolver();
+        String fqn = resolver.getQualifiedName(StrutsDiagramItem.forNode(xml, 
node));
+        assertNotNull(fqn);
+
+        StrutsDiagramItem resolved = resolver.resolveElementByFQN(fqn, 
getProject());
+
+        assertNotNull(resolved);
+        assertNotNull(resolved.getSnapshotNode());
+        assertEquals(node.getId(), resolved.getSnapshotNode().getId());
+    }
+
+    public void testExtrasProvideEditNodeHandlerForEditorDoubleClick() {
+        StrutsDiagramProvider provider = getProvider();
+        EditNodeHandler<StrutsDiagramItem> handler = 
provider.getExtras().getEditNodeHandler();
+        assertNotNull(
+                "Editor-mode double-click uses EditNodeHandler, not 
DiagramNode.navigate()",
+                handler);
+    }
+
+    public void testResolvePsiElementFromSnapshotNode() {
+        createStrutsFileSet("struts-diagram.xml");
+        VirtualFile file = myFixture.findFileInTempDir("struts-diagram.xml");
+        assertNotNull(file);
+        XmlFile xml = (XmlFile) 
PsiManager.getInstance(getProject()).findFile(file);
+        assertNotNull(xml);
+
+        StrutsConfigDiagramModel model = ReadAction.compute(() -> 
StrutsConfigDiagramModel.build(xml));
+        assertNotNull(model);
+        StrutsDiagramNode snapshotNode = model.getNodes().stream()
+                .filter(n -> n.getKind() == StrutsDiagramNode.Kind.ACTION)
+                .findFirst()
+                .orElseThrow();
+        StrutsDiagramItem item = StrutsDiagramItem.forNode(xml, snapshotNode);
+
+        PsiElement psi = StrutsDiagramExtras.resolvePsiElement(item);
+        assertNotNull(psi);
+        assertTrue(psi.isValid());
+        assertEquals(xml, psi.getContainingFile());
+
+        StrutsDiagramApiNode apiNode = new StrutsDiagramApiNode(getProvider(), 
item);
+        assertTrue(apiNode.canNavigate());
+    }
+
+    private StrutsDiagramProvider getProvider() {
+        DiagramProvider<?> provider = 
DiagramProvider.findByID(StrutsDiagramProvider.ID);
+        assertNotNull(provider);
+        assertInstanceOf(provider, StrutsDiagramProvider.class);
+        return (StrutsDiagramProvider) provider;
+    }
+}

Reply via email to