/*
* 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.SdkConstants;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LanguageQualifier;
import com.android.resources.ResourceType;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.util.ModificationTracker;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.impl.JavaConstantExpressionEvaluator;
import com.intellij.psi.xml.XmlAttributeValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.STRING_PREFIX;
/** A resource referenced in code (Java or XML) */
class InlinedResource implements ModificationTracker {
static final InlinedResource NONE = new InlinedResource(ResourceType.STRING, "", null, null, null);
private static final int FOLD_MAX_LENGTH = 60;
/** Resource type, typically a string or dimension */
private final ResourceType myType;
/** The string key, such as "foo" in {@code @string/foo} or {@code R.string.foo} */
@NotNull private String myKey;
/**
* The element absorbed by this string reference. For a parameter it might be just
* {@code R.string.foo}, but in Java code it can sometimes also include a whole
* string lookup call, such as {@code getResources().getString(R.string.foo, foo)}
* */
@Nullable private PsiElement myElement;
/** The associated folding descriptor */
@Nullable private FoldingDescriptor myDescriptor;
/** The app resources for looking up resource strings lazily */
@Nullable private LocalResourceRepository myResourceRepository;
InlinedResource(@NotNull ResourceType type,
@NotNull String key,
@Nullable LocalResourceRepository resources,
@Nullable FoldingDescriptor descriptor,
@Nullable PsiElement element) {
myType = type;
myKey = key;
myResourceRepository = resources;
myDescriptor = descriptor;
myElement = element;
}
@Nullable
FoldingDescriptor getDescriptor() {
return myDescriptor;
}
@Override
public long getModificationCount() {
// Return the project resource generation count; this ensures that when the project
// resources are updated, the folding text is refreshed
return myResourceRepository != null ? myResourceRepository.getModificationCount() : 0;
}
@Nullable
public String getResolvedString() {
if (myResourceRepository != null) {
if (myResourceRepository.hasResourceItem(myType, myKey)) {
FolderConfiguration referenceConfig = new FolderConfiguration();
// Nonexistent language qualifier: trick it to fall back to the default locale
referenceConfig.setLanguageQualifier(new LanguageQualifier("xx"));
ResourceValue value = myResourceRepository.getConfiguredValue(myType, myKey, referenceConfig);
if (value != null) {
String text = value.getValue();
if (text != null) {
if (myElement instanceof PsiMethodCallExpression) {
text = insertArguments((PsiMethodCallExpression)myElement, text);
}
if (myType == ResourceType.PLURALS && text.startsWith(STRING_PREFIX)) {
value = myResourceRepository.getConfiguredValue(ResourceType.STRING, text.substring(STRING_PREFIX.length()),
referenceConfig);
if (value != null && value.getValue() != null) {
text = value.getValue();
return '"' + StringUtil.shortenTextWithEllipsis(text, FOLD_MAX_LENGTH - 2, 0) + '"';
}
}
if (myType == ResourceType.STRING || myElement instanceof XmlAttributeValue) {
return '"' + StringUtil.shortenTextWithEllipsis(text, FOLD_MAX_LENGTH - 2, 0) + '"';
} else if (text.length() <= 1) {
// Don't just inline empty or one-character replacements: they can't be expanded by a mouse click
// so are hard to use without knowing about the folding keyboard shortcut to toggle folding.
// This is similar to how IntelliJ 14 handles call parameters
return myKey + ": " + text;
} else {
return StringUtil.shortenTextWithEllipsis(text, FOLD_MAX_LENGTH, 0);
}
}
}
}
}
return null;
}
// See lint's StringFormatDetector
private static final Pattern FORMAT = Pattern.compile("%(\\d+\\$)?([-+#, 0(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])");
@NotNull
private static String insertArguments(@NotNull PsiMethodCallExpression methodCallExpression, @NotNull String s) {
if (s.indexOf('%') == -1) {
return s;
}
final PsiExpression[] args = methodCallExpression.getArgumentList().getExpressions();
if (args.length == 0 || !args[0].isValid()) {
return s;
}
Matcher matcher = FORMAT.matcher(s);
int index = 0;
int prevIndex = 0;
int nextNumber = 1;
int start = 0;
StringBuilder sb = new StringBuilder(2 * s.length());
while (true) {
if (matcher.find(index)) {
if ("%".equals(matcher.group(6))) {
index = matcher.end();
continue;
}
int matchStart = matcher.start();
// Make sure this is not an escaped '%'
for (; prevIndex < matchStart; prevIndex++) {
char c = s.charAt(prevIndex);
if (c == '\\') {
prevIndex++;
}
}
if (prevIndex > matchStart) {
// We're in an escape, ignore this result
index = prevIndex;
continue;
}
index = matcher.end();
// Shouldn't throw a number format exception since we've already
// matched the pattern in the regexp
int number;
String numberString = matcher.group(1);
if (numberString != null) {
// Strip off trailing $
numberString = numberString.substring(0, numberString.length() - 1);
number = Integer.parseInt(numberString);
nextNumber = number + 1;
} else {
number = nextNumber++;
}
if (number > 0 && number < args.length) {
PsiExpression argExpression = args[number];
Object value = JavaConstantExpressionEvaluator.computeConstantExpression(argExpression, false);
if (value == null) {
value = args[number].getText();
}
for (int i = start; i < matchStart; i++) {
sb.append(s.charAt(i));
}
sb.append('{');
sb.append(value);
sb.append('}');
start = index;
}
} else {
for (int i = start, n = s.length(); i < n; i++) {
sb.append(s.charAt(i));
}
break;
}
}
return sb.toString();
}
}