From 9e03fb94d0109a0643cf2a87ca64311e2f78e6e2 Mon Sep 17 00:00:00 2001
From: Johannes Mey <johannes.mey@tu-dresden.de>
Date: Mon, 29 Nov 2021 02:03:10 +0100
Subject: [PATCH] work on the structure view.

---
 .../aspect/AspectStructureViewElement.java    | 93 +++++++++++++++++++
 .../aspect/AspectStructureViewFactory.java    | 27 ++++++
 .../aspect/AspectStructureViewModel.java      | 48 ++++++++++
 .../aspect/JastAddAspectAttributeFilter.java  | 44 +++++++++
 .../aspect/psi/AspectElementFactory.java      |  9 ++
 .../aspect/psi/JastAddAspectAttribute.java    |  3 +-
 ...dAspectAspectDeclarationImplExtension.java | 67 +++++++++++++
 ...JastAddAspectAstTypeNameImplExtension.java | 18 ++--
 ...pectClassOrInterfaceTypeImplExtension.java | 19 ++--
 .../impl/JastAddAspectCollAttributeImpl.java  | 63 ++++++++++++-
 .../impl/JastAddAspectInhAttributeImpl.java   | 63 ++++++++++++-
 .../impl/JastAddAspectSynAttributeImpl.java   | 64 ++++++++++++-
 .../impl/GrammarTypeDeclImplExtension.java    | 18 ++--
 src/main/resources/META-INF/plugin.xml        |  3 +-
 14 files changed, 509 insertions(+), 30 deletions(-)
 create mode 100644 src/main/java/org/jastadd/tooling/aspect/AspectStructureViewElement.java
 create mode 100644 src/main/java/org/jastadd/tooling/aspect/AspectStructureViewFactory.java
 create mode 100644 src/main/java/org/jastadd/tooling/aspect/AspectStructureViewModel.java
 create mode 100644 src/main/java/org/jastadd/tooling/aspect/JastAddAspectAttributeFilter.java
 create mode 100644 src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAspectDeclarationImplExtension.java

