package com.intellij.lang.javascript.flex.presentation; import com.intellij.diagnostic.LogMessageEx; import com.intellij.ide.projectView.ProjectViewNode; import com.intellij.ide.projectView.SelectableTreeStructureProvider; import com.intellij.ide.projectView.ViewSettings; import com.intellij.ide.projectView.impl.nodes.PsiFileNode; import com.intellij.ide.util.treeView.AbstractTreeNode; import com.intellij.javascript.flex.FlexApplicationComponent; import com.intellij.lang.javascript.ActionScriptFileType; import com.intellij.lang.javascript.psi.*; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.lang.javascript.psi.ecmal4.JSNamespaceDeclaration; import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement; import com.intellij.lang.javascript.psi.resolve.JSResolveUtil; import com.intellij.lang.javascript.psi.stubs.JSQualifiedElementIndex; import com.intellij.lang.javascript.psi.util.JSUtils; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiCompiledFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class SwfProjectViewStructureProvider implements SelectableTreeStructureProvider, DumbAware { private static final Logger LOG = Logger.getInstance(SwfProjectViewStructureProvider.class.getName()); private static final int MAX_TOTAL_SWFS_SIZE_IN_FOLDER_TO_SHOW_STRUCTURE = 5 * 1024 * 1024; // 5Mb private static final Comparator<JSQualifiedNamedElement> QNAME_COMPARATOR = (o1, o2) -> { final String qName = o1.getQualifiedName(); final String qName2 = o2.getQualifiedName(); if (qName == null || qName2 == null) return qName == null && qName2 == null ? 0 : qName != null ? 1 : -1; String[] tokens1 = qName.split("\\."); String[] tokens2 = qName2.split("\\."); for (int i = 0; i < tokens1.length && i < tokens2.length; i++) { int result = tokens1[i].compareTo(tokens2[i]); if (result != 0) { // class from package goes before subpackages if (i == tokens1.length - 1 && i != tokens2.length - 1) return -1; if (i != tokens1.length - 1 && i == tokens2.length - 1) return 1; return result; } } return 0; }; @Override public PsiElement getTopLevelElement(PsiElement element) { JSQualifiedNamedElement parent = PsiTreeUtil.getNonStrictParentOfType(element, JSClass.class, JSFunction.class, JSVariable.class, JSNamespaceDeclaration.class); if (parent != null) { PsiFile file = parent.getContainingFile(); if (file != null && (ActionScriptFileType.INSTANCE == file.getFileType() || FlexApplicationComponent.MXML == file.getFileType())) { VirtualFile vFile = file.getVirtualFile(); if (vFile != null && ProjectRootManager.getInstance(element.getProject()).getFileIndex().isInLibrarySource(vFile)) { PsiElement fromLibrary = findDecompiledElement(parent); if (fromLibrary != null) { return fromLibrary; } } } return parent; } return null; } /** * this is is needed to allow selecting classes and members in project view * @deprecated remove this method with proper check when Tree API is improved (e.g. ProjectViewNode#contains(object)) */ static boolean nodeContainsFile(ProjectViewNode node, VirtualFile file) { AbstractTreeNode parent = node.getParent(); while (parent instanceof SwfPackageElementNode) { parent = parent.getParent(); } return ((PsiFileNode)parent).contains(file); } @Nullable private static PsiElement findDecompiledElement(JSQualifiedNamedElement element) { if (DumbService.isDumb(element.getProject())) { return null; } JSQualifiedNamedElement mainElement = JSUtils.getMemberContainingClass(element); if (mainElement == null) { mainElement = element; } final String qName = mainElement.getQualifiedName(); if (qName == null) { return null; } VirtualFile elementVFile = mainElement.getContainingFile().getVirtualFile(); if (elementVFile == null) { return null; } ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(mainElement.getProject()).getFileIndex(); GlobalSearchScope searchScope = JSResolveUtil.getResolveScope(mainElement); Collection<JSQualifiedNamedElement> candidates = StubIndex.getElements(JSQualifiedElementIndex.KEY, qName.hashCode(), mainElement.getProject(), searchScope, JSQualifiedNamedElement.class); List<OrderEntry> sourceFileEntries = projectFileIndex.getOrderEntriesForFile(elementVFile); for (JSQualifiedNamedElement candidate : candidates) { if (candidate == mainElement || !qName.equals(candidate.getQualifiedName())) { continue; } VirtualFile vFile = candidate.getContainingFile().getVirtualFile(); if (vFile != null && projectFileIndex.getClassRootForFile(vFile) != null) { List<OrderEntry> candidateEntries = projectFileIndex.getOrderEntriesForFile(vFile); if (ContainerUtil.intersects(sourceFileEntries, candidateEntries)) { if (element == mainElement) { return candidate; } else { LOG.assertTrue(candidate instanceof JSClass, candidate); if (element instanceof JSVariable) { return ((JSClass)candidate).findFieldByName(element.getName()); } else { LOG.assertTrue(element instanceof JSFunction, element); return ((JSClass)candidate).findFunctionByNameAndKind(element.getName(), ((JSFunction)element).getKind()); } } } } } return null; } @NotNull @Override public Collection<AbstractTreeNode> modify(@NotNull AbstractTreeNode parent, @NotNull Collection<AbstractTreeNode> children, ViewSettings settings) { if (!(parent instanceof PsiFileNode)) { return children; } final PsiFile psiFile = ((PsiFileNode)parent).getValue(); if (!(psiFile instanceof PsiCompiledFile) || !(psiFile instanceof JSFile)) { return children; } final VirtualFile vFile = psiFile.getVirtualFile(); if (vFile == null || vFile.getFileType() != FlexApplicationComponent.SWF_FILE_TYPE) { return children; } if (isTooManySWFs(vFile.getParent())) { return children; } List<JSQualifiedNamedElement> elements = new ArrayList<>(); for (JSSourceElement e : ((JSFile)psiFile).getStatements()) { if (e instanceof JSQualifiedNamedElement) { String qName = ((JSQualifiedNamedElement)e).getQualifiedName(); if (qName == null) { final Attachment attachment = e.getParent() != null ? new Attachment("Parent element.txt", e.getParent().getText()) : new Attachment("Element text.txt", e.getText()); LOG.error(LogMessageEx.createEvent("Null qname: '" + e.getClass().getName() + "'", DebugUtil.currentStackTrace(), attachment)); continue; } elements.add((JSQualifiedNamedElement)e); } else if (e instanceof JSVarStatement) { Collections.addAll(elements, ((JSVarStatement)e).getVariables()); } } Collections.sort(elements, QNAME_COMPARATOR); return getChildrenForPackage("", elements, 0, elements.size(), psiFile.getProject(), ((PsiFileNode)parent).getSettings()); } private static boolean isTooManySWFs(final VirtualFile folder) { int size = 0; for (VirtualFile file : folder.getChildren()) { if (file.getFileType() == FlexApplicationComponent.SWF_FILE_TYPE) { size += file.getLength(); if (size > MAX_TOTAL_SWFS_SIZE_IN_FOLDER_TO_SHOW_STRUCTURE) { return true; } } } return false; } static Collection<AbstractTreeNode> getChildrenForPackage(String aPackage, List<JSQualifiedNamedElement> elements, int from, int to, Project project, ViewSettings settings) { List<AbstractTreeNode> packages = new ArrayList<>(); List<AbstractTreeNode> classes = new ArrayList<>(); int subpackageStart = -1; String currentSubpackage = null; for (int i = from; i < to; i++) { JSQualifiedNamedElement element = elements.get(i); String qName = element.getQualifiedName(); assert aPackage.isEmpty() || qName.startsWith(aPackage + ".") : qName + " does not start with " + aPackage; if (StringUtil.getPackageName(qName).equals(aPackage)) { classes.add(new SwfQualifiedNamedElementNode(project, element, settings)); } else { final int endIndex = qName.indexOf('.', aPackage.length() + 1); if (endIndex <= 0) { final Attachment attachment = element.getParent() != null ? new Attachment("Parent element.txt", element.getParent().getText()) : new Attachment("Element text.txt", element.getText()); LOG.error(LogMessageEx.createEvent("package=[" + aPackage + "], qName=[" + qName + "]", DebugUtil.currentStackTrace(), attachment)); continue; } String subpackage = settings.isFlattenPackages() ? StringUtil.getPackageName(qName) : qName.substring(0, endIndex); if (currentSubpackage == null) { subpackageStart = i; } else if (!currentSubpackage.equals(subpackage)) { packages.add(createSubpackageNode(elements, project, settings, subpackageStart, i, currentSubpackage)); subpackageStart = i; } currentSubpackage = subpackage; } } if (currentSubpackage != null) { packages.add(createSubpackageNode(elements, project, settings, subpackageStart, to, currentSubpackage)); } return ContainerUtil.concat(packages, classes); } private static SwfPackageElementNode createSubpackageNode(List<JSQualifiedNamedElement> elements, Project project, ViewSettings settings, int from, int to, String qName) { // SWF-s don't contain empty packages, so it makes no sense to handle "flatten packages and hide empty middle packages" mode if (settings.isFlattenPackages()) { return new SwfPackageElementNode(project, qName, qName, settings, elements, from, to); } else { if (settings.isHideEmptyMiddlePackages()) { String subQname = getEmptyMiddlePackageQname(elements, from, to, qName); if (subQname != null) { String displayText = qName.contains(".") ? subQname.substring(StringUtil.getPackageName(qName).length() + 1) : subQname; return new SwfPackageElementNode(project, subQname, displayText, settings, elements, from, to); } } return new SwfPackageElementNode(project, qName, StringUtil.getShortName(qName), settings, elements, from, to); } } @Nullable private static String getEmptyMiddlePackageQname(List<JSQualifiedNamedElement> elements, int from, int to, String packageName) { if (from == to) { return null; } String currentSubpackage = null; for (int i = from; i < to; i++) { String qName = elements.get(i).getQualifiedName(); int index = qName.indexOf('.', packageName.length() + 1); if (index == -1) { // class is in the package return null; } String subpackage = qName.substring(0, index); if (currentSubpackage == null) { currentSubpackage = subpackage; } else if (!currentSubpackage.equals(subpackage)) { return null; } } String deeperSubpackage = getEmptyMiddlePackageQname(elements, from, to, currentSubpackage); return deeperSubpackage != null ? deeperSubpackage : currentSubpackage; } }