package org.angularjs.codeInsight; import com.intellij.codeInsight.completion.CompletionUtil; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl; import com.intellij.lang.javascript.psi.JSDefinitionExpression; import com.intellij.lang.javascript.psi.JSFile; import com.intellij.lang.javascript.psi.JSPsiElementBase; import com.intellij.lang.javascript.psi.JSVariable; import com.intellij.lang.javascript.psi.resolve.ImplicitJSVariableImpl; import com.intellij.lang.javascript.psi.resolve.JSResolveUtil; import com.intellij.lang.javascript.psi.stubs.JSImplicitElement; import com.intellij.lang.javascript.psi.stubs.impl.JSImplicitElementImpl; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.html.HtmlTag; import com.intellij.psi.impl.source.html.HtmlEmbeddedContentImpl; import com.intellij.psi.impl.source.resolve.FileContextUtil; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.PsiModificationTracker; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.*; import com.intellij.util.Consumer; import com.intellij.xml.util.documentation.HtmlDescriptorsTable; import org.angularjs.codeInsight.attributes.AngularAttributesRegistry; import org.angularjs.lang.parser.AngularJSElementTypes; import org.angularjs.lang.psi.AngularJSRecursiveVisitor; import org.angularjs.lang.psi.AngularJSRepeatExpression; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * @author Dennis.Ushakov */ public class AngularJSProcessor { private static final Map<String, String> NG_REPEAT_IMPLICITS = new HashMap<>(); public static final String $EVENT = "$event"; static { NG_REPEAT_IMPLICITS.put("$index", "Number"); NG_REPEAT_IMPLICITS.put("$first", "Boolean"); NG_REPEAT_IMPLICITS.put("$middle", "Boolean"); NG_REPEAT_IMPLICITS.put("$last", "Boolean"); NG_REPEAT_IMPLICITS.put("$even", "Boolean"); NG_REPEAT_IMPLICITS.put("$odd", "Boolean"); } public static void process(final PsiElement element, final Consumer<JSPsiElementBase> consumer) { final PsiElement original = CompletionUtil.getOriginalOrSelf(element); PsiFile hostFile = FileContextUtil.getContextFile(original != element ? original : element.getContainingFile().getOriginalFile()); if (!(hostFile instanceof XmlFile)) { hostFile = original.getContainingFile(); } if (!(hostFile instanceof XmlFile)) return; final XmlFile file = (XmlFile)hostFile; final Collection<JSPsiElementBase> cache = CachedValuesManager.getCachedValue(file, () -> { final Collection<JSPsiElementBase> result = new ArrayList<>(); processDocument(file.getDocument(), result); return CachedValueProvider.Result.create(result, PsiModificationTracker.MODIFICATION_COUNT); }); for (JSPsiElementBase namedElement : cache) { if (scopeMatches(original, namedElement)){ consumer.consume(namedElement); } } } private static void processDocument(XmlDocument document, final Collection<JSPsiElementBase> result) { if (document == null) return; final AngularInjectedFilesVisitor visitor = new AngularInjectedFilesVisitor(result); for (XmlTag tag : PsiTreeUtil.getChildrenOfTypeAsList(document, XmlTag.class)) { new XmlBackedJSClassImpl.InjectedScriptsVisitor(tag, null, true, true, visitor, true){ @Override public boolean execute(@NotNull PsiElement element) { if (element instanceof HtmlEmbeddedContentImpl) { processDocument(PsiTreeUtil.findChildOfType(element, XmlDocument.class), result); } if (element instanceof XmlAttribute) { visitor.accept(element); } return super.execute(element); } }.go(); } } private static boolean scopeMatches(PsiElement element, PsiElement declaration) { final InjectedLanguageManager injector = InjectedLanguageManager.getInstance(element.getProject()); if (declaration instanceof JSImplicitElement) { if ($EVENT.equals(((JSImplicitElement)declaration).getName())) { return eventScopeMatches(injector, element, declaration.getParent()); } declaration = declaration.getParent(); } final PsiLanguageInjectionHost elementContainer = injector.getInjectionHost(element); final XmlTagChild elementTag = PsiTreeUtil.getNonStrictParentOfType(elementContainer, XmlTag.class, XmlText.class); final PsiLanguageInjectionHost declarationContainer = injector.getInjectionHost(declaration); final XmlTagChild declarationTag = PsiTreeUtil.getNonStrictParentOfType(declarationContainer, XmlTag.class, XmlText.class); if (declarationContainer != null && elementContainer != null && elementTag != null && declarationTag != null) { return PsiTreeUtil.isAncestor(declarationTag, elementTag, true) || (PsiTreeUtil.isAncestor(declarationTag, elementTag, false) && declarationContainer.getTextOffset() < elementContainer.getTextOffset()) || isInRepeatStartEnd(declarationTag, declarationContainer, elementContainer); } return true; } private static boolean isInRepeatStartEnd(XmlTagChild declarationTag, PsiLanguageInjectionHost declarationContainer, PsiLanguageInjectionHost elementContainer) { PsiElement parent = declarationContainer.getParent(); if (parent instanceof XmlAttribute && "ng-repeat-start".equals(((XmlAttribute)parent).getName())) { XmlTagChild next = declarationTag.getNextSiblingInTag(); while (next != null) { if (PsiTreeUtil.isAncestor(next, elementContainer, true)) return true; if (next instanceof XmlTag && ((XmlTag)next).getAttribute("ng-repeat-end") != null) break; next = next.getNextSiblingInTag(); } } return false; } private static boolean eventScopeMatches(InjectedLanguageManager injector, PsiElement element, PsiElement parent) { XmlAttribute attribute = PsiTreeUtil.getNonStrictParentOfType(element, XmlAttribute.class); if (attribute == null) { final PsiLanguageInjectionHost elementContainer = injector.getInjectionHost(element); attribute = PsiTreeUtil.getNonStrictParentOfType(elementContainer, XmlAttribute.class); } return attribute != null && CompletionUtil.getOriginalOrSelf(attribute) == CompletionUtil.getOriginalOrSelf(parent); } public static JSImplicitElementImpl.Builder createVariable(HtmlTag tag, XmlAttribute attribute, String name) { final JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(name.substring(1), attribute) .setType(JSImplicitElement.Type.Variable); final String tagName = tag.getName(); if (HtmlDescriptorsTable.getTagDescriptor(tagName) != null) { elementBuilder.setTypeString("HTML" + StringUtil.capitalize(tagName) + "Element"); } return elementBuilder; } private static class AngularInjectedFilesVisitor extends JSResolveUtil.JSInjectedFilesVisitor { private final Collection<JSPsiElementBase> myResult; public AngularInjectedFilesVisitor(Collection<JSPsiElementBase> result) { myResult = result; } @Override protected void process(JSFile file) { accept(file); } protected void accept(PsiElement element) { element.accept(new AngularJSRecursiveVisitor() { @Override public void visitJSDefinitionExpression(JSDefinitionExpression node) { myResult.add(node); super.visitJSDefinitionExpression(node); } @Override public void visitJSVariable(JSVariable node) { myResult.add(node); super.visitJSVariable(node); } @Override public void visitAngularJSRepeatExpression(AngularJSRepeatExpression repeatExpression) { if (repeatExpression.getNode().getElementType() == AngularJSElementTypes.REPEAT_EXPRESSION) { for (Map.Entry<String, String> entry : NG_REPEAT_IMPLICITS.entrySet()) { myResult.add(new ImplicitJSVariableImpl(entry.getKey(), entry.getValue(), repeatExpression)); } } super.visitAngularJSRepeatExpression(repeatExpression); } }); if (element instanceof XmlAttribute) { final String name = ((XmlAttribute)element).getName(); if (AngularAttributesRegistry.isVariableAttribute(name, element.getProject())) { final JSImplicitElementImpl.Builder builder = createVariable((HtmlTag)element.getParent(), (XmlAttribute)element, name); myResult.add(builder.toImplicitElement()); } if (AngularAttributesRegistry.isEventAttribute(name, element.getProject())) { final JSImplicitElementImpl.Builder builder = new JSImplicitElementImpl.Builder($EVENT, element). setType(JSImplicitElement.Type.Variable); builder.setTypeString("Event"); myResult.add(builder.toImplicitElement()); } } } } }