diff --git a/plugins/devkit/src/dom/impl/ExtensionDomExtender.java b/plugins/devkit/src/dom/impl/ExtensionDomExtender.java index a76c166f43f8..cbd621528699 100644 --- a/plugins/devkit/src/dom/impl/ExtensionDomExtender.java +++ b/plugins/devkit/src/dom/impl/ExtensionDomExtender.java @@ -49,6 +49,7 @@ import java.util.*; */ public class ExtensionDomExtender extends DomExtender { private static final PsiClassConverter CLASS_CONVERTER = new PluginPsiClassConverter(); + private static final Converter LANGUAGE_CONVERTER = new LanguageResolvingConverter(); private static class MyRequired implements Required { @Override @@ -236,6 +237,9 @@ public class ExtensionDomExtender extends DomExtender { final DomExtension extension = registrar.registerGenericAttributeValueChildExtension(new XmlName(attrName), clazz).setDeclaringElement(field); markAsClass(extension, fieldName, withElement); + if (clazz.equals(String.class)) { + markAsLanguage(extension, fieldName); + } } return; } @@ -265,6 +269,12 @@ public class ExtensionDomExtender extends DomExtender { } } + private static void markAsLanguage(DomExtension extension, String fieldName) { + if ("language".equals(fieldName)) { + extension.setConverter(LANGUAGE_CONVERTER); + } + } + private static void markAsClass(DomExtension extension, String fieldName, @Nullable With withElement) { if (withElement != null) { final String withClassName = withElement.getImplements().getStringValue(); @@ -275,7 +285,7 @@ public class ExtensionDomExtender extends DomExtender { } }); } - if (isClassField(fieldName) || withElement != null) { + if (withElement != null || isClassField(fieldName)) { extension.setConverter(CLASS_CONVERTER); } } diff --git a/plugins/devkit/src/dom/impl/LanguageResolvingConverter.java b/plugins/devkit/src/dom/impl/LanguageResolvingConverter.java new file mode 100644 index 000000000000..da1fb7217f5f --- /dev/null +++ b/plugins/devkit/src/dom/impl/LanguageResolvingConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.devkit.dom.impl; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.util.Condition; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xml.ConvertContext; +import com.intellij.util.xml.ResolvingConverter; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +class LanguageResolvingConverter extends ResolvingConverter { + + @NotNull + @Override + public Collection getVariants(final ConvertContext context) { + return LanguageResolvingUtil.getAllLanguageDefinitions(context); + } + + @Nullable + @Override + public LookupElement createLookupElement(LanguageResolvingUtil.LanguageDefinition o) { + return LookupElementBuilder.create(o.clazz, o.id) + .withIcon(o.icon) + .withTailText(o.displayName == null ? null : " (" + o.displayName + ")") + .withTypeText(o.clazz.getQualifiedName(), true); + } + + @Nullable + @Override + public LanguageResolvingUtil.LanguageDefinition fromString(@Nullable @NonNls final String s, ConvertContext context) { + return ContainerUtil.find(getVariants(context), new Condition() { + @Override + public boolean value(LanguageResolvingUtil.LanguageDefinition definition) { + return definition.id.equals(s); + } + }); + } + + @Nullable + @Override + public String toString(@Nullable LanguageResolvingUtil.LanguageDefinition o, ConvertContext context) { + return o != null ? o.id : null; + } + + @Override + public String getErrorMessage(@Nullable String s, ConvertContext context) { + return "Cannot resolve language with id ''" + s + "''"; + } +} diff --git a/plugins/devkit/src/dom/impl/LanguageResolvingUtil.java b/plugins/devkit/src/dom/impl/LanguageResolvingUtil.java new file mode 100644 index 000000000000..b9f036355224 --- /dev/null +++ b/plugins/devkit/src/dom/impl/LanguageResolvingUtil.java @@ -0,0 +1,254 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.devkit.dom.impl; + +import com.intellij.codeInsight.completion.CompletionContributorEP; +import com.intellij.icons.AllIcons; +import com.intellij.lang.DependentLanguage; +import com.intellij.lang.Language; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.Condition; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.GlobalSearchScopesCore; +import com.intellij.psi.search.searches.ClassInheritorsSearch; +import com.intellij.psi.util.PsiUtil; +import com.intellij.util.NullableFunction; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xml.ConvertContext; +import com.intellij.util.xml.DomJavaUtil; +import com.intellij.util.xml.GenericAttributeValue; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.devkit.dom.Extension; +import org.jetbrains.idea.devkit.dom.ExtensionPoint; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +class LanguageResolvingUtil { + + private static String ANY_LANGUAGE_DEFAULT_ID = Language.ANY.getID(); + + static Collection getAllLanguageDefinitions(ConvertContext context) { + List languageDefinitions = collectLanguageDefinitions(context); + ContainerUtil.addIfNotNull(languageDefinitions, createAnyLanguageDefinition(context)); + return languageDefinitions; + } + + private static List collectLanguageDefinitions(final ConvertContext context) { + final List libraryDefinitions = collectLibraryLanguages(context); + final List projectDefinitions = collectProjectLanguages(context, libraryDefinitions); + + final List all = new ArrayList(libraryDefinitions); + all.addAll(projectDefinitions); + return all; + } + + private static List collectLibraryLanguages(final ConvertContext context) { + return ContainerUtil.mapNotNull(Language.getRegisteredLanguages(), new NullableFunction() { + @Override + public LanguageDefinition fun(Language language) { + if (language.getID().isEmpty() || + language instanceof DependentLanguage) { + return null; + } + final PsiClass psiClass = DomJavaUtil.findClass(language.getClass().getName(), context.getInvocationElement()); + if (psiClass == null) { + return null; + } + + final LanguageFileType type = language.getAssociatedFileType(); + final Icon icon = type != null ? type.getIcon() : null; + return new LanguageDefinition(language.getID(), + psiClass, + icon, language.getDisplayName()); + } + }); + } + + private static List collectProjectLanguages(ConvertContext context, + final List libraryLanguages) { + final PsiClass languageClass = DomJavaUtil.findClass(Language.class.getName(), context.getInvocationElement()); + if (languageClass == null) { + return Collections.emptyList(); + } + + GlobalSearchScope scope = GlobalSearchScopesCore.projectProductionScope(context.getProject()); + final Collection languages = ClassInheritorsSearch.search(languageClass, + scope, true).findAll(); + + return ContainerUtil.mapNotNull(languages, new NullableFunction() { + @Nullable + @Override + public LanguageDefinition fun(final PsiClass language) { + if (language.hasModifierProperty(PsiModifier.ABSTRACT)) { + return null; + } + + if (ContainerUtil.exists(libraryLanguages, new Condition() { + @Override + public boolean value(LanguageDefinition definition) { + return definition.clazz.equals(language); + } + })) { + return null; + } + + String id = computeConstantSuperCtorCallParameter(language, 0); + if (id == null) { + id = computeConstantSuperCtorCallParameter(language, 1); + } + if (id == null) { + id = computeConstantReturnValue(language, "getID"); + } + if (id == null) { + return null; + } + + return new LanguageDefinition(id, + language, + null, + computeConstantReturnValue(language, "getDisplayName")); + } + }); + } + + @Nullable + private static String computeConstantReturnValue(PsiClass languagePsiClass, + String methodName) { + final PsiMethod[] methods = languagePsiClass.findMethodsByName(methodName, false); + if (methods.length != 1) { + return null; + } + + final PsiMethod method = methods[0]; + final PsiCodeBlock body = method.getBody(); + + if (body == null) { + return null; + } + final PsiStatement[] statements = body.getStatements(); + if (statements.length != 1) { + return null; + } + + + final PsiStatement statement = statements[0]; + if (!(statement instanceof PsiReturnStatement)) { + return null; + } + final PsiReturnStatement returnStatement = + (PsiReturnStatement)statement; + final PsiExpression returnValue = returnStatement.getReturnValue(); + if (returnValue == null || + !PsiUtil.isConstantExpression(returnValue)) { + return null; + } + + return computeConstant(languagePsiClass, returnValue); + } + + private static String computeConstantSuperCtorCallParameter(PsiClass languagePsiClass, int index) { + PsiMethod defaultConstructor = null; + for (PsiMethod constructor : languagePsiClass.getConstructors()) { + if (constructor.getParameterList().getParametersCount() == 0) { + defaultConstructor = constructor; + break; + } + } + if (defaultConstructor == null) { + return null; + } + + final PsiCodeBlock body = defaultConstructor.getBody(); + if (body == null) { + return null; + } + final PsiStatement[] statements = body.getStatements(); + if (statements.length < 1) { + return null; + } + + // super() must be first + PsiStatement statement = statements[0]; + if (!(statement instanceof PsiExpressionStatement)) { + return null; + } + PsiExpression expression = ((PsiExpressionStatement)statement).getExpression(); + if (!(expression instanceof PsiMethodCallExpression)) { + return null; + } + PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)expression; + final PsiExpression[] argumentExpressions = methodCallExpression.getArgumentList().getExpressions(); + if (argumentExpressions.length < index + 1) { + return null; + } + return computeConstant(languagePsiClass, argumentExpressions[index]); + } + + private static String computeConstant(PsiClass languagePsiClass, PsiExpression returnValue) { + final PsiConstantEvaluationHelper constantEvaluationHelper = + JavaPsiFacade.getInstance(languagePsiClass.getProject()).getConstantEvaluationHelper(); + + final Object constant = constantEvaluationHelper.computeConstantExpression(returnValue); + return constant instanceof String ? ((String)constant) : null; + } + + @Nullable + private static LanguageDefinition createAnyLanguageDefinition(ConvertContext context) { + final PsiClass languageClass = DomJavaUtil.findClass(Language.class.getName(), context.getInvocationElement()); + if (languageClass == null) return null; + + String anyLanguageId = calculateAnyLanguageId(context); + return new LanguageDefinition(anyLanguageId, languageClass, AllIcons.FileTypes.Any_type, ""); + } + + private static String calculateAnyLanguageId(ConvertContext context) { + final Extension extension = context.getInvocationElement().getParentOfType(Extension.class, true); + if (extension == null) { + return ANY_LANGUAGE_DEFAULT_ID; + } + final ExtensionPoint extensionPoint = extension.getExtensionPoint(); + if (extensionPoint == null) { + return ANY_LANGUAGE_DEFAULT_ID; + } + + final GenericAttributeValue epBeanClass = extensionPoint.getBeanClass(); + if (CompletionContributorEP.class.getName().equals(epBeanClass.getStringValue())) { + return "any"; + } + + return ANY_LANGUAGE_DEFAULT_ID; + } + + static class LanguageDefinition { + + final String id; + final PsiClass clazz; + final Icon icon; + final String displayName; + + LanguageDefinition(String id, PsiClass clazz, Icon icon, String displayName) { + this.id = id; + this.clazz = clazz; + this.icon = icon; + this.displayName = displayName; + } + } +} diff --git a/plugins/devkit/testData/codeInsight/MyLanguage.java b/plugins/devkit/testData/codeInsight/MyLanguage.java new file mode 100644 index 000000000000..7a582c48419a --- /dev/null +++ b/plugins/devkit/testData/codeInsight/MyLanguage.java @@ -0,0 +1,6 @@ +public class MyLanguage extends com.intellij.lang.Language { + + public MyLanguage() { + super("MyLanguageID"); + } +} \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/MyLanguageAttributeEPBean.java b/plugins/devkit/testData/codeInsight/MyLanguageAttributeEPBean.java new file mode 100644 index 000000000000..7e643854bbbf --- /dev/null +++ b/plugins/devkit/testData/codeInsight/MyLanguageAttributeEPBean.java @@ -0,0 +1,5 @@ +public class MyLanguageAttributeEPBean { + + @com.intellij.util.xmlb.annotations.Attribute("language") + public String language; +} \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/languageAttribute.xml b/plugins/devkit/testData/codeInsight/languageAttribute.xml new file mode 100644 index 000000000000..bdd24fac649f --- /dev/null +++ b/plugins/devkit/testData/codeInsight/languageAttribute.xml @@ -0,0 +1,16 @@ + + com.intellij.myPlugin + + + + + + + + + + INVALID_VALUE"/> + + + + \ No newline at end of file diff --git a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy index e6e588f1e4ea..da7e473fe793 100644 --- a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy +++ b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.deadCode.UnusedDeclarationInspection import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspection import com.intellij.codeInspection.unusedSymbol.UnusedSymbolLocalInspection +import com.intellij.codeInspection.xml.DeprecatedClassUsageInspection import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PluginPathManager import com.intellij.psi.ElementDescriptionUtil @@ -30,7 +31,6 @@ import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase import com.intellij.testFramework.fixtures.TempDirTestFixture import com.intellij.usageView.UsageViewNodeTextLocation import com.intellij.usageView.UsageViewTypeLocation -import com.intellij.codeInspection.xml.DeprecatedClassUsageInspection import org.jetbrains.idea.devkit.inspections.* /** @@ -204,6 +204,15 @@ public class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { myFixture.testHighlighting("extensionAttributeWithAccessors.xml", "ExtBeanWithAccessors.java"); } + public void testLanguageAttribute() { + myFixture.addClass("package com.intellij.lang; " + + "public class Language { " + + " protected Language(String id) {}" + + "}") + myFixture.testHighlighting("languageAttribute.xml", + "MyLanguageAttributeEPBean.java", "MyLanguage.java") + } + public void testPluginModule() throws Throwable { myFixture.testHighlighting("pluginWithModules.xml"); }