blob: 0c0d746c5b69ae3a47f07c9d476c1fa50479b72e [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.folding;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import static com.android.SdkConstants.STRING_PREFIX;
import static com.android.tools.idea.folding.InlinedResource.NONE;
public class ResourceFoldingBuilder extends FoldingBuilderEx {
private static final String ANDROID_RESOURCE_INT = "android.annotation.ResourceInt";
private static final boolean FORCE_PROJECT_RESOURCE_LOADING = true;
private static final boolean ONLY_FOLD_ANNOTATED_METHODS = false;
private static final boolean UNIT_TEST_MODE = ApplicationManager.getApplication().isUnitTestMode();
public static final String DIMEN_PREFIX = "@dimen/";
public static final String INTEGER_PREFIX = "@integer/";
public ResourceFoldingBuilder() {
}
private static boolean isFoldingEnabled() {
return AndroidFoldingSettings.getInstance().isCollapseAndroidStrings();
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return isFoldingEnabled();
}
@Override
public String getPlaceholderText(@NotNull ASTNode node) {
PsiElement element = SourceTreeToPsiMap.treeElementToPsi(node);
if (element != null) {
InlinedResource string = getResolvedString(element);
if (string != NONE) {
String foldLabel = string.getResolvedString();
if (foldLabel != null) {
return foldLabel;
}
}
}
return element != null ? element.getText() : node.getText();
}
@Override
@NotNull
public FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement element, @NotNull Document document, boolean quick) {
if (!(element instanceof PsiJavaFile || element instanceof XmlFile) || quick && !UNIT_TEST_MODE || !isFoldingEnabled()) {
return FoldingDescriptor.EMPTY;
}
final List<FoldingDescriptor> result = new ArrayList<FoldingDescriptor>();
if (element instanceof PsiJavaFile) {
final PsiJavaFile file = (PsiJavaFile) element;
file.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
InlinedResource inlinedResource = findJavaExpressionReference(expression);
if (inlinedResource != NONE) {
result.add(inlinedResource.getDescriptor());
}
super.visitReferenceExpression(expression);
}
});
} else {
final XmlFile file = (XmlFile) element;
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlAttributeValue(XmlAttributeValue value) {
InlinedResource inlinedResource = findXmlValueReference(value);
if (inlinedResource != NONE) {
FoldingDescriptor descriptor = inlinedResource.getDescriptor();
if (descriptor != null) {
result.add(descriptor);
}
}
super.visitXmlAttributeValue(value);
}
});
}
return result.toArray(new FoldingDescriptor[result.size()]);
}
@NotNull
private static InlinedResource getResolvedString(PsiElement element) {
if (element instanceof PsiReferenceExpression) {
return findJavaExpressionReference((PsiReferenceExpression)element);
} else if (element instanceof XmlAttributeValue) {
return findXmlValueReference((XmlAttributeValue)element);
} else if (element instanceof PsiMethodCallExpression) {
// This can happen when a folding lookup for a parameter ends up returning the
// surrounding method call as the folding region; in that case we have to map
// back to the right parameter
PsiMethodCallExpression call = (PsiMethodCallExpression)element;
for (PsiExpression expression : call.getArgumentList().getExpressions()) {
if (expression instanceof PsiReferenceExpression) {
InlinedResource string = findJavaExpressionReference((PsiReferenceExpression)expression);
if (string != NONE) {
return string;
}
}
}
}
return NONE;
}
@NotNull
private static InlinedResource findXmlValueReference(XmlAttributeValue element) {
String value = element.getValue();
if (value.startsWith(STRING_PREFIX)) {
String name = value.substring(STRING_PREFIX.length());
return createdInlinedResource(ResourceType.STRING, name, element);
} else if (value.startsWith(DIMEN_PREFIX)) {
String name = value.substring(DIMEN_PREFIX.length());
return createdInlinedResource(ResourceType.DIMEN, name, element);
} else if (value.startsWith(INTEGER_PREFIX)) {
String name = value.substring(INTEGER_PREFIX.length());
return createdInlinedResource(ResourceType.INTEGER, name, element);
} else {
return NONE;
}
}
@NotNull
private static InlinedResource findJavaExpressionReference(PsiReferenceExpression expression) {
AndroidPsiUtils.ResourceReferenceType referenceType = AndroidPsiUtils.getResourceReferenceType(expression);
if (referenceType != AndroidPsiUtils.ResourceReferenceType.APP) {
return NONE;
}
ResourceType type = AndroidPsiUtils.getResourceType(expression);
if (type == null || !(type == ResourceType.STRING || type == ResourceType.DIMEN || type == ResourceType.INTEGER ||
type == ResourceType.PLURALS)) {
return NONE;
}
PsiElement parameterList = expression.getParent();
String name = AndroidPsiUtils.getResourceName(expression);
if (parameterList instanceof PsiExpressionList) {
PsiElement call = parameterList.getParent();
if (call instanceof PsiMethodCallExpression) {
PsiMethodCallExpression callExpression = (PsiMethodCallExpression)call;
PsiReferenceExpression methodExpression = callExpression.getMethodExpression();
String methodName = methodExpression.getReferenceName();
if (methodName != null &&
(methodName.equals("getString") ||
methodName.equals("getText") ||
methodName.equals("getInteger") ||
methodName.startsWith("getDimension") ||
methodName.startsWith("getQuantityString"))) {
// This seems to be an IntelliJ bug; it complains that type can be null, but it clearly can not
// (and if I insert assert type != null it correctly says that the assertion is not necessary)
//noinspection ConstantConditions
@NotNull ResourceType resourceType = type;
//noinspection ConstantConditions
return createdInlinedResource(resourceType, name, callExpression);
}
//noinspection ConstantConditions
if (!UNIT_TEST_MODE && ONLY_FOLD_ANNOTATED_METHODS) {
PsiParameter[] parameters = null;
int parameterIndex = ArrayUtil.indexOf(callExpression.getArgumentList().getExpressions(), expression);
if (parameterIndex == -1) {
return NONE;
}
PsiMethod method = callExpression.resolveMethod();
if (!UNIT_TEST_MODE) { // For some reason, we can't resolve PsiMethods from the unit tests
if (method == null) {
return NONE;
}
parameters = method.getParameterList().getParameters();
if (parameters.length <= parameterIndex) {
return NONE;
}
}
if (!allowsResourceType(ResourceType.STRING, parameters[parameterIndex])) {
return NONE;
}
}
}
}
// Suppress null warning; see @NotNull comment further up in this method
//noinspection ConstantConditions
return createdInlinedResource(type, name, expression);
}
@Nullable
private static LocalResourceRepository getAppResources(PsiElement element) {
Module module = ModuleUtilCore.findModuleForPsiElement(element);
if (module == null) {
return null;
}
return AppResourceRepository.getAppResources(module, FORCE_PROJECT_RESOURCE_LOADING);
}
private static InlinedResource createdInlinedResource(@NotNull ResourceType type, @NotNull String name,
@NotNull PsiElement foldElement) {
// Not part of a call: just fold the R.string reference itself
LocalResourceRepository appResources = getAppResources(foldElement);
if (appResources != null && appResources.hasResourceItem(type, name)) {
ASTNode node = foldElement.getNode();
if (node != null) {
TextRange textRange = foldElement.getTextRange();
HashSet<Object> dependencies = new HashSet<Object>();
dependencies.add(foldElement);
FoldingDescriptor descriptor = new FoldingDescriptor(node, textRange, null, dependencies);
InlinedResource inlinedResource = new InlinedResource(type, name, appResources, descriptor, foldElement);
dependencies.add(inlinedResource);
return inlinedResource;
}
}
return NONE;
}
/**
* Returns true if the given modifier list owner (such as a method or parameter)
* specifies a {@code @ResourceInt} which includes the given {@code type}.
*
* @param type the resource type to check
* @param owner the potentially annotated element to check
* @return true if the resource type is allowed
*/
public static boolean allowsResourceType(@NotNull ResourceType type, @Nullable PsiModifierListOwner owner) {
if (owner == null) {
return false;
}
PsiAnnotation annotation = AnnotationUtil.findAnnotation(owner, ANDROID_RESOURCE_INT);
Boolean allowed = allowsResourceType(type, annotation);
return allowed != null && allowed.booleanValue();
}
/**
* Returns true if the given {@code @ResourceInt} annotation usage specifies that the given resource type
* is allowed
*
* @param type the resource type to check
* @param annotation an annotation usage on an element
* @return true if the resource type is allowed, false if it is not, and null if no annotation
* was found
*/
@Nullable
public static Boolean allowsResourceType(@NotNull ResourceType type, @Nullable PsiAnnotation annotation) {
if (annotation == null) {
return null;
}
assert ANDROID_RESOURCE_INT.equals(annotation.getQualifiedName());
PsiAnnotationParameterList annotationParameters = annotation.getParameterList();
for (PsiNameValuePair pair : annotationParameters.getAttributes()) {
PsiAnnotationMemberValue value = pair.getValue();
if (value instanceof PsiReferenceExpression) {
PsiReferenceExpression expression = (PsiReferenceExpression) value;
return allowsResourceType(type, expression);
} else if (value instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue) value;
for (PsiAnnotationMemberValue v : mv.getInitializers()) {
if (v instanceof PsiReferenceExpression) {
Boolean b = allowsResourceType(type, (PsiReferenceExpression)v);
if (b != null) {
return b;
}
}
}
}
}
return null;
}
private static Boolean allowsResourceType(ResourceType type, PsiReferenceExpression v) {
// When the @ResourceInt annotation is added to the SDK and potentially added to the
// user's classpath, we should resolve this constant properly as follows:
// PsiElement resolved = v.resolve();
// if (resolved instanceof PsiEnumConstant) {
// String name = ((PsiEnumConstant) resolved).getName();
// However, for now these are just annotations in the external annotations file,
// so we simply use the text tokens:
String name = v.getText();
if (name.equals("all")) { // Corresponds to ResourceInt.Type.ALL
return true;
} else if (name.equals("none")) { // Corresponds to ResourceInt.Type.NONE
return false;
} else {
return type.getName().equalsIgnoreCase(name);
}
}
}