package org.angularjs.codeInsight.attributes;
import com.intellij.lang.javascript.psi.JSImplicitElementProvider;
import com.intellij.lang.javascript.psi.stubs.JSImplicitElement;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl;
import com.intellij.psi.stubs.StubIndexKey;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ThreeState;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlAttributeDescriptorsProvider;
import com.intellij.xml.XmlElementDescriptor;
import org.angularjs.codeInsight.DirectiveUtil;
import org.angularjs.index.AngularDirectivesDocIndex;
import org.angularjs.index.AngularDirectivesIndex;
import org.angularjs.index.AngularIndexUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.angularjs.codeInsight.attributes.AngularAttributesRegistry.createDescriptor;
/**
* @author Dennis.Ushakov
*/
public class AngularJSAttributeDescriptorsProvider implements XmlAttributeDescriptorsProvider {
@Override
public XmlAttributeDescriptor[] getAttributeDescriptors(XmlTag xmlTag) {
if (xmlTag != null) {
final Map<String, XmlAttributeDescriptor> result = new LinkedHashMap<>();
final Project project = xmlTag.getProject();
final XmlElementDescriptor descriptor = xmlTag.getDescriptor();
final Collection<String> directives = AngularIndexUtil.getAllKeys(AngularDirectivesIndex.KEY, project);
if (AngularIndexUtil.hasAngularJS2(project)) {
if (descriptor instanceof HtmlElementDescriptorImpl) {
final XmlAttributeDescriptor[] descriptors = ((HtmlElementDescriptorImpl)descriptor).getDefaultAttributeDescriptors(xmlTag);
for (XmlAttributeDescriptor attributeDescriptor : descriptors) {
final String name = attributeDescriptor.getName();
if (name.startsWith("on")) {
addAttributes(project, result, "(" + name.substring(2) + ")", null);
}
}
}
for (XmlAttribute attribute : xmlTag.getAttributes()) {
final String name = attribute.getName();
if (isAngular2Attribute(name, project) || !directives.contains(name)) continue;
final PsiElement declaration = applicableDirective(project, name, xmlTag, AngularDirectivesIndex.KEY);
if (isApplicable(declaration)) {
for (XmlAttributeDescriptor binding : AngularAttributeDescriptor.getFieldBasedDescriptors((JSImplicitElement)declaration)) {
result.put(binding.getName(), binding);
}
}
}
}
final Collection<String> docDirectives = AngularIndexUtil.getAllKeys(AngularDirectivesDocIndex.KEY, project);
for (String directiveName : docDirectives) {
PsiElement declaration = applicableDirective(project, directiveName, xmlTag, AngularDirectivesDocIndex.KEY);
if (isApplicable(declaration)) {
addAttributes(project, result, directiveName, declaration);
}
}
for (String directiveName : directives) {
if (!docDirectives.contains(directiveName)) {
PsiElement declaration = applicableDirective(project, directiveName, xmlTag, AngularDirectivesIndex.KEY);
if (isApplicable(declaration)) {
addAttributes(project, result, directiveName, declaration);
}
}
}
return result.values().toArray(new XmlAttributeDescriptor[result.size()]);
}
return XmlAttributeDescriptor.EMPTY;
}
protected void addAttributes(Project project,
Map<String, XmlAttributeDescriptor> result,
String directiveName,
PsiElement declaration) {
result.put(directiveName, createDescriptor(project, directiveName, declaration));
if ("ng-repeat".equals(directiveName)) {
result.put(directiveName + "-start", createDescriptor(project, directiveName + "-start", declaration));
result.put(directiveName + "-end", createDescriptor(project, directiveName + "-end", declaration));
}
}
private static PsiElement applicableDirective(Project project, String directiveName, XmlTag tag, final StubIndexKey<String, JSImplicitElementProvider> index) {
Ref<PsiElement> result = Ref.create(PsiUtilCore.NULL_PSI_ELEMENT);
AngularIndexUtil.multiResolve(project, index, directiveName, (directive) -> {
ThreeState applicable = isApplicable(project, tag, directive);
if (applicable == ThreeState.YES) {
result.set(directive);
}
if (applicable == ThreeState.NO && result.get() == PsiUtilCore.NULL_PSI_ELEMENT) {
result.set(null);
}
return !result.isNull();
});
return result.get();
}
@NotNull
private static ThreeState isApplicable(Project project, XmlTag tag, JSImplicitElement directive) {
if (directive == null) {
return ThreeState.UNSURE;
}
final String restrictions = directive.getTypeString();
if (restrictions != null) {
final String[] split = restrictions.split(";", -1);
final String restrict = AngularIndexUtil.convertRestrictions(project, split[0]);
final String requiredTag = split[1];
if (!StringUtil.isEmpty(restrict) && !StringUtil.containsIgnoreCase(restrict, "A")) {
return ThreeState.NO;
}
if (!tagMatches(tag, requiredTag)) {
return ThreeState.NO;
}
}
return ThreeState.YES;
}
private static boolean tagMatches(XmlTag tag, String requiredTag) {
if (StringUtil.isEmpty(requiredTag) || StringUtil.equalsIgnoreCase(requiredTag, "ANY")) {
return true;
}
for (String s : requiredTag.split(",")) {
if (StringUtil.equalsIgnoreCase(tag.getName(), s.trim())) {
return true;
}
}
if ("input".equalsIgnoreCase(requiredTag)) {
PsiElement parent = tag;
while (parent != null) {
parent = parent.getParent();
if (parent instanceof XmlTag && isForm((XmlTag)parent)) {
return true;
}
}
}
return false;
}
private static boolean isForm(XmlTag parent) {
final String name = parent.getName();
return "form".equalsIgnoreCase(name) || "ng-form".equalsIgnoreCase(name);
}
@Nullable
@Override
public XmlAttributeDescriptor getAttributeDescriptor(final String attrName, XmlTag xmlTag) {
if (xmlTag != null) {
final Project project = xmlTag.getProject();
final String attributeName = DirectiveUtil.normalizeAttributeName(attrName);
PsiElement declaration = applicableDirective(project, attributeName, xmlTag, AngularDirectivesDocIndex.KEY);
if (declaration == PsiUtilCore.NULL_PSI_ELEMENT) {
declaration = applicableDirective(project, attributeName, xmlTag, AngularDirectivesIndex.KEY);
}
if (isApplicable(declaration)) {
return createDescriptor(project, attributeName, declaration);
}
if (!AngularIndexUtil.hasAngularJS2(project)) return null;
for (XmlAttribute attribute : xmlTag.getAttributes()) {
String name = attribute.getName();
if (isAngular2Attribute(name, project) || name.equals(attrName)) continue;
declaration = applicableDirective(project, name, xmlTag, AngularDirectivesIndex.KEY);
if (isApplicable(declaration)) {
for (XmlAttributeDescriptor binding : AngularAttributeDescriptor.getFieldBasedDescriptors((JSImplicitElement)declaration)) {
if (binding.getName().equals(attrName)) {
return binding;
}
}
}
}
if (AngularAttributesRegistry.isBindingAttribute(attrName, project)) {
return new AngularBindingDescriptor(xmlTag, attrName);
}
if (AngularAttributesRegistry.isEventAttribute(attrName, project)) {
return new AngularEventHandlerDescriptor(xmlTag, attrName);
}
return getAngular2Descriptor(attrName, project);
}
return null;
}
private static boolean isApplicable(PsiElement declaration) {
return declaration != null && declaration != PsiUtilCore.NULL_PSI_ELEMENT;
}
@Nullable
public static AngularAttributeDescriptor getAngular2Descriptor(String attrName, Project project) {
if (isAngular2Attribute(attrName, project)) {
return createDescriptor(project, attrName, null);
}
return null;
}
protected static boolean isAngular2Attribute(String attrName, Project project) {
return AngularAttributesRegistry.isEventAttribute(attrName, project) ||
AngularAttributesRegistry.isBindingAttribute(attrName, project) ||
AngularAttributesRegistry.isVariableAttribute(attrName, project);
}
}