diff --git a/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewElement.java b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewElement.java
new file mode 100644
index 0000000..e4e40a1
--- /dev/null
+++ b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewElement.java
@@ -0,0 +1,93 @@
+package org.jastadd.tooling.aspect;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.SortableTreeElement;
+import com.intellij.ide.util.treeView.smartTree.TreeElement;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.psi.NavigatablePsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jastadd.tooling.aspect.psi.AspectFile;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectBodyDeclaration;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectDeclaration;
+import org.jastadd.tooling.aspect.psi.JastAddAspectTypeDeclaration;
+import org.jastadd.tooling.aspect.psi.impl.JastAddAspectAspectDeclarationImpl;
+import org.jastadd.tooling.aspect.psi.impl.JastAddAspectAspectInhAttributeDeclarationImpl;
+import org.jastadd.tooling.aspect.psi.impl.JastAddAspectAspectSynAttributeDeclarationImpl;
+import org.jastadd.tooling.aspect.psi.impl.JastAddAspectCollectionAttributeImpl;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public class AspectStructureViewElement implements StructureViewTreeElement, SortableTreeElement {
+
+  private final NavigatablePsiElement myElement;
+
+  public AspectStructureViewElement(NavigatablePsiElement element) {
+    this.myElement = element;
+  }
+
+  @Override
+  public NavigatablePsiElement getValue() {
+    return myElement;
+  }
+
+  @Override
+  public void navigate(boolean requestFocus) {
+    myElement.navigate(requestFocus);
+  }
+
+  @Override
+  public boolean canNavigate() {
+    return myElement.canNavigate();
+  }
+
+  @Override
+  public boolean canNavigateToSource() {
+    return myElement.canNavigateToSource();
+  }
+
+  @NotNull
+  @Override
+  public String getAlphaSortKey() {
+    String name = myElement.getName();
+    return name != null ? name : "";
+  }
+
+  @NotNull
+  @Override
+  public ItemPresentation getPresentation() {
+    ItemPresentation presentation = myElement.getPresentation();
+    return presentation != null ? presentation : new PresentationData();
+  }
+
+  @Override
+  public TreeElement @NotNull [] getChildren() {
+    if (myElement instanceof AspectFile) {
+      return PsiTreeUtil.getChildrenOfTypeAsList(myElement, JastAddAspectTypeDeclaration.class)
+        .stream()
+        .map(JastAddAspectTypeDeclaration::getAspectDeclaration)
+        .filter(Objects::nonNull)
+        .map(decl -> new AspectStructureViewElement((JastAddAspectAspectDeclarationImpl) decl))
+        .toArray(TreeElement[]::new);
+    } else if (myElement instanceof JastAddAspectAspectDeclaration) {
+      return PsiTreeUtil.getChildrenOfTypeAsList(((JastAddAspectAspectDeclaration) myElement).getAspectBody(), JastAddAspectAspectBodyDeclaration.class)
+        .stream()
+        .map(decl -> {
+          if (decl.getAspectSynAttributeDeclaration() != null) {
+            return new AspectStructureViewElement((JastAddAspectAspectSynAttributeDeclarationImpl) decl.getAspectSynAttributeDeclaration());
+          } else if (decl.getAspectInhAttributeDeclaration() != null) {
+            return new AspectStructureViewElement((JastAddAspectAspectInhAttributeDeclarationImpl) decl.getAspectInhAttributeDeclaration());
+          } else if (decl.getCollectionAttribute() != null) {
+            return new AspectStructureViewElement((JastAddAspectCollectionAttributeImpl) decl.getCollectionAttribute());
+          }
+          return null;
+        })
+        .filter(Objects::nonNull)
+        .toArray(TreeElement[]::new);
+    }
+
+    return EMPTY_ARRAY;
+  }
+
+}
diff --git a/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewFactory.java b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewFactory.java
new file mode 100644
index 0000000..c16c033
--- /dev/null
+++ b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewFactory.java
@@ -0,0 +1,27 @@
+package org.jastadd.tooling.aspect;
+
+
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
+import com.intellij.lang.PsiStructureViewFactory;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class AspectStructureViewFactory implements PsiStructureViewFactory {
+
+  @Nullable
+  @Override
+  public StructureViewBuilder getStructureViewBuilder(@NotNull final PsiFile psiFile) {
+    return new TreeBasedStructureViewBuilder() {
+      @NotNull
+      @Override
+      public StructureViewModel createStructureViewModel(@Nullable Editor editor) {
+        return new AspectStructureViewModel(psiFile);
+      }
+    };
+  }
+
+}
diff --git a/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewModel.java b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewModel.java
new file mode 100644
index 0000000..d2af7e5
--- /dev/null
+++ b/src/main/java/org/jastadd/tooling/aspect/AspectStructureViewModel.java
@@ -0,0 +1,48 @@
+package org.jastadd.tooling.aspect;
+
+
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.StructureViewModelBase;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.Filter;
+import com.intellij.ide.util.treeView.smartTree.Grouper;
+import com.intellij.ide.util.treeView.smartTree.Sorter;
+import com.intellij.psi.PsiFile;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectDeclaration;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAttribute;
+import org.jetbrains.annotations.NotNull;
+
+public class AspectStructureViewModel extends StructureViewModelBase implements
+  StructureViewModel.ElementInfoProvider {
+
+  public AspectStructureViewModel(PsiFile psiFile) {
+    super(psiFile, new AspectStructureViewElement(psiFile));
+  }
+
+  @NotNull
+  public Sorter @NotNull [] getSorters() {
+    return new Sorter[]{Sorter.ALPHA_SORTER};
+  }
+
+
+  @Override
+  public boolean isAlwaysShowsPlus(StructureViewTreeElement element) {
+    return element.getValue() instanceof JastAddAspectAspectDeclaration;
+  }
+
+  @Override
+  public boolean isAlwaysLeaf(StructureViewTreeElement element) {
+    return element.getValue() instanceof JastAddAspectAttribute;
+  }
+
+  @Override
+  public Grouper @NotNull [] getGroupers() {
+    // TODO group by member type
+    return new Grouper[]{};
+  }
+
+  @Override
+  public Filter @NotNull [] getFilters() {
+    return new Filter[]{new JastAddAspectAttributeFilter()};
+  }
+}
diff --git a/src/main/java/org/jastadd/tooling/aspect/JastAddAspectAttributeFilter.java b/src/main/java/org/jastadd/tooling/aspect/JastAddAspectAttributeFilter.java
new file mode 100644
index 0000000..974798a
--- /dev/null
+++ b/src/main/java/org/jastadd/tooling/aspect/JastAddAspectAttributeFilter.java
@@ -0,0 +1,44 @@
+package org.jastadd.tooling.aspect;
+
+import com.intellij.ide.util.treeView.smartTree.ActionPresentation;
+import com.intellij.ide.util.treeView.smartTree.ActionPresentationData;
+import com.intellij.ide.util.treeView.smartTree.Filter;
+import com.intellij.ide.util.treeView.smartTree.TreeElement;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectDeclaration;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAttribute;
+import org.jastadd.tooling.util.JastAddIcons;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+public class JastAddAspectAttributeFilter implements Filter {
+  @NonNls
+  public static final String ID = "HIDE_ATTRIBUTE";
+
+  @Override
+  public boolean isVisible(TreeElement treeNode) {
+    if (treeNode instanceof AspectStructureViewElement) {
+      return !(((AspectStructureViewElement) treeNode).getValue() instanceof JastAddAspectAttribute);
+    }
+    else {
+      return true;
+    }
+  }
+
+  @Override
+  @NotNull
+  public ActionPresentation getPresentation() {
+    // TODO use i18n and string bundle like JavaStructureViewBundle
+    return new ActionPresentationData("Hide Attributes", null, JastAddIcons.ATTRIBUTE);
+  }
+
+  @Override
+  @NotNull
+  public String getName() {
+    return ID;
+  }
+
+  @Override
+  public boolean isReverted() {
+    return false;
+  }
+}
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/AspectElementFactory.java b/src/main/java/org/jastadd/tooling/aspect/psi/AspectElementFactory.java
index 575b08a..1b0685d 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/AspectElementFactory.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/AspectElementFactory.java
@@ -34,4 +34,13 @@ public class AspectElementFactory {
     return (AspectFile) PsiFileFactory.getInstance(project).
       createFileFromText(name, AspectFileType.INSTANCE, text);
   }
