diff --git a/.idea/dictionaries/4u7.xml b/.idea/dictionaries/4u7.xml
new file mode 100644
index 00000000000..57de8025848
--- /dev/null
+++ b/.idea/dictionaries/4u7.xml
@@ -0,0 +1,7 @@
+
+
+
+ foldable
+
+
+
\ No newline at end of file
diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt
index a7f99bc3966..5648d4089d1 100755
--- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt
+++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.addImport.AbstractAddImportTest
import org.jetbrains.kotlin.allopen.AbstractBytecodeListingTestForAllOpen
import org.jetbrains.kotlin.android.*
import org.jetbrains.kotlin.android.configure.AbstractConfigureProjectTest
+import org.jetbrains.kotlin.android.folding.AbstractAndroidResourceFoldingTest
import org.jetbrains.kotlin.android.intentions.AbstractAndroidIntentionTest
import org.jetbrains.kotlin.android.intentions.AbstractAndroidResourceIntentionTest
import org.jetbrains.kotlin.android.lint.AbstractKotlinLintTest
@@ -1222,6 +1223,10 @@ fun main(args: Array) {
testClass {
model("android/intentions", pattern = "^([\\w\\-_]+)\\.kt$")
}
+
+ testClass {
+ model("android/folding")
+ }
}
testGroup("plugins/plugins-tests/tests", "plugins/android-extensions/android-extensions-jps/testData") {
diff --git a/idea/idea-android/idea-android.iml b/idea/idea-android/idea-android.iml
index a0388a5a205..fac324c777e 100644
--- a/idea/idea-android/idea-android.iml
+++ b/idea/idea-android/idea-android.iml
@@ -21,5 +21,6 @@
+
\ No newline at end of file
diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/folding/ResourceFoldingBuilder.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/folding/ResourceFoldingBuilder.kt
new file mode 100644
index 00000000000..1aeaad546b3
--- /dev/null
+++ b/idea/idea-android/src/org/jetbrains/kotlin/android/folding/ResourceFoldingBuilder.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2010-2017 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.kotlin.android.folding
+
+import com.android.SdkConstants.*
+import com.android.ide.common.resources.configuration.FolderConfiguration
+import com.android.ide.common.resources.configuration.LocaleQualifier
+import com.android.resources.ResourceType
+import com.android.tools.idea.folding.AndroidFoldingSettings
+import com.android.tools.idea.rendering.AppResourceRepository
+import com.android.tools.idea.rendering.LocalResourceRepository
+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.components.ServiceManager
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.module.ModuleUtilCore
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassOwner
+import com.intellij.psi.PsiElement
+import com.intellij.psi.impl.source.SourceTreeToPsiMap
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.uast.*
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+import java.util.regex.Pattern
+
+
+class ResourceFoldingBuilder : FoldingBuilderEx() {
+
+ companion object {
+ // See lint's StringFormatDetector
+ private val FORMAT = Pattern.compile("%(\\d+\\$)?([-+#, 0(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])")
+ private val FOLD_MAX_LENGTH = 60
+ private val FORCE_PROJECT_RESOURCE_LOADING = true
+ private val UNIT_TEST_MODE: Boolean = ApplicationManager.getApplication().isUnitTestMode
+ private val RESOURCE_TYPES = listOf(ResourceType.STRING,
+ ResourceType.DIMEN,
+ ResourceType.INTEGER,
+ ResourceType.PLURALS)
+ }
+
+ private val isFoldingEnabled = AndroidFoldingSettings.getInstance().isCollapseAndroidStrings
+
+ override fun getPlaceholderText(node: ASTNode): String? {
+
+ tailrec fun UElement.unwrapReferenceAndGetValue(resources: LocalResourceRepository): String? = when (this) {
+ is UQualifiedReferenceExpression -> selector.unwrapReferenceAndGetValue(resources)
+ is UCallExpression -> (valueArguments.firstOrNull() as? UReferenceExpression)?.getAndroidResourceValue(resources, this)
+ else -> (this as? UReferenceExpression)?.getAndroidResourceValue(resources)
+ }
+
+ val element = SourceTreeToPsiMap.treeElementToPsi(node) ?: return null
+ val appResources = getAppResources(element) ?: return null
+ val uastContext = ServiceManager.getService(element.project, UastContext::class.java) ?: return null
+ return uastContext.convertElement(element, null, null)?.unwrapReferenceAndGetValue(appResources)
+ }
+
+ override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array {
+ if (root !is KtFile || quick && !UNIT_TEST_MODE || !isFoldingEnabled) {
+ return emptyArray()
+ }
+
+ val file = root.toUElement()
+ val result = arrayListOf()
+ file?.accept(object : AbstractUastVisitor() {
+ override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression): Boolean {
+ node.getFoldingDescriptor()?.let { result.add(it) }
+ return super.visitSimpleNameReferenceExpression(node)
+ }
+ })
+
+ return result.toTypedArray()
+ }
+
+ override fun isCollapsedByDefault(node: ASTNode): Boolean = isFoldingEnabled
+
+ private fun UReferenceExpression.getFoldingDescriptor(): FoldingDescriptor? {
+ val resolved = resolve() ?: return null
+ val resourceType = resolved.getAndroidResourceType() ?: return null
+ if (resourceType !in RESOURCE_TYPES) return null
+
+ fun UElement.createFoldingDescriptor() = psi?.let { psi ->
+ val dependencies: Set = setOf(psi)
+ FoldingDescriptor(psi.node, psi.textRange, null, dependencies)
+ }
+
+ val element = getOutermostQualified() ?: this
+
+ (element.containingElement as? UCallExpression)?.run {
+ if (isFoldableGetResourceValueCall()) {
+ return getOutermostQualified()?.createFoldingDescriptor() ?: createFoldingDescriptor()
+ }
+ }
+
+ return element.createFoldingDescriptor()
+ }
+
+ private fun UCallExpression.isFoldableGetResourceValueCall() =
+ methodName == "getString" ||
+ methodName == "getText" ||
+ methodName == "getInteger" ||
+ methodName?.startsWith("getDimension") ?: false ||
+ methodName?.startsWith("getQuantityString") ?: false
+
+ private fun PsiElement.getAndroidResourceType(): ResourceType? {
+ val elementType = parent as? PsiClass ?: return null
+ val elementPackage = elementType.parent as? PsiClass ?: return null
+ if (R_CLASS != elementPackage.name) return null
+ if (elementPackage.qualifiedName != "$ANDROID_PKG.$R_CLASS") {
+ return ResourceType.getEnum(elementType.name)
+ }
+
+ return null
+ }
+
+ private fun UReferenceExpression.getAndroidResourceValue(resources: LocalResourceRepository, call: UCallExpression? = null): String? {
+ val resourceType = resolve()?.getAndroidResourceType() ?: return null
+ val referenceConfig = FolderConfiguration().apply { localeQualifier = LocaleQualifier("xx") }
+ val key = resolvedName
+ val resourceValue = resources.getConfiguredValue(resourceType, key, referenceConfig)?.value ?: return null
+ val text = if (call != null) formatArguments(call, resourceValue) else resourceValue
+
+ if (resourceType == ResourceType.PLURALS && text.startsWith(STRING_PREFIX)) {
+ resources.getConfiguredValue(ResourceType.STRING, text.substring(STRING_PREFIX.length), referenceConfig)?.value?.let {
+ return '"' + StringUtil.shortenTextWithEllipsis(it, FOLD_MAX_LENGTH - 2, 0) + '"'
+ }
+ }
+
+ if (resourceType == ResourceType.STRING) {
+ 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 key + ": " + text
+ }
+
+ return StringUtil.shortenTextWithEllipsis(text, FOLD_MAX_LENGTH, 0)
+ }
+
+ // Converted from com.android.tools.idea.folding.InlinedResource#insertArguments
+ private fun formatArguments(callExpression: UCallExpression, formatString: String): String {
+ if (!formatString.contains('%')) {
+ return formatString
+ }
+
+ val args = callExpression.valueArguments
+ if (args.isEmpty() || !args.first().isPsiValid) {
+ return formatString
+ }
+
+ val matcher = FORMAT.matcher(formatString)
+ var index = 0
+ var prevIndex = 0
+ var nextNumber = 1
+ var start = 0
+ val sb = StringBuilder(2 * formatString.length)
+ while (true) {
+ if (matcher.find(index)) {
+ if ("%" == matcher.group(6)) {
+ index = matcher.end()
+ continue
+ }
+ val matchStart = matcher.start()
+ // Make sure this is not an escaped '%'
+ while (prevIndex < matchStart) {
+ val c = formatString[prevIndex]
+ if (c == '\\') {
+ prevIndex++
+ }
+ 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
+ val number: Int
+ var numberString: String? = 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.size) {
+ val argExpression = args[number]
+ var value: Any? = argExpression.evaluate()
+
+ if (value == null) {
+ value = args[number].asSourceString()
+ }
+
+ for (i in start..matchStart - 1) {
+ sb.append(formatString[i])
+ }
+
+ sb.append("{")
+ sb.append(value)
+ sb.append('}')
+ start = index
+ }
+ }
+ else {
+ var i = start
+ val n = formatString.length
+ while (i < n) {
+ sb.append(formatString[i])
+ i++
+ }
+ break
+ }
+ }
+
+ return sb.toString()
+ }
+
+ private fun getAppResources(element: PsiElement): LocalResourceRepository? = ModuleUtilCore.findModuleForPsiElement(element)?.let {
+ AppResourceRepository.getAppResources(it, FORCE_PROJECT_RESOURCE_LOADING)
+ }
+}
diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AbstractAndroidFoldingTest.kt b/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AbstractAndroidFoldingTest.kt
new file mode 100644
index 00000000000..e27fbde9e2b
--- /dev/null
+++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AbstractAndroidFoldingTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2017 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.kotlin.android.folding
+
+import com.android.SdkConstants
+import org.jetbrains.kotlin.android.KotlinAndroidTestCase
+
+
+abstract class AbstractAndroidResourceFoldingTest : KotlinAndroidTestCase() {
+
+ fun doTest(path: String) {
+ myFixture.copyFileToProject("values.xml", "res/values/values.xml")
+ myFixture.copyFileToProject("R.java", "gen/com/myapp/R.java")
+ myFixture.testFoldingWithCollapseStatus(path, "${myFixture.tempDirPath}/src/main.kt")
+ }
+
+ override fun createManifest() {
+ myFixture.copyFileToProject("idea/testData/android/AndroidManifest.xml", SdkConstants.FN_ANDROID_MANIFEST_XML)
+ }
+
+ override fun getTestDataPath() = "idea/testData/android/folding"
+}
\ No newline at end of file
diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AndroidResourceFoldingTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AndroidResourceFoldingTestGenerated.java
new file mode 100644
index 00000000000..2478cf7d1a5
--- /dev/null
+++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/folding/AndroidResourceFoldingTestGenerated.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2017 JetBrains s.r.o.
+ *
+ * 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 org.jetbrains.kotlin.android.folding;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
+import org.jetbrains.kotlin.test.KotlinTestUtils;
+import org.jetbrains.kotlin.test.TargetBackend;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("idea/testData/android/folding")
+@TestDataPath("$PROJECT_ROOT")
+@RunWith(JUnit3RunnerWithInners.class)
+public class AndroidResourceFoldingTestGenerated extends AbstractAndroidResourceFoldingTest {
+ public void testAllFilesPresentInFolding() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/folding"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
+ }
+
+ @TestMetadata("dimensions.kt")
+ public void testDimensions() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/folding/dimensions.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("getString.kt")
+ public void testGetString() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/folding/getString.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("plurals.kt")
+ public void testPlurals() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/folding/plurals.kt");
+ doTest(fileName);
+ }
+}
diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml
index 19692f6d7f8..6d871388068 100644
--- a/idea/src/META-INF/android.xml
+++ b/idea/src/META-INF/android.xml
@@ -27,6 +27,8 @@
+
+
diff --git a/idea/testData/android/folding/R.java b/idea/testData/android/folding/R.java
new file mode 100644
index 00000000000..63c3d41c701
--- /dev/null
+++ b/idea/testData/android/folding/R.java
@@ -0,0 +1,34 @@
+package com.myapp;
+
+public final class R {
+ public static final class string {
+ public static final int app_title=0x7f060022;
+ public static final int compex_format_string=0x7f060023;
+ public static final int empty_string=0x7f060024;
+ public static final int format_string=0x7f060025;
+ public static final int invalid_format=0x7f060026;
+ public static final int long_string=0x7f060027;
+ public static final int plural_few=0x7f060028;
+ public static final int plural_one=0x7f060029;
+ public static final int plural_other=0x7f06002a;
+ public static final int positive_button_text=0x7f06002b;
+ public static final int string1=0x7f06002c;
+ public static final int string2=0x7f06002d;
+ public static final int string3=0x7f06002e;
+ }
+ public static final class integer {
+ public static final int some_int=0x7f0c0003;
+ public static final int max_action_buttons=0x7f0c0004;
+ }
+ public static final class plurals {
+ public static final int first=0x7f0d0000;
+ public static final int second=0x7f0d0001;
+ }
+ public static final class dimen {
+ public static final int action_bar_default_height=0x7f07004f;
+ public static final int action_bar_icon_vertical_padding=0x7f070050;
+ public static final int mydimen1=0x7f07005b;
+ public static final int mydimen2=0x7f07005c;
+ public static final int mydimen3=0x7f07005d;
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/android/folding/dimensions.kt b/idea/testData/android/folding/dimensions.kt
new file mode 100644
index 00000000000..5c8a3956137
--- /dev/null
+++ b/idea/testData/android/folding/dimensions.kt
@@ -0,0 +1,19 @@
+package com.myapp
+
+import android.app.Activity
+import android.app.AlertDialog
+
+class MyActivity : Activity() {
+ fun test() {
+ val dialog = AlertDialog.Builder(this)
+ .setView(null)
+ .setPositiveButton(R.string.positive_button_text, null)
+ .create()
+ val dimension = resources.getDimension(R.dimen.action_bar_default_height)
+ val dimension2 = resources.getDimensionPixelOffset(R.dimen.action_bar_icon_vertical_padding)
+ val dimension3 = resources.getDimensionPixelSize(R.dimen.mydimen3)
+ val strings = intArrayOf(R.string.string1, R.string.string2, R.string.string3)
+ val dimensions = intArrayOf(R.dimen.mydimen1, R.dimen.mydimen2)
+ val maxButtons = resources.getInteger(R.integer.max_action_buttons)
+ }
+}
diff --git a/idea/testData/android/folding/getString.kt b/idea/testData/android/folding/getString.kt
new file mode 100644
index 00000000000..f717dc01d05
--- /dev/null
+++ b/idea/testData/android/folding/getString.kt
@@ -0,0 +1,27 @@
+package com.myapp
+
+import android.content.Context
+import com.myapp.R.string.*;
+
+fun Context.getAppTitle() = getString(R.string.app_title)
+
+fun getAppTitle(context: Context) {
+ return context.getString(app_title)
+}
+
+fun getLongString(context: Context) = context.getString(R.string.long_string)
+
+fun bar(text: String) = text
+
+fun foo(context: Context) {
+ val name = "Vasya"
+ val otherName = context.getString(R.string.no_resource)
+ val cancel = context.getString(android.R.string.cancel)
+ val text = context.getString(R.string.format_string, name)
+ val text = context.getString(R.string.format_string, otherName)
+ val emptyString = context.getString(R.string.empty_string)
+ val someInt = context.resources.getInteger(R.integer.some_int)
+ context.getString(R.string.no_resource)
+ bar(context.getResources().getString(R.string.compex_format_string, "111", "222", "333"))
+ val invalid = with(context) { getString(R.string.invalid_format, name, otherName) }
+}
diff --git a/idea/testData/android/folding/plurals.kt b/idea/testData/android/folding/plurals.kt
new file mode 100644
index 00000000000..3f18d84d847
--- /dev/null
+++ b/idea/testData/android/folding/plurals.kt
@@ -0,0 +1,9 @@
+package com.myapp
+
+import android.content.Context
+
+fun Context.testPlurals(quantity: Int) {
+ val plural1 = resources.getQuantityString(R.plurals.first, quantity)
+ val plural2 = this.getResources().getQuantityString(R.plurals.second, quantity)
+}
+
diff --git a/idea/testData/android/folding/values.xml b/idea/testData/android/folding/values.xml
new file mode 100644
index 00000000000..bcd5571bb3e
--- /dev/null
+++ b/idea/testData/android/folding/values.xml
@@ -0,0 +1,39 @@
+
+ ResourceFolding
+ My Cool Application
+ Some very very long string which should be trimmed. Lorem ipsum dolor sit amet.
+ Hello %s!
+
+ Third: %3$s Repeated: %3$s First: %1$s Second: %2$d
+ Escaped: \%s First: %1$s Invalid: %20$s
+
+ Reference One
+ Reference Few
+ Reference Other
+
+ Yes
+ 1111
+ 2222
+ 3333
+
+ 8
+ 2
+
+
+ - @string/plural_one
+ - @string/plural_few
+ - @string/plural_other
+
+
+ - Quantity One
+ - Quantity Few
+
+
+ 16dp
+ 16dp
+ 56dip
+ 4dip
+ 3dip
+ 1dip
+ 2dip
+
\ No newline at end of file