package com.intellij.javascript.flex;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixProvider;
import com.intellij.javascript.flex.css.FlexCssPropertyDescriptor;
import com.intellij.javascript.flex.mxml.MxmlJSClass;
import com.intellij.javascript.flex.mxml.schema.AnnotationBackedDescriptorImpl;
import com.intellij.javascript.flex.mxml.schema.CodeContext;
import com.intellij.lang.javascript.JavaScriptSupportLoader;
import com.intellij.lang.javascript.flex.AnnotationBackedDescriptor;
import com.intellij.lang.javascript.flex.ReferenceSupport;
import com.intellij.lang.javascript.flex.actions.newfile.CreateFlexComponentFix;
import com.intellij.lang.javascript.flex.actions.newfile.CreateFlexSkinIntention;
import com.intellij.lang.javascript.psi.JSCommonTypeNames;
import com.intellij.lang.javascript.psi.JSFunction;
import com.intellij.lang.javascript.psi.ecmal4.JSAttribute;
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeNameValuePair;
import com.intellij.lang.javascript.psi.ecmal4.impl.JSPackageWrapper;
import com.intellij.lang.javascript.psi.impl.JSReferenceSet;
import com.intellij.lang.javascript.psi.impl.JSTextReference;
import com.intellij.lang.javascript.psi.resolve.JSResolveResult;
import com.intellij.lang.javascript.psi.resolve.ResultSink;
import com.intellij.lang.javascript.validation.fixes.CreateClassIntentionWithCallback;
import com.intellij.lang.javascript.validation.fixes.CreateClassOrInterfaceFix;
import com.intellij.lang.javascript.validation.fixes.CreateFlexMobileViewIntentionAndFix;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PatternCondition;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.*;
import com.intellij.psi.css.resolve.CssReferenceProviderUtil;
import com.intellij.psi.filters.*;
import com.intellij.psi.filters.position.FilterPattern;
import com.intellij.psi.filters.position.NamespaceFilter;
import com.intellij.psi.filters.position.ParentElementFilter;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.AttributeValueSelfReference;
import com.intellij.psi.meta.PsiMetaData;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.*;
import com.intellij.util.text.StringTokenizer;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.util.XmlTagUtil;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.intellij.patterns.StandardPatterns.or;
import static com.intellij.patterns.StandardPatterns.string;
import static com.intellij.patterns.XmlPatterns.xmlAttribute;
public class MxmlReferenceContributor extends PsiReferenceContributor {
private static final String STYLE_NAME_ATTR_SUFFIX = "StyleName";
private static final String STYLE_NAME_ATTR = "styleName";
private static final String BINDING_TAG_NAME = "Binding";
private static final String FORMAT_ATTR_NAME = "format";
private static final String FILE_ATTR_VALUE = "File";
private static final String SKIN_CLASS_ATTR_NAME = "skinClass";
private static final String UI_COMPONENT_FQN = "mx.core.UIComponent";
@Override
public void registerReferenceProviders(final @NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
XmlPatterns.xmlAttributeValue().withLocalName(or(string().endsWith(STYLE_NAME_ATTR_SUFFIX),
string().equalTo(STYLE_NAME_ATTR)))
.and(new FilterPattern(new ElementFilter() {
public boolean isAcceptable(final Object element, final PsiElement context) {
return !((PsiElement)element).textContains('{');
}
public boolean isClassAcceptable(final Class hintClass) {
return true;
}
})),
CssReferenceProviderUtil.CSS_CLASS_OR_ID_KEY_PROVIDER.getProvider());
XmlUtil.registerXmlAttributeValueReferenceProvider(registrar, null, new ElementFilter() {
@Override
public boolean isAcceptable(Object element, PsiElement context) {
PsiElement parent = ((PsiElement)element).getParent();
if (parent instanceof XmlAttribute) {
XmlAttributeDescriptor descriptor = ((XmlAttribute)parent).getDescriptor();
if (descriptor instanceof AnnotationBackedDescriptorImpl) {
String format = ((AnnotationBackedDescriptor)descriptor).getFormat();
return FlexCssPropertyDescriptor.COLOR_FORMAT.equals(format);
}
}
return false;
}
@Override
public boolean isClassAcceptable(Class hintClass) {
return true;
}
}, true, new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull ProcessingContext context) {
XmlAttributeValue value = (XmlAttributeValue)element;
XmlAttribute parent = (XmlAttribute)value.getParent();
int length = value.getTextLength();
if (length >= 2) {
AnnotationBackedDescriptor descriptor = (AnnotationBackedDescriptor)parent.getDescriptor();
assert descriptor != null;
if (JSCommonTypeNames.ARRAY_CLASS_NAME.equals(descriptor.getType())) {
// drop quotes
String text = element.getText().substring(1, length - 1);
final List<PsiReference> references = new ArrayList<>();
new ArrayAttributeValueProcessor() {
@Override
protected void processElement(int start, int end) {
references.add(new FlexColorReference(element, new TextRange(start + 1, end + 1)));
}
}.process(text);
return references.toArray(new PsiReference[references.size()]);
}
else {
// inside quotes
return new PsiReference[]{new FlexColorReference(element, new TextRange(1, length - 1))};
}
}
return PsiReference.EMPTY_ARRAY;
}
});
XmlUtil.registerXmlAttributeValueReferenceProvider(registrar, null, new ElementFilter() {
public boolean isAcceptable(final Object element, final PsiElement context) {
PsiElement parent = ((PsiElement)element).getParent();
if (!(parent instanceof XmlAttribute) || !((XmlAttribute)parent).isNamespaceDeclaration()) {
return false;
}
final PsiElement parentParent = parent.getParent();
if (parentParent instanceof XmlTag && MxmlJSClass.isInsideTagThatAllowsAnyXmlContent((XmlTag)parentParent)) {
return false;
}
return true;
}
public boolean isClassAcceptable(final Class hintClass) {
return true;
}
}, true, new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
final String trimmedText = StringUtil.unquoteString(element.getText());
if (CodeContext.isPackageBackedNamespace(trimmedText)) {
final JSReferenceSet referenceSet = new JSReferenceSet(element, trimmedText, 1, false, false) {
@Override
protected JSTextReference createTextReference(String s, int offset, boolean methodRef) {
return new JSTextReference(this, s, offset, methodRef) {
@Override
protected ResolveResult[] doResolve(@NotNull PsiFile psiFile) {
if ("*".equals(getCanonicalText())) {
return new ResolveResult[]{new JSResolveResult(mySet.getElement())};
}
return super.doResolve(psiFile);
}
@Override
protected MyResolveProcessor createResolveProcessor(String name, PsiElement place, ResultSink resultSink) {
return new MyResolveProcessor(name, place, resultSink) {
@Override
public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
if (!(element instanceof JSPackageWrapper)) return true;
return super.execute(element, state);
}
};
}
};
}
};
return referenceSet.getReferences();
}
else {
return PsiReference.EMPTY_ARRAY;
}
}
});
// source attribute of Binding tag is handled in JSLanguageInjector
XmlUtil.registerXmlAttributeValueReferenceProvider(
registrar,
new String[]{FlexReferenceContributor.DESTINATION_ATTR_NAME},
new ScopeFilter(new ParentElementFilter(new AndFilter(XmlTagFilter.INSTANCE, new TagNameFilter(BINDING_TAG_NAME),
new NamespaceFilter(JavaScriptSupportLoader.LANGUAGE_NAMESPACES)), 2)),
new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element,
@NotNull final ProcessingContext context) {
final String trimmedText = StringUtil.unquoteString(element.getText());
final JSReferenceSet referenceSet =
new JSReferenceSet(element, trimmedText, 1, false);
return referenceSet.getReferences();
}
});
XmlUtil.registerXmlAttributeValueReferenceProvider(registrar, new String[]{FlexReferenceContributor.SOURCE_ATTR_NAME}, new ScopeFilter(
new ParentElementFilter(new AndFilter(XmlTagFilter.INSTANCE, new ElementFilterBase<PsiElement>(PsiElement.class) {
protected boolean isElementAcceptable(final PsiElement element, final PsiElement context) {
return true;
}
}), 2)), new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull final ProcessingContext context) {
final XmlAttribute attribute = (XmlAttribute)element.getParent();
final XmlTag tag = attribute.getParent();
final String tagName = tag.getLocalName();
final String trimmedText = StringUtil.unquoteString(element.getText());
if (JavaScriptSupportLoader.isLanguageNamespace(tag.getNamespace())) {
if (FlexPredefinedTagNames.SCRIPT.equals(tagName)) {
return ReferenceSupport.getFileRefs(element, element, 1, ReferenceSupport.LookupOptions.SCRIPT_SOURCE);
}
final String[] tagsWithSourceAttr = {
MxmlJSClass.XML_TAG_NAME, FlexPredefinedTagNames.MODEL,
JSCommonTypeNames.STRING_CLASS_NAME, JSCommonTypeNames.BOOLEAN_CLASS_NAME,
JSCommonTypeNames.INT_TYPE_NAME, JSCommonTypeNames.UINT_TYPE_NAME, JSCommonTypeNames.NUMBER_CLASS_NAME
};
if (ArrayUtil.contains(tagName, tagsWithSourceAttr)) {
return ReferenceSupport.getFileRefs(element, element, 1, ReferenceSupport.LookupOptions.XML_AND_MODEL_SOURCE);
}
if (FlexPredefinedTagNames.STYLE.equals(tagName)) {
if (trimmedText.startsWith("http:")) {
return PsiReference.EMPTY_ARRAY;
}
else {
return ReferenceSupport.getFileRefs(element, element, 1, ReferenceSupport.LookupOptions.STYLE_SOURCE);
}
}
}
if (element.textContains('{') || element.textContains('@')) {
return PsiReference.EMPTY_ARRAY;
}
final XmlAttributeDescriptor descriptor = attribute.getDescriptor();
final PsiElement psiElement = descriptor == null ? null : descriptor.getDeclaration();
if (psiElement instanceof JSFunction) {
final JSAttribute inspectableAttr = AnnotationBackedDescriptorImpl.findInspectableAttr(psiElement);
if (inspectableAttr != null) {
final JSAttributeNameValuePair attributeNameValuePair = inspectableAttr.getValueByName(FORMAT_ATTR_NAME);
if (attributeNameValuePair != null && FILE_ATTR_VALUE.equals(attributeNameValuePair.getSimpleValue())) {
return ReferenceSupport.getFileRefs(element, element, 1, ReferenceSupport.LookupOptions.NON_EMBEDDED_ASSET);
}
}
}
return PsiReference.EMPTY_ARRAY;
}
});
final Function<PsiReference, LocalQuickFix[]> quickFixProvider = reference -> {
final PsiElement element = reference.getElement();
final String classFqn = getTrimmedValueAndRange((XmlElement)element).first;
final String tagOrAttrName = element instanceof XmlAttributeValue
? ((XmlAttribute)element.getParent()).getName()
: ((XmlTag)element).getLocalName();
final CreateClassIntentionWithCallback[] intentions;
if (SKIN_CLASS_ATTR_NAME.equals(tagOrAttrName)) {
intentions = new CreateClassIntentionWithCallback[]{new CreateFlexSkinIntention(classFqn, element)};
}
else if ("firstView".equals(tagOrAttrName)) {
intentions = new CreateClassIntentionWithCallback[]{new CreateFlexMobileViewIntentionAndFix(classFqn, element, false)};
}
else {
intentions = new CreateClassIntentionWithCallback[]{
new CreateClassOrInterfaceFix(classFqn, null, element),
new CreateFlexComponentFix(classFqn, element)
};
}
for (CreateClassIntentionWithCallback intention : intentions) {
intention.setCreatedClassFqnConsumer(fqn -> {
if (!element.isValid()) return;
if (element instanceof XmlAttributeValue) {
((XmlAttribute)element.getParent()).setValue(fqn);
}
else {
((XmlTag)element).getValue().setText(fqn);
}
});
}
return intentions;
};
XmlUtil.registerXmlTagReferenceProvider(registrar, null, TrueFilter.INSTANCE, true,
createReferenceProviderForTagOrAttributeExpectingJSClass(quickFixProvider));
XmlUtil.registerXmlAttributeValueReferenceProvider(registrar, null, TrueFilter.INSTANCE,
createReferenceProviderForTagOrAttributeExpectingJSClass(quickFixProvider));
registrar.registerReferenceProvider(xmlAttribute().withParent(XmlTag.class).with(new PatternCondition<XmlAttribute>("") {
@Override
public boolean accepts(@NotNull XmlAttribute xmlAttribute, ProcessingContext context) {
String attrName = xmlAttribute.getLocalName();
int dotPos = attrName.indexOf('.');
if (dotPos == -1) return false;
return JavaScriptSupportLoader.isFlexMxmFile(xmlAttribute.getContainingFile());
}
}), new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull final ProcessingContext context) {
String attrName = ((XmlAttribute)element).getLocalName();
int dotPos = attrName.indexOf('.');
if (dotPos == -1) return PsiReference.EMPTY_ARRAY;
return new PsiReference[]{new FlexReferenceContributor.StateReference(element, new TextRange(dotPos + 1, attrName.length()))};
}
});
XmlUtil.registerXmlTagReferenceProvider(
registrar, null, new ElementFilterBase<XmlTag>(XmlTag.class) {
protected boolean isElementAcceptable(final XmlTag element, final PsiElement context) {
return element.getName().indexOf('.') != -1;
}
}, false, new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull final ProcessingContext context) {
final String name = ((XmlTag)element).getName();
int dotIndex = name.indexOf('.');
if (dotIndex == -1) return PsiReference.EMPTY_ARRAY;
final int tagOffset = element.getTextRange().getStartOffset();
final XmlToken startTagElement = XmlTagUtil.getStartTagNameElement((XmlTag)element);
final XmlToken endTagElement = XmlTagUtil.getEndTagNameElement((XmlTag)element);
if (startTagElement != null) {
if (endTagElement != null && endTagElement.getText().equals(startTagElement.getText())) {
final int start1 = startTagElement.getTextRange().getStartOffset() - tagOffset;
final int start2 = endTagElement.getTextRange().getStartOffset() - tagOffset;
return new PsiReference[]{
new FlexReferenceContributor.StateReference(element,
new TextRange(start1 + dotIndex + 1,
startTagElement.getTextRange().getEndOffset() - tagOffset)),
new FlexReferenceContributor.StateReference(element,
new TextRange(start2 + dotIndex + 1,
endTagElement.getTextRange().getEndOffset() - tagOffset)),
};
}
else {
final int start = startTagElement.getTextRange().getStartOffset() - tagOffset;
return new PsiReference[]{
new FlexReferenceContributor.StateReference(element,
new TextRange(start + dotIndex + 1,
startTagElement.getTextRange().getEndOffset() - tagOffset))};
}
}
return PsiReference.EMPTY_ARRAY;
}
}
);
XmlUtil.registerXmlAttributeValueReferenceProvider(
registrar,
new String[]{"basedOn", "fromState", "toState", FlexStateElementNames.NAME, FlexStateElementNames.STATE_GROUPS},
new ScopeFilter(new ParentElementFilter(new AndFilter(XmlTagFilter.INSTANCE, new NamespaceFilter(MxmlJSClass.MXML_URIS)), 2)),
new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull final ProcessingContext context) {
final PsiElement parent = element.getParent();
final PsiElement tag = parent.getParent();
PsiReference ref = null;
String tagName = ((XmlTag)tag).getLocalName();
String attrName = ((XmlAttribute)parent).getName();
String attrValue = ((XmlAttribute)parent).getValue();
if (attrValue != null && attrValue.contains("{")) return PsiReference.EMPTY_ARRAY;
if (FlexStateElementNames.NAME.equals(attrName)) {
if ("State".equals(tagName)) {
ref = new AttributeValueSelfReference(element);
}
else {
return PsiReference.EMPTY_ARRAY;
}
}
else if ("basedOn".equals(attrName) && element.getTextLength() == 2) {
return PsiReference.EMPTY_ARRAY;
}
else if (FlexStateElementNames.STATE_GROUPS.equals(attrName)) {
if ("State".equals(tagName)) {
return buildStateRefs(element, true);
}
else {
return PsiReference.EMPTY_ARRAY;
}
}
if (FlexReferenceContributor.TRANSITION_TAG_NAME.equals(tagName)) {
if ((element.textContains('*') &&
"*".equals(StringUtil.unquoteString(element.getText()))) ||
element.getTextLength() == 2 // empty value for attr, current state
) {
return PsiReference.EMPTY_ARRAY;
}
}
if (ref == null) {
ref = new FlexReferenceContributor.StateReference(element);
}
return new PsiReference[]{ref};
}
});
XmlUtil.registerXmlAttributeValueReferenceProvider(
registrar,
new String[]{FlexStateElementNames.EXCLUDE_FROM, FlexStateElementNames.INCLUDE_IN},
new ScopeFilter(new ParentElementFilter(XmlTagFilter.INSTANCE, 2)),
new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element,
@NotNull final ProcessingContext context) {
return buildStateRefs(element, false);
}
});
XmlUtil.registerXmlAttributeValueReferenceProvider(
registrar, new String[]{CodeContext.TARGET_ATTR_NAME},
new ScopeFilter(
new ParentElementFilter(
new AndFilter(XmlTagFilter.INSTANCE,
new TagNameFilter(CodeContext.REPARENT_TAG_NAME),
new NamespaceFilter(JavaScriptSupportLoader.MXML_URI3)
), 2
)
),
new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element,
@NotNull final ProcessingContext context) {
return new PsiReference[]{new XmlIdValueReference(element)};
}
});
}
private static PsiReferenceProvider createReferenceProviderForTagOrAttributeExpectingJSClass(final Function<PsiReference, LocalQuickFix[]> quickFixProvider) {
return new PsiReferenceProvider() {
@NotNull
public PsiReference[] getReferencesByElement(@NotNull final PsiElement element,
@NotNull final ProcessingContext context) {
final PsiMetaData descriptor;
final String name;
if (element instanceof XmlTag) {
descriptor = ((XmlTag)element).getDescriptor();
name = ((XmlTag)element).getLocalName();
}
else if (element instanceof XmlAttributeValue) {
final XmlAttribute xmlAttribute = PsiTreeUtil.getParentOfType(element, XmlAttribute.class);
descriptor = xmlAttribute == null ? null : xmlAttribute.getDescriptor();
name = xmlAttribute == null ? "" : xmlAttribute.getName();
}
else {
assert false : element;
return PsiReference.EMPTY_ARRAY;
}
if (!(descriptor instanceof AnnotationBackedDescriptor)) return PsiReference.EMPTY_ARRAY;
final String type = ((AnnotationBackedDescriptor)descriptor).getType();
if (!FlexReferenceContributor.isClassReferenceType(type)) return PsiReference.EMPTY_ARRAY;
final Pair<String, TextRange> trimmedValueAndRange = getTrimmedValueAndRange((XmlElement)element);
if (trimmedValueAndRange.second.getStartOffset() == 0) return PsiReference.EMPTY_ARRAY;
if (trimmedValueAndRange.first.indexOf('{') != -1 || trimmedValueAndRange.first.indexOf('@') != -1) return PsiReference.EMPTY_ARRAY;
final JSReferenceSet jsReferenceSet =
new JSReferenceSet(element, trimmedValueAndRange.first, trimmedValueAndRange.second.getStartOffset(), false, true) {
@Override
protected JSTextReference createTextReference(String s, int offset, boolean methodRef) {
return new MyJSTextReference(this, s, offset, methodRef, quickFixProvider);
}
};
if (SKIN_CLASS_ATTR_NAME.equals(name)) {
jsReferenceSet.setBaseClassFqns(Collections.singletonList(UI_COMPONENT_FQN));
}
return jsReferenceSet.getReferences();
}
};
}
private static class MyJSTextReference extends JSTextReference implements LocalQuickFixProvider {
private final Function<PsiReference, LocalQuickFix[]> myQuickFixProvider;
MyJSTextReference(JSReferenceSet set,
String s,
int offset,
boolean methodRef,
Function<PsiReference, LocalQuickFix[]> quickFixProvider) {
super(set, s, offset, methodRef);
myQuickFixProvider = quickFixProvider;
}
@Override
public LocalQuickFix[] getQuickFixes() {
if (myQuickFixProvider != null) {
return myQuickFixProvider.fun(this);
}
return super.getQuickFixes();
}
}
private static Pair<String, TextRange> getTrimmedValueAndRange(final @NotNull XmlElement xmlElement) {
if (xmlElement instanceof XmlTag) {
return Pair.create(((XmlTag)xmlElement).getValue().getTrimmedText(), ElementManipulators.getValueTextRange(xmlElement));
}
else if (xmlElement instanceof XmlAttributeValue) {
final String value = ((XmlAttributeValue)xmlElement).getValue();
final String trimmedText = value.trim();
final int index = xmlElement.getText().indexOf(trimmedText);
return index < 0 || trimmedText.length() == 0
? Pair.create(value, ((XmlAttributeValue)xmlElement).getValueTextRange())
: Pair.create(trimmedText, new TextRange(index, index + trimmedText.length()));
}
else {
assert false;
return Pair.create(null, null);
}
}
private static PsiReference[] buildStateRefs(PsiElement element, boolean stateGroupsOnly) {
SmartList<PsiReference> refs = new SmartList<>();
StringTokenizer t = new StringTokenizer(StringUtil.unquoteString(element.getText()), FlexReferenceContributor.DELIMS);
while (t.hasMoreElements()) {
String val = t.nextElement();
int end = t.getCurrentPosition();
refs.add(new FlexReferenceContributor.StateReference(element, new TextRange(1 + end - val.length(), 1 + end), stateGroupsOnly));
}
return refs.toArray(new PsiReference[refs.size()]);
}
}