Skip to content

Commit 3ce1703

Browse files
committed
Extract string resource intention action for android (KT-11715)
#KT-11715 Fixed
1 parent 53ea5a2 commit 3ce1703

File tree

65 files changed

+859
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+859
-0
lines changed

generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.AbstractDataFlowValueRenderingTest
2222
import org.jetbrains.kotlin.addImport.AbstractAddImportTest
2323
import org.jetbrains.kotlin.android.*
2424
import org.jetbrains.kotlin.android.configure.AbstractConfigureProjectTest
25+
import org.jetbrains.kotlin.android.intentions.AbstractAndroidResourceIntentionTest
2526
import org.jetbrains.kotlin.annotation.AbstractAnnotationProcessorBoxTest
2627
import org.jetbrains.kotlin.annotation.processing.test.sourceRetention.AbstractBytecodeListingTestForSourceRetention
2728
import org.jetbrains.kotlin.annotation.processing.test.wrappers.AbstractAnnotationProcessingTest
@@ -1153,6 +1154,10 @@ fun main(args: Array<String>) {
11531154
testClass<AbstractConfigureProjectTest>() {
11541155
model("configuration/android-gradle", pattern = """(\w+)_before\.gradle$""", testMethod = "doTestAndroidGradle")
11551156
}
1157+
1158+
testClass<AbstractAndroidResourceIntentionTest> {
1159+
model("android/resourceIntentions", extension = "test", singleClass = true)
1160+
}
11561161
}
11571162