+
+  public static JastAddAspectAspectDeclaration createAspectDeclaration(Project project, String name) {
+    final AspectFile file = createFile(project, "aspect " + name + "{}");
+    PsiElement result = file.getFirstChild().findElementAt(20);
+    if (result != null) {
+      return (JastAddAspectAspectDeclaration) result.getParent().getParent();
+    }
+    return null;
+  }
 }
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/JastAddAspectAttribute.java b/src/main/java/org/jastadd/tooling/aspect/psi/JastAddAspectAttribute.java
index 3641ed2..e3e4c7b 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/JastAddAspectAttribute.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/JastAddAspectAttribute.java
@@ -1,6 +1,7 @@
 package org.jastadd.tooling.aspect.psi;
 
 import com.intellij.psi.PsiElement;
+import org.jastadd.tooling.grammar.psi.GrammarNamedElement;
 
-public interface JastAddAspectAttribute extends PsiElement {
+public interface JastAddAspectAttribute extends PsiElement, GrammarNamedElement {
 }
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAspectDeclarationImplExtension.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAspectDeclarationImplExtension.java
new file mode 100644
index 0000000..a077cbc
--- /dev/null
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAspectDeclarationImplExtension.java
@@ -0,0 +1,67 @@
+package org.jastadd.tooling.aspect.psi.impl;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.psi.PsiElement;
+import org.jastadd.tooling.aspect.psi.AspectElementFactory;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectDeclaration;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectName;
+import org.jastadd.tooling.grammar.psi.GrammarNamedElement;
+import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
+import org.jastadd.tooling.util.JastAddIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+public class JastAddAspectAspectDeclarationImplExtension extends GrammarNamedElementImpl implements GrammarNamedElement {
+
+  public JastAddAspectAspectDeclarationImplExtension(@NotNull ASTNode node) {
+    super(node);
+  }
+
+  public String getName() {
+    return getNameIdentifier().getText();
+  }
+
+  public PsiElement setName(@NotNull String newName) {
+    ASTNode identifierNode = getNameIdentifier().getNode().getFirstChildNode();
+    if (identifierNode != null) {
+      JastAddAspectAspectDeclaration name = AspectElementFactory.createAspectDeclaration(getProject(), newName);
+      assert name != null; // we know the name is not null because we always create the same one
+      ASTNode newNameIdentifierNode = name.getAspectName().getNode().getFirstChildNode();
+      getNameIdentifier().getNode().replaceChild(identifierNode, newNameIdentifierNode);
+    }
+    return this;
+  }
+
+  @Override
+  @NotNull
+  public JastAddAspectAspectName getNameIdentifier() {
+    return ((JastAddAspectAspectDeclaration) getNode().getPsi()).getAspectName();
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    return new ItemPresentation() {
+      @Nullable
+      @Override
+      public String getPresentableText() {
+        return "aspect " + getName();
+      }
+
+      @Override
+      public String getLocationString() {
+        Document document = FileDocumentManager.getInstance().getDocument(getNode().getPsi().getContainingFile().getVirtualFile());
+        return document != null ? "l." + document.getLineNumber(getTextRange().getStartOffset() + 1) : "";
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return JastAddIcons.FILE;
+      }
+    };
+  }
+}
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAstTypeNameImplExtension.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAstTypeNameImplExtension.java
index c83758e..50be78d 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAstTypeNameImplExtension.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectAstTypeNameImplExtension.java
@@ -3,6 +3,7 @@ package org.jastadd.tooling.aspect.psi.impl;
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiElement;
 import org.jastadd.tooling.aspect.psi.AspectElementFactory;
+import org.jastadd.tooling.aspect.psi.AspectTypes;
 import org.jastadd.tooling.aspect.psi.JastAddAspectAstTypeName;
 import org.jastadd.tooling.grammar.psi.GrammarNamedElement;
 import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
@@ -15,18 +16,21 @@ public class JastAddAspectAstTypeNameImplExtension extends GrammarNamedElementIm
   }
 
   public String getName() {
-    // this finds the *first* ID, which is what we want
-    return getNode().getText();
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.AST_TYPE_NAME);
+    if (nameNode != null) {
+      return nameNode.getText();
+    } else {
+      return null;
+    }
   }
 
   public PsiElement setName(@NotNull String newName) {
-    // FIXME this can break the grammar when the type is used in an unnamed component (and in many other cases probably)
-    ASTNode keyNode = getNode().getFirstChildNode();
-    if (keyNode != null) {
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.AST_TYPE_NAME);
+    if (nameNode != null) {
       JastAddAspectAstTypeName name = AspectElementFactory.createAstTypeName(getProject(), newName);
       assert name != null; // we know the name is not null because we always create the same one
-      ASTNode newKeyNode = name.getNode().getFirstChildNode();
-      getNode().replaceChild(keyNode, newKeyNode);
+      ASTNode newNameNode = name.getNode().getFirstChildNode();
+      getNode().replaceChild(nameNode, newNameNode);
     }
     return this;
   }
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectClassOrInterfaceTypeImplExtension.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectClassOrInterfaceTypeImplExtension.java
index ca1542e..c483eff 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectClassOrInterfaceTypeImplExtension.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectClassOrInterfaceTypeImplExtension.java
@@ -3,6 +3,7 @@ package org.jastadd.tooling.aspect.psi.impl;
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiElement;
 import org.jastadd.tooling.aspect.psi.AspectElementFactory;
