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:
Simon Ogorodnik
2016-10-19 16:12:12 +03:00
parent b802ca0af5
commit f8b7ce1011
12 changed files with 170 additions and 30 deletions
@@ -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
@@ -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) {}
@@ -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
@@ -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");