Fix for KT-13695, KT-13159 J2K: Invalid conversion context setup for plain text pastes
Extending, Implementing, imports from paste target file previously not implemented
This commit is contained in:
@@ -121,3 +121,7 @@ inline fun <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long {
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
inline fun <T, C : Collection<T>, O> C.ifNotEmpty(body: C.() -> O?): O? = if (isNotEmpty()) this.body() else null
|
||||
|
||||
inline fun <T, O> Array<out T>.ifNotEmpty(body: Array<out T>.() -> O?): O? = if (isNotEmpty()) this.body() else null
|
||||
+115
-30
@@ -27,24 +27,24 @@ 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.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiErrorElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiFileFactory
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.util.LocalTimeCounter
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.jetbrains.kotlin.asJava.toLightClass
|
||||
import org.jetbrains.kotlin.idea.KotlinFileType
|
||||
import org.jetbrains.kotlin.idea.codeInsight.referenceExpression
|
||||
import org.jetbrains.kotlin.idea.editor.KotlinEditorOptions
|
||||
import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.j2k.AfterConversionPass
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.KtBlockExpression
|
||||
import org.jetbrains.kotlin.psi.KtClassBody
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.KtFunction
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
||||
import org.jetbrains.kotlin.resolve.ImportPath
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.check
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
|
||||
@@ -96,13 +96,15 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
|
||||
psiDocumentManager.commitDocument(editor.document)
|
||||
val targetFile = psiDocumentManager.getPsiFile(editor.document) as? KtFile ?: return
|
||||
|
||||
val pasteContext = detectPasteContext(targetFile, bounds.startOffset, bounds.endOffset) ?: return
|
||||
val pasteTarget = detectPasteTarget(targetFile, bounds.startOffset, bounds.endOffset) ?: return
|
||||
|
||||
val conversionContext = detectConversionContext(pasteContext, text, project) ?: return
|
||||
val conversionContext = detectConversionContext(pasteTarget.pasteContext, text, project) ?: return
|
||||
|
||||
if (!confirmConvertJavaOnPaste(project, isPlainText = true)) return
|
||||
|
||||
val convertedText = convertCodeToKotlin(text, conversionContext, project)
|
||||
val copiedJavaCode = prepareCopiedJavaCodeByContext(text, conversionContext, pasteTarget)
|
||||
|
||||
val convertedText = copiedJavaCode.convertCodeToKotlin(project).text
|
||||
|
||||
runWriteAction {
|
||||
val startOffset = bounds.startOffset
|
||||
@@ -120,7 +122,21 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectPasteContext(file: KtFile, startOffset: Int, endOffset: Int): KotlinContext? {
|
||||
private fun CopiedJavaCode.convertCodeToKotlin(project: Project): ConversionResult {
|
||||
val dataForConversion = DataForConversion.prepare(this, project)
|
||||
val conversionResult = convertCopiedCodeToKotlin(dataForConversion.elementsAndTexts, project)
|
||||
return conversionResult
|
||||
}
|
||||
|
||||
private val KtElement.pasteContext: KotlinContext
|
||||
get() = when (this) {
|
||||
is KtFile -> KotlinContext.TOP_LEVEL
|
||||
is KtClassBody -> KotlinContext.CLASS_BODY
|
||||
is KtBlockExpression -> KotlinContext.IN_BLOCK
|
||||
else -> KotlinContext.EXPRESSION
|
||||
}
|
||||
|
||||
private fun detectPasteTarget(file: KtFile, startOffset: Int, endOffset: Int): KtElement? {
|
||||
if (isNoConversionPosition(file, startOffset)) return null
|
||||
|
||||
val fileText = file.text
|
||||
@@ -128,18 +144,13 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
|
||||
val newFileText = fileText.substring(0, startOffset) + " " + dummyDeclarationText + "\n" + fileText.substring(endOffset)
|
||||
|
||||
val newFile = parseAsFile(newFileText, KotlinFileType.INSTANCE, file.project)
|
||||
(newFile as KtFile).analysisContext = file
|
||||
|
||||
val funKeyword = newFile.findElementAt(startOffset + 1) ?: return null
|
||||
if (funKeyword.node.elementType != KtTokens.FUN_KEYWORD) return null
|
||||
val declaration = funKeyword.parent as? KtFunction ?: return null
|
||||
|
||||
val parent = declaration.parent
|
||||
return when (parent) {
|
||||
is KtFile -> KotlinContext.TOP_LEVEL
|
||||
is KtClassBody -> KotlinContext.CLASS_BODY
|
||||
is KtBlockExpression -> KotlinContext.IN_BLOCK
|
||||
else -> KotlinContext.EXPRESSION
|
||||
}
|
||||
return declaration.parent as? KtElement
|
||||
}
|
||||
|
||||
private fun detectConversionContext(pasteContext: KotlinContext, text: String, project: Project): JavaContext? {
|
||||
@@ -200,26 +211,100 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
|
||||
}
|
||||
|
||||
private fun parseAsFile(text: String, fileType: LanguageFileType, project: Project): PsiFile {
|
||||
return PsiFileFactory.getInstance(project).createFileFromText("Dummy", fileType, text, LocalTimeCounter.currentTime(), false)
|
||||
return PsiFileFactory.getInstance(project).createFileFromText("Dummy", fileType, text, LocalTimeCounter.currentTime(), true)
|
||||
}
|
||||
|
||||
private fun convertCodeToKotlin(text: String, context: JavaContext, project: Project): String {
|
||||
val copiedJavaCode = when (context) {
|
||||
JavaContext.TOP_LEVEL -> createCopiedJavaCode("$", text)
|
||||
private fun prepareCopiedJavaCodeByContext(text: String, context: JavaContext, target: KtElement): CopiedJavaCode {
|
||||
|
||||
JavaContext.CLASS_BODY -> createCopiedJavaCode("class Dummy {\n$\n}", text)
|
||||
val targetFile = target.containingFile as KtFile
|
||||
|
||||
JavaContext.IN_BLOCK -> createCopiedJavaCode("class Dummy {\nvoid foo() {\n$\n}\n}", text)
|
||||
val prefix = buildString {
|
||||
targetFile.packageDirective?.let {
|
||||
if (it.text.isNotEmpty()) {
|
||||
append(it.text)
|
||||
append(";\n")
|
||||
}
|
||||
}
|
||||
|
||||
JavaContext.EXPRESSION -> createCopiedJavaCode("class Dummy {\nObject field = $\n}", text)
|
||||
fun appendImport(importPath: ImportPath) {
|
||||
append("import ")
|
||||
append(importPath)
|
||||
append(";\n")
|
||||
}
|
||||
|
||||
fun appendStaticImport(importPath: ImportPath) {
|
||||
append("import static ")
|
||||
append(importPath)
|
||||
append(";\n")
|
||||
}
|
||||
|
||||
targetFile.importDirectives.forEach {
|
||||
val importPath = it.importPath
|
||||
val importedReference = it.importedReference
|
||||
if (importPath != null && !importPath.hasAlias() && importedReference is KtDotQualifiedExpression) {
|
||||
|
||||
val receiver = importedReference
|
||||
.receiverExpression
|
||||
.referenceExpression()
|
||||
?.mainReference
|
||||
?.resolve()
|
||||
val selector = importedReference
|
||||
.selectorExpression
|
||||
?.referenceExpression()
|
||||
?.mainReference
|
||||
?.resolve()
|
||||
|
||||
val isPackageReceiver = receiver is PsiPackage
|
||||
val isClassReceiver = receiver is PsiClass
|
||||
val isClassSelector = selector is PsiClass
|
||||
|
||||
if (importPath.isAllUnder) {
|
||||
if (isClassReceiver)
|
||||
appendStaticImport(importPath)
|
||||
else if (isPackageReceiver)
|
||||
appendImport(importPath)
|
||||
}
|
||||
else {
|
||||
if (isPackageReceiver && isClassSelector)
|
||||
appendImport(importPath)
|
||||
else if (isClassReceiver)
|
||||
appendStaticImport(importPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dataForConversion = DataForConversion.prepare(copiedJavaCode, project)
|
||||
val conversionResult = convertCopiedCodeToKotlin(dataForConversion.elementsAndTexts, project)
|
||||
return conversionResult.text
|
||||
val classDef = when (context) {
|
||||
JavaContext.TOP_LEVEL -> ""
|
||||
|
||||
JavaContext.CLASS_BODY,
|
||||
JavaContext.IN_BLOCK,
|
||||
JavaContext.EXPRESSION -> {
|
||||
val lightClass = target.getParentOfType<KtClass>(false)?.toLightClass()
|
||||
|
||||
buildString {
|
||||
append("class ")
|
||||
append(lightClass?.name ?: "Dummy")
|
||||
lightClass?.extendsListTypes?.ifNotEmpty { joinTo(this@buildString, prefix = " extends ") { it.getCanonicalText(true) } }
|
||||
lightClass?.implementsListTypes?.ifNotEmpty { joinTo(this@buildString, prefix = " implements ") { it.getCanonicalText(true) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val copiedJavaCode = when (context) {
|
||||
JavaContext.TOP_LEVEL -> createCopiedJavaCode(prefix, "$", text)
|
||||
|
||||
JavaContext.CLASS_BODY -> createCopiedJavaCode(prefix, "$classDef {\n$\n}", text)
|
||||
|
||||
JavaContext.IN_BLOCK -> createCopiedJavaCode(prefix, "$classDef {\nvoid foo() {\n$\n}\n}", text)
|
||||
|
||||
JavaContext.EXPRESSION -> createCopiedJavaCode(prefix, "$classDef {\nObject field = $\n}", text)
|
||||
}
|
||||
return copiedJavaCode
|
||||
}
|
||||
|
||||
private fun createCopiedJavaCode(template: String, text: String): CopiedJavaCode {
|
||||
private fun createCopiedJavaCode(prefix: String, templateWithoutPrefix: String, text: String): CopiedJavaCode {
|
||||
val template = "$prefix\n$templateWithoutPrefix"
|
||||
val index = template.indexOf("$")
|
||||
assert(index >= 0)
|
||||
val fileText = template.substring(0, index) + text + template.substring(index + 1)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package test;
|
||||
|
||||
public class JavaParent {
|
||||
public void subject() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import test.JavaParent
|
||||
|
||||
class KotlinChild : JavaParent() {
|
||||
override fun subject() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import test.JavaParent
|
||||
|
||||
class KotlinChild : JavaParent() {
|
||||
<caret>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@Override public void subject() {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package test;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface JavaInterface {
|
||||
void subject(@Nullable String s)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import test.JavaInterface
|
||||
|
||||
class KotlinChild : JavaInterface() {
|
||||
override fun subject(s: String?) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import test.JavaInterface
|
||||
|
||||
class KotlinChild : JavaInterface() {
|
||||
<caret>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@Override public void subject(String s) {}
|
||||
+3
@@ -57,6 +57,9 @@ abstract class AbstractTextJavaToKotlinCopyPasteConversionTest : AbstractCopyPas
|
||||
myFixture.editor.selectionModel.setSelection(0, fileText.length)
|
||||
myFixture.performEditorAction(IdeActions.ACTION_COPY)
|
||||
|
||||
configureByDependencyIfExists(testName + ".dependency.kt")
|
||||
configureByDependencyIfExists(testName + ".dependency.java")
|
||||
|
||||
configureTargetFile(testName + ".to.kt")
|
||||
|
||||
ConvertTextJavaCopyPasteProcessor.conversionPerformed = false
|
||||
|
||||
+12
@@ -95,6 +95,18 @@ public class TextJavaToKotlinCopyPasteConversionTestGenerated extends AbstractTe
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("Override.txt")
|
||||
public void testOverride() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/plainTextConversion/Override.txt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("OverrideInterface.txt")
|
||||
public void testOverrideInterface() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/plainTextConversion/OverrideInterface.txt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("PostProcessing.txt")
|
||||
public void testPostProcessing() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/plainTextConversion/PostProcessing.txt");
|
||||
|
||||
Reference in New Issue
Block a user