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
+ }
0 commit comments