package com.intellij.javascript.flex.css;
import com.intellij.codeInsight.documentation.DocumentationManager;
import com.intellij.javascript.flex.FlexAnnotationNames;
import com.intellij.javascript.flex.resolve.ActionScriptClassResolver;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.lang.javascript.flex.FlexUtils;
import com.intellij.lang.javascript.psi.JSCommonTypeNames;
import com.intellij.lang.javascript.psi.JSFile;
import com.intellij.lang.javascript.psi.ecmal4.JSAttribute;
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeNameValuePair;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement;
import com.intellij.lang.javascript.psi.resolve.ActionScriptResolveUtil;
import com.intellij.lang.javascript.psi.stubs.JSQualifiedElementIndex;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.css.CssBundle;
import com.intellij.psi.css.CssPropertyValue;
import com.intellij.psi.css.descriptor.BrowserVersion;
import com.intellij.psi.css.descriptor.CssContextType;
import com.intellij.psi.css.descriptor.value.CssValueDescriptor;
import com.intellij.psi.css.impl.CssTermTypes;
import com.intellij.psi.css.impl.descriptor.CssCommonDescriptorData;
import com.intellij.psi.css.impl.descriptor.value.*;
import com.intellij.psi.css.impl.util.completion.LengthUserLookup;
import com.intellij.psi.css.impl.util.scheme.CssElementDescriptorFactory2;
import com.intellij.psi.css.impl.util.scheme.CssValueDescriptorModificator;
import com.intellij.psi.css.impl.util.table.AbstractCssPropertyDescriptor;
import com.intellij.psi.css.impl.util.table.CssLookupValue;
import com.intellij.psi.css.impl.util.table.CssPropertyValueImpl;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.stubs.StubIndex;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Eugene.Kudelevsky
*/
public class FlexCssPropertyDescriptor extends AbstractCssPropertyDescriptor {
@NotNull private final String myInherit;
private final boolean myShorthand;
@NotNull private final String myPropertyName;
@NotNull private final CssPropertyValue myValue;
@NotNull private final Set<String> myClassNames;
@NotNull private final Set<String> myFileNames;
private final FlexStyleIndexInfo myStyleInfo;
public static final String COLOR_FORMAT = "Color";
private static final String LENGTH_FORMAT = "Length";
@NotNull private final CssValueDescriptor myValueDescriptor;
public FlexCssPropertyDescriptor(@NotNull Collection<FlexStyleIndexInfo> infos) {
FlexStyleIndexInfo firstInfo = infos.iterator().next();
myStyleInfo = firstInfo;
myPropertyName = firstInfo.getAttributeName();
myInherit = firstInfo.getInherit();
myShorthand = containsShorthand(infos);
myValue = createPropertyValue(infos, myShorthand);
myValueDescriptor = createPropertyValueDescriptor(infos, myShorthand);
myClassNames = ContainerUtil.newLinkedHashSet();
myFileNames = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
if (info.isInClass()) {
myClassNames.add(info.getClassOrFileName());
}
else {
myFileNames.add(info.getClassOrFileName());
}
}
}
private static boolean containsShorthand(@NotNull Collection<FlexStyleIndexInfo> infos) {
for (FlexStyleIndexInfo info : infos) {
if (isShorthand(info)) return true;
}
return false;
}
private static boolean isShorthand(@NotNull FlexStyleIndexInfo info) {
return JSCommonTypeNames.ARRAY_CLASS_NAME.equals(info.getType());
}
private static void addValuesFromEnumerations2(@NotNull Collection<FlexStyleIndexInfo> infos, @NotNull Collection<CssValueDescriptor> children) {
Set<String> constantSet = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
String enumeration = info.getEnumeration();
if (enumeration != null) {
String[] constants = enumeration.split(",");
Collections.addAll(constantSet, constants);
}
}
if (constantSet.size() > 0) {
for (String constant : constantSet) {
String constantName = constant.trim();
children.add(CssElementDescriptorFactory2.getInstance().createNameValueDescriptor(constantName, constantName, 1, 1, null));
}
}
}
private static void addValuesFromEnumerations(@NotNull Collection<FlexStyleIndexInfo> infos, @NotNull Collection<CssPropertyValue> children) {
Set<String> constantSet = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
String enumeration = info.getEnumeration();
if (enumeration != null) {
String[] constants = enumeration.split(",");
Collections.addAll(constantSet, constants);
}
}
if (constantSet.size() > 0) {
//CssPropertyValueImpl value = new FlexCssPropertyValue(false, true);
for (String constant : constantSet) {
children.add(new FlexCssPropertyValue(constant.trim()));
}
//children.add(value);
}
}
@NotNull
public FlexStyleIndexInfo getStyleInfo() {
return myStyleInfo;
}
@NotNull
private static Set<String> addValuesFromFormats(@NotNull List<CssValueDescriptor> children, @NotNull Collection<FlexStyleIndexInfo> infos) {
Set<String> formats = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
ContainerUtil.addIfNotNull(formats, info.getFormat());
}
if (formats.contains(COLOR_FORMAT)) {
children.add(createCssColorValue());
}
if (formats.contains(LENGTH_FORMAT)) {
children.add(createCssLengthValue());
}
return formats;
}
@NotNull
private static Set<String> addValuesFromFormats(@NotNull Collection<FlexStyleIndexInfo> infos, @NotNull List<CssPropertyValue> children) {
Set<String> formats = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
String format = info.getFormat();
if (format != null) {
formats.add(format);
}
}
if (formats.contains(COLOR_FORMAT)) {
children.add(new FlexCssColorValue());
}
if (formats.contains(LENGTH_FORMAT)) {
children.add(new CssLookupValue(new LengthUserLookup(), CssTermTypes.LENGTH, CssTermTypes.NUMBER, CssTermTypes.NEGATIVE_NUMBER,
CssTermTypes.NEGATIVE_LENGTH));
}
return formats;
}
private static void addValuesFromTypes2(@NotNull Collection<FlexStyleIndexInfo> infos, @NotNull Set<String> formats, @NotNull List<CssValueDescriptor> children) {
Set<String> types = ContainerUtil.newHashSet();
for (FlexStyleIndexInfo info : infos) {
ContainerUtil.addIfNotNull(types, info.getType());
}
if (types.contains(JSCommonTypeNames.NUMBER_CLASS_NAME) && !formats.contains(LENGTH_FORMAT)) {
children.add(createCssNumberValue());
}
if (types.contains("Class")) {
children.add(CssElementDescriptorFactory2.getInstance().createFunctionInvocationValueDescriptor("ClassReference", 1, 1, null));
}
}
private static void addValuesFromTypes(@NotNull Collection<FlexStyleIndexInfo> infos, @NotNull Set<String> formats, @NotNull List<CssPropertyValue> children) {
Set<String> types = ContainerUtil.newLinkedHashSet();
for (FlexStyleIndexInfo info : infos) {
String type = info.getType();
if (type != null) {
types.add(type);
}
}
if (types.contains(JSCommonTypeNames.NUMBER_CLASS_NAME) && !formats.contains(LENGTH_FORMAT)) {
children.add(new CssLookupValue(CssPropertyValueImpl.Type.OR, CssTermTypes.NUMBER, CssTermTypes.NEGATIVE_NUMBER));
}
}
@NotNull
private static CssValueDescriptor createPropertyValueDescriptor(@NotNull Collection<FlexStyleIndexInfo> infos, boolean shorthand) {
List<CssValueDescriptor> children = ContainerUtil.newArrayList();
Set<String> formats = addValuesFromFormats(children, infos);
addValuesFromEnumerations2(infos, children);
addValuesFromTypes2(infos, formats, children);
CssGroupValue result = CssElementDescriptorFactory2.getInstance().createGroupValue(CssGroupValue.Type.OR, -1, 1, null, null);
if (!children.isEmpty()) {
for (CssValueDescriptor child : children) {
result.addChild(CssValueDescriptorModificator.withParent(child, result));
}
if (!formats.contains(COLOR_FORMAT)) {
result.addChild(CssElementDescriptorFactory2.getInstance().createStringValueDescriptor(null, 1, 1, result));
}
}
else {
result.addChild(CssElementDescriptorFactory2.getInstance().createAnyValueDescriptor(1, 1, result));
}
result.addChild(CssElementDescriptorFactory2.getInstance().createNameValueDescriptor("undefined", "undefined", 1, 1, result));
result.addChild(CssElementDescriptorFactory2.getInstance().createFunctionInvocationValueDescriptor("PropertyReference", 1, 1, result));
result.addChild(CssElementDescriptorFactory2.getInstance().createFunctionInvocationValueDescriptor("Embed", 1, 1, result));
return result;
}
private static CssColorValue createCssColorValue() {
String id = CssBundle.message("color.value.presentable.name");
CssCommonDescriptorData commonDescriptorData = new CssCommonDescriptorData(id, id, CssContextType.EMPTY_ARRAY, BrowserVersion.EMPTY_ARRAY, CssVersion.UNKNOWN, null, "");
CssValueDescriptorData valueDescriptorData = new CssValueDescriptorData(true, 1, 1, null, null, null, null, false);
return new CssColorValue(commonDescriptorData, valueDescriptorData);
}
private static CssLengthValue createCssLengthValue() {
String id = CssBundle.message("length.value.presentable.name");
CssCommonDescriptorData commonDescriptorData = new CssCommonDescriptorData(id, id, CssContextType.EMPTY_ARRAY, BrowserVersion.EMPTY_ARRAY, CssVersion.UNKNOWN, null, "");
CssValueDescriptorData valueDescriptorData = new CssValueDescriptorData(true, 1, 1, null, null, null, null, false);
return new CssLengthValue(commonDescriptorData, valueDescriptorData);
}
private static CssNumberValue createCssNumberValue() {
String id = CssBundle.message("number.value.presentable.name");
CssCommonDescriptorData commonDescriptorData = new CssCommonDescriptorData(id, id, CssContextType.EMPTY_ARRAY, BrowserVersion.EMPTY_ARRAY, CssVersion.UNKNOWN, null, "");
CssValueDescriptorData valueDescriptorData = new CssValueDescriptorData(true, 1, 1, null, null, null, null, false);
return new CssNumberValue(commonDescriptorData, valueDescriptorData);
}
/**
* @deprecated use this#createPropertyValueDescriptor
*/
@NotNull
private static CssPropertyValueImpl createPropertyValue(@NotNull Collection<FlexStyleIndexInfo> infos, boolean shorthand) {
List<CssPropertyValue> children = new ArrayList<>();
Set<String> formats = addValuesFromFormats(infos, children);
addValuesFromEnumerations(infos, children);
addValuesFromTypes(infos, formats, children);
CssPropertyValueImpl value = null;
if (children.size() >= 1) {
value = new FlexCssPropertyValue(shorthand, false);
for (CssPropertyValue child : children) {
value.addChild(child);
}
}
if (value == null) {
value = new FlexCssPropertyValue(shorthand, true);
}
else if (!formats.contains(COLOR_FORMAT)) {
value.addChild(new FlexStringPropertyValue());
}
value.addChild(new FlexCssPropertyValue("undefined"));
return value;
}
private static class DocumentationElement {
String header;
String documentation;
private DocumentationElement(@NotNull String header, @NotNull String documentation) {
this.header = header;
this.documentation = documentation;
}
}
@Nullable
public String getDocumentationString(@Nullable PsiElement context) {
if (context == null) return null;
PsiElement[] declarations = getDeclarations(context);
List<DocumentationElement> docElements = new ArrayList<>();
for (PsiElement declaration : declarations) {
PsiFile file = declaration.getContainingFile();
if (file != null) {
DocumentationProvider provider = DocumentationManager.getProviderFromElement(declaration);
String docForDeclaration = provider.generateDoc(declaration, declaration);
if (docForDeclaration != null) {
JSClass jsClass = PsiTreeUtil.getParentOfType(declaration, JSClass.class);
String header = jsClass != null ? jsClass.getQualifiedName() : file.getName();
docElements.add(new DocumentationElement(header, docForDeclaration));
}
}
}
Collections.sort(docElements, (e1, e2) -> Comparing.compare(e1.header, e2.header));
StringBuilder builder = new StringBuilder();
for (int i = 0, n = docElements.size(); i < n; i++) {
DocumentationElement docElement = docElements.get(i);
builder.append("<b>").append(docElement.header).append("</b>").append("<br>\n");
builder.append(docElement.documentation);
if (i != n - 1) {
builder.append("<br><br>\n\n");
}
}
return builder.toString();
}
@NotNull
@Override
public CssPropertyValue getValue() {
return myValue;
}
private boolean checkIncludes(JSClass c) {
Set<String> includes = ContainerUtil.newLinkedHashSet();
FlexCssUtil.collectAllIncludes(c, includes);
for (String name : myFileNames) {
if (includes.contains(name)) {
return true;
}
}
return false;
}
private boolean findStyleAttributesInFile(@NotNull JSFile jsFile, @NotNull Set<JSFile> visited, @NotNull Map<PsiElement, PairInfo> navElement2pair) {
if (!visited.add(jsFile)) return false;
JSAttributeNameValuePair result = null;
String fileName = jsFile.getName();
if (myFileNames.contains(fileName)) {
MyMetaDataProcessor processor = new MyMetaDataProcessor();
FlexUtils.processMetaAttributesForClass(jsFile, processor);
result = processor.myResult;
}
PsiElement navElement = result != null ? result.getNavigationElement() : null;
if (result != null) {
navElement2pair.put(navElement != null ? navElement : result, new PairInfo(result, fileName));
return true;
}
return false;
}
private boolean findStyleAttributesInClassOrSuper(@NotNull JSClass c, @NotNull Set<JSClass> visited, @NotNull Map<PsiElement, PairInfo> navElement2pair) {
if (!visited.add(c)) return false;
JSAttributeNameValuePair result = null;
String qName = c.getQualifiedName();
if (myClassNames.contains(qName) || checkIncludes(c)) {
MyMetaDataProcessor processor = new MyMetaDataProcessor();
FlexUtils.processMetaAttributesForClass(c, processor);
result = processor.myResult;
}
PsiElement navElement = result != null ? result.getNavigationElement() : null;
if (result == null || navElement == null) {
boolean found = false;
for (JSClass superClass : c.getSupers()) {
found = found || findStyleAttributesInClassOrSuper(superClass, visited, navElement2pair);
}
if (found) return true;
}
if (result != null) {
navElement2pair.put(navElement != null ? navElement : result, new PairInfo(result, qName));
return true;
}
return false;
}
private void findStyleAttributes(@NotNull Collection<JSQualifiedNamedElement> elements,
@NotNull Set<JSClass> visited,
@NotNull Map<PsiElement, PairInfo> navElement2pair) {
for (JSQualifiedNamedElement element : elements) {
if (element instanceof JSClass) {
findStyleAttributesInClassOrSuper((JSClass)element, visited, navElement2pair);
}
}
}
private static class PairInfo {
final PsiElement myPair;
final String myJsClassQName;
private PairInfo(PsiElement pair, String jsClassQName) {
myPair = pair;
myJsClassQName = jsClassQName;
}
}
@NotNull
@Override
public PsiElement[] getDeclarations(@NotNull PsiElement context) {
Map<PsiElement, PairInfo> navElement2pairInfo = new HashMap<>();
final Project project = context.getProject();
GlobalSearchScope scope = FlexCssUtil.getResolveScope(context);
Set<JSClass> visited = ContainerUtil.newLinkedHashSet();
for (String className : myClassNames) {
Collection<JSQualifiedNamedElement> candidates = StubIndex.getElements(JSQualifiedElementIndex.KEY, className.hashCode(), project,
scope, JSQualifiedNamedElement.class);
findStyleAttributes(candidates, visited, navElement2pairInfo);
// search in MXML files
PsiElement jsClass = ActionScriptClassResolver.findClassByQNameStatic(className, scope);
if (jsClass instanceof JSClass) {
findStyleAttributesInClassOrSuper((JSClass)jsClass, visited, navElement2pairInfo);
}
}
Set<JSFile> visitedFiles = ContainerUtil.newLinkedHashSet();
for (String fileName : myFileNames) {
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(project, fileName, scope);
for (final VirtualFile file : files) {
PsiFile psiFile = ReadAction.compute(() -> PsiManager.getInstance(project).findFile(file));
if (psiFile instanceof JSFile) {
findStyleAttributesInFile((JSFile)psiFile, visitedFiles, navElement2pairInfo);
}
}
}
Set<PsiElement> navPairs = navElement2pairInfo.keySet();
Map<String, PsiElement> qName2ResultElement = new HashMap<>();
for (PsiElement navPair : navPairs) {
PairInfo pairInfo = navElement2pairInfo.get(navPair);
String jsClassQName = pairInfo.myJsClassQName;
PsiElement navPairInOtherClassWithSameQName = jsClassQName != null ? qName2ResultElement.get(jsClassQName) : null;
if (navPairInOtherClassWithSameQName == null ||
navPairInOtherClassWithSameQName == navElement2pairInfo.get(navPairInOtherClassWithSameQName) &&
pairInfo.myPair != navPair) {
qName2ResultElement.put(jsClassQName, navPair);
}
}
Collection<PsiElement> result = qName2ResultElement.values();
return PsiUtilCore.toPsiElementArray(result);
}
public boolean isShorthandValue() {
return myShorthand;
}
@NotNull
public String getPropertyName() {
return myPropertyName;
}
public boolean getInherited() {
return "yes".equalsIgnoreCase(myInherit) || "true".equals(myInherit);
}
private class MyMetaDataProcessor implements ActionScriptResolveUtil.MetaDataProcessor {
private JSAttributeNameValuePair myResult;
public boolean process(@NotNull JSAttribute jsAttribute) {
if (FlexAnnotationNames.STYLE.equals(jsAttribute.getName())) {
JSAttributeNameValuePair pair = jsAttribute.getValueByName("name");
if (pair != null && myPropertyName.equals(pair.getSimpleValue())) {
myResult = pair;
return false;
}
}
return true;
}
public boolean handleOtherElement(PsiElement el, PsiElement context, @Nullable Ref<PsiElement> continuePassElement) {
return true;
}
}
@NotNull
@Override
public CssValueDescriptor getValueDescriptor() {
return myValueDescriptor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FlexCssPropertyDescriptor)) return false;
FlexCssPropertyDescriptor that = (FlexCssPropertyDescriptor)o;
if (myShorthand != that.myShorthand) return false;
if (!myClassNames.equals(that.myClassNames)) return false;
if (!myFileNames.equals(that.myFileNames)) return false;
if (!myInherit.equals(that.myInherit)) return false;
if (!myPropertyName.equals(that.myPropertyName)) return false;
if (!myStyleInfo.equals(that.myStyleInfo)) return false;
if (!myValue.equals(that.myValue)) return false;
if (!myValueDescriptor.equals(that.myValueDescriptor)) return false;
return true;
}
@Override
public int hashCode() {
int result = myInherit.hashCode();
result = 31 * result + (myShorthand ? 1 : 0);
result = 31 * result + (myPropertyName.hashCode());
result = 31 * result + (myValue.hashCode());
result = 31 * result + (myValueDescriptor.hashCode());
result = 31 * result + (myClassNames.hashCode());
result = 31 * result + (myFileNames.hashCode());
result = 31 * result + (myStyleInfo.hashCode());
return result;
}
}