package com.intellij.javascript.flex.mxml; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl; import com.intellij.lang.javascript.psi.JSFile; import com.intellij.lang.javascript.psi.JSFunction; import com.intellij.lang.javascript.psi.JSNamedElement; import com.intellij.lang.javascript.psi.JSVariable; import com.intellij.lang.javascript.psi.ecmal4.XmlBackedJSClass; import com.intellij.lang.javascript.psi.ecmal4.XmlBackedJSClassFactory; import com.intellij.lang.javascript.psi.resolve.JSResolveUtil; import com.intellij.lang.javascript.psi.resolve.ResolveProcessor; import com.intellij.lang.javascript.structureView.JSStructureItemPresentation; import com.intellij.lang.javascript.types.JSFileElementType; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.NavigationItem; import com.intellij.navigation.PsiElementNavigationItem; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; import com.intellij.psi.*; import com.intellij.psi.impl.FakePsiElement; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.util.Consumer; import com.intellij.util.containers.HashMap; import com.intellij.util.indexing.*; import com.intellij.util.io.EnumeratorStringDescriptor; import com.intellij.util.io.KeyDescriptor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.ArrayList; import java.util.Collection; import java.util.Map; public class FlexXmlBackedMembersIndex extends ScalarIndexExtension<String> { private static final int INDEX_VERSION = 1; public static final ID<String, Void> NAME = ID.create("FlexXmlBackedMembersIndex"); @Override @NotNull public ID<String, Void> getName() { return NAME; } @Override @NotNull public DataIndexer<String, Void, FileContent> getIndexer() { return new DataIndexer<String, Void, FileContent>() { @Override @NotNull public Map<String, Void> map(@NotNull FileContent inputData) { final XmlFile file = (XmlFile)inputData.getPsiFile(); final Map<String, Void> result = new HashMap<>(); process(file, element -> { String name = getName(element); if (name != null) { result.put(name, null); } }, false); return result; } }; } private static void process(XmlFile file, final Consumer<PsiElement> consumer, boolean isPhysical) { visitScriptTagInjectedFilesForIndexing(file, new JSResolveUtil.JSInjectedFilesVisitor() { @Override protected void process(JSFile file) { ResolveState state = ResolveState.initial().put(XmlBackedJSClass.PROCESS_XML_BACKED_CLASS_MEMBERS_HINT, Boolean.TRUE); file.processDeclarations(new ResolveProcessor(null) { { setSkipImplicitDeclarations(true); } @Override public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) { if (element instanceof JSFunction || element instanceof JSVariable) { consumer.consume(element); } return true; } @Override public <T> T getHint(@NotNull Key<T> hintKey) { return null; } @Override public void handleEvent(@NotNull Event event, Object associated) { } }, state, null, file); } }, isPhysical); file.accept(new PsiRecursiveElementVisitor() { @Override public void visitElement(PsiElement element) { if (element instanceof XmlTag) { XmlTag tag = (XmlTag)element; if (tag.getAttributeValue("id") != null && MxmlJSClass.canBeReferencedById(tag)) { consumer.consume(tag); } } super.visitElement(element); } }); } @NotNull @Override public KeyDescriptor<String> getKeyDescriptor() { return EnumeratorStringDescriptor.INSTANCE; } @NotNull @Override public FileBasedIndex.InputFilter getInputFilter() { return new DefaultFileTypeSpecificInputFilter(JavaScriptSupportLoader.getMxmlFileType()) { @Override public boolean acceptInput(@NotNull VirtualFile file) { return JavaScriptSupportLoader.isMxmlOrFxgFile(file); } }; } @Override public boolean dependsOnFileContent() { return true; } @Override public int getVersion() { return JSFileElementType.getVersion() + INDEX_VERSION; } public static Collection<String> getSymbolNames(Project project) { return FileBasedIndex.getInstance().getAllKeys(NAME, project); } public static Collection<NavigationItem> getItemsByName(final String name, Project project) { Collection<VirtualFile> files = FileBasedIndex.getInstance().getContainingFiles(NAME, name, GlobalSearchScope.projectScope(project)); final Collection<NavigationItem> result = new ArrayList<>(); for (VirtualFile vFile : files) { PsiFile file = PsiManager.getInstance(project).findFile(vFile); if (!(file instanceof XmlFile)) { continue; } process((XmlFile)file, element -> { if (name.equals(getName(element))) { if (element instanceof JSNamedElement) { result.add((JSNamedElement)element); } else { XmlAttribute id = ((XmlTag)element).getAttribute("id"); if (id != null) { XmlAttributeValue valueElement = id.getValueElement(); PsiElement[] children; if (valueElement != null && (children = valueElement.getChildren()).length == 3) { result.add(new TagNavigationItem(children[1], name)); } } } } }, true); } return result; } @Nullable private static String getName(PsiElement element) { if (element instanceof XmlTag) { return ((XmlTag)element).getAttributeValue("id"); } else { return ((JSNamedElement)element).getName(); } } private static class TagNavigationItem extends FakePsiElement implements PsiElementNavigationItem, ItemPresentation { private final PsiElement myElement; private final String myName; public TagNavigationItem(PsiElement element, String name) { myElement = element; myName = name; } @Override public String getName() { return myName; } @Override public ItemPresentation getPresentation() { return this; } @Override public PsiElement getTargetElement() { return myElement; } @Override public void navigate(boolean requestFocus) { ((Navigatable)myElement).navigate(requestFocus); } @Override public boolean canNavigate() { return ((Navigatable)myElement).canNavigate(); } @Override public boolean canNavigateToSource() { return ((Navigatable)myElement).canNavigateToSource(); } @Override public String getPresentableText() { return getName(); } @Override public String getLocationString() { PsiFile file = myElement.getContainingFile(); String packageName = JSResolveUtil.getExpectedPackageNameFromFile(file.getVirtualFile(), myElement.getProject()); StringBuilder result = new StringBuilder(); result.append(StringUtil.getQualifiedName(packageName, FileUtil.getNameWithoutExtension(file.getName()))); result.append("(").append(file.getName()).append(")"); return result.toString(); } @Override public Icon getIcon(boolean open) { return JSStructureItemPresentation.getIcon(PsiTreeUtil.getParentOfType(myElement, XmlTag.class)); } @Override public PsiElement getParent() { return myElement.getParent(); } @Override public PsiFile getContainingFile() { return myElement.getContainingFile(); } } // We use light version of visitInjectedFiles that process injections only for Script tags, // all other tags / attributes need cross file resolve public static void visitScriptTagInjectedFilesForIndexing(XmlFile file, final XmlBackedJSClassImpl.InjectedFileVisitor visitor, boolean physical) { new XmlBackedJSClassImpl.InjectedScriptsVisitor(XmlBackedJSClassFactory.getRootTag(file), MxmlJSClassProvider.getInstance(), false, true, visitor, physical).go(); } }