diff --git a/src/main/java/de/espend/idea/php/drupal/DrupalIcons.java b/src/main/java/de/espend/idea/php/drupal/DrupalIcons.java new file mode 100644 index 000000000..05525b09c --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/DrupalIcons.java @@ -0,0 +1,12 @@ +package de.espend.idea.php.drupal; + +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; + +/** + * @author Daniel Espendiller + */ +public class DrupalIcons { + public static final Icon DRUPAL = IconLoader.getIcon("icons/drupal.png"); +} diff --git a/src/main/java/de/espend/idea/php/drupal/DrupalProjectComponent.java b/src/main/java/de/espend/idea/php/drupal/DrupalProjectComponent.java new file mode 100644 index 000000000..e6983ab91 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/DrupalProjectComponent.java @@ -0,0 +1,22 @@ +package de.espend.idea.php.drupal; + + +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + */ +public class DrupalProjectComponent { + + public static boolean isEnabled(Project project) { + return Symfony2ProjectComponent.isEnabled(project); + } + + public static boolean isEnabled(@Nullable PsiElement psiElement) { + return psiElement != null && isEnabled(psiElement.getProject()); + } + +} diff --git a/src/main/java/de/espend/idea/php/drupal/annotation/ConfigEntityTypeAnnotation.java b/src/main/java/de/espend/idea/php/drupal/annotation/ConfigEntityTypeAnnotation.java new file mode 100644 index 000000000..80f909cca --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/annotation/ConfigEntityTypeAnnotation.java @@ -0,0 +1,74 @@ +package de.espend.idea.php.drupal.annotation; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +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; +import de.espend.idea.php.annotation.extension.parameter.AnnotationCompletionProviderParameter; +import de.espend.idea.php.annotation.extension.parameter.AnnotationPropertyParameter; +import de.espend.idea.php.annotation.extension.parameter.PhpAnnotationReferenceProviderParameter; +import de.espend.idea.php.drupal.index.PermissionIndex; +import de.espend.idea.php.drupal.utils.IndexUtil; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + * + * "@ContentEntityTypeAnnotation" + */ +public class ConfigEntityTypeAnnotation implements PhpAnnotationCompletionProvider, PhpAnnotationReferenceProvider { + @Override + public void getPropertyValueCompletions(AnnotationPropertyParameter parameter, AnnotationCompletionProviderParameter completionProviderParameter) { + if(!isSupported(parameter)) { + return; + } + + if("admin_permission".equalsIgnoreCase(parameter.getPropertyName())) { + completionProviderParameter.getResult().addAllElements(IndexUtil.getIndexedKeyLookup(parameter.getProject(), PermissionIndex.KEY)); + } + } + + private boolean isSupported(@NotNull AnnotationPropertyParameter parameter) { + return parameter.getType() != AnnotationPropertyParameter.Type.DEFAULT && + PhpLangUtil.equalsClassNames(StringUtils.stripStart(parameter.getPhpClass().getFQN(), "\\"), "Drupal\\Core\\Entity\\Annotation\\ConfigEntityType"); + } + + @Nullable + @Override + public PsiReference[] getPropertyReferences(AnnotationPropertyParameter parameter, PhpAnnotationReferenceProviderParameter phpAnnotationReferenceProviderParameter) { + if(!isSupported(parameter)) { + return new PsiReference[0]; + } + + String propertyName = parameter.getPropertyName(); + + if("admin_permission".equalsIgnoreCase(propertyName)) { + String contents = getContents(parameter.getElement()); + if(StringUtils.isBlank(contents)) { + return new PsiReference[0]; + } + + return new PsiReference[] {new ContentEntityTypeAnnotation.MyPermissionPsiPolyVariantReferenceBase(parameter.getElement(), contents)}; + } + + return new PsiReference[0]; + } + + @Nullable + private String getContents(@NotNull PsiElement element) { + if(!(element instanceof StringLiteralExpression)) { + return null; + } + + String contents = ((StringLiteralExpression) element).getContents(); + if(StringUtils.isBlank(contents)) { + return null; + } + + return contents; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/annotation/ContentEntityTypeAnnotation.java b/src/main/java/de/espend/idea/php/drupal/annotation/ContentEntityTypeAnnotation.java new file mode 100644 index 000000000..8f00d4d24 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/annotation/ContentEntityTypeAnnotation.java @@ -0,0 +1,143 @@ +package de.espend.idea.php.drupal.annotation; + +import com.intellij.psi.*; +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; +import de.espend.idea.php.annotation.extension.parameter.AnnotationCompletionProviderParameter; +import de.espend.idea.php.annotation.extension.parameter.AnnotationPropertyParameter; +import de.espend.idea.php.annotation.extension.parameter.PhpAnnotationReferenceProviderParameter; +import de.espend.idea.php.drupal.index.PermissionIndex; +import de.espend.idea.php.drupal.registrar.YamlPermissionGotoCompletion; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + * + * "@ContentEntityTypeAnnotation" + */ +public class ContentEntityTypeAnnotation implements PhpAnnotationCompletionProvider, PhpAnnotationReferenceProvider { + @Override + public void getPropertyValueCompletions(AnnotationPropertyParameter parameter, AnnotationCompletionProviderParameter completionProviderParameter) { + if(!isSupported(parameter)) { + return; + } + + if("field_ui_base_route".equalsIgnoreCase(parameter.getPropertyName())) { + completionProviderParameter.getResult().addAllElements(RouteHelper.getRoutesLookupElements(parameter.getProject())); + } + + if("admin_permission".equalsIgnoreCase(parameter.getPropertyName())) { + completionProviderParameter.getResult().addAllElements(IndexUtil.getIndexedKeyLookup(parameter.getProject(), PermissionIndex.KEY)); + } + } + + private boolean isSupported(@NotNull AnnotationPropertyParameter parameter) { + return parameter.getType() != AnnotationPropertyParameter.Type.DEFAULT && + PhpLangUtil.equalsClassNames(StringUtils.stripStart(parameter.getPhpClass().getFQN(), "\\"), "Drupal\\Core\\Entity\\Annotation\\ContentEntityType"); + } + + @Nullable + @Override + public PsiReference[] getPropertyReferences(AnnotationPropertyParameter parameter, PhpAnnotationReferenceProviderParameter phpAnnotationReferenceProviderParameter) { + if(!isSupported(parameter)) { + return new PsiReference[0]; + } + + String propertyName = parameter.getPropertyName(); + + if("field_ui_base_route".equalsIgnoreCase(propertyName)) { + String contents = getContents(parameter.getElement()); + if(StringUtils.isBlank(contents)) { + return new PsiReference[0]; + } + + return new PsiReference[] {new MyRoutePsiPolyVariantReferenceBase(parameter.getElement(), contents)}; + } else if("admin_permission".equalsIgnoreCase(propertyName)) { + String contents = getContents(parameter.getElement()); + if(StringUtils.isBlank(contents)) { + return new PsiReference[0]; + } + + return new PsiReference[] {new MyPermissionPsiPolyVariantReferenceBase(parameter.getElement(), contents)}; + } + + return new PsiReference[0]; + } + + @Nullable + private String getContents(@NotNull PsiElement element) { + if(!(element instanceof StringLiteralExpression)) { + return null; + } + + String contents = ((StringLiteralExpression) element).getContents(); + if(StringUtils.isBlank(contents)) { + return null; + } + + return contents; + } + + private static class MyRoutePsiPolyVariantReferenceBase extends PsiPolyVariantReferenceBase { + @NotNull + private final PsiElement element; + + @NotNull + private final String contents; + + MyRoutePsiPolyVariantReferenceBase(@NotNull PsiElement element, @NotNull String contents) { + super(element); + this.element = element; + this.contents = contents; + } + + @NotNull + @Override + public Object[] getVariants() { + return new Object[0]; + } + + @NotNull + @Override + public ResolveResult[] multiResolve(boolean b) { + PsiElement routeNameTarget = RouteHelper.getRouteNameTarget(element.getProject(), contents); + if(routeNameTarget == null) { + return new ResolveResult[0]; + } + + return PsiElementResolveResult.createResults(routeNameTarget); + } + } + + public static class MyPermissionPsiPolyVariantReferenceBase extends PsiPolyVariantReferenceBase { + @NotNull + private final PsiElement element; + + @NotNull + private final String contents; + + MyPermissionPsiPolyVariantReferenceBase(@NotNull PsiElement element, @NotNull String contents) { + super(element); + this.element = element; + this.contents = contents; + } + + @NotNull + @Override + public Object[] getVariants() { + return new Object[0]; + } + + @NotNull + @Override + public ResolveResult[] multiResolve(boolean b) { + return PsiElementResolveResult.createResults(YamlPermissionGotoCompletion.getPermissionPsiElements(element.getProject(), contents)); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/annotation/DrupalVirtualProperties.java b/src/main/java/de/espend/idea/php/drupal/annotation/DrupalVirtualProperties.java new file mode 100644 index 000000000..1f338e872 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/annotation/DrupalVirtualProperties.java @@ -0,0 +1,75 @@ +package de.espend.idea.php.drupal.annotation; + +import de.espend.idea.php.annotation.dict.AnnotationPropertyEnum; +import de.espend.idea.php.annotation.extension.PhpAnnotationVirtualProperties; +import de.espend.idea.php.annotation.extension.parameter.AnnotationCompletionProviderParameter; +import de.espend.idea.php.annotation.extension.parameter.AnnotationVirtualPropertyCompletionParameter; +import de.espend.idea.php.annotation.extension.parameter.AnnotationVirtualPropertyTargetsParameter; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class DrupalVirtualProperties implements PhpAnnotationVirtualProperties { + final private static Map> ITEMS = Collections.unmodifiableMap(new HashMap>() {{ + put("Drupal\\Core\\Entity\\Annotation\\ContentEntityType", Collections.unmodifiableMap(new HashMap() {{ + put("id", AnnotationPropertyEnum.STRING); + put("label", AnnotationPropertyEnum.STRING); + put("handlers", AnnotationPropertyEnum.ARRAY); + put("admin_permission", AnnotationPropertyEnum.STRING); + put("base_table", AnnotationPropertyEnum.STRING); + put("data_table", AnnotationPropertyEnum.STRING); + put("label_callback", AnnotationPropertyEnum.STRING); + put("translatable", AnnotationPropertyEnum.BOOLEAN); + put("entity_keys", AnnotationPropertyEnum.ARRAY); + put("links", AnnotationPropertyEnum.ARRAY); + put("field_ui_base_route", AnnotationPropertyEnum.STRING); + put("common_reference_target", AnnotationPropertyEnum.STRING); + }})); + + put("Drupal\\Core\\Entity\\Annotation\\ConfigEntityType", Collections.unmodifiableMap(new HashMap() {{ + put("id", AnnotationPropertyEnum.STRING); + put("label", AnnotationPropertyEnum.STRING); + put("handlers", AnnotationPropertyEnum.STRING); + put("entity_keys", AnnotationPropertyEnum.ARRAY); + put("admin_permission", AnnotationPropertyEnum.STRING); + put("list_cache_tags", AnnotationPropertyEnum.ARRAY); + put("config_export", AnnotationPropertyEnum.ARRAY); + }})); + + put("Drupal\\Component\\Annotation\\Plugin", Collections.unmodifiableMap(new HashMap() {{ + put("id", AnnotationPropertyEnum.STRING); + put("title", AnnotationPropertyEnum.STRING); + put("description", AnnotationPropertyEnum.STRING); + }})); + }}); + + @Override + public void addCompletions(@NotNull AnnotationVirtualPropertyCompletionParameter virtualPropertyParameter, @NotNull AnnotationCompletionProviderParameter parameter) { + String fqn = StringUtils.stripStart(virtualPropertyParameter.getPhpClass().getFQN(), "\\"); + if(!ITEMS.containsKey(fqn)) { + return; + } + + for (Map.Entry item : ITEMS.get(fqn).entrySet()) { + virtualPropertyParameter.addLookupElement(item.getKey(), item.getValue()); + } + } + + @Override + public void getTargets(@NotNull AnnotationVirtualPropertyTargetsParameter parameter) { + String fqn = StringUtils.stripStart(parameter.getPhpClass().getFQN(), "\\"); + if(!ITEMS.containsKey(fqn)) { + return; + } + + if(ITEMS.containsKey(fqn) && ITEMS.get(fqn).containsKey(parameter.getProperty())) { + parameter.addTarget(parameter.getPhpClass()); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/annotation/GlobalNamespaceLoader.java b/src/main/java/de/espend/idea/php/drupal/annotation/GlobalNamespaceLoader.java new file mode 100644 index 000000000..0c5223683 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/annotation/GlobalNamespaceLoader.java @@ -0,0 +1,57 @@ +package de.espend.idea.php.drupal.annotation; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.psi.util.CachedValue; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.PsiModificationTracker; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.annotation.extension.PhpAnnotationGlobalNamespacesLoader; +import de.espend.idea.php.annotation.extension.parameter.AnnotationGlobalNamespacesLoaderParameter; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashSet; + +/** + * @author Daniel Espendiller + */ +public class GlobalNamespaceLoader implements PhpAnnotationGlobalNamespacesLoader { + + private static final Key>> CACHE = new Key<>("DRUPAL_GLOBAL_NAMESPACE"); + + @Override + @NotNull + public Collection getGlobalNamespaces(@NotNull AnnotationGlobalNamespacesLoaderParameter parameter) { + Project project = parameter.getProject(); + + CachedValue> cache = project.getUserData(CACHE); + + if(cache == null) { + cache = CachedValuesManager.getManager(project).createCachedValue(() -> + CachedValueProvider.Result.create(getGlobalNamespacesInner(project), PsiModificationTracker.MODIFICATION_COUNT), false + ); + + project.putUserData(CACHE, cache); + } + + return cache.getValue(); + } + + @NotNull + private static Collection getGlobalNamespacesInner(@NotNull Project project) { + Collection namespaces = new HashSet<>(); + + for (PhpClass phpClass : PhpIndex.getInstance(project).getAllSubclasses("Drupal\\Component\\Annotation\\AnnotationInterface")) { + String namespaceName = StringUtils.strip(phpClass.getNamespaceName(), "\\"); + if(namespaceName.endsWith("Annotation")) { + namespaces.add(namespaceName); + } + } + + return namespaces; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/annotation/TranslationAnnotationReference.java b/src/main/java/de/espend/idea/php/drupal/annotation/TranslationAnnotationReference.java new file mode 100644 index 000000000..6ebe5cc9a --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/annotation/TranslationAnnotationReference.java @@ -0,0 +1,30 @@ +package de.espend.idea.php.drupal.annotation; + +import com.jetbrains.php.lang.PhpLangUtil; +import de.espend.idea.php.annotation.extension.PhpAnnotationCompletionProvider; +import de.espend.idea.php.annotation.extension.parameter.AnnotationCompletionProviderParameter; +import de.espend.idea.php.annotation.extension.parameter.AnnotationPropertyParameter; +import de.espend.idea.php.drupal.utils.TranslationUtil; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + * + * "@Translation("")" + */ +public class TranslationAnnotationReference implements PhpAnnotationCompletionProvider { + @Override + public void getPropertyValueCompletions(AnnotationPropertyParameter parameter, AnnotationCompletionProviderParameter annotationCompletionProviderParameter) { + if(!isSupported(parameter)) { + return; + } + + TranslationUtil.attachGetTextLookupKeys(annotationCompletionProviderParameter.getResult(), parameter.getProject()); + } + + private boolean isSupported(@NotNull AnnotationPropertyParameter parameter) { + return parameter.getType() == AnnotationPropertyParameter.Type.DEFAULT && + PhpLangUtil.equalsClassNames(StringUtils.stripStart(parameter.getPhpClass().getFQN(), "\\"), "Drupal\\Core\\Annotation\\Translation"); + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/completion/PhpCompletionContributor.java b/src/main/java/de/espend/idea/php/drupal/completion/PhpCompletionContributor.java new file mode 100644 index 000000000..eb6aa56b7 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/completion/PhpCompletionContributor.java @@ -0,0 +1,83 @@ +package de.espend.idea.php.drupal.completion; + +import com.intellij.codeInsight.completion.*; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.PsiElement; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.lang.parser.PhpElementTypes; +import com.jetbrains.php.lang.psi.elements.*; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.utils.TranslationUtil; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + */ +public class PhpCompletionContributor extends CompletionContributor { + + public PhpCompletionContributor() { + + // t('foo'); + // @TODO: pattern + extend(CompletionType.BASIC, PlatformPatterns.psiElement(), new CompletionProvider() { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + + PsiElement psiElement = completionParameters.getOriginalPosition(); + if(psiElement == null || !DrupalProjectComponent.isEnabled(psiElement)) { + return; + } + + PsiElement literal = psiElement.getContext(); + if (!(literal instanceof StringLiteralExpression)) { + return; + } + + PsiElement parameterList = literal.getParent(); + if (!(parameterList instanceof ParameterList)) { + return; + } + + PsiElement functionReference = parameterList.getParent(); + if (!(functionReference instanceof FunctionReference) || !"t".equals(((FunctionReference) functionReference).getName())) { + return; + } + + TranslationUtil.attachGetTextLookupKeys(completionResultSet, psiElement.getProject()); + + } + + }); + + // 'route_name' => 'foo'; + // @TODO: pattern + extend(CompletionType.BASIC, PlatformPatterns.psiElement(), new CompletionProvider() { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + + PsiElement psiElement = completionParameters.getOriginalPosition(); + if(psiElement == null || !DrupalProjectComponent.isEnabled(psiElement)) { + return; + } + + PsiElement arrayValueString = psiElement.getContext(); + if (arrayValueString instanceof StringLiteralExpression) { + PsiElement arrayValue = arrayValueString.getParent(); + if(arrayValue != null && arrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) { + PsiElement arrayHashElement = arrayValue.getParent(); + if(arrayHashElement instanceof ArrayHashElement) { + PhpPsiElement arrayKey = ((ArrayHashElement) arrayHashElement).getKey(); + if(arrayKey instanceof StringLiteralExpression && "route_name".equals(((StringLiteralExpression) arrayKey).getContents())) { + completionResultSet.addAllElements(RouteHelper.getRoutesLookupElements(psiElement.getProject())); + } + } + } + } + + } + + }); + + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/completion/TwigCompletionContributor.java b/src/main/java/de/espend/idea/php/drupal/completion/TwigCompletionContributor.java new file mode 100644 index 000000000..3e6a66b85 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/completion/TwigCompletionContributor.java @@ -0,0 +1,40 @@ +package de.espend.idea.php.drupal.completion; + + +import com.intellij.codeInsight.completion.*; +import com.intellij.psi.PsiElement; +import com.intellij.util.ProcessingContext; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.utils.TranslationUtil; +import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + */ +public class TwigCompletionContributor extends CompletionContributor { + + public TwigCompletionContributor() { + + // ''|t; + extend(CompletionType.BASIC, TwigPattern.getTranslationPattern("t"), new CompletionProvider() { + + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + + PsiElement psiElement = completionParameters.getOriginalPosition(); + if(psiElement == null || !DrupalProjectComponent.isEnabled(psiElement)) { + return; + } + + TranslationUtil.attachGetTextLookupKeys(completionResultSet, psiElement.getProject()); + + } + + }); + + } + + + +} diff --git a/src/main/java/de/espend/idea/php/drupal/completion/YamlCompletionContributor.java b/src/main/java/de/espend/idea/php/drupal/completion/YamlCompletionContributor.java new file mode 100644 index 000000000..35de5bf81 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/completion/YamlCompletionContributor.java @@ -0,0 +1,42 @@ +package de.espend.idea.php.drupal.completion; + +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.util.ProcessingContext; +import de.espend.idea.php.drupal.DrupalIcons; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.psi.YAMLDocument; + +/** + * @author Daniel Espendiller + */ +public class YamlCompletionContributor extends CompletionContributor { + + final private static String[] MODULE_KEYS = new String[] {"name", "type", "description", "package", "version", "core", "configure", "dependencies", "required"}; + + public YamlCompletionContributor() { + + extend( + CompletionType.BASIC, PlatformPatterns.psiElement().withParent(PlatformPatterns.psiElement(YAMLDocument.class)).inFile( + PlatformPatterns.psiFile().withName(PlatformPatterns.string().endsWith(".info.yml")) + ), + new CompletionProvider() { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + + if(!DrupalProjectComponent.isEnabled(completionParameters.getOriginalPosition())) { + return; + } + + for(String key: MODULE_KEYS) { + completionResultSet.addElement(LookupElementBuilder.create(key).withIcon(DrupalIcons.DRUPAL)); + } + + } + } + ); + } + +} diff --git a/src/main/java/de/espend/idea/php/drupal/config/ConfigCompletionGoto.java b/src/main/java/de/espend/idea/php/drupal/config/ConfigCompletionGoto.java new file mode 100644 index 000000000..6830cbe3c --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/config/ConfigCompletionGoto.java @@ -0,0 +1,125 @@ +package de.espend.idea.php.drupal.config; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.indexing.FileBasedIndex; +import com.jetbrains.php.lang.PhpLanguage; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.index.ConfigSchemaIndex; +import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors; +import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; +import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLDocument; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +/** + * @author Daniel Espendiller + */ +public class ConfigCompletionGoto implements GotoCompletionRegistrar { + + final private static MethodMatcher.CallToSignature[] CONFIG = new MethodMatcher.CallToSignature[] { + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Config\\ConfigFactory", "get"), + }; + + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> { + + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + PsiElement parent = psiElement.getParent(); + if(parent == null) { + return null; + } + + MethodMatcher.MethodMatchParameter methodMatchParameter = MethodMatcher.getMatchedSignatureWithDepth(parent, CONFIG); + if(methodMatchParameter == null) { + return null; + } + + return new FormReferenceCompletionProvider(parent); + + }); + } + + private static class FormReferenceCompletionProvider extends GotoCompletionProvider { + + private FormReferenceCompletionProvider(PsiElement element) { + super(element); + } + + @NotNull + @Override + public Collection getLookupElements() { + Collection lookupElements = new ArrayList<>(); + + for(String config: SymfonyProcessors.createResult(getProject(), ConfigSchemaIndex.KEY)) { + lookupElements.add(LookupElementBuilder.create(config).withIcon(Symfony2Icons.CONFIG_VALUE)); + } + + return lookupElements; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + + PsiElement element = psiElement.getParent(); + if(!(element instanceof StringLiteralExpression)) { + return Collections.emptyList(); + } + + final String contents = ((StringLiteralExpression) element).getContents(); + if(StringUtils.isBlank(contents)) { + return Collections.emptyList(); + } + + final Collection psiElements = new ArrayList<>(); + FileBasedIndex.getInstance().getFilesWithKey(ConfigSchemaIndex.KEY, new HashSet<>(Collections.singletonList(contents)), virtualFile -> { + + PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(virtualFile); + if(psiFile == null) { + return true; + } + + YAMLDocument yamlDocument = PsiTreeUtil.getChildOfType(psiFile, YAMLDocument.class); + if(yamlDocument == null) { + return true; + } + + for(YAMLKeyValue yamlKeyValue: PsiTreeUtil.getChildrenOfTypeAsList(yamlDocument, YAMLKeyValue.class)) { + String keyText = PsiElementUtils.trimQuote(yamlKeyValue.getKeyText()); + if(StringUtils.isNotBlank(keyText) && keyText.equals(contents)) { + psiElements.add(yamlKeyValue); + } + } + + return true; + }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(getProject()), YAMLFileType.YML)); + + return psiElements; + } + } + +} diff --git a/src/main/java/de/espend/idea/php/drupal/gettext/GettextResourceBundle.java b/src/main/java/de/espend/idea/php/drupal/gettext/GettextResourceBundle.java new file mode 100644 index 000000000..7071821ff --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/gettext/GettextResourceBundle.java @@ -0,0 +1,151 @@ +package de.espend.idea.php.drupal.gettext; + + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Tom Schaible + * + * A resource bundle that is created from a gettext PO file + */ +public class GettextResourceBundle extends ResourceBundle { + + private static final Logger LOG = Logger + .getLogger(GettextResourceBundle.class); + + private static final Pattern LINE_PATTERN = Pattern + .compile("^([\\w_\\[\\]]*)\\s*\\\"(.*)\\\"$"); + + private Map resources = new HashMap<>(); + + public GettextResourceBundle(InputStream inputStream) { + init(new LineNumberReader(new InputStreamReader(inputStream))); + } + + public GettextResourceBundle(Reader reader) { + init(new LineNumberReader(reader)); + } + + public GettextResourceBundle(File file) { + try { + init(new LineNumberReader(new FileReader(file))); + } catch (FileNotFoundException e) { + LOG.error("GettextResourceBundle could not be initialized", e); + } + } + + /** + * initialize the ResourceBundle from a PO file + * + * if + * + * @param reader + * the reader to read the contents of the PO file from + */ + private void init(LineNumberReader reader) { + if (reader != null) { + String line = null; + String key = null; + String value = null; + try { + while ((line = reader.readLine()) != null) { + if (line.startsWith("#")) { + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, comment skipped [" + line + + "]"); + } else if (line.trim().length() == 0) { + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, whitespace line skipped"); + } else { + Matcher matcher = LINE_PATTERN.matcher(line); + if (matcher.matches()) { + String type = matcher.group(1); + String str = matcher.group(2); + if ("msgid".equals(type)) { + if ( key != null && value != null ) { + LOG.debug("Parsing PO file, key,value pair found [" + key + " => " + value + "]"); + resources.put(StringEscapeUtils.unescapeJava(key), StringEscapeUtils.unescapeJava(value)); + key = null; + value = null; + } + key = str; + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, msgid found [" + key + + "]"); + } + else if ("msgstr".equals(type)) { + value = str; + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, msgstr found [" + value + + "]"); + } + else if ( type == null || type.length()==0 ) { + if ( value == null ) { + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, addition to msgid found [" + str + + "]"); + key += str; + } + else { + LOG.trace(reader.getLineNumber() + + ": Parsing PO file, addition to msgstr found [" + str + + "]"); + value += str; + } + + + } + } else { + LOG.error(reader.getLineNumber() + + ": Parsing PO file, invalid syntax [" + + line + "]"); + } + } + + } + + if ( key != null && value != null ) { + LOG.debug("Parsing PO file, key,value pair found [" + key + " => " + value + "]"); + resources.put(StringEscapeUtils.unescapeJava(key), StringEscapeUtils.unescapeJava(value)); + key = null; + value = null; + } + } catch (IOException e) { + LOG.error("GettextResourceBundle could not be initialized", e); + } + + } else { + LOG.warn("GettextResourceBundle could not be initialized, input was null"); + } + LOG.info("GettextResourceBundle initialization complete, " + resources.size() + " resources loaded"); + } + + /* + * (non-Javadoc) + * + * @see java.util.ResourceBundle#getKeys() + */ + @NotNull + @Override + public Enumeration getKeys() { + return Collections.enumeration(resources.keySet()); + } + + /* + * (non-Javadoc) + * + * @see java.util.ResourceBundle#handleGetObject(java.lang.String) + */ + @Override + protected Object handleGetObject(@NotNull String key) { + return resources.get(key); + } + +} \ No newline at end of file diff --git a/src/main/java/de/espend/idea/php/drupal/icons/drupal.png b/src/main/java/de/espend/idea/php/drupal/icons/drupal.png new file mode 100644 index 000000000..3dfd966d8 Binary files /dev/null and b/src/main/java/de/espend/idea/php/drupal/icons/drupal.png differ diff --git a/src/main/java/de/espend/idea/php/drupal/icons/drupal@2x.png b/src/main/java/de/espend/idea/php/drupal/icons/drupal@2x.png new file mode 100644 index 000000000..315f40142 Binary files /dev/null and b/src/main/java/de/espend/idea/php/drupal/icons/drupal@2x.png differ diff --git a/src/main/java/de/espend/idea/php/drupal/index/ConfigEntityTypeAnnotationIndex.java b/src/main/java/de/espend/idea/php/drupal/index/ConfigEntityTypeAnnotationIndex.java new file mode 100644 index 000000000..e67216ce6 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/index/ConfigEntityTypeAnnotationIndex.java @@ -0,0 +1,171 @@ +package de.espend.idea.php.drupal.index; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiRecursiveElementVisitor; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment; +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag; +import com.jetbrains.php.lang.psi.PhpFile; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.elements.PhpPsiElement; +import com.jetbrains.php.lang.psi.stubs.indexes.PhpConstantNameIndex; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import gnu.trove.THashMap; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Daniel Espendiller + */ +public class ConfigEntityTypeAnnotationIndex extends FileBasedIndexExtension { + + public static final ID KEY = ID.create("de.espend.idea.php.drupal.config_entity_type_annotation"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public DataIndexer getIndexer() { + return inputData -> { + final Map map = new THashMap<>(); + + PsiFile psiFile = inputData.getPsiFile(); + if(!(psiFile instanceof PhpFile)) { + return map; + } + + if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) { + return Collections.emptyMap(); + } + + if(!IndexUtil.isValidForIndex(inputData, psiFile)) { + return map; + } + + psiFile.accept(new MyPsiRecursiveElementWalkingVisitor(map)); + + return map; + }; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + @Override + public DataExternalizer getValueExternalizer() { + return StringDataExternalizer.STRING_DATA_EXTERNALIZER; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return PhpConstantNameIndex.PHP_INPUT_FILTER; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 1; + } + + @NotNull + @Override + public ID getName() { + return KEY; + } + + private static class StringDataExternalizer implements DataExternalizer { + + public static final StringDataExternalizer STRING_DATA_EXTERNALIZER = new StringDataExternalizer(); + private final EnumeratorStringDescriptor myStringEnumerator = new EnumeratorStringDescriptor(); + + @Override + public void save(@NotNull DataOutput out, String value) throws IOException { + + if(value == null) { + value = ""; + } + + this.myStringEnumerator.save(out, value); + } + + @Override + public String read(@NotNull DataInput in) throws IOException { + + String value = this.myStringEnumerator.read(in); + + // EnumeratorStringDescriptor writes out "null" as string, so workaround here + if("null".equals(value)) { + value = ""; + } + + // it looks like this is our "null keys not supported" #238, #277 + // so dont force null values here + + return value; + } + } + + private class MyPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementVisitor { + @NotNull + private final Map map; + + private MyPsiRecursiveElementWalkingVisitor(@NotNull Map map) { + this.map = map; + } + + @Override + public void visitElement(PsiElement element) { + if(!(element instanceof PhpDocTag)) { + super.visitElement(element); + return; + } + + String annotationName = StringUtils.stripStart(((PhpDocTag) element).getName(), "@"); + if(!"ConfigEntityType".equals(annotationName)) { + super.visitElement(element); + return; + } + + PsiElement phpDocComment = element.getParent(); + if(!(phpDocComment instanceof PhpDocComment)) { + super.visitElement(element); + return; + } + + PhpPsiElement phpClass = ((PhpDocComment) phpDocComment).getNextPsiSibling(); + if(!(phpClass instanceof PhpClass)) { + super.visitElement(element); + return; + } + + String tagValue = element.getText(); + Matcher matcher = Pattern.compile("id\\s*=\\s*\"([\\w\\.-]+)\"").matcher(tagValue); + if (matcher.find()) { + map.put(matcher.group(1), StringUtils.stripStart(((PhpClass) phpClass).getFQN(), "\\")); + } + + super.visitElement(element); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/index/ConfigSchemaIndex.java b/src/main/java/de/espend/idea/php/drupal/index/ConfigSchemaIndex.java new file mode 100644 index 000000000..35b49cf34 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/index/ConfigSchemaIndex.java @@ -0,0 +1,118 @@ +package de.espend.idea.php.drupal.index; + +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.StringSetDataExternalizer; +import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; +import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; +import gnu.trove.THashMap; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLFile; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Daniel Espendiller + */ +public class ConfigSchemaIndex extends FileBasedIndexExtension> { + + public static final ID> KEY = ID.create("de.espend.idea.php.drupal.config_schema"); + private static final StringSetDataExternalizer EXTERNALIZER = new StringSetDataExternalizer(); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public ID> getName() { + return KEY; + } + + @NotNull + @Override + public DataIndexer, FileContent> getIndexer() { + return inputData -> { + Map> map = new THashMap<>(); + + PsiFile psiFile = inputData.getPsiFile(); + if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) { + return map; + } + + if(!(psiFile instanceof YAMLFile) || !isValidForIndex(psiFile)) { + return map; + } + + for(YAMLKeyValue yamlKeyValue: YamlHelper.getTopLevelKeyValues((YAMLFile) psiFile)) { + String key = PsiElementUtils.trimQuote(yamlKeyValue.getKeyText()); + + if(StringUtils.isBlank(key) || key.contains("*")) { + continue; + } + + Set mappings = new HashSet<>(); + YAMLKeyValue mapping = YamlHelper.getYamlKeyValue(yamlKeyValue, "mapping"); + if(mapping == null) { + continue; + } + + Set keySet = YamlHelper.getKeySet(mapping); + if(keySet != null) { + mappings.addAll(keySet); + } + + map.put(key, mappings); + } + + return map; + }; + + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + @Override + public DataExternalizer> getValueExternalizer() { + return EXTERNALIZER; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return file -> + file.getFileType() == YAMLFileType.YML || file.getFileType() == XmlFileType.INSTANCE; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 2; + } + + private static boolean isValidForIndex(@NotNull PsiFile psiFile) { + + String fileName = psiFile.getName(); + + return !(fileName.startsWith(".") || !fileName.endsWith(".schema.yml")); + } +} + + + diff --git a/src/main/java/de/espend/idea/php/drupal/index/GetTextFileIndex.java b/src/main/java/de/espend/idea/php/drupal/index/GetTextFileIndex.java new file mode 100644 index 000000000..24d641d8c --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/index/GetTextFileIndex.java @@ -0,0 +1,84 @@ +package de.espend.idea.php.drupal.index; + +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.intellij.util.io.VoidDataExternalizer; +import de.espend.idea.php.drupal.gettext.GettextResourceBundle; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.*; + +/** + * @author Daniel Espendiller + */ +public class GetTextFileIndex extends FileBasedIndexExtension { + + public static final ID KEY = ID.create("de.espend.idea.php.drupal.gettext"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public ID getName() { + return KEY; + } + + @NotNull + @Override + public DataIndexer getIndexer() { + return fileContent -> { + PsiFile psiFile = fileContent.getPsiFile(); + if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) { + return Collections.emptyMap(); + } + + Map msgId = new HashMap<>(); + + try { + GettextResourceBundle gettextResourceBundle = new GettextResourceBundle(fileContent.getFile().getInputStream()); + Enumeration tests = gettextResourceBundle.getKeys(); + for(String test: new HashSet<>(Collections.list(tests))) { + if(StringUtils.isNotBlank(test)) { + msgId.put(test, null); + } + } + } catch (IOException e) { + return msgId; + } + return msgId; + }; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + @Override + public DataExternalizer getValueExternalizer() { + return VoidDataExternalizer.INSTANCE; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return file -> "po".equals(file.getExtension()); + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 1; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/index/MenuIndex.java b/src/main/java/de/espend/idea/php/drupal/index/MenuIndex.java new file mode 100644 index 000000000..ebc2b954a --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/index/MenuIndex.java @@ -0,0 +1,92 @@ +package de.espend.idea.php.drupal.index; + +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.intellij.util.io.VoidDataExternalizer; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; +import gnu.trove.THashMap; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLFile; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +import java.util.Collections; +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class MenuIndex extends FileBasedIndexExtension { + + public static final ID KEY = ID.create("de.espend.idea.php.drupal.menu_keys"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public DataIndexer getIndexer() { + + return inputData -> { + + Map map = new THashMap<>(); + + PsiFile psiFile = inputData.getPsiFile(); + if(!(psiFile instanceof YAMLFile) || !psiFile.getName().endsWith(".menu.yml")) { + return map; + } + + if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) { + return Collections.emptyMap(); + } + + for (YAMLKeyValue yamlKeyValue : YamlHelper.getTopLevelKeyValues((YAMLFile) psiFile)) { + String keyText = yamlKeyValue.getKeyText(); + if(StringUtils.isBlank(keyText)) { + continue; + } + + map.put(keyText, null); + } + + return map; + }; + } + + @NotNull + @Override + public ID getName() { + return KEY; + } + + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + public DataExternalizer getValueExternalizer() { + return VoidDataExternalizer.INSTANCE; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return virtualFile -> virtualFile.getFileType() == YAMLFileType.YML; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 1; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/index/PermissionIndex.java b/src/main/java/de/espend/idea/php/drupal/index/PermissionIndex.java new file mode 100644 index 000000000..f41e4ea44 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/index/PermissionIndex.java @@ -0,0 +1,92 @@ +package de.espend.idea.php.drupal.index; + +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.intellij.util.io.VoidDataExternalizer; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; +import gnu.trove.THashMap; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLFile; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +import java.util.Collections; +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class PermissionIndex extends FileBasedIndexExtension { + + public static final ID KEY = ID.create("de.espend.idea.php.drupal.permission_keys"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public DataIndexer getIndexer() { + + return inputData -> { + + Map map = new THashMap<>(); + + PsiFile psiFile = inputData.getPsiFile(); + if(!(psiFile instanceof YAMLFile) || !psiFile.getName().endsWith(".permissions.yml")) { + return map; + } + + if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) { + return Collections.emptyMap(); + } + + for (YAMLKeyValue yamlKeyValue : YamlHelper.getTopLevelKeyValues((YAMLFile) psiFile)) { + String keyText = yamlKeyValue.getKeyText(); + if(StringUtils.isBlank(keyText)) { + continue; + } + + map.put(keyText, null); + } + + return map; + }; + } + + @NotNull + @Override + public ID getName() { + return KEY; + } + + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + public DataExternalizer getValueExternalizer() { + return VoidDataExternalizer.INSTANCE; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return virtualFile -> virtualFile.getFileType() == YAMLFileType.YML; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 1; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/linemarker/RouteFormLineMarkerProvider.java b/src/main/java/de/espend/idea/php/drupal/linemarker/RouteFormLineMarkerProvider.java new file mode 100644 index 000000000..9f9204d5e --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/linemarker/RouteFormLineMarkerProvider.java @@ -0,0 +1,109 @@ +package de.espend.idea.php.drupal.linemarker; + +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.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.yaml.psi.YAMLKeyValue; +import org.jetbrains.yaml.psi.YAMLMapping; +import org.jetbrains.yaml.psi.YAMLScalar; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Daniel Espendiller + */ +public class RouteFormLineMarkerProvider implements LineMarkerProvider { + @Nullable + @Override + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) { + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List psiElements, @NotNull Collection> results) { + if(psiElements.size() == 0) { + return; + } + + Project project = psiElements.get(0).getProject(); + if(!DrupalProjectComponent.isEnabled(project)) { + return; + } + + for (PsiElement psiElement : psiElements) { + collectRouteInlineClasses(results, project, psiElement); + } + } + + private void collectRouteInlineClasses(@NotNull Collection> results, @NotNull Project project, @NotNull PsiElement psiElement) { + + if(!(YamlElementPatternHelper.getSingleLineScalarKey("_form").accepts(psiElement) || + YamlElementPatternHelper.getSingleLineScalarKey("_entity_form").accepts(psiElement)) + ) { + return; + } + + PsiElement yamlScalar = psiElement.getParent(); + if(!(yamlScalar instanceof YAMLScalar)) { + return; + } + + String textValue = ((YAMLScalar) yamlScalar).getTextValue(); + + Collection classesInterface = new ArrayList<>(PhpElementsUtil.getClassesInterface(project, textValue)); + classesInterface.addAll(IndexUtil.getFormClassForId(project, textValue)); + + if(classesInterface.size() == 0) { + return; + } + + PsiElement yamlKeyValue = yamlScalar.getParent(); + if(!(yamlKeyValue instanceof YAMLKeyValue)) { + return; + } + + YAMLMapping parentMapping = ((YAMLKeyValue) yamlKeyValue).getParentMapping(); + if(parentMapping == null) { + return; + } + + PsiElement parent = parentMapping.getParent(); + if(!(parent instanceof YAMLKeyValue)) { + return; + } + + String keyText = ((YAMLKeyValue) parent).getKeyText(); + + if(!"defaults".equals(keyText)) { + return; + } + + YAMLMapping parentMapping1 = ((YAMLKeyValue) parent).getParentMapping(); + if(parentMapping1 == null) { + return; + } + + PsiElement parent1 = parentMapping1.getParent(); + if(!(parent1 instanceof YAMLKeyValue)) { + return; + } + + NavigationGutterIconBuilder builder = NavigationGutterIconBuilder.create(Symfony2Icons.FORM_TYPE_LINE_MARKER). + setTargets(classesInterface). + setTooltipText("Navigate to form"); + + results.add(builder.createLineMarkerInfo(parent1.getFirstChild())); + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/navigation/PhpGoToDeclarationHandler.java b/src/main/java/de/espend/idea/php/drupal/navigation/PhpGoToDeclarationHandler.java new file mode 100644 index 000000000..09abee549 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/navigation/PhpGoToDeclarationHandler.java @@ -0,0 +1,47 @@ +package de.espend.idea.php.drupal.navigation; + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.utils.DrupalPattern; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Daniel Espendiller + */ +public class PhpGoToDeclarationHandler implements GotoDeclarationHandler { + + @Nullable + @Override + public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int i, Editor editor) { + + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return new PsiElement[0]; + } + + List psiElementList = new ArrayList<>(); + + PsiElement context = psiElement.getContext(); + if(context instanceof StringLiteralExpression && DrupalPattern.isAfterArrayKey(psiElement, "route_name")) { + PsiElement routeTarget = RouteHelper.getRouteNameTarget(psiElement.getProject(), ((StringLiteralExpression) context).getContents()); + if(routeTarget != null) { + psiElementList.add(routeTarget); + } + } + + return psiElementList.toArray(new PsiElement[psiElementList.size()]); + } + + @Nullable + @Override + public String getActionText(DataContext dataContext) { + return null; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/references/PhpRouteReferenceContributor.java b/src/main/java/de/espend/idea/php/drupal/references/PhpRouteReferenceContributor.java new file mode 100644 index 000000000..64701f7ac --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/references/PhpRouteReferenceContributor.java @@ -0,0 +1,122 @@ +package de.espend.idea.php.drupal.references; + +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.*; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.lang.PhpLanguage; +import com.jetbrains.php.lang.psi.elements.ClassReference; +import com.jetbrains.php.lang.psi.elements.NewExpression; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteParameterReference; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteReference; +import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; +import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; +import org.jetbrains.annotations.NotNull; + +/** + * @author Daniel Espendiller + */ +public class PhpRouteReferenceContributor extends PsiReferenceContributor { + + final private static MethodMatcher.CallToSignature[] GENERATOR_SIGNATURES = new MethodMatcher.CallToSignature[] { + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Routing\\UrlGeneratorInterface", "getPathFromRoute"), // <- <@TODO: remove: pre Drupal8 beta + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Routing\\UrlGeneratorInterface", "generateFromRoute"), // <- <@TODO: remove: pre Drupal8 beta + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Routing\\UrlGenerator", "getPathFromRoute"), + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Routing\\UrlGenerator", "generateFromRoute"), + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Url", "fromRoute"), + new MethodMatcher.CallToSignature("\\Drupal\\Core\\Form\\FormStateInterface", "setRedirect"), + new MethodMatcher.CallToSignature("\\Drupal", "url"), + }; + + @Override + public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferenceRegistrar) { + + psiReferenceRegistrar.registerReferenceProvider( + PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), + new PsiReferenceProvider() { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { + + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return new PsiReference[0]; + } + + if (MethodMatcher.getMatchedSignatureWithDepth(psiElement, GENERATOR_SIGNATURES) == null) { + return new PsiReference[0]; + } + + return new PsiReference[]{ new RouteReference((StringLiteralExpression) psiElement) }; + } + } + ); + + psiReferenceRegistrar.registerReferenceProvider( + PlatformPatterns.psiElement(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), + new PsiReferenceProvider() { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { + + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return new PsiReference[0]; + } + + if(psiElement instanceof StringLiteralExpression) { + PsiElement parameterList = psiElement.getParent(); + if(parameterList instanceof ParameterList) { + PsiElement newExpression = parameterList.getParent(); + if(newExpression instanceof NewExpression) { + ClassReference classReference = ((NewExpression) newExpression).getClassReference(); + if(classReference != null && "Url".equals(classReference.getName())) { + String fqn = classReference.getFQN(); + if(fqn != null && fqn.equals("\\Drupal\\Core\\Url")) { + return new PsiReference[]{ new RouteReference((StringLiteralExpression) psiElement) }; + } + } + } + } + } + + return new PsiReference[0]; + } + } + ); + + psiReferenceRegistrar.registerReferenceProvider( + PlatformPatterns.psiElement(StringLiteralExpression.class), + new PsiReferenceProvider() { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { + + if(!Symfony2ProjectComponent.isEnabled(psiElement)) { + return new PsiReference[0]; + } + + MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.ArrayParameterMatcher(psiElement, 1) + .withSignature(GENERATOR_SIGNATURES) + .match(); + + if(methodMatchParameter == null) { + return new PsiReference[0]; + } + + String routeName = PsiElementUtils.getMethodParameterAt(methodMatchParameter.getMethodReference(), 0); + if(routeName == null) { + return new PsiReference[0]; + } + + return new PsiReference[]{ new RouteParameterReference((StringLiteralExpression) psiElement, routeName) }; + + } + + } + + ); + + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/ControllerCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/ControllerCompletion.java new file mode 100644 index 000000000..3b725754d --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/ControllerCompletion.java @@ -0,0 +1,78 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.Method; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.utils.DrupalUtil; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import fr.adrienbrault.idea.symfony2plugin.util.PhpIndexUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * @author Daniel Espendiller + */ +public class ControllerCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + registrar.register(YamlElementPatternHelper.getSingleLineScalarKey("_controller"), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new MyGotoCompletionProvider(psiElement); + }); + + } + + private static class MyGotoCompletionProvider extends GotoCompletionProvider { + MyGotoCompletionProvider(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + + Collection lookupElements = new ArrayList<>(); + + Set moduleNames = DrupalUtil.getModuleNames(getProject()); + + Set phpClasses = new HashSet<>( + PhpIndex.getInstance(getProject()).getAllSubclasses("Drupal\\Core\\Controller\\ControllerBase") + ); + + for (String moduleName : moduleNames) { + phpClasses.addAll( + PhpIndexUtil.getPhpClassInsideNamespace(getProject(), "\\Drupal\\" + moduleName + "\\Controller") + ); + } + + for (PhpClass phpClass : phpClasses) { + for (Method method : phpClass.getOwnMethods()) { + if(!method.getAccess().isPublic() || method.isAbstract() || method.getName().startsWith("__")) { + continue; + } + + lookupElements.add(LookupElementBuilder.create(phpClass.getFQN() + "::" + method.getName()).withIcon(method.getIcon())); + } + } + + return lookupElements; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/PhpRouteParameterCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/PhpRouteParameterCompletion.java new file mode 100644 index 000000000..6aa5dc44a --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/PhpRouteParameterCompletion.java @@ -0,0 +1,169 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.parser.PhpElementTypes; +import com.jetbrains.php.lang.psi.elements.*; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * @author Daniel Espendiller + */ +public class PhpRouteParameterCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + registrar.register(getElementPatternPattern(), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new MyGotoCompletionProvider(psiElement); + }); + } + + private PsiElementPattern.Capture getElementPatternPattern() { + return PlatformPatterns.psiElement().withParent( + PlatformPatterns.psiElement(StringLiteralExpression.class) + .withParent( + PlatformPatterns.or(PlatformPatterns.psiElement(PhpElementTypes.ARRAY_KEY), PlatformPatterns.psiElement(PhpElementTypes.ARRAY_VALUE)) + ) + ) + ; + } + + private static class MyGotoCompletionProvider extends GotoCompletionProvider { + MyGotoCompletionProvider(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + String routeName = getRouteName(getElement()); + if(StringUtils.isBlank(routeName)) { + return Collections.emptyList(); + } + + return Arrays.asList(RouteHelper.getRouteParameterLookupElements(getElement().getProject(), routeName)); + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + String routeName = getRouteName(psiElement); + if(StringUtils.isBlank(routeName)) { + return Collections.emptyList(); + } + + PsiElement parent = psiElement.getParent(); + if(!(parent instanceof StringLiteralExpression)) { + return Collections.emptyList(); + } + + return Arrays.asList(RouteHelper.getRouteParameterPsiElements( + getElement().getProject(), routeName, ((StringLiteralExpression) parent).getContents() + )); + } + + /** + * new Foo('route_name', ['foo' => '']) + */ + @Nullable + private String getRouteName(@NotNull PsiElement psiElement) { + PsiElement parent = psiElement.getParent(); + if(!(parent instanceof StringLiteralExpression)) { + return null; + } + + ArrayCreationExpression arrayCreationExpression = findArrayCreationExpression((StringLiteralExpression) parent); + if(arrayCreationExpression == null) { + return null; + } + + PsiElement parameterList = arrayCreationExpression.getParent(); + if(!(parameterList instanceof ParameterList)) { + return null; + } + + PsiElement newExpression = parameterList.getParent(); + if(!(newExpression instanceof NewExpression)) { + return null; + } + + ClassReference classReference = ((NewExpression) newExpression).getClassReference(); + if(classReference == null) { + return null; + } + + if(!"Drupal\\Core\\Url".equalsIgnoreCase(StringUtils.stripStart(classReference.getFQN(), "\\"))) { + return null; + } + + PsiElement[] parameters = ((NewExpression) newExpression).getParameters(); + if(parameters.length == 0) { + return null; + } + + String stringValue = PhpElementsUtil.getStringValue(parameters[0]); + if(StringUtils.isBlank(stringValue)) { + return null; + } + + return stringValue; + } + } + + @Nullable + private static ArrayCreationExpression findArrayCreationExpression(@NotNull StringLiteralExpression psiElement) { + + // value inside array + // $menu->addChild(array( + // 'foo' => '', + // )); + PsiElement arrayKey = psiElement.getContext(); + if(arrayKey == null) { + return null; + } + + if(arrayKey.getNode().getElementType() == PhpElementTypes.ARRAY_KEY) { + PsiElement arrayHashElement = arrayKey.getContext(); + if(arrayHashElement instanceof ArrayHashElement) { + PsiElement arrayCreationExpression = arrayHashElement.getContext(); + if(arrayCreationExpression instanceof ArrayCreationExpression) { + PsiElement parameterList = arrayCreationExpression.getParent(); + if(parameterList instanceof ParameterList) { + return (ArrayCreationExpression) arrayCreationExpression; + } + } + } + } else if(arrayKey.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) { + // on array creation key dont have value, so provide completion here also + // array('foo' => 'bar', '') + + PsiElement arrayCreationExpression = arrayKey.getContext(); + if(arrayCreationExpression instanceof ArrayCreationExpression) { + PsiElement parameterList = arrayCreationExpression.getParent(); + if(parameterList instanceof ParameterList) { + return (ArrayCreationExpression) arrayCreationExpression; + } + } + } + + return null; + } + +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/YamlEntityFormGotoCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/YamlEntityFormGotoCompletion.java new file mode 100644 index 000000000..e6bc69b8b --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/YamlEntityFormGotoCompletion.java @@ -0,0 +1,56 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.psi.PsiElement; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.index.ConfigEntityTypeAnnotationIndex; +import de.espend.idea.php.drupal.registrar.utils.YamlRegistrarUtil; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * @author Daniel Espendiller + */ +public class YamlEntityFormGotoCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + registrar.register(YamlElementPatternHelper.getSingleLineScalarKey("_entity_form"), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new MyGotoCompletionProvider(psiElement); + }); + } + + private static class MyGotoCompletionProvider extends GotoCompletionProvider { + public MyGotoCompletionProvider(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + return IndexUtil.getIndexedKeyLookup(getProject(), ConfigEntityTypeAnnotationIndex.KEY); + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + String text = YamlRegistrarUtil.getYamlScalarKey(psiElement); + if(text == null) { + return Collections.emptyList(); + } + + return new ArrayList<>(IndexUtil.getFormClassForId(getProject(), text)); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/YamlMenuGotoCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/YamlMenuGotoCompletion.java new file mode 100644 index 000000000..1295d95d1 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/YamlMenuGotoCompletion.java @@ -0,0 +1,95 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import de.espend.idea.php.drupal.DrupalIcons; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.index.MenuIndex; +import de.espend.idea.php.drupal.registrar.utils.YamlRegistrarUtil; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * @author Daniel Espendiller + */ +public class YamlMenuGotoCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + PsiElementPattern.Capture menuPattern = PlatformPatterns.psiElement().inFile(PlatformPatterns.psiFile().withName(PlatformPatterns.string().endsWith(".menu.yml"))); + + registrar.register(PlatformPatterns.and(YamlElementPatternHelper.getSingleLineScalarKey("parent"), menuPattern), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new ParentMenu(psiElement); + }); + + + registrar.register(PlatformPatterns.and(YamlElementPatternHelper.getWithFirstRootKey(), menuPattern), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new MenuKeys(psiElement); + }); + } + + private static class ParentMenu extends GotoCompletionProvider { + ParentMenu(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + return IndexUtil.getIndexedKeyLookup(getProject(), MenuIndex.KEY); + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + String text = YamlRegistrarUtil.getYamlScalarKey(psiElement); + if(text == null) { + return Collections.emptyList(); + } + + return new ArrayList<>(IndexUtil.getMenuForId(getProject(), text)); + } + } + + private static class MenuKeys extends GotoCompletionProvider { + MenuKeys(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + + Collection lookupElements = new ArrayList<>(); + for (String s : new String[]{"title", "route_name", "enabled", "parent", "description", "weight"}) { + lookupElements.add(LookupElementBuilder.create(s).withIcon(DrupalIcons.DRUPAL).withTypeText("Menu", true)); + } + + return lookupElements; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/YamlPermissionGotoCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/YamlPermissionGotoCompletion.java new file mode 100644 index 000000000..73236a920 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/YamlPermissionGotoCompletion.java @@ -0,0 +1,89 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.indexing.FileBasedIndex; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import de.espend.idea.php.drupal.index.PermissionIndex; +import de.espend.idea.php.drupal.registrar.utils.YamlRegistrarUtil; +import de.espend.idea.php.drupal.utils.IndexUtil; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLUtil; +import org.jetbrains.yaml.psi.YAMLFile; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +/** + * @author Daniel Espendiller + */ +public class YamlPermissionGotoCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + registrar.register(YamlElementPatternHelper.getSingleLineScalarKey("_permission"), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new MyGotoCompletionProvider(psiElement); + }); + } + + private static class MyGotoCompletionProvider extends GotoCompletionProvider { + public MyGotoCompletionProvider(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + return IndexUtil.getIndexedKeyLookup(getProject(), PermissionIndex.KEY); + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + String text = YamlRegistrarUtil.getYamlScalarKey(psiElement); + if(text == null) { + return Collections.emptyList(); + } + + return getPermissionPsiElements(getProject(), text); + } + } + + @NotNull + public static Collection getPermissionPsiElements(@NotNull Project project, @NotNull String text) { + Collection targets = new ArrayList<>(); + + Collection virtualFiles = new ArrayList<>(); + + FileBasedIndex.getInstance().getFilesWithKey(PermissionIndex.KEY, new HashSet<>(Collections.singletonList(text)), virtualFile -> { + virtualFiles.add(virtualFile); + return true; + }, GlobalSearchScope.allScope(project)); + + for (VirtualFile virtualFile : virtualFiles) { + PsiFile file = PsiManager.getInstance(project).findFile(virtualFile); + if(!(file instanceof YAMLFile)) { + continue; + } + + ContainerUtil.addIfNotNull(targets, YAMLUtil.getQualifiedKeyInFile((YAMLFile) file, text)); + } + + return targets; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/YamlRouteKeyCompletion.java b/src/main/java/de/espend/idea/php/drupal/registrar/YamlRouteKeyCompletion.java new file mode 100644 index 000000000..ffb011ffa --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/YamlRouteKeyCompletion.java @@ -0,0 +1,90 @@ +package de.espend.idea.php.drupal.registrar; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import de.espend.idea.php.drupal.DrupalIcons; +import de.espend.idea.php.drupal.DrupalProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * @author Daniel Espendiller + */ +public class YamlRouteKeyCompletion implements GotoCompletionRegistrar { + @Override + public void register(GotoCompletionRegistrarParameter registrar) { + PsiElementPattern.Capture filePattern = PlatformPatterns.psiElement().inFile(PlatformPatterns.psiFile().withName(PlatformPatterns.string().endsWith(".routing.yml"))); + + registrar.register(PlatformPatterns.and(YamlElementPatternHelper.getParentKeyName("defaults"), filePattern), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new DefaultRoutes(psiElement); + }); + + registrar.register(PlatformPatterns.and(YamlElementPatternHelper.getParentKeyName("requirements"), filePattern), psiElement -> { + if(!DrupalProjectComponent.isEnabled(psiElement)) { + return null; + } + + return new EntityAccessRoutes(psiElement); + }); + } + + private static class DefaultRoutes extends GotoCompletionProvider { + DefaultRoutes(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + Collection lookupElements = new ArrayList<>(); + for (String s : new String[]{"_entity_form", "_title_callback", "op", "_entity_access", "_entity_list", "_controller"}) { + lookupElements.add(LookupElementBuilder.create(s).withIcon(DrupalIcons.DRUPAL).withTypeText("Routing", true)); + } + + return lookupElements; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + return Collections.emptyList(); + } + } + + private static class EntityAccessRoutes extends GotoCompletionProvider { + EntityAccessRoutes(PsiElement psiElement) { + super(psiElement); + } + + @NotNull + @Override + public Collection getLookupElements() { + Collection lookupElements = new ArrayList<>(); + for (String s : new String[]{"_entity_access"}) { + lookupElements.add(LookupElementBuilder.create(s).withIcon(DrupalIcons.DRUPAL).withTypeText("Routing", true)); + } + + return lookupElements; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement psiElement) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/registrar/utils/YamlRegistrarUtil.java b/src/main/java/de/espend/idea/php/drupal/registrar/utils/YamlRegistrarUtil.java new file mode 100644 index 000000000..4232238f2 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/registrar/utils/YamlRegistrarUtil.java @@ -0,0 +1,29 @@ +package de.espend.idea.php.drupal.registrar.utils; + +import com.intellij.psi.PsiElement; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.yaml.psi.YAMLScalar; + +/** + * @author Daniel Espendiller + */ +public class YamlRegistrarUtil { + + @Nullable + public static String getYamlScalarKey(@NotNull PsiElement psiElement) { + PsiElement parent = psiElement.getParent(); + + if(!(parent instanceof YAMLScalar)) { + return null; + } + + String text = ((YAMLScalar) parent).getTextValue(); + if(StringUtils.isBlank(text)) { + return null; + } + + return text; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/utils/DrupalPattern.java b/src/main/java/de/espend/idea/php/drupal/utils/DrupalPattern.java new file mode 100644 index 000000000..7d9878740 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/utils/DrupalPattern.java @@ -0,0 +1,32 @@ +package de.espend.idea.php.drupal.utils; + +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.parser.PhpElementTypes; +import com.jetbrains.php.lang.psi.elements.ArrayHashElement; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; + +public class DrupalPattern { + public static boolean isAfterArrayKey(PsiElement psiElement, String arrayKeyName) { + + PsiElement literal = psiElement.getContext(); + if(!(literal instanceof StringLiteralExpression)) { + return false; + } + + PsiElement arrayValue = literal.getParent(); + if(arrayValue.getNode().getElementType() != PhpElementTypes.ARRAY_VALUE) { + return false; + } + + PsiElement arrayHashElement = arrayValue.getParent(); + if(!(arrayHashElement instanceof ArrayHashElement)) { + return false; + } + + PsiElement arrayKey = ((ArrayHashElement) arrayHashElement).getKey(); + String keyString = PhpElementsUtil.getStringValue(arrayKey); + + return arrayKeyName.equals(keyString); + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/utils/DrupalUtil.java b/src/main/java/de/espend/idea/php/drupal/utils/DrupalUtil.java new file mode 100644 index 000000000..512b864b0 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/utils/DrupalUtil.java @@ -0,0 +1,37 @@ +package de.espend.idea.php.drupal.utils; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.search.FilenameIndex; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Daniel Espendiller + */ +public class DrupalUtil { + + @NotNull + public static Set getModuleNames(@NotNull Project project) { + Set allFilesByExt = new HashSet<>(); + + for (VirtualFile virtualFile : FilenameIndex.getAllFilesByExt(project, "yml")) { + if(!virtualFile.getName().endsWith(".info.yml")) { + continue; + } + + allFilesByExt.add(StringUtils.stripEnd(virtualFile.getName(), ".info.yml")); + } + + allFilesByExt.addAll(FilenameIndex.getAllFilesByExt(project, "module").stream() + .map(virtualFile -> StringUtils.stripEnd(virtualFile.getName(), ".module")) + .collect(Collectors.toList())); + + return allFilesByExt; + } + +} diff --git a/src/main/java/de/espend/idea/php/drupal/utils/IndexUtil.java b/src/main/java/de/espend/idea/php/drupal/utils/IndexUtil.java new file mode 100644 index 000000000..3ca4495f5 --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/utils/IndexUtil.java @@ -0,0 +1,108 @@ +package de.espend.idea.php.drupal.utils; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.FileContent; +import com.intellij.util.indexing.ID; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.drupal.DrupalIcons; +import de.espend.idea.php.drupal.index.ConfigEntityTypeAnnotationIndex; +import de.espend.idea.php.drupal.index.MenuIndex; +import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLUtil; +import org.jetbrains.yaml.psi.YAMLFile; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.stream.Collectors; + +/** + * @author Daniel Espendiller + */ +public class IndexUtil { + + @NotNull + public static Collection getFormClassForId(@NotNull Project project, @NotNull String id) { + Collection phpClasses = new ArrayList<>(); + + for (String key : SymfonyProcessors.createResult(project, ConfigEntityTypeAnnotationIndex.KEY)) { + if(!id.equals(key)) { + continue; + } + + for (String value : FileBasedIndex.getInstance().getValues(ConfigEntityTypeAnnotationIndex.KEY, key, GlobalSearchScope.allScope(project))) { + phpClasses.addAll(PhpElementsUtil.getClassesInterface(project, value)); + } + } + + return phpClasses; + } + + @NotNull + public static Collection getMenuForId(@NotNull Project project, @NotNull String text) { + Collection virtualFiles = new ArrayList<>(); + + FileBasedIndex.getInstance().getFilesWithKey(MenuIndex.KEY, new HashSet<>(Collections.singletonList(text)), virtualFile -> { + virtualFiles.add(virtualFile); + return true; + }, GlobalSearchScope.allScope(project)); + + Collection targets = new ArrayList<>(); + + for (VirtualFile virtualFile : virtualFiles) { + PsiFile file = PsiManager.getInstance(project).findFile(virtualFile); + if(!(file instanceof YAMLFile)) { + continue; + } + + ContainerUtil.addIfNotNull(targets, YAMLUtil.getQualifiedKeyInFile((YAMLFile) file, text)); + } + + return targets; + } + + @NotNull + public static Collection getIndexedKeyLookup(@NotNull Project project, @NotNull ID var1) { + Collection lookupElements = new ArrayList<>(); + + lookupElements.addAll(SymfonyProcessors.createResult(project, var1).stream().map( + s -> LookupElementBuilder.create(s).withIcon(DrupalIcons.DRUPAL)).collect(Collectors.toList()) + ); + + return lookupElements; + } + + public static boolean isValidForIndex(@NotNull FileContent inputData, @NotNull PsiFile psiFile) { + + String fileName = psiFile.getName(); + if(fileName.startsWith(".") || fileName.endsWith("Test")) { + return false; + } + + VirtualFile baseDir = inputData.getProject().getBaseDir(); + if(baseDir == null) { + return false; + } + + // is Test file in path name + String relativePath = VfsUtil.getRelativePath(inputData.getFile(), baseDir, '/'); + if(relativePath != null && (relativePath.contains("/Test/") || relativePath.contains("/Fixtures/"))) { + return false; + } + + return true; + } +} diff --git a/src/main/java/de/espend/idea/php/drupal/utils/TranslationUtil.java b/src/main/java/de/espend/idea/php/drupal/utils/TranslationUtil.java new file mode 100644 index 000000000..6c623534c --- /dev/null +++ b/src/main/java/de/espend/idea/php/drupal/utils/TranslationUtil.java @@ -0,0 +1,17 @@ +package de.espend.idea.php.drupal.utils; + +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.project.Project; +import de.espend.idea.php.drupal.DrupalIcons; +import de.espend.idea.php.drupal.index.GetTextFileIndex; +import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors; +import org.jetbrains.annotations.NotNull; + +public class TranslationUtil { + public static void attachGetTextLookupKeys(@NotNull CompletionResultSet completionResultSet, @NotNull Project project) { + for(String phpClassName: SymfonyProcessors.createResult(project, GetTextFileIndex.KEY)) { + completionResultSet.addElement(LookupElementBuilder.create(phpClassName).withIcon(DrupalIcons.DRUPAL)); + } + } +} diff --git a/src/main/resources/META-INF/drupal.xml b/src/main/resources/META-INF/drupal.xml new file mode 100644 index 000000000..5eb426f85 --- /dev/null +++ b/src/main/resources/META-INF/drupal.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b8388dc8e..3455418e7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -633,6 +633,7 @@ com.jetbrains.php.dql de.espend.idea.php.toolbox com.jetbrains.plugins.webDeployment + com.jetbrains.php com.jetbrains.php diff --git a/src/test/java/de/espend/idea/php/drupal/tests/DrupalLightCodeInsightFixtureTestCase.java b/src/test/java/de/espend/idea/php/drupal/tests/DrupalLightCodeInsightFixtureTestCase.java new file mode 100644 index 000000000..1e6837edf --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/DrupalLightCodeInsightFixtureTestCase.java @@ -0,0 +1,567 @@ +package de.espend.idea.php.drupal.tests; + +import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; +import com.intellij.codeInsight.completion.CompletionProgressIndicator; +import com.intellij.codeInsight.completion.CompletionType; +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.codeInsight.daemon.LineMarkerProviders; +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo; +import com.intellij.codeInsight.daemon.impl.AnnotationHolderImpl; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.intention.IntentionManager; +import com.intellij.codeInsight.lookup.Lookup; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; +import com.intellij.codeInspection.*; +import com.intellij.lang.LanguageAnnotators; +import com.intellij.lang.annotation.Annotation; +import com.intellij.lang.annotation.AnnotationSession; +import com.intellij.lang.annotation.Annotator; +import com.intellij.navigation.GotoRelatedItem; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.patterns.ElementPattern; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; +import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.indexing.FileBasedIndexImpl; +import com.intellij.util.indexing.ID; +import com.intellij.util.ui.UIUtil; +import com.jetbrains.php.lang.psi.elements.Function; +import com.jetbrains.php.lang.psi.elements.Method; +import com.jetbrains.php.lang.psi.elements.PhpReference; +import fr.adrienbrault.idea.symfony2plugin.Settings; +import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.*; + +public abstract class DrupalLightCodeInsightFixtureTestCase extends LightCodeInsightFixtureTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + Settings.getInstance(myFixture.getProject()).pluginEnabled = true; + } + + public void assertCompletionContains(LanguageFileType languageFileType, String configureByText, String... lookupStrings) { + + myFixture.configureByText(languageFileType, configureByText); + myFixture.completeBasic(); + + checkContainsCompletion(lookupStrings); + } + + public void assertCompletionNotContains(String text, String configureByText, String... lookupStrings) { + + myFixture.configureByText(text, configureByText); + myFixture.completeBasic(); + + assertFalse(myFixture.getLookupElementStrings().containsAll(Arrays.asList(lookupStrings))); + } + + public void assertCompletionNotContains(LanguageFileType languageFileType, String configureByText, String... lookupStrings) { + + myFixture.configureByText(languageFileType, configureByText); + myFixture.completeBasic(); + + assertFalse(myFixture.getLookupElementStrings().containsAll(Arrays.asList(lookupStrings))); + } + + public void assertCompletionContains(String filename, String configureByText, String... lookupStrings) { + + myFixture.configureByText(filename, configureByText); + myFixture.completeBasic(); + + completionContainsAssert(lookupStrings); + } + + private void completionContainsAssert(String[] lookupStrings) { + List lookupElements = myFixture.getLookupElementStrings(); + if(lookupElements == null) { + fail(String.format("failed that empty completion contains %s", Arrays.toString(lookupStrings))); + } + + for (String s : Arrays.asList(lookupStrings)) { + if(!lookupElements.contains(s)) { + fail(String.format("failed that completion contains %s in %s", s, lookupElements.toString())); + } + } + } + + public void assertNavigationContains(LanguageFileType languageFileType, String configureByText, String targetShortcut) { + myFixture.configureByText(languageFileType, configureByText); + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + assertNavigationContains(psiElement, targetShortcut); + } + + public void assertNavigationContains(PsiElement psiElement, String targetShortcut) { + + if(!targetShortcut.startsWith("\\")) { + targetShortcut = "\\" + targetShortcut; + } + + Set classTargets = new HashSet(); + + for (GotoDeclarationHandler gotoDeclarationHandler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) { + PsiElement[] gotoDeclarationTargets = gotoDeclarationHandler.getGotoDeclarationTargets(psiElement, 0, myFixture.getEditor()); + if(gotoDeclarationTargets != null && gotoDeclarationTargets.length > 0) { + + for (PsiElement gotoDeclarationTarget : gotoDeclarationTargets) { + if(gotoDeclarationTarget instanceof Method) { + + String meName = ((Method) gotoDeclarationTarget).getName(); + + String clName = ((Method) gotoDeclarationTarget).getContainingClass().getPresentableFQN(); + if(!clName.startsWith("\\")) { + clName = "\\" + clName; + } + + classTargets.add(clName + "::" + meName); + } else if(gotoDeclarationTarget instanceof Function) { + classTargets.add("\\" + ((Function) gotoDeclarationTarget).getName()); + } + } + + } + } + + if(!classTargets.contains(targetShortcut)) { + fail(String.format("failed that PsiElement (%s) navigate to %s on %s", psiElement.toString(), targetShortcut, classTargets.toString())); + } + + } + + public void assertNavigationMatch(String filename, String configureByText, ElementPattern pattern) { + myFixture.configureByText(filename, configureByText); + assertNavigationMatch(pattern); + } + + public void assertNavigationMatch(LanguageFileType languageFileType, String configureByText, ElementPattern pattern) { + myFixture.configureByText(languageFileType, configureByText); + assertNavigationMatch(pattern); + } + + public void assertNavigationMatch(LanguageFileType languageFileType, String configureByText) { + myFixture.configureByText(languageFileType, configureByText); + assertNavigationMatch(PlatformPatterns.psiElement()); + } + + public void assertNavigationMatch(String filename, String configureByText) { + myFixture.configureByText(filename, configureByText); + assertNavigationMatch(PlatformPatterns.psiElement()); + } + + private void assertNavigationIsEmpty() { + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + for (GotoDeclarationHandler gotoDeclarationHandler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) { + PsiElement[] gotoDeclarationTargets = gotoDeclarationHandler.getGotoDeclarationTargets(psiElement, 0, myFixture.getEditor()); + if(gotoDeclarationTargets != null && gotoDeclarationTargets.length > 0) { + fail(String.format("failed that PsiElement (%s) navigate is empty; found target in '%s'", psiElement.toString(), gotoDeclarationHandler.getClass())); + } + } + } + + private void assertNavigationMatch(ElementPattern pattern) { + + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + Set targetStrings = new HashSet(); + + for (GotoDeclarationHandler gotoDeclarationHandler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) { + + PsiElement[] gotoDeclarationTargets = gotoDeclarationHandler.getGotoDeclarationTargets(psiElement, 0, myFixture.getEditor()); + if(gotoDeclarationTargets == null || gotoDeclarationTargets.length == 0) { + continue; + } + + for (PsiElement gotoDeclarationTarget : gotoDeclarationTargets) { + targetStrings.add(gotoDeclarationTarget.toString()); + if(pattern.accepts(gotoDeclarationTarget)) { + return; + } + } + } + + fail(String.format("failed that PsiElement (%s) navigate matches one of %s", psiElement.toString(), targetStrings.toString())); + } + + public void assertCompletionResultEquals(String filename, String complete, String result) { + myFixture.configureByText(filename, complete); + myFixture.completeBasic(); + myFixture.checkResult(result); + } + + public void assertCompletionResultEquals(@NotNull FileType fileType, @NotNull String contents, @NotNull String result, @NotNull SymfonyLightCodeInsightFixtureTestCase.LookupElementInsert.Assert assertion) { + myFixture.configureByText(fileType, contents); + UIUtil.invokeAndWaitIfNeeded(new MyLookupElementConditionalInsertRunnable(assertion)); + myFixture.checkResult(result); + } + + public void assertCompletionResultEquals(LanguageFileType languageFileType, String complete, String result) { + myFixture.configureByText(languageFileType, complete); + myFixture.completeBasic(); + myFixture.checkResult(result); + } + + public void assertIndexContains(@NotNull ID id, @NotNull String... keys) { + assertIndex(id, false, keys); + } + + public void assertIndex(@NotNull ID id, boolean notCondition, @NotNull String... keys) { + for (String key : keys) { + + final Collection virtualFiles = new ArrayList(); + + FileBasedIndexImpl.getInstance().getFilesWithKey(id, new HashSet(Arrays.asList(key)), new Processor() { + @Override + public boolean process(VirtualFile virtualFile) { + virtualFiles.add(virtualFile); + return true; + } + }, GlobalSearchScope.allScope(getProject())); + + if(notCondition && virtualFiles.size() > 0) { + fail(String.format("Fail that ID '%s' not contains '%s'", id.toString(), key)); + } else if(!notCondition && virtualFiles.size() == 0) { + fail(String.format("Fail that ID '%s' contains '%s'", id.toString(), key)); + } + } + } + + public void assertIndexContainsKeyWithValue(@NotNull ID id, @NotNull String key, @NotNull String value) { + assertContainsElements(FileBasedIndexImpl.getInstance().getValues(id, key, GlobalSearchScope.allScope(getProject())), value); + } + + public void assertIndexContainsKeyWithValue(@NotNull ID id, @NotNull String key, @NotNull IndexValue.Assert tAssert) { + List values = FileBasedIndexImpl.getInstance().getValues(id, key, GlobalSearchScope.allScope(getProject())); + for (T t : values) { + if(tAssert.match(t)) { + return; + } + } + + fail(String.format("Fail that Key '%s' matches on of '%s' values", key, values.size())); + } + + public void assertLocalInspectionContains(String filename, String content, String contains) { + Set matches = new HashSet(); + + Pair, Integer> localInspectionsAtCaret = getLocalInspectionsAtCaret(filename, content); + for (ProblemDescriptor result : localInspectionsAtCaret.getFirst()) { + TextRange textRange = result.getPsiElement().getTextRange(); + if (textRange.contains(localInspectionsAtCaret.getSecond()) && result.toString().equals(contains)) { + return; + } + + matches.add(result.toString()); + } + + fail(String.format("Fail matches '%s' with one of %s", contains, matches)); + } + + public void assertAnnotationContains(String filename, String content, String contains) { + List matches = new ArrayList(); + for (Annotation annotation : getAnnotationsAtCaret(filename, content)) { + matches.add(annotation.toString()); + if(annotation.getMessage().contains(contains)) { + return; + } + } + + fail(String.format("Fail matches '%s' with one of %s", contains, matches)); + } + + @NotNull + private AnnotationHolderImpl getAnnotationsAtCaret(String filename, String content) { + PsiFile psiFile = myFixture.configureByText(filename, content); + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + AnnotationHolderImpl annotations = new AnnotationHolderImpl(new AnnotationSession(psiFile)); + + for (Annotator annotator : LanguageAnnotators.INSTANCE.allForLanguage(psiFile.getLanguage())) { + annotator.annotate(psiElement, annotations); + } + + return annotations; + } + + public void assertAnnotationNotContains(String filename, String content, String contains) { + for (Annotation annotation : getAnnotationsAtCaret(filename, content)) { + if(annotation.getMessage().contains(contains)) { + fail(String.format("Fail not matching '%s' with '%s'", contains, annotation)); + } + } + } + + public void assertIntentionIsAvailable(LanguageFileType languageFileType, String configureByText, String intentionText) { + myFixture.configureByText(languageFileType, configureByText); + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + for (IntentionAction intentionAction : IntentionManager.getInstance().getIntentionActions()) { + if(intentionAction.isAvailable(getProject(), getEditor(), psiElement.getContainingFile()) && intentionAction.getText().equals(intentionText)) { + return; + } + } + + fail(String.format("Fail intention action '%s' is available in element '%s'", intentionText, psiElement.getText())); + } + + public void assertLocalInspectionNotContains(String filename, String content, String contains) { + Pair, Integer> localInspectionsAtCaret = getLocalInspectionsAtCaret(filename, content); + + for (ProblemDescriptor result : localInspectionsAtCaret.getFirst()) { + TextRange textRange = result.getPsiElement().getTextRange(); + if (textRange.contains(localInspectionsAtCaret.getSecond()) && result.toString().contains(contains)) { + fail(String.format("Fail inspection not contains '%s'", contains)); + } + } + } + + private Pair, Integer> getLocalInspectionsAtCaret(String filename, String content) { + + PsiElement psiFile = myFixture.configureByText(filename, content); + + int caretOffset = myFixture.getCaretOffset(); + if(caretOffset <= 0) { + fail("Please provide tag"); + } + + ProblemsHolder problemsHolder = new ProblemsHolder(InspectionManager.getInstance(getProject()), psiFile.getContainingFile(), false); + + for (LocalInspectionEP localInspectionEP : LocalInspectionEP.LOCAL_INSPECTION.getExtensions()) { + Object object = localInspectionEP.getInstance(); + if(!(object instanceof LocalInspectionTool)) { + continue; + } + + final PsiElementVisitor psiElementVisitor = ((LocalInspectionTool) object).buildVisitor(problemsHolder, false); + + psiFile.acceptChildren(new PsiRecursiveElementVisitor() { + @Override + public void visitElement(PsiElement element) { + psiElementVisitor.visitElement(element); + super.visitElement(element); + } + }); + + psiElementVisitor.visitFile(psiFile.getContainingFile());; + } + + return new Pair, Integer>(problemsHolder.getResults(), caretOffset); + } + + protected void assertLocalInspectionIsEmpty(String filename, String content) { + Pair, Integer> localInspectionsAtCaret = getLocalInspectionsAtCaret(filename, content); + + for (ProblemDescriptor result : localInspectionsAtCaret.getFirst()) { + TextRange textRange = result.getPsiElement().getTextRange(); + if (textRange.contains(localInspectionsAtCaret.getSecond())) { + fail("Fail that matches is empty"); + } + } + } + + protected void createDummyFiles(String... files) throws Exception { + for (String file : files) { + String path = myFixture.getProject().getBaseDir().getPath() + "/" + file; + File f = new File(path); + f.getParentFile().mkdirs(); + f.createNewFile(); + } + } + + private void checkContainsCompletion(String[] lookupStrings) { + completionContainsAssert(lookupStrings); + } + + public void assertLineMarker(@NotNull PsiElement psiElement, @NotNull LineMarker.Assert assertMatch) { + + final List elements = collectPsiElementsRecursive(psiElement); + + for (LineMarkerProvider lineMarkerProvider : LineMarkerProviders.getInstance().allForLanguage(psiElement.getLanguage())) { + Collection lineMarkerInfos = new ArrayList(); + lineMarkerProvider.collectSlowLineMarkers(elements, lineMarkerInfos); + + if(lineMarkerInfos.size() == 0) { + continue; + } + + for (LineMarkerInfo lineMarkerInfo : lineMarkerInfos) { + if(assertMatch.match(lineMarkerInfo)) { + return; + } + } + } + + fail(String.format("Fail that '%s' matches on of '%s' PsiElements", assertMatch.getClass(), elements.size())); + } + + @NotNull + private List collectPsiElementsRecursive(@NotNull PsiElement psiElement) { + final List elements = new ArrayList(); + elements.add(psiElement.getContainingFile()); + + psiElement.acceptChildren(new PsiRecursiveElementVisitor() { + @Override + public void visitElement(PsiElement element) { + elements.add(element); + super.visitElement(element); + } + }); + return elements; + } + + public static class IndexValue { + public interface Assert { + boolean match(@NotNull T value); + } + } + + public static class LineMarker { + public interface Assert { + boolean match(@NotNull LineMarkerInfo markerInfo); + } + + public static class ToolTipEqualsAssert implements Assert { + @NotNull + private final String toolTip; + + public ToolTipEqualsAssert(@NotNull String toolTip) { + this.toolTip = toolTip; + } + + @Override + public boolean match(@NotNull LineMarkerInfo markerInfo) { + return markerInfo.getLineMarkerTooltip() != null && markerInfo.getLineMarkerTooltip().equals(toolTip); + } + } + + public static class TargetAcceptsPattern implements Assert { + + @NotNull + private final String toolTip; + @NotNull + private final ElementPattern pattern; + + public TargetAcceptsPattern(@NotNull String toolTip, @NotNull ElementPattern pattern) { + this.toolTip = toolTip; + this.pattern = pattern; + } + + @Override + public boolean match(@NotNull LineMarkerInfo markerInfo) { + if(markerInfo.getLineMarkerTooltip() == null || !markerInfo.getLineMarkerTooltip().equals(toolTip)) { + return false; + } + + if(!(markerInfo instanceof RelatedItemLineMarkerInfo)) { + return false; + } + + for (Object o : ((RelatedItemLineMarkerInfo) markerInfo).createGotoRelatedItems()) { + if(o instanceof GotoRelatedItem && this.pattern.accepts(((GotoRelatedItem) o).getElement())) { + return true; + } + } + + return false; + } + } + } + + public static class LookupElementInsert { + public interface Assert { + boolean match(@NotNull LookupElement lookupElement); + } + + public static class Icon implements Assert { + + @NotNull + private final javax.swing.Icon icon; + + public Icon(@NotNull javax.swing.Icon icon) { + this.icon = icon; + } + + @Override + public boolean match(@NotNull LookupElement lookupElement) { + LookupElementPresentation presentation = new LookupElementPresentation(); + lookupElement.renderElement(presentation); + return presentation.getIcon() == this.icon; + } + } + } + + public static class LookupElementPresentationAssert { + public interface Assert { + boolean match(@NotNull LookupElementPresentation lookupElement); + } + } + + private class MyLookupElementConditionalInsertRunnable implements Runnable { + + @NotNull + private final SymfonyLightCodeInsightFixtureTestCase.LookupElementInsert.Assert insert; + + public MyLookupElementConditionalInsertRunnable(@NotNull SymfonyLightCodeInsightFixtureTestCase.LookupElementInsert.Assert insert) { + this.insert = insert; + } + + @Override + public void run() { + CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() { + @Override + public void run() { + final CodeCompletionHandlerBase handler = new CodeCompletionHandlerBase(CompletionType.BASIC) { + + @Override + protected void completionFinished(final CompletionProgressIndicator indicator, boolean hasModifiers) { + + // find our lookup element + final LookupElement lookupElement = ContainerUtil.find(indicator.getLookup().getItems(), new Condition() { + @Override + public boolean value(LookupElement lookupElement) { + return insert.match(lookupElement); + } + }); + + if(lookupElement == null) { + fail("No matching lookup element found"); + } + + // overwrite behavior and force completion + insertHandler + CommandProcessor.getInstance().executeCommand(indicator.getProject(), new Runnable() { + @Override + public void run() { + CommandProcessor.getInstance().setCurrentCommandGroupId("Completion" + indicator.hashCode()); + indicator.getLookup().finishLookup(Lookup.AUTO_INSERT_SELECT_CHAR, lookupElement); + } + }, "Autocompletion", null); + } + }; + + Editor editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(getEditor(), getFile()); + handler.invokeCompletion(getProject(), editor); + PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); + } + }, null, null); + } + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigEntityTypeAnnotationIndexTest.java b/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigEntityTypeAnnotationIndexTest.java new file mode 100644 index 000000000..08216e169 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigEntityTypeAnnotationIndexTest.java @@ -0,0 +1,28 @@ +package de.espend.idea.php.drupal.tests.index; + +import de.espend.idea.php.drupal.index.ConfigEntityTypeAnnotationIndex; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see ConfigEntityTypeAnnotationIndex + */ +public class ConfigEntityTypeAnnotationIndexTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/index/fixtures"; + } + + public void testThatMappingValueIsInIndex() { + assertIndexContains(ConfigEntityTypeAnnotationIndex.KEY, "contact_form"); + + assertIndexContainsKeyWithValue(ConfigEntityTypeAnnotationIndex.KEY, "contact_form", "Drupal\\Foo\\Entity\\ContactForm"); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigSchemaIndexTest.java b/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigSchemaIndexTest.java new file mode 100644 index 000000000..29d2182e1 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/ConfigSchemaIndexTest.java @@ -0,0 +1,29 @@ +package de.espend.idea.php.drupal.tests.index; + +import de.espend.idea.php.drupal.index.ConfigSchemaIndex; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + */ +public class ConfigSchemaIndexTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("action.schema.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/index/fixtures"; + } + + public void testThatMappingValueIsInIndex() { + assertIndexContains(ConfigSchemaIndex.KEY, "action.settings"); + + assertIndexContainsKeyWithValue(ConfigSchemaIndex.KEY, "action.settings", value -> + value.contains("recursion_limit") + ); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/MenuIndexTest.java b/src/test/java/de/espend/idea/php/drupal/tests/index/MenuIndexTest.java new file mode 100644 index 000000000..b9c6691c7 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/MenuIndexTest.java @@ -0,0 +1,27 @@ +package de.espend.idea.php.drupal.tests.index; + +import de.espend.idea.php.drupal.index.MenuIndex; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see MenuIndex + */ +public class MenuIndexTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("search.menu.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/index/fixtures"; + } + + public void testThatMappingValueIsInIndex() { + assertIndexContains(MenuIndex.KEY, "search.view"); + assertIndexContains(MenuIndex.KEY, "entity.search_page.collection"); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/PermissionIndexTest.java b/src/test/java/de/espend/idea/php/drupal/tests/index/PermissionIndexTest.java new file mode 100644 index 000000000..dd2c06adf --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/PermissionIndexTest.java @@ -0,0 +1,26 @@ +package de.espend.idea.php.drupal.tests.index; + +import de.espend.idea.php.drupal.index.PermissionIndex; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see PermissionIndex + */ +public class PermissionIndexTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("config.permissions.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/index/fixtures"; + } + + public void testThatMappingValueIsInIndex() { + assertIndexContains(PermissionIndex.KEY, "import configuration"); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/action.schema.yml b/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/action.schema.yml new file mode 100644 index 000000000..421e05fd1 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/action.schema.yml @@ -0,0 +1,40 @@ +# Schema for the configuration files of the Action module. + +action.settings: + type: config_object + label: 'Action settings' + mapping: + recursion_limit: + type: integer + label: 'Recursion limit for actions' + + +action.configuration.action_send_email_action: + type: mapping + label: 'Send email configuration' + mapping: + recipient: + type: string + label: 'Recipient' + subject: + type: label + label: 'Subject' + message: + type: text + label: 'Message' + +action.configuration.action_goto_action: + type: mapping + label: 'Redirect to URL configuration' + mapping: + url: + type: string + label: 'URL' + +action.configuration.action_message_action: + type: mapping + label: 'Display a message to the user configuration' + mapping: + message: + type: text + label: 'Message' diff --git a/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/classes.php b/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/classes.php new file mode 100644 index 000000000..f05d9c66a --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/index/fixtures/classes.php @@ -0,0 +1,17 @@ + + * @see de.espend.idea.php.drupal.linemarker.RouteFormLineMarkerProvider + */ +public class RouteFormLineMarkerProviderTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/linemarker/fixtures"; + } + + public void testLinemarkerForForm() { + assertLineMarker(myFixture.configureByText(YAMLFileType.YML, "" + + "config.export_full:\n" + + " defaults:\n" + + " _form: '\\Foo\\FooBar'" + ), new LineMarker.ToolTipEqualsAssert("Navigate to form")); + } + + public void testLinemarkerForEntityForm() { + assertLineMarker(myFixture.configureByText(YAMLFileType.YML, "" + + "config.export_full:\n" + + " defaults:\n" + + " _entity_form: 'contact_form'" + ), new LineMarker.ToolTipEqualsAssert("Navigate to form")); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/linemarker/fixtures/classes.php b/src/test/java/de/espend/idea/php/drupal/tests/linemarker/fixtures/classes.php new file mode 100644 index 000000000..556b6ba12 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/linemarker/fixtures/classes.php @@ -0,0 +1,23 @@ + + * @see de.espend.idea.php.drupal.registrar.ControllerCompletion + */ +public class ControllerCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testThatEntityFormCompletesAndNavigates() { + assertCompletionContains(YAMLFileType.YML, "" + + "config.import_full:\n" + + " defaults:\n" + + " _controller: ''", + "\\Drupal\\contact\\Controller\\ContactController::foo" + ); + + assertCompletionNotContains(YAMLFileType.YML, "" + + "config.import_full:\n" + + " defaults:\n" + + " _controller: ''", + "\\Drupal\\contact\\Controller\\ContactController::privateBar" + ); + + assertCompletionNotContains(YAMLFileType.YML, "" + + "config.import_full:\n" + + " defaults:\n" + + " _controller: ''", + "\\Drupal\\contact\\Controller\\ContactController::__construct" + ); + } + +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/PhpRouteParameterCompletionTest.java b/src/test/java/de/espend/idea/php/drupal/tests/registrar/PhpRouteParameterCompletionTest.java new file mode 100644 index 000000000..2fecfe090 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/PhpRouteParameterCompletionTest.java @@ -0,0 +1,43 @@ +package de.espend.idea.php.drupal.tests.registrar; + +import com.intellij.patterns.PlatformPatterns; +import com.jetbrains.php.lang.PhpFileType; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.drupal.registrar.ControllerCompletion + */ +public class PhpRouteParameterCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("foo.routing.yml"); + myFixture.copyFileToProject("routing.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testRouteParameterForClassConstructorCompletion() { + assertCompletionContains(PhpFileType.INSTANCE, "'])", + "foobar", "foobar2" + ); + + assertCompletionContains(PhpFileType.INSTANCE, "' => ''])", + "foobar", "foobar2" + ); + } + + public void testRouteParameterForClassConstructorNavigation() { + assertNavigationMatch(PhpFileType.INSTANCE, "bar'])", + PlatformPatterns.psiElement() + ); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlEntityFormGotoCompletionTest.java b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlEntityFormGotoCompletionTest.java new file mode 100644 index 000000000..e8732c847 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlEntityFormGotoCompletionTest.java @@ -0,0 +1,41 @@ +package de.espend.idea.php.drupal.tests.registrar; + +import com.intellij.patterns.PlatformPatterns; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; +import org.jetbrains.yaml.YAMLFileType; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.drupal.registrar.YamlPermissionGotoCompletion + */ +public class YamlEntityFormGotoCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testThatEntityFormCompletesAndNavigates() { + assertCompletionContains(YAMLFileType.YML, "" + + "config.import_full:\n" + + " defaults:\n" + + " _entity_form: ''", + "contact_form" + ); + + assertNavigationMatch(YAMLFileType.YML, "" + + "config.import_full:\n" + + " defaults:\n" + + " _entity_form: 'contact_form'", + PlatformPatterns.psiElement(PhpClass.class) + ); + } + +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlMenuGotoCompletionTest.java b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlMenuGotoCompletionTest.java new file mode 100644 index 000000000..28dd4cba6 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlMenuGotoCompletionTest.java @@ -0,0 +1,43 @@ +package de.espend.idea.php.drupal.tests.registrar; + +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.drupal.registrar.YamlMenuGotoCompletion + */ +public class YamlMenuGotoCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("search.menu.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testCompletesAndNavigates() { + assertCompletionContains("foo.menu.yml", "" + + "config.import_full:\n" + + " parent: ''" + + "search.view" + ); + + assertCompletionContains("foo.menu.yml", "" + + "config.import_full:\n" + + " parent: " + + "search.view" + ); + } + + public void testMenuKeyCompletion() { + assertCompletionContains("foo.menu.yml", "" + + "config.import_full:\n" + + " : foo" + + "route_name" + ); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlPermissionGotoCompletionTest.java b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlPermissionGotoCompletionTest.java new file mode 100644 index 000000000..9808545dd --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlPermissionGotoCompletionTest.java @@ -0,0 +1,49 @@ +package de.espend.idea.php.drupal.tests.registrar; + +import com.intellij.patterns.PatternCondition; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.util.ProcessingContext; +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.drupal.registrar.YamlPermissionGotoCompletion + */ +public class YamlPermissionGotoCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("config.permissions.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testThatRoutePermissionCompletesAndNavigates() { + assertCompletionContains(YAMLFileType.YML, "" + + "config.import_full:\n" + + " requirements:\n" + + " _permission: ''", + "synchronize configuration" + ); + + assertNavigationMatch(YAMLFileType.YML, "" + + "config.import_full:\n" + + " requirements:\n" + + " _permission: 'synchronize configuration'", + PlatformPatterns.psiElement(YAMLKeyValue.class).with(new PatternCondition("key") { + @Override + public boolean accepts(@NotNull YAMLKeyValue yamlKeyValue, ProcessingContext processingContext) { + return "synchronize configuration".equals(yamlKeyValue.getKeyText()); + } + }) + ); + } + +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlRouteKeyCompletionTest.java b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlRouteKeyCompletionTest.java new file mode 100644 index 000000000..a8d9b0bbb --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/YamlRouteKeyCompletionTest.java @@ -0,0 +1,37 @@ +package de.espend.idea.php.drupal.tests.registrar; + +import de.espend.idea.php.drupal.tests.DrupalLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.drupal.registrar.YamlRouteKeyCompletion + */ +public class YamlRouteKeyCompletionTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("search.menu.yml"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures"; + } + + public void testCompletion() { + assertCompletionContains("foo.routing.yml", "" + + "config.import_full:\n" + + " defaults:" + + " : foo" + + "_entity_form" + ); + + assertCompletionContains("foo.routing.yml", "" + + "config.import_full:\n" + + " requirements:" + + " : foo" + + "_entity_access" + ); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures/classes.php b/src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures/classes.php new file mode 100644 index 000000000..89c532e61 --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/registrar/fixtures/classes.php @@ -0,0 +1,31 @@ + + */ +public class IndexUtilTest extends DrupalLightCodeInsightFixtureTestCase { + + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return "src/test/java/de/espend/idea/php/drupal/tests/utils/fixtures"; + } + + public void testIdIsResolved() { + assertNotNull(ContainerUtil.find(IndexUtil.getFormClassForId(getProject(), "contact_form"), phpClass -> + "\\Drupal\\Foo\\Entity\\ContactForm".equals(phpClass.getFQN()) + )); + } +} diff --git a/src/test/java/de/espend/idea/php/drupal/tests/utils/fixtures/classes.php b/src/test/java/de/espend/idea/php/drupal/tests/utils/fixtures/classes.php new file mode 100644 index 000000000..f05d9c66a --- /dev/null +++ b/src/test/java/de/espend/idea/php/drupal/tests/utils/fixtures/classes.php @@ -0,0 +1,17 @@ +