diff --git a/idea/performanceTests/org/jetbrains/kotlin/idea/perf/AbstractPerformanceJavaToKotlinCopyPasteConversionTest.kt b/idea/performanceTests/org/jetbrains/kotlin/idea/perf/AbstractPerformanceJavaToKotlinCopyPasteConversionTest.kt index 116de6e0ce9..b95c24ab433 100644 --- a/idea/performanceTests/org/jetbrains/kotlin/idea/perf/AbstractPerformanceJavaToKotlinCopyPasteConversionTest.kt +++ b/idea/performanceTests/org/jetbrains/kotlin/idea/perf/AbstractPerformanceJavaToKotlinCopyPasteConversionTest.kt @@ -9,6 +9,7 @@ import com.intellij.ide.highlighter.JavaFileType import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.application.runWriteAction import com.intellij.psi.PsiDocumentManager +import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.conversion.copy.AbstractJavaToKotlinCopyPasteConversionTest import org.jetbrains.kotlin.idea.conversion.copy.ConvertJavaCopyPasteProcessor @@ -118,6 +119,7 @@ abstract class AbstractPerformanceJavaToKotlinCopyPasteConversionTest(private va private fun perfTestCore() { myFixture.performEditorAction(IdeActions.ACTION_PASTE) + UIUtil.dispatchAllInvocationEvents() } private fun stats() = stats[j2kIndex()] diff --git a/idea/src/org/jetbrains/kotlin/idea/codeInsight/BasicKotlinReferenceTransferableData.kt b/idea/src/org/jetbrains/kotlin/idea/codeInsight/BasicKotlinReferenceTransferableData.kt new file mode 100644 index 00000000000..c9ae183e525 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/codeInsight/BasicKotlinReferenceTransferableData.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.codeInsight + +import com.intellij.codeInsight.editorActions.TextBlockTransferableData +import com.intellij.openapi.util.TextRange +import java.awt.datatransfer.DataFlavor +import java.io.Serializable + +class BasicKotlinReferenceTransferableData( + val sourceFileUrl: String, + val packageName: String, + val imports: List, + val sourceText: String, + val textRanges: List +) : TextBlockTransferableData, Cloneable, Serializable { + override fun getFlavor() = dataFlavor + override fun getOffsetCount() = 0 + + override fun getOffsets(offsets: IntArray?, index: Int) = index + override fun setOffsets(offsets: IntArray?, index: Int) = index + + companion object { + val dataFlavor: DataFlavor? by lazy { + try { + val dataClass = KotlinReferenceData::class.java + DataFlavor( + DataFlavor.javaJVMLocalObjectMimeType + ";class=" + dataClass.name, + "BasicKotlinReferenceTransferableData", + dataClass.classLoader + ) + } catch (e: NoClassDefFoundError) { + null + } catch (e: IllegalArgumentException) { + null + } + } + } + + public override fun clone(): BasicKotlinReferenceTransferableData { + try { + return super.clone() as BasicKotlinReferenceTransferableData + } catch (e: CloneNotSupportedException) { + throw RuntimeException() + } + } + +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinCopyPasteReferenceProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinCopyPasteReferenceProcessor.kt index 6bd2dda6444..6b89a7d808f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinCopyPasteReferenceProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinCopyPasteReferenceProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 JetBrains s.r.o. + * Copyright 2010-2020 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. @@ -19,21 +19,25 @@ package org.jetbrains.kotlin.idea.codeInsight import com.intellij.codeInsight.CodeInsightSettings import com.intellij.codeInsight.editorActions.CopyPastePostProcessor import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.ReadAction.nonBlocking import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.util.Ref import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.psi.* import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.concurrency.AppExecutorUtil import org.jetbrains.annotations.TestOnly -import org.jetbrains.kotlin.descriptors.CallableDescriptor -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.idea.caches.resolve.allowResolveInDispatchThread import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference @@ -45,19 +49,19 @@ import org.jetbrains.kotlin.idea.imports.importableFqName import org.jetbrains.kotlin.idea.kdoc.KDocReference import org.jetbrains.kotlin.idea.references.* import org.jetbrains.kotlin.idea.util.ImportInsertHelper -import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.ProgressIndicatorUtils +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.idea.util.getFileResolutionScope +import org.jetbrains.kotlin.idea.util.getSourceRoot import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.kdoc.psi.api.KDocElement import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType -import org.jetbrains.kotlin.psi.psiUtil.elementsInRange -import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType -import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf +import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.scopes.LexicalScope import org.jetbrains.kotlin.resolve.scopes.utils.findFunction @@ -69,21 +73,15 @@ import java.awt.datatransfer.Transferable import java.awt.datatransfer.UnsupportedFlavorException import java.io.IOException import java.util.* +import java.util.function.Consumer -//NOTE: this class is based on CopyPasteReferenceProcessor and JavaCopyPasteReferenceProcessor -class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor() { - private val LOG = Logger.getInstance(KotlinCopyPasteReferenceProcessor::class.java) +class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor() { - private val IGNORE_REFERENCES_INSIDE: Array> = arrayOf( - KtImportList::class.java, - KtPackageDirective::class.java - ) - - override fun extractTransferableData(content: Transferable): List { + override fun extractTransferableData(content: Transferable): List { if (CodeInsightSettings.getInstance().ADD_IMPORTS_ON_PASTE != CodeInsightSettings.NO) { try { val flavor = KotlinReferenceData.dataFlavor ?: return listOf() - val data = content.getTransferData(flavor) as? KotlinReferenceTransferableData ?: return listOf() + val data = content.getTransferData(flavor) as? BasicKotlinReferenceTransferableData ?: return listOf() // copy to prevent changing of original by convertLineSeparators return listOf(data.clone()) } catch (ignored: UnsupportedFlavorException) { @@ -99,30 +97,56 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor { + ): List { if (file !is KtFile || DumbService.getInstance(file.getProject()).isDumb) return listOf() - val collectedData = try { - collectReferenceData(file, startOffsets, endOffsets) - } catch (e: ProcessCanceledException) { - // supposedly analysis can only be canceled from another thread - // do not log ProcessCanceledException as it is rethrown by IdeaLogger and code won't be copied - LOG.debug("ProcessCanceledException while analyzing references in ${file.getName()}. References can't be processed.") - return listOf() - } catch (e: Throwable) { - LOG.error("Exception in processing references for copy paste in file ${file.getName()}}", e) - return listOf() + check(startOffsets.size == endOffsets.size) { + "startOffsets ${startOffsets.contentToString()} has to have same size as endOffsets ${endOffsets.contentToString()}" } - if (collectedData.isEmpty()) return listOf() + val packageName = file.packageDirective?.fqName?.asString() ?: "" + val imports = file.importDirectives.map { it.text } + val endOfImportsOffset = file.importDirectives.map { it.endOffset }.max() ?: file.packageDirective?.endOffset ?: 0 + val offsetDelta = if (startOffsets.any { it <= endOfImportsOffset }) 0 else endOfImportsOffset + val text = file.text.substring(offsetDelta) + val ranges = startOffsets.indices.map { + TextRange(startOffsets[it] - offsetDelta, endOffsets[it] - offsetDelta) + } - return listOf(KotlinReferenceTransferableData(collectedData.toTypedArray())) + return listOf( + BasicKotlinReferenceTransferableData( + sourceFileUrl = file.virtualFile.url, + packageName = packageName, + imports = imports, + sourceText = text, + textRanges = ranges + ) + ) } + private fun collectReferenceData( + textRanges: List, + file: KtFile, + targetFile: KtFile, + fakePackageName: String, + sourcePackageName: String + ): List = + collectReferenceData( + file, + textRanges.map { it.startOffset }.toIntArray(), + textRanges.map { it.endOffset }.toIntArray(), + fakePackageName, + sourcePackageName, + targetFile.packageDirective?.fqName?.asString() ?: "" + ) + fun collectReferenceData( file: KtFile, startOffsets: IntArray, - endOffsets: IntArray + endOffsets: IntArray, + fakePackageName: String? = null, + sourcePackageName: String? = null, + targetPackageName: String? = null ): List { val ranges = toTextRanges(startOffsets, endOffsets) val elementsByRange = ranges.associateBy({ it }, { textRange -> @@ -130,60 +154,90 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor() } + // TODO: allowResolveInDispatchThread could be dropped as soon as + // ConvertJavaCopyPasteProcessor will perform it on non UI thread val bindingContext = allowResolveInDispatchThread { file.getResolutionFacade().analyze(allElementsToResolve, BodyResolveMode.PARTIAL) } - val result = ArrayList() + val result = mutableListOf() for ((range, elements) in elementsByRange) { - for (element in elements) { - result.addReferenceDataInsideElement(element, file, range.start, startOffsets, endOffsets, bindingContext) + elements.forEachDescendant { ktElement -> + result.addReferenceDataInsideElement( + ktElement, file, range.start, ranges, bindingContext, + fakePackageName = fakePackageName, sourcePackageName = sourcePackageName, targetPackageName = targetPackageName + ) } } return result } - private fun MutableCollection.addReferenceDataInsideElement( - element: PsiElement, - file: KtFile, - startOffset: Int, - startOffsets: IntArray, - endOffsets: IntArray, - bindingContext: BindingContext - ) { - if (PsiTreeUtil.getNonStrictParentOfType(element, *IGNORE_REFERENCES_INSIDE) != null) return + private fun List.forEachDescendant(consumer: (KtElement) -> Unit) { + this.forEach { it.forEachDescendant(consumer) } + } - element.forEachDescendantOfType(canGoInside = { it::class.java as Class<*> !in IGNORE_REFERENCES_INSIDE }) { ktElement -> - val reference = ktElement.mainReference ?: return@forEachDescendantOfType + private fun PsiElement.forEachDescendant(consumer: (KtElement) -> Unit) { + if (PsiTreeUtil.getNonStrictParentOfType(this, *IGNORE_REFERENCES_INSIDE) != null) return - val descriptors = resolveReference(reference, bindingContext) - //check whether this reference is unambiguous - if (reference !is KtMultiReference<*> && descriptors.size > 1) return@forEachDescendantOfType - - for (descriptor in descriptors) { - val effectiveReferencedDescriptors = DescriptorToSourceUtils.getEffectiveReferencedDescriptors(descriptor).asSequence() - val declaration = effectiveReferencedDescriptors - .map { DescriptorToSourceUtils.getSourceFromDescriptor(it) } - .singleOrNull() - if (declaration != null && declaration.isInCopiedArea(file, startOffsets, endOffsets)) continue - - if (!reference.canBeResolvedViaImport(descriptor, bindingContext)) continue - - val fqName = descriptor.importableFqName ?: continue - val kind = KotlinReferenceData.Kind.fromDescriptor(descriptor) ?: continue - val isQualifiable = KotlinReferenceData.isQualifiable(ktElement, descriptor) - val relativeStart = ktElement.range.start - startOffset - val relativeEnd = ktElement.range.end - startOffset - add(KotlinReferenceData(relativeStart, relativeEnd, fqName.asString(), isQualifiable, kind)) - } + this.forEachDescendantOfType(canGoInside = { + it::class.java as Class<*> !in IGNORE_REFERENCES_INSIDE + }) { ktElement -> + consumer(ktElement) } } - private data class ReferenceToRestoreData( - val reference: KtReference, - val refData: KotlinReferenceData - ) + private fun MutableCollection.addReferenceDataInsideElement( + ktElement: KtElement, + file: KtFile, + startOffset: Int, + textRanges: List, + bindingContext: BindingContext, + fakePackageName: String? = null, + sourcePackageName: String? = null, + targetPackageName: String? = null + ) { + val reference = ktElement.mainReference ?: return + + val descriptors = resolveReference(reference, bindingContext) + //check whether this reference is unambiguous + if (reference !is KtMultiReference<*> && descriptors.size > 1) return + + for (descriptor in descriptors) { + val effectiveReferencedDescriptors = + DescriptorToSourceUtils.getEffectiveReferencedDescriptors(descriptor).asSequence() + val declaration = effectiveReferencedDescriptors + .map { DescriptorToSourceUtils.getSourceFromDescriptor(it) } + .singleOrNull() + + if (declaration?.isInCopiedArea(file, textRanges) == true || + !reference.canBeResolvedViaImport(descriptor, bindingContext) + ) continue + + val importableFqName = descriptor.importableFqName ?: continue + val importableName = importableFqName.asString() + val pkgName = descriptor.findPackageFqNameSafe()?.asString() ?: "" + val importableShortName = importableFqName.shortName().asString() + + val fqName = if (fakePackageName == pkgName) { + // It is possible to resolve unnecessary references from a target package (as we resolve it from a fake package) + if (sourcePackageName == targetPackageName && importableName == "$fakePackageName.$importableShortName") { + continue + } + // roll back to original package name when we faced faked pkg name + sourcePackageName + importableName.substring(fakePackageName.length) + } else importableName + + val kind = KotlinReferenceData.Kind.fromDescriptor(descriptor) ?: continue + val isQualifiable = KotlinReferenceData.isQualifiable(ktElement, descriptor) + val relativeStart = ktElement.range.start - startOffset + val relativeEnd = ktElement.range.end - startOffset + add(KotlinReferenceData(relativeStart, relativeEnd, fqName, isQualifiable, kind)) + } + } + + private data class ReferenceToRestoreData(val reference: KtReference, val refData: KotlinReferenceData) + private data class PsiElementByTextRange(val textRange: TextRange, val element: PsiElement) override fun processTransferableData( project: Project, @@ -191,29 +245,237 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor, - values: List + values: List ) { - if (DumbService.getInstance(project).isDumb) return + if (DumbService.getInstance(project).isDumb || + CodeInsightSettings.getInstance().ADD_IMPORTS_ON_PASTE == CodeInsightSettings.NO + ) return val document = editor.document val file = PsiDocumentManager.getInstance(project).getPsiFile(document) if (file !is KtFile) return - val referenceData = values.single().data + processReferenceData(project, file, bounds.startOffset, values.single()) + } - processReferenceData(project, file, bounds.startOffset, referenceData) + private fun processReferenceData( + project: Project, + file: KtFile, + blockStart: Int, + transferableData: BasicKotlinReferenceTransferableData + ) { + PsiDocumentManager.getInstance(project).commitAllDocuments() + + // figure out candidate elements in UI thread in paste phase + // as psi elements could be changed later on - relative offsets are used for mapping purposes + val elementsByRange = transferableData.textRanges.flatMap { originalTextRange -> + val textRange = TextRange(blockStart, blockStart + originalTextRange.endOffset - originalTextRange.startOffset) + file.elementsInRange(textRange) + .filter { it is KtElement || it is KDocElement } + .map { PsiElementByTextRange(it.textRange, it) } + } + + processReferenceData(project, file) { indicator: ProgressIndicator -> + findReferenceDataToRestore( + file, + blockStart, + indicator, + elementsByRange, + transferableData + ) + } } fun processReferenceData(project: Project, file: KtFile, blockStart: Int, referenceData: Array) { - PsiDocumentManager.getInstance(project).commitAllDocuments() + processReferenceData(project, file) { indicator: ProgressIndicator -> + findReferencesToRestore(file, blockStart, referenceData) + } + } - val referencesPossibleToRestore = findReferencesToRestore(file, blockStart, referenceData) + private fun processReferenceData( + project: Project, + file: KtFile, + findReferenceProvider: (indicator: ProgressIndicator) -> List + ) { + val task: Task.Backgroundable = object : Task.Backgroundable(project, "Resolve pasted references", true) { + override fun run(indicator: ProgressIndicator) { + assert(!ApplicationManager.getApplication().isWriteAccessAllowed) { + "Resolving references on dispatch thread leads to live lock" + } + ProgressIndicatorUtils.awaitWithCheckCanceled( + nonBlocking> { + return@nonBlocking findReferenceProvider(indicator) + } + .finishOnUiThread( + ModalityState.defaultModalityState(), + Consumer> { referencesPossibleToRestore -> + val selectedReferencesToRestore = + showRestoreReferencesDialog(project, referencesPossibleToRestore) + if (selectedReferencesToRestore.isEmpty()) return@Consumer - val selectedReferencesToRestore = showRestoreReferencesDialog(project, referencesPossibleToRestore) - if (selectedReferencesToRestore.isEmpty()) return + project.executeWriteCommand("resolve pasted references") { + restoreReferences(selectedReferencesToRestore, file) + } + } + ) + .withDocumentsCommitted(project) + .submit(AppExecutorUtil.getAppExecutorService()) + ) + } + } + ProgressManager.getInstance().run(task) + } - runWriteAction { - restoreReferences(selectedReferencesToRestore, file) + private fun findReferenceDataToRestore( + file: PsiFile, + blockStart: Int, + indicator: ProgressIndicator, + elementsByRange: List, + transferableData: BasicKotlinReferenceTransferableData + ): List { + if (file !is KtFile) return emptyList() + + val referencesByRange = elementsByRange.flatMap { elementByTextRange -> + val element = elementByTextRange.element + if (!element.isValid) return@flatMap emptyList>() + val refElementsRanges = mutableListOf>() + val offsetDelta = element.startOffset - elementByTextRange.textRange.startOffset + element.forEachDescendant { ktElement -> + indicator.checkCanceled() + + ktElement.mainReference?.let { ref -> + val textRange = ref.element.textRange + findReference(file, textRange)?.let { + // remap reference to the original (as it was on paste phase) text range + val range = if (offsetDelta == 0) textRange + else + TextRange( + textRange.startOffset - offsetDelta, + textRange.endOffset - offsetDelta + ) + refElementsRanges.add(range to it) + } + } + } + refElementsRanges + }.toMap() + + val sourcePackageName = transferableData.packageName + val imports: List = transferableData.imports + + // Step 0. Recreate original source file (i.e. source file as it was on copy action) and resolve references from it + val ctxFile = sourceFile(file.project, transferableData) ?: file + + // put original source file to some fake package to avoid ambiguous resolution ( a real file VS a virtual file ) + val fakePackageName = "__kotlin.__some.__funky.__package" + + val dummyOrigFileProlog = + """ + package $fakePackageName + + ${buildDummySourceScope(sourcePackageName, imports, fakePackageName, file, transferableData, ctxFile)} + + """.trimIndent() + + val dummyOriginalFile = KtPsiFactory(file.project) + .createAnalyzableFile( + "dummy-original.kt", + "$dummyOrigFileProlog${transferableData.sourceText}", + ctxFile + ) + + val offsetDelta = dummyOrigFileProlog.length + + val dummyOriginalFileTextRanges = + // it is required as it is shifted by dummy prolog + transferableData.textRanges.map { TextRange(it.startOffset + offsetDelta, it.endOffset + offsetDelta) } + + // Step 1. Find references in copied blocks of (recreated) source file + val sourceFileBasedReferences = + collectReferenceData(dummyOriginalFileTextRanges, dummyOriginalFile, file, fakePackageName, sourcePackageName) + + indicator.checkCanceled() + + // Step 2. Find references to restore in a target file + return findReferencesToRestore(file, blockStart, sourceFileBasedReferences, referencesByRange) + } + + private fun buildDummySourceScope( + sourcePkgName: String, + imports: List, + fakePkgName: String, + file: KtFile, + transferableData: BasicKotlinReferenceTransferableData, + ctxFile: KtFile + ): String { + // it could be that copied block contains inner classes or enums + // to solve this problem a special file is build: + // it contains imports from source package replaced with a fake package prefix + // + // result scope has to contain + // - those fake package imports those are successfully resolved (i.e. present in copied block) + // - those source package imports those are not present in a fake package + // - all rest imports + + val sourceImportPrefix = "import $sourcePkgName" + val fakeImportPrefix = "import $fakePkgName" + + val affectedSourcePkgImports = imports.filter { it.startsWith(sourceImportPrefix) } + val fakePkgImports = affectedSourcePkgImports.map { fakeImportPrefix + it.substring(sourceImportPrefix.length) } + + fun joinLines(items: Collection) = items.joinToString("\n") + + val dummyImportsFile = KtPsiFactory(file.project) + .createAnalyzableFile( + "dummy-imports.kt", + "package $fakePkgName\n" + + "${joinLines(fakePkgImports)}\n" + + transferableData.sourceText, + ctxFile + ) + + val dummyFileImports = dummyImportsFile.collectDescendantsOfType().mapNotNull { directive -> + val importedReference = + directive.importedReference?.getQualifiedElementSelector()?.mainReference?.resolve() as? KtNamedDeclaration + importedReference?.let { directive.text } + } + + val dummyFileImportsSet = dummyFileImports.toSet() + val filteredImports = imports.filter { + !it.startsWith(sourceImportPrefix) || + !dummyFileImportsSet.contains(fakeImportPrefix + it.substring(sourceImportPrefix.length)) + } + + return """ + ${joinLines(dummyFileImports)} + import ${sourcePkgName}.* + ${joinLines(filteredImports)} + """ + } + + private fun sourceFile(project: Project, transferableData: BasicKotlinReferenceTransferableData): KtFile? { + val sourceFile = VirtualFileManager.getInstance().findFileByUrl(transferableData.sourceFileUrl) ?: return null + if (sourceFile.getSourceRoot(project) == null) return null + + return PsiManager.getInstance(project).findFile(sourceFile) as? KtFile + } + + private fun filterReferenceData( + refData: KotlinReferenceData, + fileResolutionScope: LexicalScope + ): Boolean { + if (refData.isQualifiable) return true + + val originalFqName = FqName(refData.fqName) + val name = originalFqName.shortName() + return when (refData.kind) { + // filter if function is already imported + KotlinReferenceData.Kind.FUNCTION -> fileResolutionScope + .findFunction(name, NoLookupLocation.FROM_IDE) { it.importableFqName == originalFqName } == null + // filter if property is already imported + KotlinReferenceData.Kind.PROPERTY -> fileResolutionScope + .findVariable(name, NoLookupLocation.FROM_IDE) { it.importableFqName == originalFqName } == null + else -> true } } @@ -221,33 +483,52 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor - ): List { - if (file !is KtFile) return listOf() + ): List = findReferences(file, referenceData.map { it to findReference(it, file as KtFile, blockStart) }) + + private fun findReferencesToRestore( + file: PsiFile, + blockStart: Int, + referenceData: List, + referencesByPosition: Map + ): List = + // use already found reference candidates - so file could be changed + findReferences(file, referenceData + .map { + val textRange = TextRange(it.startOffset + blockStart, it.endOffset + blockStart) + val reference = referencesByPosition[textRange] + it to if (reference?.element?.isValid == true) reference else null + }) + + private fun findReferences( + file: PsiFile, + references: List> + ): List { + val ktFile = file as? KtFile ?: return listOf() - val references = referenceData.map { it to findReference(it, file, blockStart) } val bindingContext = try { - file.getResolutionFacade().analyze(references.mapNotNull { it.second?.element }, BodyResolveMode.PARTIAL) + ktFile.getResolutionFacade().analyze(references.mapNotNull { it.second?.element }, BodyResolveMode.PARTIAL) } catch (e: Throwable) { if (e is ControlFlowException) throw e LOG.error("Failed to analyze references after copy paste", e) return emptyList() } - val fileResolutionScope = file.getResolutionFacade().getFileResolutionScope(file) + val fileResolutionScope = ktFile.getResolutionFacade().getFileResolutionScope(ktFile) return references.mapNotNull { pair -> val data = pair.first val reference = pair.second - if (reference != null) - createReferenceToRestoreData(reference, data, file, fileResolutionScope, bindingContext) - else - null + reference?.let { createReferenceToRestoreData(it, data, ktFile, fileResolutionScope, bindingContext) } } } - private fun findReference(data: KotlinReferenceData, file: KtFile, blockStart: Int): KtReference? { - val startOffset = data.startOffset + blockStart - val endOffset = data.endOffset + blockStart - val element = file.findElementAt(startOffset) ?: return null - val desiredRange = TextRange(startOffset, endOffset) + private fun findReference(data: KotlinReferenceData, file: KtFile, blockStart: Int): KtReference? = + findReference(file, TextRange(data.startOffset + blockStart, data.endOffset + blockStart)) + + private fun findReference(file: KtFile, desiredRange: TextRange): KtReference? { + val element = file.findElementAt(desiredRange.startOffset) ?: return null + return findReference(element, desiredRange) + } + + private fun findReference(element: PsiElement, desiredRange: TextRange): KtReference? { for (current in element.parentsWithSelf) { val range = current.range if (current is KtElement && range == desiredRange) { @@ -265,21 +546,9 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor { + private fun resolveReference(reference: KtReference, bindingContext: BindingContext): List { val element = reference.element if (element is KtNameReferenceExpression && reference is KtSimpleNameReference) { - bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element] - ?.let { return listOf(it) } + val classifierDescriptor = + bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element] + (classifierDescriptor ?: bindingContext[BindingContext.REFERENCE_TARGET, element])?.let { return listOf(it) } } - return reference.resolveToDescriptors(bindingContext) + return reference.resolveToDescriptors(bindingContext).toList() } private fun restoreReferences(referencesToRestore: Collection, file: KtFile) { @@ -318,11 +588,13 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor() for ((reference, refData) in referencesToRestore) { + if (!reference.element.isValid) continue val fqName = FqName(refData.fqName) if (refData.isQualifiable) { if (reference is KtSimpleNameReference) { - val pointer = smartPointerManager.createSmartPsiElementPointer(reference.element, file) + val pointer = + smartPointerManager.createSmartPsiElementPointer(reference.element, file) bindingRequests.add(BindingRequest(pointer, fqName)) } else if (reference is KDocReference) { descriptorsToImport.addAll(findImportableDescriptors(fqName, file)) @@ -351,11 +623,19 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor this.fqNameOrNull() + containingDeclaration == null -> this.fqNameOrNull() + else -> this.containingDeclaration!!.findPackageFqNameSafe() + } + } + private fun showRestoreReferencesDialog( project: Project, referencesToRestore: List ): Collection { - val fqNames = referencesToRestore.asSequence().map { it.refData.fqName }.toSortedSet() + val fqNames = referencesToRestore.map { it.refData.fqName }.toSortedSet() if (ApplicationManager.getApplication().isUnitTestMode) { declarationsToImportSuggested = fqNames @@ -378,13 +658,21 @@ class KotlinCopyPasteReferenceProcessor : CopyPastePostProcessor): Boolean { if (containingFile != fileCopiedFrom) return false - return toTextRanges(startOffsets, endOffsets).any { this.range in it } + return textRanges.any { this.range in it } } companion object { @get:TestOnly var declarationsToImportSuggested: Collection = emptyList() + + private val LOG = Logger.getInstance(KotlinCopyPasteReferenceProcessor::class.java) + + private val IGNORE_REFERENCES_INSIDE: Array> = arrayOf( + KtImportList::class.java, + KtPackageDirective::class.java + ) } + } diff --git a/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceTransferableData.kt b/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceData.kt similarity index 78% rename from idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceTransferableData.kt rename to idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceData.kt index e08103c80e6..00b672a992b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceTransferableData.kt +++ b/idea/src/org/jetbrains/kotlin/idea/codeInsight/KotlinReferenceData.kt @@ -16,7 +16,6 @@ package org.jetbrains.kotlin.idea.codeInsight -import com.intellij.codeInsight.editorActions.TextBlockTransferableData import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.psi.KtCallableReferenceExpression @@ -30,36 +29,7 @@ import org.jetbrains.kotlin.types.expressions.DoubleColonLHS import java.awt.datatransfer.DataFlavor import java.io.Serializable -class KotlinReferenceTransferableData( - val data: Array -) : TextBlockTransferableData, Cloneable, Serializable { - - override fun getFlavor() = KotlinReferenceData.dataFlavor - - override fun getOffsetCount() = data.size * 2 - - override fun getOffsets(offsets: IntArray, index: Int): Int { - var i = index - for (d in data) { - offsets[i++] = d.startOffset - offsets[i++] = d.endOffset - } - return i - } - - override fun setOffsets(offsets: IntArray, index: Int): Int { - var i = index - for (d in data) { - d.startOffset = offsets[i++] - d.endOffset = offsets[i++] - } - return i - } - - public override fun clone() = KotlinReferenceTransferableData(Array(data.size) { data[it].clone() }) -} - -class KotlinReferenceData( +data class KotlinReferenceData( var startOffset: Int, var endOffset: Int, val fqName: String, diff --git a/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt b/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt new file mode 100644 index 00000000000..13cf0e40aa7 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.util + +import java.util.concurrent.Future + +object ProgressIndicatorUtils { + @JvmStatic + fun awaitWithCheckCanceled(future: Future) = + com.intellij.openapi.progress.util.BackgroundTaskUtil.awaitWithCheckCanceled(future) +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt.193 b/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt.193 new file mode 100644 index 00000000000..7140fba5442 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/util/ProgressIndicatorUtils.kt.193 @@ -0,0 +1,14 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.util + +import java.util.concurrent.Future + +object ProgressIndicatorUtils { + @JvmStatic + fun awaitWithCheckCanceled(future: Future) = + com.intellij.openapi.progress.util.ProgressIndicatorUtils.awaitWithCheckCanceled(future) +} \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/EntireFile.dependency.kt b/idea/testData/copyPaste/imports/EntireFile.dependency.kt new file mode 100644 index 00000000000..c35e97d0c91 --- /dev/null +++ b/idea/testData/copyPaste/imports/EntireFile.dependency.kt @@ -0,0 +1,3 @@ +package foo + +class Bar(val v: Int){} \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/EntireFile.expected.kt b/idea/testData/copyPaste/imports/EntireFile.expected.kt new file mode 100644 index 00000000000..ad51978e4b3 --- /dev/null +++ b/idea/testData/copyPaste/imports/EntireFile.expected.kt @@ -0,0 +1,20 @@ +package to + +import d.A + +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package d + +/* + * Some comment in front of imports + */ +import foo.Bar + +/** + * Some kdoc for class Foo + */ +class Foo(val a: A) { +} diff --git a/idea/testData/copyPaste/imports/EntireFile.expected.names b/idea/testData/copyPaste/imports/EntireFile.expected.names new file mode 100644 index 00000000000..ecaf3d61381 --- /dev/null +++ b/idea/testData/copyPaste/imports/EntireFile.expected.names @@ -0,0 +1 @@ +d.A diff --git a/idea/testData/copyPaste/imports/EntireFile.kt b/idea/testData/copyPaste/imports/EntireFile.kt new file mode 100644 index 00000000000..f689537e523 --- /dev/null +++ b/idea/testData/copyPaste/imports/EntireFile.kt @@ -0,0 +1,19 @@ +// WITH_LIBRARY: copyPaste/imports/KotlinLibrary + +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package d + +/* + * Some comment in front of imports + */ +import foo.Bar + +/** + * Some kdoc for class Foo + */ +class Foo(val a: A) { +} + \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/EntireFile.to.kt b/idea/testData/copyPaste/imports/EntireFile.to.kt new file mode 100644 index 00000000000..56cb67d7c89 --- /dev/null +++ b/idea/testData/copyPaste/imports/EntireFile.to.kt @@ -0,0 +1,2 @@ +package to + \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.dependency.kt b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.dependency.kt new file mode 100644 index 00000000000..c35e97d0c91 --- /dev/null +++ b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.dependency.kt @@ -0,0 +1,3 @@ +package foo + +class Bar(val v: Int){} \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.kt b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.kt new file mode 100644 index 00000000000..216f97c6752 --- /dev/null +++ b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.kt @@ -0,0 +1,15 @@ +package to + +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import d.A +import foo.Bar + +/** + * Some kdoc for class Foo + */ +class Foo(val a: A) { +} diff --git a/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.names b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.names new file mode 100644 index 00000000000..07b9961ed13 --- /dev/null +++ b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.expected.names @@ -0,0 +1 @@ +d.A \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.kt b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.kt new file mode 100644 index 00000000000..d31849ddf10 --- /dev/null +++ b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.kt @@ -0,0 +1,16 @@ +// WITH_LIBRARY: copyPaste/imports/KotlinLibrary +package d + +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import foo.Bar + +/** + * Some kdoc for class Foo + */ +class Foo(val a: A) { +} + \ No newline at end of file diff --git a/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.to.kt b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.to.kt new file mode 100644 index 00000000000..56cb67d7c89 --- /dev/null +++ b/idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.to.kt @@ -0,0 +1,2 @@ +package to + \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInsertImportOnPasteTest.kt b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInsertImportOnPasteTest.kt index db730bbcb4c..1cc82bea0c3 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInsertImportOnPasteTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInsertImportOnPasteTest.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.codeInsight import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.AbstractCopyPasteTest import org.jetbrains.kotlin.idea.test.dumpTextWithErrors import org.jetbrains.kotlin.idea.util.application.runWriteAction @@ -15,6 +16,7 @@ import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.KotlinTestUtils abstract class AbstractInsertImportOnPasteTest : AbstractCopyPasteTest() { + private val TODO_INVESTIGATE_DIRECTIVE = "// TODO: Investigation is required" private val NO_ERRORS_DUMP_DIRECTIVE = "// NO_ERRORS_DUMP" private val DELETE_DEPENDENCIES_BEFORE_PASTE_DIRECTIVE = "// DELETE_DEPENDENCIES_BEFORE_PASTE" @@ -48,6 +50,12 @@ abstract class AbstractInsertImportOnPasteTest : AbstractCopyPasteTest() { configureTargetFile(testFileName.replace(".kt", ".to.kt")) performNotWriteEditorAction(IdeActions.ACTION_PASTE) + UIUtil.dispatchAllInvocationEvents() + + if (InTextDirectivesUtils.isDirectiveDefined(testFileText, TODO_INVESTIGATE_DIRECTIVE)) { + println("File $testFile has $TODO_INVESTIGATE_DIRECTIVE") + return + } val namesToImportDump = KotlinCopyPasteReferenceProcessor.declarationsToImportSuggested.joinToString("\n") KotlinTestUtils.assertEqualsToFile(testDataFile(testFileName.replace(".kt", ".expected.names")), namesToImportDump) diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractMoveOnCutPasteTest.kt b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractMoveOnCutPasteTest.kt index 11459fa55ba..f0bdb6f8f46 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractMoveOnCutPasteTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractMoveOnCutPasteTest.kt @@ -9,6 +9,7 @@ import com.intellij.codeInsight.actions.OptimizeImportsProcessor import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.util.io.FileUtil import com.intellij.psi.PsiDocumentManager +import com.intellij.util.ui.UIUtil import junit.framework.TestCase import org.jetbrains.kotlin.idea.AbstractCopyPasteTest import org.jetbrains.kotlin.idea.core.moveCaret @@ -48,6 +49,7 @@ abstract class AbstractMoveOnCutPasteTest : AbstractCopyPasteTest() { val targetFileExists = testDataFile(targetFileName).exists() val targetPsiFile = if (targetFileExists) configureTargetFile(targetFileName) else null performNotWriteEditorAction(IdeActions.ACTION_PASTE) + UIUtil.dispatchAllInvocationEvents() val shouldBeAvailable = InTextDirectivesUtils.getPrefixedBoolean(testFileText, IS_AVAILABLE_DIRECTIVE) ?: true val cookie = editor.getUserData(MoveDeclarationsEditorCookie.KEY) diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InsertImportOnPasteTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InsertImportOnPasteTestGenerated.java index 07d7d3cec60..179107990f7 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InsertImportOnPasteTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InsertImportOnPasteTestGenerated.java @@ -130,6 +130,11 @@ public class InsertImportOnPasteTestGenerated extends AbstractInsertImportOnPast runTest("idea/testData/copyPaste/imports/DependencyOnStdLib.kt"); } + @TestMetadata("EntireFile.kt") + public void testEntireFile() throws Exception { + runTest("idea/testData/copyPaste/imports/EntireFile.kt"); + } + @TestMetadata("EnumEntries.kt") public void testEnumEntries() throws Exception { runTest("idea/testData/copyPaste/imports/EnumEntries.kt"); @@ -195,6 +200,11 @@ public class InsertImportOnPasteTestGenerated extends AbstractInsertImportOnPast runTest("idea/testData/copyPaste/imports/ImportDirective.kt"); } + @TestMetadata("ImportDirectiveAndClassBody.kt") + public void testImportDirectiveAndClassBody() throws Exception { + runTest("idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.kt"); + } + @TestMetadata("ImportableEntityInExtensionLiteral.kt") public void testImportableEntityInExtensionLiteral() throws Exception { runTest("idea/testData/copyPaste/imports/ImportableEntityInExtensionLiteral.kt"); @@ -473,6 +483,11 @@ public class InsertImportOnPasteTestGenerated extends AbstractInsertImportOnPast runTest("idea/testData/copyPaste/imports/DependencyOnStdLib.kt"); } + @TestMetadata("EntireFile.kt") + public void testEntireFile() throws Exception { + runTest("idea/testData/copyPaste/imports/EntireFile.kt"); + } + @TestMetadata("EnumEntries.kt") public void testEnumEntries() throws Exception { runTest("idea/testData/copyPaste/imports/EnumEntries.kt"); @@ -538,6 +553,11 @@ public class InsertImportOnPasteTestGenerated extends AbstractInsertImportOnPast runTest("idea/testData/copyPaste/imports/ImportDirective.kt"); } + @TestMetadata("ImportDirectiveAndClassBody.kt") + public void testImportDirectiveAndClassBody() throws Exception { + runTest("idea/testData/copyPaste/imports/ImportDirectiveAndClassBody.kt"); + } + @TestMetadata("ImportableEntityInExtensionLiteral.kt") public void testImportableEntityInExtensionLiteral() throws Exception { runTest("idea/testData/copyPaste/imports/ImportableEntityInExtensionLiteral.kt"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/conversion/copy/AbstractJavaToKotlinCopyPasteConversionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/conversion/copy/AbstractJavaToKotlinCopyPasteConversionTest.kt index 95d6e60d676..5af6de6c5fc 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/conversion/copy/AbstractJavaToKotlinCopyPasteConversionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/conversion/copy/AbstractJavaToKotlinCopyPasteConversionTest.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.idea.conversion.copy import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.editor.KotlinEditorOptions import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor import org.jetbrains.kotlin.idea.test.PluginTestCaseBase @@ -53,6 +54,7 @@ abstract class AbstractJavaToKotlinCopyPasteConversionTest : AbstractJ2kCopyPast ConvertJavaCopyPasteProcessor.conversionPerformed = false myFixture.performEditorAction(IdeActions.ACTION_PASTE) + UIUtil.dispatchAllInvocationEvents() assertEquals( noConversionExpected, !ConvertJavaCopyPasteProcessor.conversionPerformed,