package net.jangaroo.ide.idea.exml; import com.intellij.codeInsight.TargetElementUtilBase; import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; import com.intellij.lang.javascript.psi.JSFunction; import com.intellij.lang.javascript.psi.ecmal4.JSAttribute; import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar; 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.PsiReference; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlToken; import net.jangaroo.exml.api.Exmlc; import net.jangaroo.exml.utils.ExmlUtils; import net.jangaroo.ide.idea.Utils; import net.jangaroo.utils.CompilerUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Frank Wienberg */ public class ExmlElementGotoDeclarationHandler implements GotoDeclarationHandler { @Nullable @Override public PsiElement[] getGotoDeclarationTargets(PsiElement element, int offset, Editor editor) { if (element instanceof XmlToken && element.getContainingFile().getName().endsWith(Exmlc.EXML_SUFFIX)) { PsiReference reference = TargetElementUtilBase.findReference(editor, offset); if (reference != null) { PsiElement resolvedElement = reference.resolve(); if (resolvedElement instanceof XmlTag) { return getGotoDeclarationTargets((XmlTag)resolvedElement); } } } return null; } @Nullable public PsiElement[] getGotoDeclarationTargets(XmlTag resolvedTag) { String[] targetName = getGotoDeclarationTargetName(resolvedTag); if (targetName == null) { return null; } String configClassName = targetName[0]; String configPropertyName = targetName.length > 1 ? targetName[1] : null; JSClass configClass = Utils.getActionScriptClass(resolvedTag, configClassName); if (configClass != null) { // found config class that should have an [ExtConfig(target="...")] annotation: String targetClassName = findTargetClassName(configClass); if (targetClassName != null) { // always prefer EXML file: PsiElement exmlDeclarationTarget = getGotoDeclarationTargetInExmlFile(resolvedTag, targetClassName, configPropertyName); if (exmlDeclarationTarget != null) { return new PsiElement[]{exmlDeclarationTarget}; } } if (configPropertyName != null) { // find ActionScript property setter public function set <attributeValue>: JSFunction attributeSetter = configClass.findFunctionByNameAndKind(configPropertyName, JSFunction.FunctionKind.SETTER); if (attributeSetter != null) { return new PsiElement[]{attributeSetter}; } } if (targetClassName != null) { JSClass targetClass = Utils.getActionScriptClass(resolvedTag, targetClassName); if (targetClass != null) { return new PsiElement[]{configClass, targetClass}; } } return new PsiElement[]{configClass}; } return null; } private static PsiElement getGotoDeclarationTargetInExmlFile(XmlTag resolvedTag, String targetClassName, String configPropertyName) { PsiElement exmlDeclarationTarget = null; XmlFile exmlPsiFile = findExmlPsiFile(resolvedTag.getProject(), targetClassName); if (exmlPsiFile != null) { exmlDeclarationTarget = exmlPsiFile; if (configPropertyName != null) { // find element <exml:cfg name="<configPropertyName>">: XmlTag rootTag = exmlPsiFile.getRootTag(); if (rootTag != null) { XmlTag[] topLevelElements = rootTag.getSubTags(); for (XmlTag topLevelElement : topLevelElements) { if (Exmlc.EXML_NAMESPACE_URI.equals(topLevelElement.getNamespace()) && Exmlc.EXML_CFG_NODE_NAME.equals(topLevelElement.getLocalName()) && configPropertyName.equals(topLevelElement.getAttributeValue(Exmlc.EXML_CFG_NAME_ATTRIBUTE))) { exmlDeclarationTarget = topLevelElement; } } } } } return exmlDeclarationTarget; } @Nullable private static String[] getGotoDeclarationTargetName(XmlTag resolvedTag) { String resolvedTagName = resolvedTag.getLocalName(); if (XSD.ELEMENT_ELEMENT_NAME.equals(resolvedTagName)) { return getGotoDeclarationTargetNameForElement(resolvedTag); } else if (XSD.ATTRIBUTE_ELEMENT_NAME.equals(resolvedTagName)) { return getGotoDeclarationTargetNameForAttribute(resolvedTag); } return null; } private static String[] getGotoDeclarationTargetNameForElement(XmlTag resolvedTag) { String qualifiedType = resolvedTag.getAttributeValue(XSD.TYPE_ATTRIBUTE_NAME); if (qualifiedType != null) { String[] parts = qualifiedType.split(":"); if (parts.length == 2) { return new String[]{parts[1]}; } } // an element can also be a config option: return getGotoDeclarationTargetNameForAttribute(resolvedTag); } @Nullable private static String[] getGotoDeclarationTargetNameForAttribute(XmlTag resolvedTag) { String configPropertyName = resolvedTag.getAttributeValue(XSD.NAME_ATTRIBUTE_NAME); if (configPropertyName != null) { // An XML attribute definition in an XSD generated by exmlc. // Find the defining type node: XmlTag current = resolvedTag.getParentTag(); while (current != null) { if (XSD.COMPLEX_TYPE_ELEMENT_NAME.equals(current.getLocalName())) { String configClassName = current.getAttributeValue(XSD.NAME_ATTRIBUTE_NAME); if (configClassName != null) { return new String[]{configClassName, configPropertyName}; } } current = current.getParentTag(); } } return null; } @Nullable public static String findTargetClassName(XmlTag xmlTag) { JSClass actionScriptClass = getASClass(xmlTag); return actionScriptClass == null ? null : findTargetClassName(actionScriptClass); } @Nullable public static String findTargetClassName(JSClass asClass) { JSAttributeList attributeList = asClass.getAttributeList(); if (attributeList != null) { for (JSAttribute attribute : attributeList.getAttributes()) { if ("ExtConfig".equals(attribute.getName())) { return attribute.getValueByName("target").getSimpleValue(); } } } return null; } private static JSClass getASClass(XmlTag xmlTag) { String packageName = ExmlUtils.parsePackageFromNamespace(xmlTag.getNamespace()); if (packageName != null) { String className = CompilerUtils.qName(packageName, xmlTag.getLocalName()); JSClass asClass = Utils.getActionScriptClass(xmlTag, className); if (asClass != null) { return asClass; } } return null; } public String getActionText(DataContext paramDataContext) { return null; } @Nullable private static XmlFile findExmlPsiFile(Project project, String targetClassName) { VirtualFile exmlFile = findExmlFile(project, targetClassName); if (exmlFile != null) { PsiFile file = PsiManager.getInstance(project).findFile(exmlFile); if (file instanceof XmlFile && file.isValid()) { return (XmlFile)file; } } return null; } @Nullable private static VirtualFile findExmlFile(Project project, String className) { String exmlFileName = className.replace('.', '/') + Exmlc.EXML_SUFFIX; Module[] modules = ModuleManager.getInstance(project).getModules(); for (Module module : modules) { VirtualFile exmlFile = findExmlFile(ModuleRootManager.getInstance(module).getSourceRoots(), exmlFileName); if (exmlFile != null) { return exmlFile; } } LibraryTable table = LibraryTablesRegistrar.getInstance().getLibraryTableByLevel(LibraryTablesRegistrar.PROJECT_LEVEL, project); if (table != null) { for (Library library : table.getLibraries()) { VirtualFile exmlFile = findExmlFile(library.getFiles(OrderRootType.SOURCES), exmlFileName); if (exmlFile != null) { return exmlFile; } } } return null; } @Nullable private static VirtualFile findExmlFile(@NotNull VirtualFile[] sourceRoots, @NotNull String exmlFileName) { for (VirtualFile contentRoot : sourceRoots) { VirtualFile exmlFile = contentRoot.findFileByRelativePath(exmlFileName); if (exmlFile != null) { return exmlFile; } } return null; } }