/* * 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(); } }