package com.intellij.lang.javascript.uml; import com.intellij.diagram.ChangeTracker; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.injection.MultiHostInjector; import com.intellij.lang.javascript.JSTargetedInjector; import com.intellij.lang.javascript.JSTokenTypes; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl; import com.intellij.lang.javascript.psi.*; import com.intellij.lang.javascript.psi.ecmal4.*; import com.intellij.lang.javascript.psi.resolve.JSResolveUtil; import com.intellij.lang.javascript.psi.util.JSUtils; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.changes.PsiChangeTracker; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.util.PsiFilter; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlText; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class FlashUmlChangeTracker extends ChangeTracker<JSClass, JSNamedElement, JSReferenceExpression> { private static class NameFilter<T extends PsiNamedElement> extends PsiFilter<T> { private NameFilter(@NotNull Class<T> filter) { super(filter); } @Override public boolean areEquivalent(T e1, T e2) { return Comparing.equal(e1.getName(), e2.getName()); } } private static class MethodFilter extends NameFilter<JSFunction> { private final JSFunction.FunctionKind myKind; private MethodFilter(JSFunction.FunctionKind kind) { super(JSFunction.class); myKind = kind; } @Override public boolean accept(JSFunction element) { return JSUtils.getMemberContainingClass(element) != null && element.getKind() == myKind; } @Override public Visitor<JSFunction> createVisitor(List<JSFunction> elements) { return new InjectingVisitor<>(this, elements); } } private static final PsiFilter<JSClass> CLASS_FILTER = new PsiFilter<JSClass>(JSClass.class) { @Override public boolean accept(JSClass element) { return element instanceof XmlBackedJSClassImpl || element.getParent() instanceof JSPackageStatement; } @Override public boolean areEquivalent(JSClass e1, JSClass e2) { if (e1 instanceof XmlBackedJSClassImpl && e2 instanceof XmlBackedJSClassImpl) { return e1.getQualifiedName().equals(e2.getQualifiedName()); } return super.areEquivalent(e1, e2); } }; private static final PsiFilter<JSFunction> SIMPLE_METHOD_FILTER = new MethodFilter(JSFunction.FunctionKind.SIMPLE); private static final PsiFilter<JSFunction> CONSTRUCTOR_FILTER = new MethodFilter(JSFunction.FunctionKind.CONSTRUCTOR); private static final PsiFilter<JSFunction> GETTER_FILTER = new MethodFilter(JSFunction.FunctionKind.GETTER); private static final PsiFilter<JSFunction> SETTER_FILTER = new MethodFilter(JSFunction.FunctionKind.SETTER); private static final PsiFilter<JSVariable> FIELD_FILTER = new NameFilter<JSVariable>(JSVariable.class) { @Override public boolean accept(JSVariable element) { return JSUtils.getMemberContainingClass(element) != null; } @Override public Visitor<JSVariable> createVisitor(List<JSVariable> elements) { return new InjectingVisitor<>(this, elements); } }; private static final PsiFilter<JSReferenceExpression> EXTENDS_FILTER = new ReferenceListFilter(true); private static final PsiFilter<JSReferenceExpression> IMPLEMENTS_FILTER = new ReferenceListFilter(false); private Map<JSClass, FileStatus> myNodeElements; public FlashUmlChangeTracker(Project project, @Nullable PsiFile before, @Nullable PsiFile after) { super(project, before, after); } @Override public PsiFilter<JSClass>[] getNodeFilters() { return new PsiFilter[]{CLASS_FILTER}; } @Override public PsiFilter<JSNamedElement>[] getNodeContentFilters() { return new PsiFilter[]{SIMPLE_METHOD_FILTER, CONSTRUCTOR_FILTER, FIELD_FILTER, GETTER_FILTER, SETTER_FILTER}; } @Override public PsiFilter<JSReferenceExpression>[] getRelationshipFilters() { return new PsiFilter[]{EXTENDS_FILTER, IMPLEMENTS_FILTER}; } @Override public String getPresentableName(PsiNamedElement e) { if (e instanceof JSVariable) { return FlashUmlElementManager.getFieldText((JSVariable)e); } else if (e instanceof JSFunction) { return FlashUmlElementManager.getMethodText((JSFunction)e); } else { return super.getPresentableName(e); } } @Override public String getType(JSNamedElement jsNamedElement) { return FlashUmlElementManager.getPresentableTypeStatic(jsNamedElement); } @Override public Icon getIcon(PsiNamedElement e) { return FlashUmlElementManager.getNodeElementIconStatic(e); } @Override public String getQualifiedName(JSClass e, VirtualFile containingFile) { return e.getQualifiedName(); } @Override public Map<JSClass, FileStatus> getNodeElements() { if (myNodeElements == null) { myNodeElements = new HashMap<>(); Pair<PsiElement, PsiElement> beforeAndAfter = adjustBeforeAfter(); for (PsiFilter<JSClass> filter : getNodeFilters()) { myNodeElements.putAll(PsiChangeTracker.getElementsChanged(beforeAndAfter.second, beforeAndAfter.first, filter)); } } return myNodeElements; } private Pair<PsiElement, PsiElement> adjustBeforeAfter() { PsiElement before = getBefore(); PsiElement after = getAfter(); if (after != null && JavaScriptSupportLoader.isFlexMxmFile((PsiFile)after) && before == null) { after = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)getAfter()); } else if (before != null && JavaScriptSupportLoader.isFlexMxmFile((PsiFile)before) && after == null) { before = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)getBefore()); } else if (before != null && JavaScriptSupportLoader.isFlexMxmFile((PsiFile)before) && after != null && JavaScriptSupportLoader.isFlexMxmFile((PsiFile)after)) { before = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)before); after = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)after); } return Pair.create(before, after); } @Override public RelationshipInfo[] getRelationships() { final List<RelationshipInfo> result = new ArrayList<>(); Pair<PsiElement, PsiElement> beforeAndAfter = adjustBeforeAfter(); for (PsiFilter<JSReferenceExpression> filter : getRelationshipFilters()) { final Map<JSReferenceExpression, FileStatus> map = PsiChangeTracker.getElementsChanged(beforeAndAfter.second, beforeAndAfter.first, filter); for (JSReferenceExpression expression : map.keySet()) { JSClass sourceClass = PsiTreeUtil.getParentOfType(expression, JSClass.class); if (sourceClass == null) { continue; } if (InjectedLanguageManager.getInstance(sourceClass.getProject()).getInjectionHost(sourceClass) != null) { sourceClass = JSResolveUtil.getXmlBackedClass((JSFile)sourceClass.getContainingFile()); } JSReferenceList refList = PsiTreeUtil.getParentOfType(expression, JSReferenceList.class); assert refList != null; final JSExpression[] references = refList.getExpressions(); final JSClass[] referencedClasses = refList.getReferencedClasses(); JSClass targetClass = null; for (int i = 0; i < references.length; i++) { if (references[i] == expression) { targetClass = i < referencedClasses.length ? referencedClasses[i] : null; break; } } if (targetClass == null) { continue; } EdgeType edgeType = filter == IMPLEMENTS_FILTER ? EdgeType.IMPLEMENTS : EdgeType.EXTENDS; result.add(new RelationshipInfo(sourceClass.getQualifiedName(), targetClass.getQualifiedName(), edgeType, map.get(expression))); } } return result.toArray(new RelationshipInfo[result.size()]); } @Override public PsiNamedElement findElementByFQN(Project project, String fqn) { Object o = FlashUmlVfsResolver.resolveElementByFqnStatic(fqn, project); return o instanceof JSClass ? (PsiNamedElement)o : null; } private static class ReferenceListFilter extends PsiFilter<JSReferenceExpression> { private final boolean myExtends; public ReferenceListFilter(boolean isExtends) { super(JSReferenceExpression.class); myExtends = isExtends; } @Override public boolean accept(JSReferenceExpression element) { final PsiElement parent = element.getParent(); return parent instanceof JSReferenceListMember && parent.getParent().getNode().findChildByType(myExtends ? JSTokenTypes.EXTENDS_KEYWORD : JSTokenTypes.IMPLEMENTS_KEYWORD) != null; } @Override public Visitor<JSReferenceExpression> createVisitor(List<JSReferenceExpression> elements) { return new InjectingVisitor<>(this, elements); } } private static class InjectingVisitor<T extends PsiElement> extends PsiFilter.Visitor<T> { public InjectingVisitor(PsiFilter<T> filter, List<T> elements) { super(filter, elements); } @Override public void visitElement(PsiElement element) { super.visitElement(element); if (element instanceof XmlText || element instanceof XmlAttributeValue) { final XmlTag parentTag = PsiTreeUtil.getParentOfType(element, XmlTag.class); // actually we need just any tag here for (MultiHostInjector injector : Extensions.getExtensions(MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME, element.getProject())) { if (injector instanceof JSTargetedInjector) { injector.getLanguagesToInject(new XmlBackedJSClassImpl.InjectedScriptsVisitor.MyRegistrar(parentTag, new JSResolveUtil.JSInjectedFilesVisitor() { @Override protected void process(JSFile file) { file.acceptChildren(InjectingVisitor.this); } }), element); } } } } } }