+import org.jastadd.tooling.aspect.psi.AspectTypes;
 import org.jastadd.tooling.aspect.psi.JastAddAspectClassOrInterfaceType;
 import org.jastadd.tooling.grammar.psi.GrammarNamedElement;
 import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
@@ -16,22 +17,28 @@ public class JastAddAspectClassOrInterfaceTypeImplExtension extends GrammarNamed
 
   public String getName() {
     // this finds the *first* ID, which is what we want
-    return getNode().getText();
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.JAVA_IDENTIFIER);
+    if (nameNode != null) {
+      return nameNode.getText();
+    } else {
+      return null;
+    }
   }
 
   public PsiElement setName(@NotNull String newName) {
-    // FIXME this can break the grammar when the type is used in an unnamed component (and in many other cases probably)
-    ASTNode keyNode = getNode().getFirstChildNode();
-    if (keyNode != null) {
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.JAVA_IDENTIFIER);
+    if (nameNode != null) {
       JastAddAspectClassOrInterfaceType name = AspectElementFactory.createClassOrInterfaceType(getProject(), newName);
       assert name != null; // we know the name is not null because we always create the same one
-      ASTNode newKeyNode = name.getNode().getFirstChildNode();
-      getNode().replaceChild(keyNode, newKeyNode);
+      assert !name.getJavaIdentifierList().isEmpty(); // we know there is always one name - the class name
+      ASTNode newKeyNode = name.getJavaIdentifierList().get(0).getNode().getFirstChildNode();
+      getNode().replaceChild(nameNode, newKeyNode);
     }
     return this;
   }
 
   public PsiElement getNameIdentifier() {
+    // the entire thing is the identifier
     return getNode().getPsi();
   }
 
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectCollAttributeImpl.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectCollAttributeImpl.java
index 0381a57..931466e 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectCollAttributeImpl.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectCollAttributeImpl.java
@@ -1,12 +1,71 @@
 package org.jastadd.tooling.aspect.psi.impl;
 
