diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt.192 b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt.192 new file mode 100644 index 00000000000..9cdc6b40c9a --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/injection/KotlinLanguageInjectionSupport.kt.192 @@ -0,0 +1,254 @@ +/* + * Copyright 2010-2016 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.idea.injection + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.lang.Language +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.util.Ref +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiTreeUtil.getDeepestLast +import com.intellij.util.Processor +import org.intellij.plugins.intelliLang.Configuration +import org.intellij.plugins.intelliLang.inject.* +import org.intellij.plugins.intelliLang.inject.config.BaseInjection +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.idea.patterns.KotlinPatterns +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.idea.util.findAnnotation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +@NonNls val KOTLIN_SUPPORT_ID = "kotlin" + +class KotlinLanguageInjectionSupport : AbstractLanguageInjectionSupport() { + override fun getId(): String = KOTLIN_SUPPORT_ID + + override fun getPatternClasses() = arrayOf(KotlinPatterns::class.java) + + override fun isApplicableTo(host: PsiLanguageInjectionHost?) = host is KtElement + + override fun useDefaultInjector(host: PsiLanguageInjectionHost?): Boolean = false + + override fun addInjectionInPlace(language: Language?, host: PsiLanguageInjectionHost?): Boolean { + if (language == null || host == null) return false + + val configuration = Configuration.getProjectInstance(host.project).advancedConfiguration + if (!configuration.isSourceModificationAllowed) { + // It's not allowed to modify code without explicit permission. Postpone adding @Inject or comment till it granted. + host.putUserData(InjectLanguageAction.FIX_KEY, Processor { fixHost: PsiLanguageInjectionHost? -> + fixHost != null && addInjectionInstructionInCode(language, fixHost) + }) + return false + } + + if (!addInjectionInstructionInCode(language, host)) { + return false + } + + TemporaryPlacesRegistry.getInstance(host.project).addHostWithUndo(host, InjectedLanguage.create(language.id)) + return true + } + + override fun removeInjectionInPlace(psiElement: PsiLanguageInjectionHost?): Boolean { + if (psiElement == null || psiElement !is KtElement) return false + + val project = psiElement.getProject() + + val injectInstructions = listOfNotNull( + findAnnotationInjection(psiElement), + findInjectionComment(psiElement) + ) + + TemporaryPlacesRegistry.getInstance(project).removeHostWithUndo(project, psiElement) + + project.executeWriteCommand("remove injection in-code instructions") { + injectInstructions.forEach(PsiElement::delete) + } + + return true + } + + override fun findCommentInjection(host: PsiElement, commentRef: Ref?): BaseInjection? { + // Do not inject through CommentLanguageInjector, because it injects as simple injection. + // We need to behave special for interpolated strings. + return null + } + + fun findCommentInjection(host: KtElement): BaseInjection? { + return InjectorUtils.findCommentInjection(host, "", null) + } + + private fun findInjectionComment(host: KtElement): PsiComment? { + val commentRef = Ref.create(null) + InjectorUtils.findCommentInjection(host, "", commentRef) ?: return null + + return commentRef.get() as? PsiComment + } + + internal fun findAnnotationInjectionLanguageId(host: KtElement): InjectionInfo? { + val annotationEntry = findAnnotationInjection(host) ?: return null + val extractLanguageFromInjectAnnotation = extractLanguageFromInjectAnnotation(annotationEntry) ?: return null + val prefix = extractStringArgumentByName(annotationEntry, "prefix") + val suffix = extractStringArgumentByName(annotationEntry, "suffix") + return InjectionInfo(extractLanguageFromInjectAnnotation, prefix, suffix) + } +} + +private fun extractStringArgumentByName(annotationEntry: KtAnnotationEntry, name: String): String? { + val namedArgument: ValueArgument = annotationEntry.valueArguments.firstOrNull { it.getArgumentName()?.asName?.asString() == name } ?: return null + return extractStringValue(namedArgument) +} + + +private fun extractLanguageFromInjectAnnotation(annotationEntry: KtAnnotationEntry): String? { + val firstArgument: ValueArgument = annotationEntry.valueArguments.firstOrNull() ?: return null + return extractStringValue(firstArgument) +} + +private fun extractStringValue(valueArgument: ValueArgument): String? { + val firstStringArgument = valueArgument.getArgumentExpression() as? KtStringTemplateExpression ?: return null + val firstStringEntry = firstStringArgument.entries.singleOrNull() ?: return null + + return firstStringEntry.text +} + +private fun findAnnotationInjection(host: KtElement): KtAnnotationEntry? { + val modifierListOwner = findElementToInjectWithAnnotation(host) ?: return null + + val modifierList = modifierListOwner.modifierList ?: return null + + // Host can't be before annotation + if (host.startOffset < modifierList.endOffset) return null + + return modifierListOwner.findAnnotation(FqName(AnnotationUtil.LANGUAGE)) +} + +private fun canInjectWithAnnotation(host: PsiElement): Boolean { + val module = ModuleUtilCore.findModuleForPsiElement(host) ?: return false + val javaPsiFacade = JavaPsiFacade.getInstance(module.project) + + return javaPsiFacade.findClass(AnnotationUtil.LANGUAGE, GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module)) != null +} + +private fun findElementToInjectWithAnnotation(host: KtElement): KtModifierListOwner? { + return PsiTreeUtil.getParentOfType( + host, + KtModifierListOwner::class.java, + false, /* strict */ + KtBlockExpression::class.java, KtParameterList::class.java, KtTypeParameterList::class.java /* Stop at */ + ) +} + +private fun findElementToInjectWithComment(host: KtElement): KtExpression? { + val parentBlockExpression = PsiTreeUtil.getParentOfType( + host, + KtBlockExpression::class.java, + true, /* strict */ + KtDeclaration::class.java /* Stop at */ + ) ?: return null + + return parentBlockExpression.statements.firstOrNull { statement -> + PsiTreeUtil.isAncestor(statement, host, false) && checkIsValidPlaceForInjectionWithLineComment(statement, host) + } +} + +private fun addInjectionInstructionInCode(language: Language, host: PsiLanguageInjectionHost): Boolean { + val ktHost = host as? KtElement ?: return false + val project = ktHost.project + + // Find the place where injection can be stated with annotation or comment + val modifierListOwner = findElementToInjectWithAnnotation(ktHost) + + if (modifierListOwner != null && canInjectWithAnnotation(ktHost)) { + project.executeWriteCommand("Add injection annotation") { + modifierListOwner.addAnnotation(FqName(AnnotationUtil.LANGUAGE), "\"${language.id}\"") + } + + return true + } + + // Find the place where injection can be done with one-line comment + val commentBeforeAnchor: PsiElement = + modifierListOwner?.firstNonCommentChild() ?: + findElementToInjectWithComment(ktHost) ?: + return false + + val psiFactory = KtPsiFactory(project) + val injectComment = psiFactory.createComment("//language=" + language.id) + + project.executeWriteCommand("Add injection comment") { + commentBeforeAnchor.parent.addBefore(injectComment, commentBeforeAnchor) + } + + return true +} + +// Inspired with InjectorUtils.findCommentInjection() +private fun checkIsValidPlaceForInjectionWithLineComment(statement: KtExpression, host: KtElement): Boolean { + // make sure comment is close enough and ... + val statementStartOffset = statement.startOffset + val hostStart = host.startOffset + if (hostStart < statementStartOffset || hostStart - statementStartOffset > 120) { + return false + } + + if (hostStart - statementStartOffset > 2) { + // ... there's no non-empty valid host in between comment and e2 + if (prevWalker(host, statement).asSequence().takeWhile { it != null }.any { + it is PsiLanguageInjectionHost && it.isValidHost && !StringUtil.isEmptyOrSpaces(it.text) + }) { + return false + } + } + + return true +} + +private fun PsiElement.firstNonCommentChild(): PsiElement? { + return firstChild.siblings().dropWhile { it is PsiComment || it is PsiWhiteSpace }.firstOrNull() +} + +// Based on InjectorUtils.prevWalker +private fun prevWalker(element: PsiElement, scope: PsiElement): Iterator { + return object : Iterator { + private var e: PsiElement? = element + + override fun hasNext(): Boolean = true + override fun next(): PsiElement? { + val current = e + + if (current == null || current === scope) return null + val prev = current.prevSibling + e = if (prev != null) { + getDeepestLast(prev) + } + else { + val parent = current.parent + if (parent === scope || parent is PsiFile) null else parent + } + return e + } + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsWizard.kt.192 b/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsWizard.kt.192 new file mode 100644 index 00000000000..eba0b209154 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/actions/generate/KotlinGenerateEqualsWizard.kt.192 @@ -0,0 +1,130 @@ +/* + * Copyright 2010-2015 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.idea.actions.generate + +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.codeInsight.CodeInsightSettings +import com.intellij.codeInsight.generation.ui.AbstractGenerateEqualsWizard +import com.intellij.ide.wizard.StepAdapter +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.VerticalFlowLayout +import com.intellij.refactoring.classMembers.AbstractMemberInfoModel +import com.intellij.ui.NonFocusableCheckBox +import com.intellij.util.containers.HashMap +import org.jetbrains.kotlin.idea.core.isInheritable +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.utils.keysToMap +import javax.swing.JLabel +import javax.swing.JPanel + +class KotlinGenerateEqualsWizard( + project: Project, + klass: KtClass, + properties: List, + needEquals: Boolean, + needHashCode: Boolean +) : AbstractGenerateEqualsWizard( + project, BuilderImpl(klass, properties, needEquals, needHashCode) +) { + private object MemberInfoModelImpl : AbstractMemberInfoModel() + + private class BuilderImpl( + private val klass: KtClass, + properties: List, + needEquals: Boolean, + needHashCode: Boolean + ) : AbstractGenerateEqualsWizard.Builder() { + private val equalsPanel: KotlinMemberSelectionPanel? + private val hashCodePanel: KotlinMemberSelectionPanel? + + private val memberInfos = properties.map { createMemberInfo(it) } + + private val membersToHashCode = HashMap(properties.keysToMap { createMemberInfo(it) }) + + init { + equalsPanel = if (needEquals) { + KotlinMemberSelectionPanel("Choose properties to be included in equals()", memberInfos, null).apply { + table.memberInfoModel = MemberInfoModelImpl + } + } else null + + hashCodePanel = if (needHashCode) { + KotlinMemberSelectionPanel("Choose properties to be included in hashCode()", memberInfos, null).apply { + table.memberInfoModel = MemberInfoModelImpl + } + } else null + } + + private fun createMemberInfo(it: KtNamedDeclaration) = KotlinMemberInfo(it).apply { isChecked = true } + + override fun getPsiClass() = klass + + override fun getClassFields() = memberInfos + + override fun getFieldsToHashCode() = membersToHashCode + + override fun getFieldsToNonNull() = HashMap() + + override fun getEqualsPanel() = equalsPanel + + override fun getHashCodePanel() = hashCodePanel + + override fun getNonNullPanel() = null + + override fun updateHashCodeMemberInfos(equalsMemberInfos: MutableCollection) { + hashCodePanel?.table?.setMemberInfos(equalsMemberInfos.map { membersToHashCode[it.member] }) + } + + override fun updateNonNullMemberInfos(equalsMemberInfos: MutableCollection?) { + + } + } + + private object OptionsStep : StepAdapter() { + private val panel = JPanel(VerticalFlowLayout()) + + init { + with(NonFocusableCheckBox(CodeInsightBundle.message("generate.equals.hashcode.accept.sublcasses"))) { + isSelected = CodeInsightSettings.getInstance().USE_INSTANCEOF_ON_EQUALS_PARAMETER + addActionListener { CodeInsightSettings.getInstance().USE_INSTANCEOF_ON_EQUALS_PARAMETER = isSelected } + panel.add(this) + } + panel.add(JLabel(CodeInsightBundle.message("generate.equals.hashcode.accept.sublcasses.explanation"))) + } + + override fun getComponent() = panel + } + + override fun addSteps() { + if (myEqualsPanel != null && myClass.isInheritable()) { + addStep(OptionsStep) + } + super.addSteps() + } + + override fun doOKAction() { + myEqualsPanel?.let { updateHashCodeMemberInfos(it.table.selectedMemberInfos) } + super.doOKAction() + } + + fun getPropertiesForEquals() = myEqualsPanel?.table?.selectedMemberInfos?.map { it.member } ?: emptyList() + + fun getPropertiesForHashCode() = myHashCodePanel?.table?.selectedMemberInfos?.map { it.member } ?: emptyList() +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformUnresolvedProvider.kt.192 b/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformUnresolvedProvider.kt.192 new file mode 100644 index 00000000000..3e927a427be --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/PlatformUnresolvedProvider.kt.192 @@ -0,0 +1,60 @@ +/* + * Copyright 2010-2016 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.idea.inspections + +import com.intellij.codeInsight.daemon.HighlightDisplayKey +import com.intellij.codeInsight.daemon.QuickFixActionRegistrar +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider +import com.intellij.openapi.util.Condition +import com.intellij.openapi.util.TextRange +import com.intellij.psi.SmartPsiElementPointer +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.core.quickfix.QuickFixUtil +import org.jetbrains.kotlin.idea.quickfix.KotlinIntentionActionFactoryWithDelegate +import org.jetbrains.kotlin.idea.quickfix.QuickFixWithDelegateFactory +import org.jetbrains.kotlin.idea.quickfix.detectPriority +import org.jetbrains.kotlin.idea.references.KtSimpleNameReference +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import java.util.* + +object PlatformUnresolvedProvider : KotlinIntentionActionFactoryWithDelegate() { + override fun getElementOfInterest(diagnostic: Diagnostic) = QuickFixUtil.getParentElementOfType(diagnostic, KtNameReferenceExpression::class.java) + override fun extractFixData(element: KtNameReferenceExpression, diagnostic: Diagnostic) = element.getReferencedName() + + override fun createFixes(originalElementPointer: SmartPsiElementPointer, diagnostic: Diagnostic, quickFixDataFactory: () -> String?): List { + val result = ArrayList() + + originalElementPointer.element?.references?.filterIsInstance()?.firstOrNull()?.let { reference -> + UnresolvedReferenceQuickFixProvider.registerReferenceFixes(reference, object: QuickFixActionRegistrar { + override fun register(action: IntentionAction) { + result.add(QuickFixWithDelegateFactory(action.detectPriority()) { action }) + } + + override fun register(fixRange: TextRange, action: IntentionAction, key: HighlightDisplayKey?) { + register(action) + } + + override fun unregister(condition: Condition) { + } + }) + } + + return result + } + +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt.192 b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt.192 new file mode 100644 index 00000000000..92a7e66ac5a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceDereferenceUsage.kt.192 @@ -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.idea.slicer + +import com.intellij.psi.PsiElement +import com.intellij.slicer.SliceUsage +import com.intellij.usages.UsagePresentation +import com.intellij.util.Processor + +class KotlinSliceDereferenceUsage( + element: PsiElement, + parent: KotlinSliceUsage, + lambdaLevel: Int +) : KotlinSliceUsage(element, parent, lambdaLevel, false) { + override fun processChildren(processor: Processor) { + // no children + } + + override fun getPresentation() = object : UsagePresentation by super.getPresentation() { + override fun getTooltipText() = "Variable dereferenced" + } +}