11581163
testGroup("plugins/plugins-tests/tests", "plugins/android-extensions/android-extensions-jps/testData") {

idea/idea-android/idea-android.iml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<orderEntry type="module" module-name="idea" />
1515
<orderEntry type="module" module-name="ide-common" />
1616
<orderEntry type="module" module-name="idea-analysis" />
17+
<orderEntry type="module" module-name="idea-test-framework" scope="TEST" />
1718
<orderEntry type="module" module-name="util" />
1819
<orderEntry type="module" module-name="frontend" />
1920
<orderEntry type="module" module-name="frontend.java" />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.android.intentions
18+
19+
import com.intellij.openapi.util.Key
20+
21+
22+
val CREATE_XML_RESOURCE_PARAMETERS_NAME_KEY = Key<String>("CREATE_XML_RESOURCE_PARAMETERS_NAME_KEY")
23+
24+
class CreateXmlResourceParameters(val name: String,
25+
val value: String,
26+
val fileName: String,
27+
val directoryNames: List<String>)
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.android.intentions
18+
19+
import com.android.resources.ResourceType
20+
import com.intellij.CommonBundle
21+
import com.intellij.codeInsight.template.*
22+
import com.intellij.codeInsight.template.impl.*
23+
import com.intellij.codeInsight.template.macro.VariableOfTypeMacro
24+
import com.intellij.openapi.application.Application
25+
import com.intellij.openapi.application.ApplicationManager
26+
import com.intellij.openapi.command.undo.UndoUtil
27+
import com.intellij.openapi.editor.Editor
28+
import com.intellij.openapi.module.Module
29+
import com.intellij.openapi.ui.Messages
30+
import com.intellij.psi.*
31+
import com.intellij.psi.codeStyle.JavaCodeStyleManager
32+
import com.intellij.psi.util.PsiTreeUtil
33+
import org.jetbrains.android.actions.CreateXmlResourceDialog
34+
import org.jetbrains.android.facet.AndroidFacet
35+
import org.jetbrains.android.util.AndroidBundle
36+
import org.jetbrains.android.util.AndroidResourceUtil
37+
import org.jetbrains.kotlin.builtins.isExtensionFunctionType
38+
import org.jetbrains.kotlin.descriptors.ClassDescriptor
39+
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
40+
import org.jetbrains.kotlin.idea.caches.resolve.analyze
41+
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
42+
import org.jetbrains.kotlin.idea.intentions.SelfTargetingIntention
43+
import org.jetbrains.kotlin.psi.*
44+
import org.jetbrains.kotlin.resolve.BindingContext
45+
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
46+
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
47+
48+
49+
class KotlinAndroidAddStringResource : SelfTargetingIntention<KtLiteralStringTemplateEntry>(KtLiteralStringTemplateEntry::class.java,
50+
"Extract string resource") {
51+
private val GET_STRING_METHOD = "getString"
52+
private val EXTRACT_RESOURCE_DIALOG_TITLE = "Extract Resource"
53+
private val PACKAGE_NOT_FOUND_ERROR = "package.not.found.error"
54+
55+
override fun isApplicableTo(element: KtLiteralStringTemplateEntry, caretOffset: Int): Boolean {
56+
if (AndroidFacet.getInstance(element.containingFile) == null) {
57+
return false
58+
}
59+
60+
return element.parent.children.size == 1
61+
}
62+
63+
override fun applyTo(element: KtLiteralStringTemplateEntry, editor: Editor?) {
64+
val facet = AndroidFacet.getInstance(element.containingFile)
65+
if (editor == null) {
66+
throw IllegalArgumentException("This intention requires an editor.")
67+
}
68+
69+
if (facet == null) {
70+
throw IllegalStateException("This intention requires android facet.")
71+
}
72+
73+
val file = element.containingFile
74+
val project = file.project
75+
76+
val manifestPackage = getManifestPackage(facet)
77+
if (manifestPackage == null) {
78+
Messages.showErrorDialog(project, AndroidBundle.message(PACKAGE_NOT_FOUND_ERROR), CommonBundle.getErrorTitle())
79+
return
80+
}
81+
82+
val parameters = getCreateXmlResourceParameters(facet.module, element) ?:
83+
return
84+
85+
if (!AndroidResourceUtil.createValueResource(facet.module, parameters.name, ResourceType.STRING,
86+
parameters.fileName, parameters.directoryNames, parameters.value)) {
87+
return
88+
}
89+
90+
createResourceReference(facet.module, editor, file, element, manifestPackage, parameters.name, ResourceType.STRING)
91+
PsiDocumentManager.getInstance(project).commitAllDocuments()
92+
UndoUtil.markPsiFileForUndo(file)
93+
}
94+
95+
private fun getCreateXmlResourceParameters(module: Module, element: KtLiteralStringTemplateEntry): CreateXmlResourceParameters? {
96+
97+
val stringValue = element.text
98+
99+
val showDialog = !ApplicationManager.getApplication().isUnitTestMode
100+
val resourceName = element.getUserData(CREATE_XML_RESOURCE_PARAMETERS_NAME_KEY)
101+
102+
val dialog = CreateXmlResourceDialog(module, ResourceType.STRING, resourceName, stringValue, true)
103+
dialog.title = EXTRACT_RESOURCE_DIALOG_TITLE
104+
if (showDialog && !dialog.showAndGet()) {
105+
return null
106+
}
107+
108+
return CreateXmlResourceParameters(dialog.resourceName,
109+
dialog.value,
110+
dialog.fileName,
111+
dialog.dirNames)
112+
}
113+
114+
private fun createResourceReference(module: Module, editor: Editor, file: PsiFile, element: PsiElement, aPackage: String,
115+
resName: String, resType: ResourceType) {
116+
val rFieldName = AndroidResourceUtil.getRJavaFieldName(resName)
117+
val fieldName = "$aPackage.R.$resType.$rFieldName"
118+
119+
val template: TemplateImpl
120+
if (!needContextReceiver(element)) {
121+
template = TemplateImpl("", "$GET_STRING_METHOD($fieldName)", "")
122+
}
123+
else {
124+
template = TemplateImpl("", "\$context\$.$GET_STRING_METHOD($fieldName)", "")
125+
val marker = MacroCallNode(VariableOfTypeMacro())
126+
marker.addParameter(ConstantNode("android.content.Context"))
127+
template.addVariable("context", marker, ConstantNode("context"), true)
128+
}
129+
130+
val containingLiteralExpression = element.parent
131+
editor.caretModel.moveToOffset(containingLiteralExpression.textOffset)
132+
editor.document.deleteString(containingLiteralExpression.textRange.startOffset, containingLiteralExpression.textRange.endOffset)
133+
val marker = editor.document.createRangeMarker(containingLiteralExpression.textOffset, containingLiteralExpression.textOffset)
134+
marker.isGreedyToLeft = true
135+
marker.isGreedyToRight = true
136+
137+
TemplateManager.getInstance(module.project).startTemplate(editor, template, false, null, object : TemplateEditingAdapter() {
138+
override fun waitingForInput(template: Template?) {
139+
JavaCodeStyleManager.getInstance(module.project).shortenClassReferences(file, marker.startOffset, marker.endOffset)
140+
}
141+
142+
override fun beforeTemplateFinished(state: TemplateState?, template: Template?) {
143+
JavaCodeStyleManager.getInstance(module.project).shortenClassReferences(file, marker.startOffset, marker.endOffset)
144+
}
145+
})
146+
}
147+
148+
private fun needContextReceiver(element: PsiElement): Boolean {
149+
val classesWithGetSting = listOf("android.content.Context", "android.app.Fragment", "android.support.v4.app.Fragment")
150+
val viewClass = listOf("android.view.View")
151+
var parent = PsiTreeUtil.findFirstParent(element, true) { it is KtClassOrObject || it is KtFunction || it is KtLambdaExpression }
152+
153+
while (parent != null) {
154+
155+
if (parent.isSubclassOrSubclassExtension(classesWithGetSting)) {
156+
return false
157+
}
158+
159+
if (parent.isSubclassOrSubclassExtension(viewClass) ||
160+
(parent is KtClassOrObject && !parent.isInnerClass() && !parent.isObjectLiteral())) {
161+
return true
162+
}
163+
164+
parent = PsiTreeUtil.findFirstParent(parent, true) { it is KtClassOrObject || it is KtFunction || it is KtLambdaExpression }
165+
}
166+
167+
return true
168+
}
169+
170+
private fun getManifestPackage(facet: AndroidFacet) = facet.manifest?.`package`?.value
171+
172+
private fun PsiElement.isSubclassOrSubclassExtension(baseClasses: Collection<String>) =
173+
(this as? KtClassOrObject)?.isSubclassOfAny(baseClasses) ?:
174+
this.isSubclassExtensionOfAny(baseClasses)
175+
176+
private fun PsiElement.isSubclassExtensionOfAny(baseClasses: Collection<String>) =
177+
(this as? KtLambdaExpression)?.isSubclassExtensionOfAny(baseClasses) ?:
178+
(this as? KtFunction)?.isSubclassExtensionOfAny(baseClasses) ?:
179+
false
180+
181+
private fun KtClassOrObject.isObjectLiteral() = (this as? KtObjectDeclaration)?.isObjectLiteral() ?: false
182+
183+
private fun KtClassOrObject.isInnerClass() = (this as? KtClass)?.isInner() ?: false
184+
185+
private fun KtFunction.isSubclassExtensionOfAny(baseClasses: Collection<String>): Boolean {
186+
val descriptor = resolveToDescriptor() as FunctionDescriptor
187+
val extendedTypeDescriptor = descriptor.extensionReceiverParameter?.type?.constructor?.declarationDescriptor as? ClassDescriptor
188+
return extendedTypeDescriptor != null && baseClasses.any { extendedTypeDescriptor.isSubclassOf(it) }
189+
}
190+
191+
private fun KtLambdaExpression.isSubclassExtensionOfAny(baseClasses: Collection<String>): Boolean {
192+
val bindingContext = analyze(BodyResolveMode.PARTIAL)
193+
val type = bindingContext.getType(this)
194+
195+
if (type == null || !type.isExtensionFunctionType) {
196+
return false
197+
}
198+
199+
val extendedTypeDescriptor = type.arguments.first().type.constructor.declarationDescriptor
200+
if (extendedTypeDescriptor is ClassDescriptor) {
201+
return baseClasses.any { extendedTypeDescriptor.isSubclassOf(it) }
202+
}
203+
204+
return false
205+
}
206+
207+
private fun KtClassOrObject.isSubclassOfAny(baseClasses: Collection<String>): Boolean {
208+
val bindingContext = analyze(BodyResolveMode.PARTIAL)
209+
val declarationDescriptor = bindingContext.get(BindingContext.CLASS, this)
210+
return baseClasses.any { declarationDescriptor?.isSubclassOf(it) ?: false }
211+
}
212+
213+
private fun ClassDescriptor.isSubclassOf(className: String): Boolean {
214+
return fqNameSafe.asString() == className || defaultType.constructor.supertypes.any {
215+
(it.constructor.declarationDescriptor as? ClassDescriptor)?.isSubclassOf(className) ?: false
216+
}
217+
}
218+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.android.intentions
18+
19+
import com.android.SdkConstants
20+
import com.google.gson.JsonObject
21+
import com.google.gson.JsonParser
22+
import com.intellij.codeInsight.intention.IntentionAction
23+
import com.intellij.openapi.fileEditor.FileDocumentManager
24+
import com.intellij.openapi.util.io.FileUtil
25+
import com.intellij.openapi.vfs.LocalFileSystem
26+
import com.intellij.testFramework.PlatformTestUtil
27+
import com.intellij.util.PathUtil
28+
import junit.framework.TestCase
29+
import org.jetbrains.kotlin.android.KotlinAndroidTestCase
30+
import org.jetbrains.kotlin.idea.jsonUtils.getString
31+
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
32+
import org.jetbrains.kotlin.psi.KtFile
33+
import java.io.File
34+
35+
36+
abstract class AbstractAndroidResourceIntentionTest : KotlinAndroidTestCase() {
37+
38+
fun doTest(path: String) {
39+
val configFile = File(path)
40+
val testDataPath = configFile.parent
41+
42+
myFixture.testDataPath = testDataPath
43+
44+
val config = JsonParser().parse(FileUtil.loadFile(File(path), true)) as JsonObject
45+
46+
val intentionClass = config.getString("intentionClass")
47+
val isApplicableExpected = if (config.has("isApplicable")) config.get("isApplicable").asBoolean else true
48+
val rFile = if (config.has("rFile")) config.get("rFile").asString else null
49+
val resDirectory = if (config.has("resDirectory")) config.get("resDirectory").asString else null
50+
51+
if (rFile != null) {
52+
myFixture.copyFileToProject(rFile, "gen/" + PathUtil.getFileName(rFile))
53+
}
54+
else {
55+
if (File(testDataPath + "/R.java").isFile) {
56+
myFixture.copyFileToProject("R.java", "gen/R.java")
57+
}
58+
}
59+
60+
if (resDirectory != null) {
61+
myFixture.copyDirectoryToProject(resDirectory, "res")
62+
}
63+
else {
64+
if (File(testDataPath + "/res").isDirectory) {
65+
myFixture.copyDirectoryToProject("res", "res")
66+
}
67+
}
68+
69+
val sourceFile = myFixture.copyFileToProject("main.kt", "src/main.kt")
70+
myFixture.configureFromExistingVirtualFile(sourceFile)
71+
72+
DirectiveBasedActionUtils.checkForUnexpectedErrors(myFixture.file as KtFile)
73+
74+
val intentionAction = Class.forName(intentionClass).newInstance() as IntentionAction
75+
76+
TestCase.assertEquals(isApplicableExpected, intentionAction.isAvailable(myFixture.project, myFixture.editor, myFixture.file))
77+
if (!isApplicableExpected) {
78+
return
79+
}
80+
81+
val element = getTargetElement()
82+
element?.putUserData(CREATE_XML_RESOURCE_PARAMETERS_NAME_KEY, "resource_id")
83+
84+
myFixture.launchAction(intentionAction)
85+
86+
FileDocumentManager.getInstance().saveAllDocuments()
87+
DirectiveBasedActionUtils.checkForUnexpectedErrors(myFixture.file as KtFile)
88+
89+
myFixture.checkResultByFile("/expected/main.kt")
90+
assertResourcesEqual(testDataPath + "/expected/res")
91+
}
92+
93+
fun assertResourcesEqual(expectedPath: String) {
94+
PlatformTestUtil.assertDirectoriesEqual(LocalFileSystem.getInstance().findFileByPath(expectedPath), getResourceDirectory())
95+
}
96+
97+
fun getResourceDirectory() = LocalFileSystem.getInstance().findFileByPath(myFixture.tempDirPath + "/res")
98+
99+
fun getTargetElement() = myFixture.file.findElementAt(myFixture.caretOffset)?.parent
100+
101+
override fun createManifest() {
102+
myFixture.copyFileToProject("idea/testData/android/AndroidManifest.xml", SdkConstants.FN_ANDROID_MANIFEST_XML)
103+
}
104+
}

0 commit comments

Comments
 (0)