Introduce Type Alias
#KT-12902 Fixed
This commit is contained in:
@@ -157,6 +157,12 @@
|
||||
<add-to-group group-id="IntroduceActionsGroup" anchor="after" relative-to-action="ExtractFunction"/>
|
||||
</action>
|
||||
|
||||
<action id="IntroduceTypeAlias" class="org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.IntroduceTypeAliasAction"
|
||||
text="Type _Alias...">
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="control alt shift A"/>
|
||||
<add-to-group group-id="IntroduceActionsGroup" anchor="after" relative-to-action="ExtractFunctionToScope"/>
|
||||
</action>
|
||||
|
||||
<!-- Kotlin Console REPL-->
|
||||
<action id="KotlinConsoleREPL" class="org.jetbrains.kotlin.console.actions.RunKotlinConsoleAction"
|
||||
text="Kotlin REPL"
|
||||
|
||||
@@ -6,6 +6,7 @@ introduce.property=Introduce Property
|
||||
introduce.parameter=Introduce Parameter
|
||||
cannot.refactor.no.container=Cannot refactor in this place
|
||||
cannot.refactor.no.expression=Cannot perform refactoring without an expression
|
||||
cannot.refactor.no.type=Cannot perform refactoring without a type
|
||||
cannot.refactor.syntax.errors=Cannot refactor due to erroneous code
|
||||
cannot.refactor.expression.has.unit.type=Cannot introduce expression of unit type
|
||||
cannot.refactor.package.expression=Cannot introduce package reference
|
||||
|
||||
+2
-2
@@ -24,11 +24,11 @@ import com.intellij.psi.PsiNameIdentifierOwner
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
|
||||
import org.jetbrains.kotlin.idea.codeInsight.KotlinFileReferencesResolver
|
||||
import org.jetbrains.kotlin.idea.core.compareDescriptors
|
||||
import org.jetbrains.kotlin.idea.refactoring.getContextForContainingDeclarationBody
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.ExtractableSubstringInfo
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.extractableSubstringInfo
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.substringContextOrThis
|
||||
@@ -114,7 +114,7 @@ data class ExtractionData(
|
||||
|
||||
val commonParent = PsiTreeUtil.findCommonParent(physicalElements) as KtElement
|
||||
|
||||
val bindingContext: BindingContext? by lazy { commonParent.getContextForContainingDeclarationBody() }
|
||||
val bindingContext: BindingContext? by lazy { commonParent.analyze() }
|
||||
|
||||
private val itFakeDeclaration by lazy { KtPsiFactory(originalFile).createParameter("it: Any?") }
|
||||
private val synthesizedInvokeDeclaration by lazy { KtPsiFactory(originalFile).createFunction("fun invoke() {}") }
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.refactoring.introduce.introduceTypeAlias
|
||||
|
||||
import com.intellij.lang.refactoring.RefactoringSupportProvider
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.refactoring.RefactoringActionHandler
|
||||
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
|
||||
import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively
|
||||
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.AbstractIntroduceAction
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.processDuplicates
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.ui.KotlinIntroduceTypeAliasDialog
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.selectElementsWithTargetSibling
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
|
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.KtTypeAlias
|
||||
import org.jetbrains.kotlin.psi.KtTypeElement
|
||||
|
||||
object KotlinIntroduceTypeAliasHandler : RefactoringActionHandler {
|
||||
@JvmField
|
||||
val REFACTORING_NAME = "Introduce Type Alias"
|
||||
|
||||
fun selectElements(editor: Editor, file: KtFile, continuation: (elements: List<PsiElement>, targetSibling: PsiElement) -> Unit) {
|
||||
selectElementsWithTargetSibling(
|
||||
REFACTORING_NAME,
|
||||
editor,
|
||||
file,
|
||||
"Select target code block",
|
||||
CodeInsightUtils.ElementKind.TYPE_ELEMENT,
|
||||
{ elements, parent -> parent.getExtractionContainers(strict = true, includeAll = true) },
|
||||
continuation
|
||||
)
|
||||
}
|
||||
|
||||
private fun runRefactoring(descriptor: IntroduceTypeAliasDescriptor, project: Project, editor: Editor) {
|
||||
val typeAlias = project.executeWriteCommand<KtTypeAlias>(REFACTORING_NAME) { descriptor.generateTypeAlias() }
|
||||
|
||||
val duplicateReplacers = findDuplicates(typeAlias)
|
||||
if (duplicateReplacers.isNotEmpty()) {
|
||||
processDuplicates(duplicateReplacers, project, editor)
|
||||
}
|
||||
}
|
||||
|
||||
fun doInvoke(
|
||||
project: Project,
|
||||
editor: Editor,
|
||||
elements: List<PsiElement>,
|
||||
targetSibling: PsiElement,
|
||||
descriptorSubstitutor: ((IntroduceTypeAliasDescriptor) -> IntroduceTypeAliasDescriptor)? = null
|
||||
) {
|
||||
val typeElement = elements.singleOrNull() as? KtTypeElement
|
||||
?: return showErrorHint(project, editor, "No type to refactor", REFACTORING_NAME)
|
||||
val introduceData = IntroduceTypeAliasData(typeElement, targetSibling)
|
||||
val analysisResult = introduceData.analyze()
|
||||
when (analysisResult) {
|
||||
is IntroduceTypeAliasAnalysisResult.Error -> {
|
||||
return showErrorHint(project, editor, analysisResult.message, REFACTORING_NAME)
|
||||
}
|
||||
|
||||
is IntroduceTypeAliasAnalysisResult.Success -> {
|
||||
val originalDescriptor = analysisResult.descriptor
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
val (descriptor, conflicts) = descriptorSubstitutor!!(originalDescriptor).validate()
|
||||
project.checkConflictsInteractively(conflicts) { runRefactoring(descriptor, project, editor) }
|
||||
}
|
||||
else {
|
||||
KotlinIntroduceTypeAliasDialog(project, originalDescriptor) { runRefactoring(it.currentDescriptor, project, editor) }.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
|
||||
if (file !is KtFile) return
|
||||
selectElements(editor, file) { elements, targetSibling -> doInvoke(project, editor, elements, targetSibling) }
|
||||
}
|
||||
|
||||
override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
|
||||
throw AssertionError("$REFACTORING_NAME can only be invoked from editor")
|
||||
}
|
||||
}
|
||||
|
||||
class IntroduceTypeAliasAction : AbstractIntroduceAction() {
|
||||
override fun getRefactoringHandler(provider: RefactoringSupportProvider) = KotlinIntroduceTypeAliasHandler
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.refactoring.introduce.introduceTypeAlias
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.util.containers.MultiMap
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
|
||||
class TypeReferenceInfo(val reference: KtTypeReference, val type: KotlinType)
|
||||
|
||||
internal var KtTypeReference.resolveInfo : TypeReferenceInfo? by CopyableUserDataProperty(Key.create("RESOLVE_INFO"))
|
||||
|
||||
class IntroduceTypeAliasData(
|
||||
val originalType: KtTypeElement,
|
||||
val targetSibling: PsiElement
|
||||
) : Disposable {
|
||||
val resolutionFacade = originalType.getResolutionFacade()
|
||||
val bindingContext = resolutionFacade.analyze(originalType, BodyResolveMode.PARTIAL)
|
||||
|
||||
init {
|
||||
markReferences()
|
||||
}
|
||||
|
||||
private fun markReferences() {
|
||||
val visitor = object : KtTreeVisitorVoid() {
|
||||
override fun visitTypeReference(typeReference: KtTypeReference) {
|
||||
val typeElement = typeReference.typeElement ?: return
|
||||
|
||||
val kotlinType = bindingContext[BindingContext.ABBREVIATED_TYPE, typeReference] ?:
|
||||
bindingContext[BindingContext.TYPE, typeReference] ?:
|
||||
return
|
||||
typeReference.resolveInfo = TypeReferenceInfo(typeReference, kotlinType)
|
||||
|
||||
typeElement.typeArgumentsAsTypes.forEach { it.accept(this) }
|
||||
}
|
||||
}
|
||||
(originalType.parent as? KtTypeReference ?: originalType).accept(visitor)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (!originalType.isValid) return
|
||||
originalType.forEachDescendantOfType<KtTypeReference> { it.resolveInfo = null }
|
||||
}
|
||||
}
|
||||
|
||||
data class TypeParameter(val name: String, val typeReferenceInfos: Collection<TypeReferenceInfo>)
|
||||
|
||||
data class IntroduceTypeAliasDescriptor(
|
||||
val originalData: IntroduceTypeAliasData,
|
||||
val name: String,
|
||||
val visibility: KtModifierKeywordToken?,
|
||||
val typeParameters: List<TypeParameter>
|
||||
)
|
||||
|
||||
data class IntroduceTypeAliasDescriptorWithConflicts(
|
||||
val descriptor: IntroduceTypeAliasDescriptor,
|
||||
val conflicts: MultiMap<PsiElement, String>
|
||||
)
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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.refactoring.introduce.introduceTypeAlias
|
||||
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.util.containers.LinkedMultiMap
|
||||
import com.intellij.util.containers.MultiMap
|
||||
import org.jetbrains.kotlin.descriptors.TypeAliasDescriptor
|
||||
import org.jetbrains.kotlin.idea.analysis.analyzeInContext
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
|
||||
import org.jetbrains.kotlin.idea.core.CollectingNameValidator
|
||||
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
|
||||
import org.jetbrains.kotlin.idea.core.compareDescriptors
|
||||
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
||||
import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange
|
||||
import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier
|
||||
import org.jetbrains.kotlin.idea.util.psi.patternMatching.UnifierParameter
|
||||
import org.jetbrains.kotlin.idea.util.psi.patternMatching.toRange
|
||||
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
|
||||
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.lexer.KtTokens.*
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
|
||||
import org.jetbrains.kotlin.utils.SmartList
|
||||
|
||||
sealed class IntroduceTypeAliasAnalysisResult {
|
||||
class Error(val message: String) : IntroduceTypeAliasAnalysisResult()
|
||||
class Success(val descriptor: IntroduceTypeAliasDescriptor) : IntroduceTypeAliasAnalysisResult()
|
||||
}
|
||||
|
||||
private fun IntroduceTypeAliasData.getTargetScope() = targetSibling.getResolutionScope(bindingContext, resolutionFacade)
|
||||
|
||||
fun IntroduceTypeAliasData.analyze(): IntroduceTypeAliasAnalysisResult {
|
||||
val psiFactory = KtPsiFactory(originalType)
|
||||
|
||||
val contextExpression = originalType.getStrictParentOfType<KtExpression>()!!
|
||||
val targetScope = getTargetScope()
|
||||
|
||||
val dummyVar = psiFactory.createProperty("val a: Int").apply {
|
||||
typeReference!!.replace(originalType.parent as? KtTypeReference ?: psiFactory.createType(originalType))
|
||||
}
|
||||
val newReferences = dummyVar.typeReference!!.collectDescendantsOfType<KtTypeReference> { it.resolveInfo != null }
|
||||
val newContext = dummyVar.analyzeInContext(targetScope, contextExpression)
|
||||
val project = originalType.project
|
||||
|
||||
val unifier = KotlinPsiUnifier.DEFAULT
|
||||
val groupedBrokenReferences = LinkedMultiMap<TypeReferenceInfo, TypeReferenceInfo>()
|
||||
for (newReference in newReferences) {
|
||||
val resolveInfo = newReference.resolveInfo!!
|
||||
|
||||
val originalDescriptor = resolveInfo.type.constructor.declarationDescriptor
|
||||
val newDescriptor = newContext[BindingContext.TYPE, newReference]?.constructor?.declarationDescriptor
|
||||
if (compareDescriptors(project, originalDescriptor, newDescriptor)) continue
|
||||
|
||||
val equivalenceRepresentative = groupedBrokenReferences
|
||||
.keySet()
|
||||
.firstOrNull { unifier.unify(it.reference, resolveInfo.reference).matched }
|
||||
if (equivalenceRepresentative != null) {
|
||||
groupedBrokenReferences.putValue(equivalenceRepresentative, resolveInfo)
|
||||
}
|
||||
else {
|
||||
groupedBrokenReferences.putValue(resolveInfo, resolveInfo)
|
||||
}
|
||||
|
||||
val brokenReferenceInfoIterator = groupedBrokenReferences.values().iterator()
|
||||
while (brokenReferenceInfoIterator.hasNext()) {
|
||||
val brokenReferenceInfo = brokenReferenceInfoIterator.next()
|
||||
if (resolveInfo.reference.isAncestor(brokenReferenceInfo.reference, true)) {
|
||||
brokenReferenceInfoIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val typeParameterNameValidator = CollectingNameValidator()
|
||||
val brokenReferences = groupedBrokenReferences.keySet().filter { groupedBrokenReferences[it].isNotEmpty() }
|
||||
val typeParameterNames = KotlinNameSuggester.suggestNamesForTypeParameters(brokenReferences.size, typeParameterNameValidator)
|
||||
val typeParameters = (typeParameterNames zip brokenReferences).map { TypeParameter(it.first, groupedBrokenReferences[it.second]) }
|
||||
|
||||
if (typeParameters.any { it.typeReferenceInfos.any { it.reference.typeElement == originalType } }) {
|
||||
return IntroduceTypeAliasAnalysisResult.Error("Type alias cannot refer to types which aren't accessible in the scope where it's defined")
|
||||
}
|
||||
|
||||
return IntroduceTypeAliasAnalysisResult.Success(IntroduceTypeAliasDescriptor(this, "", null, typeParameters))
|
||||
}
|
||||
|
||||
fun IntroduceTypeAliasData.getApplicableVisibilities(): List<KtModifierKeywordToken>{
|
||||
val parent = targetSibling.parent
|
||||
return when (parent) {
|
||||
is KtClassBody -> listOf(PRIVATE_KEYWORD, PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD)
|
||||
is KtFile -> listOf(PRIVATE_KEYWORD, PUBLIC_KEYWORD, INTERNAL_KEYWORD)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun IntroduceTypeAliasDescriptor.validate(): IntroduceTypeAliasDescriptorWithConflicts {
|
||||
val conflicts = MultiMap<PsiElement, String>()
|
||||
|
||||
val originalType = originalData.originalType
|
||||
if (name.isEmpty()) {
|
||||
conflicts.putValue(originalType, "No name provided for type alias")
|
||||
}
|
||||
else if (!KotlinNameSuggester.isIdentifier(name)) {
|
||||
conflicts.putValue(originalType, "Type alias name must be a valid identifier: $name")
|
||||
}
|
||||
else if (originalData.getTargetScope().findClassifier(Name.identifier(name), NoLookupLocation.FROM_IDE) != null) {
|
||||
conflicts.putValue(originalType, "Type $name already exists in the target scope")
|
||||
}
|
||||
|
||||
if (typeParameters.distinctBy { it.name }.size != typeParameters.size) {
|
||||
conflicts.putValue(originalType, "Type parameter names must be distinct")
|
||||
}
|
||||
|
||||
if (visibility != null && visibility !in originalData.getApplicableVisibilities()) {
|
||||
conflicts.putValue(originalType, "'$visibility' is not allowed in the target context")
|
||||
}
|
||||
|
||||
return IntroduceTypeAliasDescriptorWithConflicts(this, conflicts)
|
||||
}
|
||||
|
||||
fun findDuplicates(typeAlias: KtTypeAlias): Map<KotlinPsiRange, () -> Unit> {
|
||||
val aliasName = typeAlias.name ?: return emptyMap()
|
||||
val typeAliasDescriptor = typeAlias.resolveToDescriptor() as TypeAliasDescriptor
|
||||
|
||||
val unifierParameters = typeAliasDescriptor.declaredTypeParameters.map { UnifierParameter(it, null) }
|
||||
val unifier = KotlinPsiUnifier(unifierParameters)
|
||||
|
||||
val psiFactory = KtPsiFactory(typeAlias)
|
||||
|
||||
fun replaceOccurrence(occurrence: KtTypeElement, arguments: List<KtTypeElement>) {
|
||||
val typeText = if (arguments.isNotEmpty()) "$aliasName<${arguments.joinToString { it.text }}>" else aliasName
|
||||
occurrence.replace(psiFactory.createType(typeText).typeElement!!)
|
||||
}
|
||||
|
||||
val aliasRange = typeAlias.textRange
|
||||
return typeAlias
|
||||
.getTypeReference()
|
||||
?.typeElement
|
||||
.toRange()
|
||||
.match(typeAlias.parent, unifier)
|
||||
.asSequence()
|
||||
.filter { !(it.range.getTextRange().intersects(aliasRange)) }
|
||||
.mapNotNull { match ->
|
||||
val occurrence = match.range.elements.singleOrNull() as? KtTypeElement ?: return@mapNotNull null
|
||||
val arguments = unifierParameters.mapNotNull { (match.substitution[it] as? KtTypeReference)?.typeElement }
|
||||
if (arguments.size != unifierParameters.size) return@mapNotNull null
|
||||
match.range to { replaceOccurrence(occurrence, arguments) }
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private var KtTypeReference.typeParameterInfo : TypeParameter? by CopyableUserDataProperty(Key.create("TYPE_PARAMETER_INFO"))
|
||||
|
||||
fun IntroduceTypeAliasDescriptor.generateTypeAlias(previewOnly: Boolean = false): KtTypeAlias {
|
||||
val originalType = originalData.originalType
|
||||
val targetSibling = originalData.targetSibling
|
||||
val psiFactory = KtPsiFactory(originalType)
|
||||
|
||||
for (typeParameter in typeParameters)
|
||||
for (it in typeParameter.typeReferenceInfos) {
|
||||
it.reference.typeParameterInfo = typeParameter
|
||||
}
|
||||
|
||||
val typeAlias = psiFactory.createTypeAlias(name, typeParameters.map { it.name }, originalType)
|
||||
if (visibility != null && visibility != KtTokens.DEFAULT_VISIBILITY_KEYWORD) {
|
||||
typeAlias.addModifier(visibility)
|
||||
}
|
||||
|
||||
for (typeParameter in typeParameters)
|
||||
for (it in typeParameter.typeReferenceInfos) {
|
||||
it.reference.typeParameterInfo = null
|
||||
}
|
||||
|
||||
fun replaceUsage() {
|
||||
val aliasInstanceText = if (typeParameters.isNotEmpty()) {
|
||||
"$name<${typeParameters.joinToString { it.typeReferenceInfos.first().reference.text }}>"
|
||||
}
|
||||
else {
|
||||
name
|
||||
}
|
||||
originalType.replace(psiFactory.createType(aliasInstanceText).typeElement!!)
|
||||
}
|
||||
|
||||
fun introduceTypeParameters() {
|
||||
typeAlias.getTypeReference()!!.forEachDescendantOfType<KtTypeReference> {
|
||||
val typeParameter = it.typeParameterInfo ?: return@forEachDescendantOfType
|
||||
val typeParameterReference = psiFactory.createType(typeParameter.name)
|
||||
it.replace(typeParameterReference)
|
||||
}
|
||||
}
|
||||
|
||||
fun insertDeclaration(): KtTypeAlias {
|
||||
val targetParent = originalData.targetSibling.parent
|
||||
|
||||
val anchorCandidates = SmartList<PsiElement>()
|
||||
anchorCandidates.add(targetSibling)
|
||||
if (targetSibling is KtEnumEntry) {
|
||||
anchorCandidates.add(targetSibling.siblings().last { it is KtEnumEntry })
|
||||
}
|
||||
|
||||
val anchor = anchorCandidates.minBy { it.startOffset }!!.parentsWithSelf.first { it.parent == targetParent }
|
||||
val targetContainer = anchor.parent!!
|
||||
return (targetContainer.addBefore(typeAlias, anchor) as KtTypeAlias).apply {
|
||||
targetContainer.addBefore(psiFactory.createWhiteSpace("\n\n"), anchor)
|
||||
}
|
||||
}
|
||||
|
||||
return if (previewOnly) {
|
||||
introduceTypeParameters()
|
||||
typeAlias
|
||||
}
|
||||
else {
|
||||
replaceUsage()
|
||||
introduceTypeParameters()
|
||||
insertDeclaration()
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.refactoring.introduce.introduceTypeAlias.ui
|
||||
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.TypeParameter
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.ui.AbstractParameterTablePanel
|
||||
import java.util.*
|
||||
|
||||
open class IntroduceTypeAliasParameterTablePanel : AbstractParameterTablePanel<TypeParameter, IntroduceTypeAliasParameterTablePanel.TypeParameterInfo>() {
|
||||
class TypeParameterInfo(
|
||||
originalParameter: TypeParameter
|
||||
) : AbstractParameterTablePanel.AbstractParameterInfo<TypeParameter>(originalParameter) {
|
||||
init {
|
||||
name = originalParameter.name
|
||||
}
|
||||
|
||||
override fun toParameter() = originalParameter.copy(name)
|
||||
}
|
||||
|
||||
fun init(parameters: List<TypeParameter>) {
|
||||
parameterInfos = parameters.mapTo(ArrayList()) { TypeParameterInfo(it) }
|
||||
super.init()
|
||||
}
|
||||
|
||||
val selectedTypeParameterInfos: List<TypeParameterInfo>
|
||||
get() = parameterInfos.filter { it.isEnabled }
|
||||
|
||||
val selectedTypeParameters: List<TypeParameter>
|
||||
get() = selectedTypeParameterInfos.map { it.toParameter() }
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.ui.KotlinIntroduceTypeAliasDialog">
|
||||
<grid id="27dc6" binding="contentPane" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="522" height="396"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<grid id="34b30" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<grid id="fa2d1" layout-manager="BorderLayout" hgap="0" vgap="0">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<grid id="2aa47" class="com.intellij.ui.TitledSeparator" binding="inputParametersPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
|
||||
<constraints border-constraint="Center"/>
|
||||
<properties>
|
||||
<text value=""/>
|
||||
</properties>
|
||||
<border type="none" title="Parameters"/>
|
||||
<children/>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
<grid id="b45d1" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
|
||||
<preferred-size width="-1" height="200"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<grid id="4266d" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none" title="Signature Preview"/>
|
||||
<children>
|
||||
<component id="7722b" class="org.jetbrains.kotlin.idea.refactoring.introduce.ui.KotlinSignatureComponent" binding="signaturePreviewField" custom-create="true">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
|
||||
<minimum-size width="500" height="100"/>
|
||||
<preferred-size width="500" height="100"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text value=""/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
<grid id="5fe96" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<enabled value="true"/>
|
||||
<visible value="true"/>
|
||||
</properties>
|
||||
<border type="empty"/>
|
||||
<children>
|
||||
<component id="d1d09" class="javax.swing.JLabel" binding="aliasNameLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text value="&Name:"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="52aa7" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||
<preferred-size width="114" height="16"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties>
|
||||
<labelFor value="71ee7"/>
|
||||
<text value="&Visibility:"/>
|
||||
<visible value="true"/>
|
||||
</properties>
|
||||
</component>
|
||||
<grid id="d0f58" binding="aliasNamePanel" layout-manager="BorderLayout" hgap="0" vgap="0">
|
||||
<constraints>
|
||||
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
|
||||
<preferred-size width="150" height="-1"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children/>
|
||||
</grid>
|
||||
<component id="71ee7" class="javax.swing.JComboBox" binding="visibilityBox">
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="0" indent="0" use-parent-layout="false">
|
||||
<preferred-size width="114" height="26"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties>
|
||||
<visible value="true"/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
<vspacer id="cd10f">
|
||||
<constraints>
|
||||
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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.refactoring.introduce.introduceTypeAlias.ui;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.refactoring.ui.NameSuggestionsField;
|
||||
import com.intellij.ui.TitledSeparator;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function0;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.idea.KotlinFileType;
|
||||
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester;
|
||||
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt;
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.IntroduceTypeAliasDescriptor;
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.IntroduceTypeAliasImplKt;
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.KotlinIntroduceTypeAliasHandler;
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.TypeParameter;
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.ui.KotlinSignatureComponent;
|
||||
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken;
|
||||
import org.jetbrains.kotlin.lexer.KtTokens;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public class KotlinIntroduceTypeAliasDialog extends DialogWrapper {
|
||||
private JPanel contentPane;
|
||||
private TitledSeparator inputParametersPanel;
|
||||
private JComboBox visibilityBox;
|
||||
private KotlinSignatureComponent signaturePreviewField;
|
||||
private JPanel aliasNamePanel;
|
||||
private NameSuggestionsField aliasNameField;
|
||||
private JLabel aliasNameLabel;
|
||||
private IntroduceTypeAliasParameterTablePanel parameterTablePanel;
|
||||
|
||||
private final Project project;
|
||||
|
||||
private final IntroduceTypeAliasDescriptor originalDescriptor;
|
||||
private IntroduceTypeAliasDescriptor currentDescriptor;
|
||||
|
||||
private final Function1<KotlinIntroduceTypeAliasDialog, Unit> onAccept;
|
||||
|
||||
public KotlinIntroduceTypeAliasDialog(
|
||||
@NotNull Project project,
|
||||
@NotNull IntroduceTypeAliasDescriptor originalDescriptor,
|
||||
@NotNull Function1<KotlinIntroduceTypeAliasDialog, Unit> onAccept) {
|
||||
super(project, true);
|
||||
|
||||
this.project = project;
|
||||
this.originalDescriptor = originalDescriptor;
|
||||
this.currentDescriptor = originalDescriptor;
|
||||
this.onAccept = onAccept;
|
||||
|
||||
setModal(true);
|
||||
setTitle(KotlinIntroduceTypeAliasHandler.REFACTORING_NAME);
|
||||
init();
|
||||
update();
|
||||
}
|
||||
|
||||
private void createUIComponents() {
|
||||
this.signaturePreviewField = new KotlinSignatureComponent("", project);
|
||||
}
|
||||
|
||||
private boolean isVisibilitySectionAvailable() {
|
||||
return !getApplicableVisibilities().isEmpty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<KtModifierKeywordToken> getApplicableVisibilities() {
|
||||
return IntroduceTypeAliasImplKt.getApplicableVisibilities(originalDescriptor.getOriginalData());
|
||||
}
|
||||
|
||||
private String getAliasName() {
|
||||
return aliasNameField.getEnteredName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private KtModifierKeywordToken getVisibility() {
|
||||
if (!isVisibilitySectionAvailable()) return null;
|
||||
return (KtModifierKeywordToken) visibilityBox.getSelectedItem();
|
||||
}
|
||||
|
||||
private boolean checkNames() {
|
||||
if (!KotlinNameSuggester.INSTANCE.isIdentifier(getAliasName())) return false;
|
||||
if (parameterTablePanel != null) {
|
||||
for (IntroduceTypeAliasParameterTablePanel.TypeParameterInfo parameterInfo : parameterTablePanel.getSelectedTypeParameterInfos()) {
|
||||
if (!KotlinNameSuggester.INSTANCE.isIdentifier(parameterInfo.getName())) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
this.currentDescriptor = createDescriptor();
|
||||
|
||||
setOKActionEnabled(checkNames());
|
||||
signaturePreviewField.setText(IntroduceTypeAliasImplKt.generateTypeAlias(currentDescriptor, true).getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
|
||||
//noinspection unchecked
|
||||
visibilityBox.setModel(new DefaultComboBoxModel(getApplicableVisibilities().toArray()));
|
||||
//noinspection unchecked
|
||||
visibilityBox.setRenderer(
|
||||
new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(
|
||||
JList list,
|
||||
Object value,
|
||||
int index,
|
||||
boolean isSelected,
|
||||
boolean cellHasFocus
|
||||
) {
|
||||
String tokenValue = ((KtModifierKeywordToken) value).getValue();
|
||||
return super.getListCellRendererComponent(list, tokenValue, index, isSelected, cellHasFocus);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
aliasNameField = new NameSuggestionsField(ArrayUtil.EMPTY_STRING_ARRAY, project, KotlinFileType.INSTANCE);
|
||||
aliasNameField.addDataChangedListener(
|
||||
new NameSuggestionsField.DataChanged() {
|
||||
@Override
|
||||
public void dataChanged() {
|
||||
update();
|
||||
}
|
||||
}
|
||||
);
|
||||
aliasNamePanel.add(aliasNameField, BorderLayout.CENTER);
|
||||
aliasNameLabel.setLabelFor(aliasNameField);
|
||||
|
||||
boolean enableVisibility = isVisibilitySectionAvailable();
|
||||
visibilityBox.setEnabled(enableVisibility);
|
||||
if (enableVisibility) {
|
||||
KtModifierKeywordToken defaultVisibility = originalDescriptor.getVisibility();
|
||||
if (defaultVisibility == null) {
|
||||
defaultVisibility = KtTokens.PUBLIC_KEYWORD;
|
||||
}
|
||||
visibilityBox.setSelectedItem(defaultVisibility);
|
||||
}
|
||||
visibilityBox.addItemListener(
|
||||
new ItemListener() {
|
||||
@Override
|
||||
public void itemStateChanged(@NotNull ItemEvent e) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!originalDescriptor.getTypeParameters().isEmpty()) {
|
||||
parameterTablePanel = new IntroduceTypeAliasParameterTablePanel() {
|
||||
@Override
|
||||
protected void updateSignature() {
|
||||
KotlinIntroduceTypeAliasDialog.this.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnterAction() {
|
||||
doOKAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelAction() {
|
||||
doCancelAction();
|
||||
}
|
||||
};
|
||||
parameterTablePanel.init(originalDescriptor.getTypeParameters());
|
||||
|
||||
inputParametersPanel.setText("Type &Parameters");
|
||||
inputParametersPanel.setLabelFor(parameterTablePanel.getTable());
|
||||
inputParametersPanel.add(parameterTablePanel);
|
||||
}
|
||||
else {
|
||||
inputParametersPanel.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousMethodCalls")
|
||||
@Override
|
||||
protected void doOKAction() {
|
||||
MultiMap<PsiElement, String> conflicts = IntroduceTypeAliasImplKt.validate(currentDescriptor).getConflicts();
|
||||
KotlinRefactoringUtilKt.checkConflictsInteractively(
|
||||
project,
|
||||
conflicts,
|
||||
new Function0<Unit>() {
|
||||
@Override
|
||||
public Unit invoke() {
|
||||
close(OK_EXIT_CODE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
},
|
||||
new Function0<Unit>() {
|
||||
@Override
|
||||
public Unit invoke() {
|
||||
KotlinIntroduceTypeAliasDialog.super.doOKAction();
|
||||
return onAccept.invoke(KotlinIntroduceTypeAliasDialog.this);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getPreferredFocusedComponent() {
|
||||
return aliasNameField;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JComponent createCenterPanel() {
|
||||
return contentPane;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected JComponent createContentPane() {
|
||||
return contentPane;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private IntroduceTypeAliasDescriptor createDescriptor() {
|
||||
return originalDescriptor.copy(
|
||||
originalDescriptor.getOriginalData(),
|
||||
getAliasName(),
|
||||
getVisibility(),
|
||||
parameterTablePanel != null ? parameterTablePanel.getSelectedTypeParameters() : Collections.<TypeParameter>emptyList()
|
||||
);
|
||||
}
|
||||
|
||||
public IntroduceTypeAliasDescriptor getCurrentDescriptor() {
|
||||
return currentDescriptor;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,11 @@ fun selectElementsWithTargetParent(
|
||||
|
||||
val elements = CodeInsightUtils.findElements(file, startOffset, endOffset, elementKind)
|
||||
if (elements.isEmpty()) {
|
||||
showErrorHintByKey("cannot.refactor.no.expression")
|
||||
val messageKey = when (elementKind) {
|
||||
CodeInsightUtils.ElementKind.EXPRESSION -> "cannot.refactor.no.expression"
|
||||
CodeInsightUtils.ElementKind.TYPE_ELEMENT -> "cannot.refactor.no.type"
|
||||
}
|
||||
showErrorHintByKey(messageKey)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -212,4 +216,4 @@ fun KtExpression.mustBeParenthesizedInInitializerPosition(): Boolean {
|
||||
|
||||
if (left?.mustBeParenthesizedInInitializerPosition() ?: false) return true
|
||||
return PsiChildRange(left, operationReference).any { (it is PsiWhiteSpace) && it.textContains('\n') }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user