diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml
index cc6424c2..3139a39c 100644
--- a/META-INF/plugin.xml
+++ b/META-INF/plugin.xml
@@ -164,6 +164,7 @@
+
@@ -176,6 +177,8 @@
+
+
+ */
+public class AnnotationUsageIndex extends FileBasedIndexExtension> {
+ public static final ID> KEY = ID.create("espend.php.annotation.usage");
+ private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
+ private static StringSetDataExternalizer EXTERNALIZER = new StringSetDataExternalizer();
+
+ @NotNull
+ @Override
+ public ID> getName() {
+ return KEY;
+ }
+
+ @NotNull
+ @Override
+ public DataIndexer, FileContent> getIndexer() {
+ return inputData -> {
+ final Map> map = new THashMap<>();
+
+ PsiFile psiFile = inputData.getPsiFile();
+ if(!(psiFile instanceof PhpFile)) {
+ return map;
+ }
+
+ if(!AnnotationUtil.isValidForIndex(inputData)) {
+ return map;
+ }
+
+ psiFile.accept(new PhpDocTagAnnotationRecursiveElementWalkingVisitor(pair -> {
+ map.put(pair.getFirst(), new HashSet<>());
+ return true;
+ }));
+
+ return map;
+ };
+ }
+
+ @NotNull
+ @Override
+ public KeyDescriptor getKeyDescriptor() {
+ return this.myKeyDescriptor;
+ }
+
+ @NotNull
+ @Override
+ public DataExternalizer> getValueExternalizer() {
+ return EXTERNALIZER;
+ }
+
+ @NotNull
+ @Override
+ public FileBasedIndex.InputFilter getInputFilter() {
+ return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+
+ private static class StringSetDataExternalizer implements DataExternalizer> {
+ public synchronized void save(@NotNull DataOutput out, Set value) throws IOException {
+ out.writeInt(value.size());
+ Iterator var = value.iterator();
+
+ while(var.hasNext()) {
+ String s = (String)var.next();
+ EnumeratorStringDescriptor.INSTANCE.save(out, s);
+ }
+ }
+
+ public synchronized Set read(@NotNull DataInput in) throws IOException {
+ Set set = new THashSet<>();
+
+ for(int r = in.readInt(); r > 0; --r) {
+ set.add(EnumeratorStringDescriptor.INSTANCE.read(in));
+ }
+
+ return set;
+ }
+ }
+}
diff --git a/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java b/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java
index 1edfb662..dcb982f2 100644
--- a/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java
+++ b/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java
@@ -4,7 +4,6 @@
import com.intellij.psi.PsiReference;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.completion.PhpCompletionUtil;
-import com.jetbrains.php.lang.PhpLangUtil;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import de.espend.idea.php.annotation.extension.PhpAnnotationCompletionProvider;
import de.espend.idea.php.annotation.extension.PhpAnnotationReferenceProvider;
diff --git a/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java b/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java
new file mode 100644
index 00000000..ee6ad31b
--- /dev/null
+++ b/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java
@@ -0,0 +1,106 @@
+package de.espend.idea.php.annotation.navigation;
+
+import com.intellij.codeInsight.daemon.LineMarkerInfo;
+import com.intellij.codeInsight.daemon.LineMarkerProvider;
+import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.NotNullLazyValue;
+import com.intellij.patterns.PlatformPatterns;
+import com.intellij.patterns.PsiElementPattern;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.jetbrains.php.PhpIcons;
+import com.jetbrains.php.lang.PhpFileType;
+import com.jetbrains.php.lang.PhpLanguage;
+import com.jetbrains.php.lang.lexer.PhpTokenTypes;
+import com.jetbrains.php.lang.psi.elements.PhpClass;
+import de.espend.idea.php.annotation.AnnotationUsageIndex;
+import de.espend.idea.php.annotation.util.AnnotationUtil;
+import org.apache.commons.lang.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * @author Daniel Espendiller
+ */
+public class AnnotationUsageLineMarkerProvider implements LineMarkerProvider {
+ @Nullable
+ @Override
+ public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) {
+ return null;
+ }
+
+ @Override
+ public void collectSlowLineMarkers(@NotNull List psiElements, @NotNull Collection results) {
+ for(PsiElement psiElement: psiElements) {
+ if(!getClassNamePattern().accepts(psiElement)) {
+ continue;
+ }
+
+ PsiElement phpClass = psiElement.getContext();
+ if(!(phpClass instanceof PhpClass) || !AnnotationUtil.isAnnotationClass((PhpClass) phpClass)) {
+ return;
+ }
+
+ String fqn = StringUtils.stripStart(((PhpClass) phpClass).getFQN(), "\\");
+
+ // find one index annotation class and stop processing on first match
+ final boolean[] processed = {false};
+ FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(fqn)), virtualFile -> {
+ processed[0] = true;
+
+ // stop on first match
+ return false;
+ }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(psiElement.getProject()), PhpFileType.INSTANCE));
+
+ // we found at least one target to provide lazy target linemarker
+ if(processed[0]) {
+ NavigationGutterIconBuilder builder = NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTS)
+ .setTargets(new CollectionNotNullLazyValue(psiElement.getProject(), fqn))
+ .setTooltipText("Navigate to implementations");
+
+ results.add(builder.createLineMarkerInfo(psiElement));
+ }
+ }
+ }
+
+ /**
+ * class "Foo" extends
+ */
+ private static PsiElementPattern.Capture getClassNamePattern() {
+ return PlatformPatterns
+ .psiElement(PhpTokenTypes.IDENTIFIER)
+ .afterLeafSkipping(
+ PlatformPatterns.psiElement(PsiWhiteSpace.class),
+ PlatformPatterns.psiElement(PhpTokenTypes.kwCLASS)
+ )
+ .withParent(PhpClass.class)
+ .withLanguage(PhpLanguage.INSTANCE);
+ }
+
+ private static class CollectionNotNullLazyValue extends NotNullLazyValue> {
+ @NotNull
+ private final Project project;
+
+ @NotNull
+ private final String fqn;
+
+ CollectionNotNullLazyValue(@NotNull Project project, @NotNull String fqn) {
+ this.project = project;
+ this.fqn = fqn;
+ }
+
+ @NotNull
+ @Override
+ protected Collection extends PsiElement> compute() {
+ return AnnotationUtil.getImplementationsForAnnotation(project, fqn);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/de/espend/idea/php/annotation/util/AnnotationUtil.java b/src/de/espend/idea/php/annotation/util/AnnotationUtil.java
index f91af583..d7a9e80e 100644
--- a/src/de/espend/idea/php/annotation/util/AnnotationUtil.java
+++ b/src/de/espend/idea/php/annotation/util/AnnotationUtil.java
@@ -3,9 +3,12 @@
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
@@ -14,6 +17,7 @@
import com.intellij.util.indexing.FileContent;
import com.intellij.util.indexing.ID;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
+import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.documentation.phpdoc.PhpDocUtil;
import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes;
import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes;
@@ -22,6 +26,7 @@
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import de.espend.idea.php.annotation.AnnotationStubIndex;
+import de.espend.idea.php.annotation.AnnotationUsageIndex;
import de.espend.idea.php.annotation.dict.AnnotationTarget;
import de.espend.idea.php.annotation.dict.PhpAnnotation;
import de.espend.idea.php.annotation.dict.PhpDocCommentAnnotation;
@@ -493,6 +498,55 @@ public static StringLiteralExpression getPropertyValueAsPsiElement(@NotNull PhpD
return null;
}
+ @NotNull
+ private static Collection getFilesImplementingAnnotation(@NotNull Project project, @NotNull String phpClassName) {
+ Collection files = new HashSet<>();
+
+ FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(phpClassName)), virtualFile -> {
+ files.add(virtualFile);
+ return true;
+ }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE));
+
+ Collection elements = new ArrayList<>();
+
+ for (VirtualFile file : files) {
+ PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+
+ if(psiFile == null) {
+ continue;
+ }
+
+ elements.add(psiFile);
+ }
+
+ return elements;
+ }
+
+ /**
+ * Find all annotation usages for given class name
+ *
+ * Doctrine\ORM\Mapping\Entity => ORM\Entity(), Entity()
+ *
+ * @param project current Project
+ * @param fqnClassName Foobar\ClassName
+ * @return targets
+ */
+ public static Collection getImplementationsForAnnotation(@NotNull Project project, @NotNull String fqnClassName) {
+ Collection psiElements = new HashSet<>();
+
+ for (PsiFile psiFile : getFilesImplementingAnnotation(project, fqnClassName)) {
+ psiFile.accept(new PhpDocTagAnnotationRecursiveElementWalkingVisitor(pair -> {
+ if(StringUtils.stripStart(pair.getFirst(), "\\").equalsIgnoreCase(StringUtils.stripStart(fqnClassName, "\\"))) {
+ psiElements.add(pair.getSecond());
+ }
+
+ return false;
+ }));
+ }
+
+ return psiElements;
+ }
+
/**
* matches "@Callback(propertyName="")"
*/
diff --git a/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java b/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java
new file mode 100644
index 00000000..814f409f
--- /dev/null
+++ b/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java
@@ -0,0 +1,82 @@
+package de.espend.idea.php.annotation.util;
+
+import com.intellij.openapi.util.Pair;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
+import com.intellij.util.Processor;
+import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
+import org.apache.commons.lang.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+/**
+ * @author Daniel Espendiller
+ */
+public class PhpDocTagAnnotationRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor {
+ @NotNull
+ private final Processor> processor;
+
+ public PhpDocTagAnnotationRecursiveElementWalkingVisitor(@NotNull Processor> processor) {
+ this.processor = processor;
+ }
+
+ @Override
+ public void visitElement(PsiElement element) {
+ if ((element instanceof PhpDocTag)) {
+ visitPhpDocTag((PhpDocTag) element);
+ }
+
+ super.visitElement(element);
+ }
+
+ private void visitPhpDocTag(@NotNull PhpDocTag phpDocTag) {
+ // "@var" and user non related tags dont need an action
+ if(AnnotationUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
+ return;
+ }
+
+ String annotationFqnName = StringUtils.stripStart(getClassNameReference(phpDocTag, AnnotationUtil.getUseImportMap(phpDocTag)), "\\");
+
+ if(annotationFqnName != null && StringUtils.isNotBlank(annotationFqnName)) {
+ this.processor.process(Pair.create(annotationFqnName, phpDocTag));
+ }
+ }
+
+ @Nullable
+ private static String getClassNameReference(@NotNull PhpDocTag phpDocTag, @NotNull Map useImports) {
+
+ if(useImports.size() == 0) {
+ return null;
+ }
+
+ String annotationName = phpDocTag.getName();
+ if(StringUtils.isBlank(annotationName)) {
+ return null;
+ }
+
+ if(annotationName.startsWith("@")) {
+ annotationName = annotationName.substring(1);
+ }
+
+ String className = annotationName;
+ String subNamespaceName = "";
+ if(className.contains("\\")) {
+ className = className.substring(0, className.indexOf("\\"));
+ subNamespaceName = annotationName.substring(className.length());
+ }
+
+ if(!useImports.containsKey(className)) {
+ return null;
+ }
+
+ // normalize name
+ String annotationFqnName = useImports.get(className) + subNamespaceName;
+ if(!annotationFqnName.startsWith("\\")) {
+ annotationFqnName = "\\" + annotationFqnName;
+ }
+
+ return annotationFqnName;
+ }
+}
\ No newline at end of file
diff --git a/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java b/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java
new file mode 100644
index 00000000..9bd1f23b
--- /dev/null
+++ b/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java
@@ -0,0 +1,25 @@
+package de.espend.idea.php.annotation.tests;
+
+import de.espend.idea.php.annotation.AnnotationUsageIndex;
+
+import java.io.File;
+
+/**
+ * @author Daniel Espendiller
+ * @see de.espend.idea.php.annotation.AnnotationUsageIndex
+ */
+public class AnnotationUsageIndexTest extends AnnotationLightCodeInsightFixtureTestCase {
+ public void setUp() throws Exception {
+ super.setUp();
+ myFixture.copyFileToProject("classes.php");
+ myFixture.copyFileToProject("usages.php");
+ }
+
+ public String getTestDataPath() {
+ return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath();
+ }
+
+ public void testThatUsagesAreInIndex() {
+ assertIndexContains(AnnotationUsageIndex.KEY, "Doctrine\\ORM\\Mapping\\Embedded");
+ }
+}
diff --git a/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php b/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php
new file mode 100644
index 00000000..90923063
--- /dev/null
+++ b/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php
@@ -0,0 +1,36 @@
+
+ */
+public class AnnotationUsageLineMarkerProviderTest extends AnnotationLightCodeInsightFixtureTestCase {
+ public void setUp() throws Exception {
+ super.setUp();
+ myFixture.copyFileToProject("AnnotationUsageLineMarkerProvider.php");
+ }
+
+ public String getTestDataPath() {
+ return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath();
+ }
+
+ public void testThatLineMarkerIsProvidedForAnnotationClass() {
+ assertLineMarker(PhpPsiElementFactory.createPsiFileFromText(getProject(), "