-import com.intellij.extapi.psi.ASTWrapperPsiElement;
 import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jastadd.tooling.aspect.psi.AspectTypes;
 import org.jastadd.tooling.aspect.psi.JastAddAspectAttribute;
+import org.jastadd.tooling.aspect.psi.JastAddAspectCollectionAttribute;
+import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
+import org.jastadd.tooling.util.JastAddIcons;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-public abstract class JastAddAspectCollAttributeImpl extends ASTWrapperPsiElement implements JastAddAspectAttribute {
+import javax.swing.*;
+
+public abstract class JastAddAspectCollAttributeImpl extends GrammarNamedElementImpl implements JastAddAspectAttribute {
   public JastAddAspectCollAttributeImpl(@NotNull ASTNode node) {
     super(node);
   }
+
+  public String getName() {
+    // this finds the *first* ID, which is what we want
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.ATTRIBUTE_NAME);
+    if (nameNode != null) {
+      return nameNode.getText();
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public @Nullable PsiElement getNameIdentifier() {
+    return ((JastAddAspectCollectionAttribute) this).getAttributeName();
+  }
+
+  @Override
+  public PsiElement setName(@NlsSafe @NotNull String name) throws IncorrectOperationException {
+    throw new IncorrectOperationException("Renaming collection attributes is not supported.");
+  }
+
+  @Override
+  public @Nullable PsiElement getIdentifyingElement() {
+    return this;
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    return new ItemPresentation() {
+      @Nullable
+      @Override
+      public String getPresentableText() {
+        return "coll " + getName();
+      }
+
+      @Override
+      public String getLocationString() {
+        Document document = FileDocumentManager.getInstance().getDocument(getNode().getPsi().getContainingFile().getVirtualFile());
+        return document != null ? "l." + document.getLineNumber(getTextRange().getStartOffset() + 1) : "";
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return JastAddIcons.COL_ATTRIBUTE;
+      }
+    };
+  }
 }
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectInhAttributeImpl.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectInhAttributeImpl.java
index 6bd54d1..5b2692b 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectInhAttributeImpl.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectInhAttributeImpl.java
@@ -1,12 +1,71 @@
 package org.jastadd.tooling.aspect.psi.impl;
 
-import com.intellij.extapi.psi.ASTWrapperPsiElement;
 import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jastadd.tooling.aspect.psi.AspectTypes;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectInhAttributeDeclaration;
 import org.jastadd.tooling.aspect.psi.JastAddAspectAttribute;
+import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
+import org.jastadd.tooling.util.JastAddIcons;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-public abstract class JastAddAspectInhAttributeImpl extends ASTWrapperPsiElement implements JastAddAspectAttribute {
+import javax.swing.*;
+
+public abstract class JastAddAspectInhAttributeImpl extends GrammarNamedElementImpl implements JastAddAspectAttribute {
   public JastAddAspectInhAttributeImpl(@NotNull ASTNode node) {
     super(node);
   }
+
+  public String getName() {
+    // this finds the *first* ID, which is what we want
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.ATTRIBUTE_NAME);
+    if (nameNode != null) {
+      return nameNode.getText();
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public @Nullable PsiElement getNameIdentifier() {
+    return ((JastAddAspectAspectInhAttributeDeclaration) this).getAttributeName();
+  }
+
+  @Override
+  public PsiElement setName(@NlsSafe @NotNull String name) throws IncorrectOperationException {
+    throw new IncorrectOperationException("Renaming collection attributes is not supported.");
+  }
+
+  @Override
+  public @Nullable PsiElement getIdentifyingElement() {
+    return this;
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    return new ItemPresentation() {
+      @Nullable
+      @Override
+      public String getPresentableText() {
+        return "inh " + getName();
+      }
+
+      @Override
+      public String getLocationString() {
+        Document document = FileDocumentManager.getInstance().getDocument(getNode().getPsi().getContainingFile().getVirtualFile());
+        return document != null ? "l." + document.getLineNumber(getTextRange().getStartOffset() + 1) : "";
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return JastAddIcons.INH_ATTRIBUTE;
+      }
+    };
+  }
 }
diff --git a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectSynAttributeImpl.java b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectSynAttributeImpl.java
index 1b9443a..358e467 100644
--- a/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectSynAttributeImpl.java
+++ b/src/main/java/org/jastadd/tooling/aspect/psi/impl/JastAddAspectSynAttributeImpl.java
@@ -1,12 +1,72 @@
 package org.jastadd.tooling.aspect.psi.impl;
 
-import com.intellij.extapi.psi.ASTWrapperPsiElement;
 import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+import org.jastadd.tooling.aspect.psi.AspectTypes;
+import org.jastadd.tooling.aspect.psi.JastAddAspectAspectSynAttributeDeclaration;
 import org.jastadd.tooling.aspect.psi.JastAddAspectAttribute;
+import org.jastadd.tooling.grammar.psi.impl.GrammarNamedElementImpl;
+import org.jastadd.tooling.util.JastAddIcons;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-public abstract class JastAddAspectSynAttributeImpl extends ASTWrapperPsiElement implements JastAddAspectAttribute {
+import javax.swing.*;
+
+public abstract class JastAddAspectSynAttributeImpl extends GrammarNamedElementImpl implements JastAddAspectAttribute {
   public JastAddAspectSynAttributeImpl(@NotNull ASTNode node) {
     super(node);
   }
+
+
+  public String getName() {
+    // this finds the *first* ID, which is what we want
+    ASTNode nameNode = getNode().findChildByType(AspectTypes.ATTRIBUTE_NAME);
+    if (nameNode != null) {
+      return nameNode.getText();
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public @Nullable PsiElement getNameIdentifier() {
+    return ((JastAddAspectAspectSynAttributeDeclaration) this).getAttributeName();
+  }
+
+  @Override
+  public PsiElement setName(@NlsSafe @NotNull String name) throws IncorrectOperationException {
+    throw new IncorrectOperationException("Renaming collection attributes is not supported.");
+  }
+
+  @Override
+  public @Nullable PsiElement getIdentifyingElement() {
+    return this;
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    return new ItemPresentation() {
+      @Nullable
+      @Override
+      public String getPresentableText() {
+        return "syn " + getName();
+      }
+
+      @Override
+      public String getLocationString() {
+        Document document = FileDocumentManager.getInstance().getDocument(getNode().getPsi().getContainingFile().getVirtualFile());
+        return document != null ? "l." + document.getLineNumber(getTextRange().getStartOffset() + 1) : "";
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return JastAddIcons.SYN_ATTRIBUTE;
+      }
+    };
+  }
 }
diff --git a/src/main/java/org/jastadd/tooling/grammar/psi/impl/GrammarTypeDeclImplExtension.java b/src/main/java/org/jastadd/tooling/grammar/psi/impl/GrammarTypeDeclImplExtension.java
index 39bd24d..7be798b 100644
--- a/src/main/java/org/jastadd/tooling/grammar/psi/impl/GrammarTypeDeclImplExtension.java
+++ b/src/main/java/org/jastadd/tooling/grammar/psi/impl/GrammarTypeDeclImplExtension.java
@@ -16,28 +16,28 @@ public class GrammarTypeDeclImplExtension extends GrammarNamedElementImpl implem
 
   public String getName() {
     // this finds the *first* ID, which is what we want
-    ASTNode keyNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
-    if (keyNode != null) {
-      return keyNode.getText();
+    ASTNode nameNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
+    if (nameNode != null) {
+      return nameNode.getText();
     } else {
       return null;
     }
   }
 
   public PsiElement setName(@NotNull String newName) {
-    ASTNode keyNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
-    if (keyNode != null) {
+    ASTNode nameNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
+    if (nameNode != null) {
       GrammarTypeName name = GrammarElementFactory.createTypeName(getProject(), newName);
       ASTNode newKeyNode = name.getNode();
-      getNode().replaceChild(keyNode, newKeyNode);
+      getNode().replaceChild(nameNode, newKeyNode);
     }
     return this;
   }
 
   public PsiElement getNameIdentifier() {
-    ASTNode keyNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
-    if (keyNode != null) {
-      return keyNode.getPsi();
+    ASTNode nameNode = getNode().findChildByType(GrammarTypes.TYPE_NAME);
+    if (nameNode != null) {
+      return nameNode.getPsi();
     } else {
       return null;
     }
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index a416248..d84dd36 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -91,7 +91,8 @@
         <lang.formatter language="JastAddAspect" implementationClass="org.jastadd.tooling.aspect.AspectFormattingModelBuilder"/>
 
         <lang.foldingBuilder language="JastAddAspect" implementationClass="org.jastadd.tooling.aspect.AspectFoldingBuilder"/>
-        FoldingBuilder"/>
+
+        <lang.psiStructureViewFactory language="JastAddAspect" implementationClass="org.jastadd.tooling.aspect.AspectStructureViewFactory"/>
     </extensions>
 
     <actions>
-- 
GitLab