J2K, minor: clean copy-paste conversion code

This commit is contained in:
Ilya Kirillov
2019-08-02 20:30:34 +03:00
parent f1482177b4
commit 2dabb91a1f
7 changed files with 202 additions and 164 deletions
@@ -54,6 +54,7 @@ import java.util.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferableData>() { class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferableData>() {
@Suppress("PrivatePropertyName")
private val LOG = Logger.getInstance(ConvertJavaCopyPasteProcessor::class.java) private val LOG = Logger.getInstance(ConvertJavaCopyPasteProcessor::class.java)
override fun extractTransferableData(content: Transferable): List<TextBlockTransferableData> { override fun extractTransferableData(content: Transferable): List<TextBlockTransferableData> {
@@ -61,20 +62,30 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
if (content.isDataFlavorSupported(CopiedJavaCode.DATA_FLAVOR)) { if (content.isDataFlavorSupported(CopiedJavaCode.DATA_FLAVOR)) {
return listOf(content.getTransferData(CopiedJavaCode.DATA_FLAVOR) as TextBlockTransferableData) return listOf(content.getTransferData(CopiedJavaCode.DATA_FLAVOR) as TextBlockTransferableData)
} }
} } catch (e: Throwable) {
catch (e: Throwable) {
LOG.error(e) LOG.error(e)
} }
return listOf() return listOf()
} }
override fun collectTransferableData(file: PsiFile, editor: Editor, startOffsets: IntArray, endOffsets: IntArray): List<TextBlockTransferableData> { override fun collectTransferableData(
file: PsiFile,
editor: Editor,
startOffsets: IntArray,
endOffsets: IntArray
): List<TextBlockTransferableData> {
if (file !is PsiJavaFile) return listOf() if (file !is PsiJavaFile) return listOf()
return listOf(CopiedJavaCode(file.getText()!!, startOffsets, endOffsets)) return listOf(CopiedJavaCode(file.getText()!!, startOffsets, endOffsets))
} }
override fun processTransferableData(project: Project, editor: Editor, bounds: RangeMarker, caretOffset: Int, indented: Ref<Boolean>, values: List<TextBlockTransferableData>) { override fun processTransferableData(
project: Project,
editor: Editor,
bounds: RangeMarker,
caretOffset: Int,
indented: Ref<Boolean>,
values: List<TextBlockTransferableData>
) {
if (DumbService.getInstance(project).isDumb) return if (DumbService.getInstance(project).isDumb) return
if (!KotlinEditorOptions.getInstance().isEnableJavaToKotlinConversion) return if (!KotlinEditorOptions.getInstance().isEnableJavaToKotlinConversion) return
@@ -104,7 +115,11 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
return Result(text, referenceData, result.importsToAdd, result.converterContext) return Result(text, referenceData, result.importsToAdd, result.converterContext)
} }
fun insertImports(bounds: TextRange, referenceData: Collection<KotlinReferenceData>, explicitImports: Collection<FqName>): TextRange? { fun insertImports(
bounds: TextRange,
referenceData: Collection<KotlinReferenceData>,
explicitImports: Collection<FqName>
): TextRange? {
if (referenceData.isEmpty() && explicitImports.isEmpty()) return bounds if (referenceData.isEmpty() && explicitImports.isEmpty()) return bounds
PsiDocumentManager.getInstance(project).commitAllDocuments() PsiDocumentManager.getInstance(project).commitAllDocuments()
@@ -133,31 +148,31 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
if (conversionResult!!.text != null) return false if (conversionResult!!.text != null) return false
insertImports(bounds.range ?: return true, conversionResult!!.referenceData, conversionResult!!.explicitImports) insertImports(
bounds.range ?: return true,
conversionResult!!.referenceData,
conversionResult!!.explicitImports
)
return true return true
} }
val textLength = data.startOffsets.indices.sumBy { data.endOffsets[it] - data.startOffsets[it] } val textLength = data.startOffsets.indices.sumBy { data.endOffsets[it] - data.startOffsets[it] }
if (textLength < 1000) { // if the text to convert is short enough, try to do conversion without permission from user and skip the dialog if nothing converted // if the text to convert is short enough, try to do conversion without permission from user and skip the dialog if nothing converted
if (doConversionAndInsertImportsIfUnchanged()) return if (textLength < 1000 && doConversionAndInsertImportsIfUnchanged()) return
}
fun convert() { fun convert() {
if (conversionResult == null) { if (conversionResult == null && doConversionAndInsertImportsIfUnchanged()) return
if (doConversionAndInsertImportsIfUnchanged()) return
}
val (text, referenceData, explicitImports) = conversionResult!! val (text, referenceData, explicitImports) = conversionResult!!
text!! // otherwise we should get true from doConversionAndInsertImportsIfUnchanged and return above text!! // otherwise we should get true from doConversionAndInsertImportsIfUnchanged and return above
val boundsAfterReplace = val boundsAfterReplace = runWriteAction {
runWriteAction { val startOffset = bounds.startOffset
val startOffset = bounds.startOffset document.replaceString(startOffset, bounds.endOffset, text)
document.replaceString(startOffset, bounds.endOffset, text)
val endOffsetAfterCopy = startOffset + text.length val endOffsetAfterCopy = startOffset + text.length
editor.caretModel.moveToOffset(endOffsetAfterCopy) editor.caretModel.moveToOffset(endOffsetAfterCopy)
TextRange(startOffset, endOffsetAfterCopy) TextRange(startOffset, endOffsetAfterCopy)
} }
val newBounds = insertImports(boundsAfterReplace, referenceData, explicitImports) val newBounds = insertImports(boundsAfterReplace, referenceData, explicitImports)
@@ -181,7 +196,12 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
} }
} }
private fun buildReferenceData(text: String, parseContext: ParseContext, importsAndPackage: String, targetFile: KtFile): Collection<KotlinReferenceData> { private fun buildReferenceData(
text: String,
parseContext: ParseContext,
importsAndPackage: String,
targetFile: KtFile
): Collection<KotlinReferenceData> {
var blockStart: Int? = null var blockStart: Int? = null
var blockEnd: Int? = null var blockEnd: Int? = null
val fileText = buildString { val fileText = buildString {
@@ -202,7 +222,6 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
} }
val dummyFile = KtPsiFactory(targetFile.project).createAnalyzableFile("dummy.kt", fileText, targetFile) val dummyFile = KtPsiFactory(targetFile.project).createAnalyzableFile("dummy.kt", fileText, targetFile)
return KotlinCopyPasteReferenceProcessor().collectReferenceData(dummyFile, intArrayOf(blockStart!!), intArrayOf(blockEnd!!)) return KotlinCopyPasteReferenceProcessor().collectReferenceData(dummyFile, intArrayOf(blockStart!!), intArrayOf(blockEnd!!))
} }
@@ -216,16 +235,17 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
} }
companion object { companion object {
@get:TestOnly var conversionPerformed: Boolean = false @get:TestOnly
var conversionPerformed: Boolean = false
} }
} }
internal class ConversionResult( internal class ConversionResult(
val text: String, val text: String,
val parseContext: ParseContext, val parseContext: ParseContext,
val importsToAdd: Set<FqName>, val importsToAdd: Set<FqName>,
val textChanged: Boolean, val textChanged: Boolean,
val converterContext: ConverterContext? val converterContext: ConverterContext?
) )
internal fun ElementAndTextList.convertCodeToKotlin(project: Project, targetModule: Module?, useNewJ2k: Boolean): ConversionResult { internal fun ElementAndTextList.convertCodeToKotlin(project: Project, targetModule: Module?, useNewJ2k: Boolean): ConversionResult {
@@ -237,16 +257,16 @@ internal fun ElementAndTextList.convertCodeToKotlin(project: Project, targetModu
IdeaJavaToKotlinServices IdeaJavaToKotlinServices
) )
val inputElements = this.toList().filterIsInstance<PsiElement>() val inputElements = toList().filterIsInstance<PsiElement>()
val (results, _, converterContext) = val (results, _, converterContext) =
ProgressManager.getInstance().runProcessWithProgressSynchronously( ProgressManager.getInstance().runProcessWithProgressSynchronously(
ThrowableComputable<Result, Exception> { ThrowableComputable<Result, Exception> {
runReadAction { converter.elementsToKotlin(inputElements) } runReadAction { converter.elementsToKotlin(inputElements) }
}, },
JavaToKotlinAction.title, JavaToKotlinAction.title,
false, false,
project project
) )
val importsToAdd = LinkedHashSet<FqName>() val importsToAdd = LinkedHashSet<FqName>()
@@ -267,8 +287,7 @@ internal fun ElementAndTextList.convertCodeToKotlin(project: Project, targetModu
parseContext = result.parseContext parseContext = result.parseContext
} }
importsToAdd.addAll(result.importsToAdd) importsToAdd.addAll(result.importsToAdd)
} } else { // failed to convert element to Kotlin, insert "as is"
else { // failed to convert element to Kotlin, insert "as is"
convertedCodeBuilder.append(originalText) convertedCodeBuilder.append(originalText)
} }
} }
@@ -297,11 +316,11 @@ internal fun isNoConversionPosition(file: KtFile, offset: Int): Boolean {
if (token !is PsiWhiteSpace && token.endOffset != offset) return true // pasting into the middle of token if (token !is PsiWhiteSpace && token.endOffset != offset) return true // pasting into the middle of token
for (element in token.parentsWithSelf) { for (element in token.parentsWithSelf) {
if (element is PsiComment) { when (element) {
return element.node.elementType == KtTokens.EOL_COMMENT || offset != element.endOffset is PsiComment -> return element.node.elementType == KtTokens.EOL_COMMENT || offset != element.endOffset
is KtStringTemplateEntryWithExpression -> return false
is KtStringTemplateExpression -> return true
} }
if (element is KtStringTemplateEntryWithExpression) return false
if (element is KtStringTemplateExpression) return true
} }
return false return false
} }
@@ -61,11 +61,17 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
override fun setOffsets(offsets: IntArray?, index: Int) = index override fun setOffsets(offsets: IntArray?, index: Int) = index
companion object { companion object {
val DATA_FLAVOR: DataFlavor = DataFlavor(ConvertTextJavaCopyPasteProcessor::class.java, "class: ConvertTextJavaCopyPasteProcessor") val DATA_FLAVOR: DataFlavor =
DataFlavor(ConvertTextJavaCopyPasteProcessor::class.java, "class: ConvertTextJavaCopyPasteProcessor")
} }
} }
override fun collectTransferableData(file: PsiFile, editor: Editor, startOffsets: IntArray, endOffsets: IntArray): List<TextBlockTransferableData> { override fun collectTransferableData(
file: PsiFile,
editor: Editor,
startOffsets: IntArray,
endOffsets: IntArray
): List<TextBlockTransferableData> {
if (file is KtFile) return listOf(CopiedKotlinCode(file.text, startOffsets, endOffsets)) if (file is KtFile) return listOf(CopiedKotlinCode(file.text, startOffsets, endOffsets))
return emptyList() return emptyList()
} }
@@ -73,22 +79,28 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
override fun extractTransferableData(content: Transferable): List<TextBlockTransferableData> { override fun extractTransferableData(content: Transferable): List<TextBlockTransferableData> {
try { try {
if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) { if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {
if (content.isDataFlavorSupported(CopiedKotlinCode.DATA_FLAVOR) || if (content.isDataFlavorSupported(CopiedKotlinCode.DATA_FLAVOR) ||
/* Handled by ConvertJavaCopyPasteProcessor */ /* Handled by ConvertJavaCopyPasteProcessor */
content.isDataFlavorSupported(CopiedJavaCode.DATA_FLAVOR)) return emptyList() content.isDataFlavorSupported(CopiedJavaCode.DATA_FLAVOR)
) return emptyList()
val text = content.getTransferData(DataFlavor.stringFlavor) as String val text = content.getTransferData(DataFlavor.stringFlavor) as String
return listOf(MyTransferableData(text)) return listOf(MyTransferableData(text))
} }
} } catch (e: Throwable) {
catch (e: Throwable) {
LOG.error(e) LOG.error(e)
} }
return emptyList() return emptyList()
} }
override fun processTransferableData(project: Project, editor: Editor, bounds: RangeMarker, caretOffset: Int, indented: Ref<Boolean>, values: List<TextBlockTransferableData>) { override fun processTransferableData(
project: Project,
editor: Editor,
bounds: RangeMarker,
caretOffset: Int,
indented: Ref<Boolean>,
values: List<TextBlockTransferableData>
) {
if (DumbService.getInstance(project).isDumb) return if (DumbService.getInstance(project).isDumb) return
if (!KotlinEditorOptions.getInstance().isEnableJavaToKotlinConversion) return //TODO: use another option? if (!KotlinEditorOptions.getInstance().isEnableJavaToKotlinConversion) return //TODO: use another option?
@@ -100,11 +112,8 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
val useNewJ2k = checkUseNewJ2k(targetFile) val useNewJ2k = checkUseNewJ2k(targetFile)
val targetModule = targetFile.module val targetModule = targetFile.module
val pasteTarget = detectPasteTarget(targetFile, bounds.startOffset, bounds.endOffset) ?: return val pasteTarget = detectPasteTarget(targetFile, bounds.startOffset, bounds.endOffset) ?: return
val conversionContext = detectConversionContext(pasteTarget.pasteContext, text, project) ?: return val conversionContext = detectConversionContext(pasteTarget.pasteContext, text, project) ?: return
if (!confirmConvertJavaOnPaste(project, isPlainText = true)) return if (!confirmConvertJavaOnPaste(project, isPlainText = true)) return
val copiedJavaCode = prepareCopiedJavaCodeByContext(text, conversionContext, pasteTarget) val copiedJavaCode = prepareCopiedJavaCodeByContext(text, conversionContext, pasteTarget)
@@ -121,6 +130,7 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
val importsInsertOffset = targetFile.importList?.endOffset ?: 0 val importsInsertOffset = targetFile.importList?.endOffset ?: 0
if (targetFile.importDirectives.isEmpty() && importsInsertOffset > 0) if (targetFile.importDirectives.isEmpty() && importsInsertOffset > 0)
convertedImportsText = "\n" + convertedImportsText convertedImportsText = "\n" + convertedImportsText
if (convertedImportsText.isNotBlank()) if (convertedImportsText.isNotBlank())
editor.document.insertString(importsInsertOffset, convertedImportsText) editor.document.insertString(importsInsertOffset, convertedImportsText)
@@ -139,9 +149,7 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
conversionPerformed = true conversionPerformed = true
} }
val conversionTime = measureTimeMillis { val conversionTime = measureTimeMillis { convert() }
convert()
}
logJ2kConversionStatistics( logJ2kConversionStatistics(
ConversionType.TEXT_EXPRESSION, ConversionType.TEXT_EXPRESSION,
checkUseNewJ2k(targetFile), checkUseNewJ2k(targetFile),
@@ -151,7 +159,7 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
) )
} }
private fun DataForConversion.convertCodeToKotlin(project: Project, targetModule: Module?,useNewJ2k: Boolean): ConversionResult { private fun DataForConversion.convertCodeToKotlin(project: Project, targetModule: Module?, useNewJ2k: Boolean): ConversionResult {
return elementsAndTexts.convertCodeToKotlin(project, targetModule, useNewJ2k) return elementsAndTexts.convertCodeToKotlin(project, targetModule, useNewJ2k)
} }
@@ -168,7 +176,7 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
val fileText = file.text val fileText = file.text
val dummyDeclarationText = "fun dummy(){}" val dummyDeclarationText = "fun dummy(){}"
val newFileText = fileText.substring(0, startOffset) + " " + dummyDeclarationText + "\n" + fileText.substring(endOffset) val newFileText = "${fileText.substring(0, startOffset)} $dummyDeclarationText\n${fileText.substring(endOffset)}"
val newFile = parseAsFile(newFileText, KotlinFileType.INSTANCE, file.project) val newFile = parseAsFile(newFileText, KotlinFileType.INSTANCE, file.project)
(newFile as KtFile).analysisContext = file (newFile as KtFile).analysisContext = file
@@ -183,9 +191,8 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
private fun detectConversionContext(pasteContext: KotlinContext, text: String, project: Project): JavaContext? { private fun detectConversionContext(pasteContext: KotlinContext, text: String, project: Project): JavaContext? {
if (isParsedAsKotlinCode(text, pasteContext, project)) return null if (isParsedAsKotlinCode(text, pasteContext, project)) return null
fun JavaContext.check(): JavaContext? { fun JavaContext.check(): JavaContext? =
return takeIf { isParsedAsJavaCode(text, it, project) } takeIf { isParsedAsJavaCode(text, it, project) }
}
when (pasteContext) { when (pasteContext) {
KotlinContext.TOP_LEVEL -> { KotlinContext.TOP_LEVEL -> {
@@ -238,19 +245,21 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
} }
private fun parseAsFile(text: String, fileType: LanguageFileType, project: Project): PsiFile { private fun parseAsFile(text: String, fileType: LanguageFileType, project: Project): PsiFile {
return PsiFileFactory.getInstance(project).createFileFromText("Dummy." + fileType.defaultExtension, fileType, text, LocalTimeCounter.currentTime(), true) return PsiFileFactory.getInstance(project)
.createFileFromText("Dummy." + fileType.defaultExtension, fileType, text, LocalTimeCounter.currentTime(), true)
} }
private fun DataForConversion.tryResolveImports(targetFile: KtFile): ElementAndTextList { private fun DataForConversion.tryResolveImports(targetFile: KtFile): ElementAndTextList {
val importResolver = PlainTextPasteImportResolver(this, targetFile) val importResolver = PlainTextPasteImportResolver(this, targetFile)
importResolver.addImportsFromTargetFile() importResolver.addImportsFromTargetFile()
importResolver.tryResolveReferences() importResolver.tryResolveReferences()
return ElementAndTextList(importResolver.addedImports.flatMap { listOf("\n", it) } + "\n\n") //TODO Non-manual formatting for import list return ElementAndTextList(importResolver.addedImports.flatMap { importStatement ->
listOf("\n", importStatement)
} + "\n\n") //TODO Non-manual formatting for import list
} }
private fun prepareCopiedJavaCodeByContext(text: String, context: JavaContext, target: KtElement): CopiedJavaCode { private fun prepareCopiedJavaCodeByContext(text: String, context: JavaContext, target: KtElement): CopiedJavaCode {
val targetFile = target.containingFile as KtFile val targetFile = target.containingFile as KtFile
val (localDeclarations, memberDeclarations) = javaContextDeclarationRenderer.render(target) val (localDeclarations, memberDeclarations) = javaContextDeclarationRenderer.render(target)
val prefix = buildString { val prefix = buildString {
@@ -265,16 +274,25 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
val classDef = when (context) { val classDef = when (context) {
JavaContext.TOP_LEVEL -> "" JavaContext.TOP_LEVEL -> ""
JavaContext.CLASS_BODY, JavaContext.CLASS_BODY, JavaContext.IN_BLOCK, JavaContext.EXPRESSION -> {
JavaContext.IN_BLOCK,
JavaContext.EXPRESSION -> {
val lightClass = target.getParentOfType<KtClass>(false)?.toLightClass() val lightClass = target.getParentOfType<KtClass>(false)?.toLightClass()
buildString { buildString {
append("class ") append("class ")
append(lightClass?.name ?: "Dummy") append(lightClass?.name ?: "Dummy")
lightClass?.extendsListTypes?.ifNotEmpty { joinTo(this@buildString, prefix = " extends ") { it.getCanonicalText(true) } } lightClass?.extendsListTypes?.ifNotEmpty {
lightClass?.implementsListTypes?.ifNotEmpty { joinTo(this@buildString, prefix = " implements ") { it.getCanonicalText(true) } } joinTo(
this@buildString,
prefix = " extends "
) { it.getCanonicalText(true) }
}
lightClass?.implementsListTypes?.ifNotEmpty {
joinTo(this@buildString, prefix = " implements ") {
it.getCanonicalText(
true
)
}
}
} }
} }
} }
@@ -300,6 +318,7 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
} }
companion object { companion object {
@get:TestOnly var conversionPerformed: Boolean = false @get:TestOnly
var conversionPerformed: Boolean = false
} }
} }
@@ -34,11 +34,10 @@ import org.jetbrains.kotlin.types.KotlinType
import java.util.* import java.util.*
data class DataForConversion private constructor( data class DataForConversion private constructor(
val elementsAndTexts: ElementAndTextList /* list consisting of PsiElement's to convert and plain String's */, val elementsAndTexts: ElementAndTextList /* list consisting of PsiElement's to convert and plain String's */,
val importsAndPackage: String, val importsAndPackage: String,
val file: PsiJavaFile val file: PsiJavaFile
) { ) {
companion object { companion object {
fun prepare(copiedCode: CopiedJavaCode, project: Project): DataForConversion { fun prepare(copiedCode: CopiedJavaCode, project: Project): DataForConversion {
@@ -65,7 +64,6 @@ data class DataForConversion private constructor(
return DataForConversion(elementsAndTexts, importsAndPackage, file) return DataForConversion(elementsAndTexts, importsAndPackage, file)
} }
private fun clipTextIfNeeded(file: PsiJavaFile, fileText: String, startOffsets: IntArray, endOffsets: IntArray): String? { private fun clipTextIfNeeded(file: PsiJavaFile, fileText: String, startOffsets: IntArray, endOffsets: IntArray): String? {
val ranges = startOffsets.indices.map { TextRange(startOffsets[it], endOffsets[it]) }.sortedBy { it.start } val ranges = startOffsets.indices.map { TextRange(startOffsets[it], endOffsets[it]) }.sortedBy { it.start }
@@ -126,8 +124,7 @@ data class DataForConversion private constructor(
val offset = this[i] val offset = this[i]
if (offset >= range.end) { if (offset >= range.end) {
this[i] = offset - range.length this[i] = offset - range.length
} } else {
else {
assert(offset <= range.start) assert(offset <= range.start)
} }
} }
@@ -141,7 +138,10 @@ data class DataForConversion private constructor(
} }
private fun PsiElement.maximalParentToClip(range: TextRange): PsiElement? { private fun PsiElement.maximalParentToClip(range: TextRange): PsiElement? {
val firstNotInRange = parentsWithSelf.takeWhile { it !is PsiDirectory }.firstOrNull { it.range !in range } ?: return null val firstNotInRange = parentsWithSelf
.takeWhile { it !is PsiDirectory }
.firstOrNull { it.range !in range }
?: return null
return firstNotInRange.parentsWithSelf.lastOrNull { it.minimizedTextRange() in range } return firstNotInRange.parentsWithSelf.lastOrNull { it.minimizedTextRange() in range }
} }
@@ -155,41 +155,42 @@ data class DataForConversion private constructor(
private fun canDropElementFromText(element: PsiElement): Boolean { private fun canDropElementFromText(element: PsiElement): Boolean {
return when (element) { return when (element) {
is PsiWhiteSpace, is PsiComment, is PsiModifierList, is PsiAnnotation -> true is PsiWhiteSpace, is PsiComment, is PsiModifierList, is PsiAnnotation -> true
is PsiJavaToken -> { is PsiJavaToken -> {
when (element.tokenType) { when (element.tokenType) {
// modifiers // modifiers
JavaTokenType.PUBLIC_KEYWORD, JavaTokenType.PROTECTED_KEYWORD, JavaTokenType.PRIVATE_KEYWORD, JavaTokenType.PUBLIC_KEYWORD, JavaTokenType.PROTECTED_KEYWORD, JavaTokenType.PRIVATE_KEYWORD,
JavaTokenType.STATIC_KEYWORD, JavaTokenType.ABSTRACT_KEYWORD, JavaTokenType.FINAL_KEYWORD, JavaTokenType.STATIC_KEYWORD, JavaTokenType.ABSTRACT_KEYWORD, JavaTokenType.FINAL_KEYWORD,
JavaTokenType.NATIVE_KEYWORD, JavaTokenType.SYNCHRONIZED_KEYWORD, JavaTokenType.STRICTFP_KEYWORD, JavaTokenType.NATIVE_KEYWORD, JavaTokenType.SYNCHRONIZED_KEYWORD, JavaTokenType.STRICTFP_KEYWORD,
JavaTokenType.TRANSIENT_KEYWORD, JavaTokenType.VOLATILE_KEYWORD, JavaTokenType.DEFAULT_KEYWORD -> element.getParent() is PsiModifierList JavaTokenType.TRANSIENT_KEYWORD, JavaTokenType.VOLATILE_KEYWORD, JavaTokenType.DEFAULT_KEYWORD ->
element.getParent() is PsiModifierList
JavaTokenType.SEMICOLON -> true JavaTokenType.SEMICOLON -> true
else -> false else -> false
} }
} }
is PsiCodeBlock -> element.getParent() is PsiMethod is PsiCodeBlock -> element.getParent() is PsiMethod
else -> element.firstChild == null else -> element.firstChild == null
} }
} }
private fun tryClipLeftSide(element: PsiElement, leftBound: Int) private fun tryClipLeftSide(element: PsiElement, leftBound: Int) =
= tryClipSide(element, leftBound, { range }, { allChildren }) tryClipSide(element, leftBound, { range }, { allChildren })
private fun tryClipRightSide(element: PsiElement, rightBound: Int): Int? { private fun tryClipRightSide(element: PsiElement, rightBound: Int): Int? {
fun Int.transform() = Int.MAX_VALUE - this fun Int.transform() = Int.MAX_VALUE - this
fun TextRange.transform() = TextRange(end.transform(), start.transform()) fun TextRange.transform() = TextRange(end.transform(), start.transform())
return tryClipSide(element, rightBound.transform(), { range.transform() }, { lastChild.siblings(forward = false) })?.transform() return tryClipSide(
element,
rightBound.transform(),
{ range.transform() },
{ lastChild.siblings(forward = false) }
)?.transform()
} }
private fun tryClipSide( private fun tryClipSide(
element: PsiElement, element: PsiElement,
rangeBound: Int, rangeBound: Int,
rangeFunction: PsiElement.() -> TextRange, rangeFunction: PsiElement.() -> TextRange,
childrenFunction: PsiElement.() -> Sequence<PsiElement> childrenFunction: PsiElement.() -> Sequence<PsiElement>
): Int? { ): Int? {
if (element.firstChild == null) return null if (element.firstChild == null) return null
@@ -202,12 +203,10 @@ data class DataForConversion private constructor(
if (childRange.start >= rangeBound) { // we have cut enough already if (childRange.start >= rangeBound) { // we have cut enough already
break break
} } else if (childRange.end <= rangeBound) { // need to drop the whole element
else if (childRange.end <= rangeBound) { // need to drop the whole element
if (!canDropElementFromText(child)) return null if (!canDropElementFromText(child)) return null
clipTo = childRange.end clipTo = childRange.end
} } else { // rangeBound is inside child's range
else { // rangeBound is inside child's range
if (child is PsiWhiteSpace) break // no need to cut whitespace - we can leave it as is if (child is PsiWhiteSpace) break // no need to cut whitespace - we can leave it as is
return tryClipSide(child, rangeBound, rangeFunction, childrenFunction) return tryClipSide(child, rangeBound, rangeFunction, childrenFunction)
} }
@@ -217,15 +216,14 @@ data class DataForConversion private constructor(
} }
private fun ElementAndTextList.collectElementsToConvert( private fun ElementAndTextList.collectElementsToConvert(
file: PsiJavaFile, file: PsiJavaFile,
fileText: String, fileText: String,
range: TextRange range: TextRange
) { ) {
val elements = file.elementsInRange(range) val elements = file.elementsInRange(range)
if (elements.isEmpty()) { if (elements.isEmpty()) {
add(fileText.substring(range.start, range.end)) add(fileText.substring(range.start, range.end))
} } else {
else {
add(fileText.substring(range.start, elements.first().range.start)) add(fileText.substring(range.start, elements.first().range.start))
elements.forEach { elements.forEach {
if (shouldExpandToChildren(it)) if (shouldExpandToChildren(it))
@@ -243,7 +241,7 @@ data class DataForConversion private constructor(
private fun buildImportsAndPackage(sourceFile: PsiJavaFile): String { private fun buildImportsAndPackage(sourceFile: PsiJavaFile): String {
return buildString { return buildString {
val packageName = sourceFile.packageName val packageName = sourceFile.packageName
if (!packageName.isEmpty()) { if (packageName.isNotEmpty()) {
append("package $packageName\n") append("package $packageName\n")
} }
@@ -253,8 +251,7 @@ data class DataForConversion private constructor(
val qualifiedName = import.qualifiedName ?: continue val qualifiedName = import.qualifiedName ?: continue
if (import.isOnDemand) { if (import.isOnDemand) {
append("import $qualifiedName.*\n") append("import $qualifiedName.*\n")
} } else {
else {
val fqName = FqNameUnsafe(qualifiedName) val fqName = FqNameUnsafe(qualifiedName)
// skip explicit imports of platform classes mapped into Kotlin classes // skip explicit imports of platform classes mapped into Kotlin classes
if (fqName.isSafe && JavaToKotlinClassMap.isJavaPlatformClass(fqName.toSafe())) continue if (fqName.isSafe && JavaToKotlinClassMap.isJavaPlatformClass(fqName.toSafe())) continue
@@ -42,10 +42,10 @@ class ElementAndTextList() {
fun toList(): List<Any> = elementsAndTexts.toList() fun toList(): List<Any> = elementsAndTexts.toList()
fun process(processor: ElementsAndTextsProcessor) { fun process(processor: ElementsAndTextsProcessor) {
elementsAndTexts.forEach { elementsAndTexts.forEach { element ->
when (it) { when (element) {
is PsiElement -> processor.processElement(it) is PsiElement -> processor.processElement(element)
is String -> processor.processText(it) is String -> processor.processText(element)
} }
} }
} }
@@ -26,7 +26,6 @@ data class ContextDeclarations(
val memberDeclarationsJavaStubs: String val memberDeclarationsJavaStubs: String
) )
class JavaContextDeclarationRenderer { class JavaContextDeclarationRenderer {
private val KtElement.memberDeclarations private val KtElement.memberDeclarations
get() = parents() get() = parents()
@@ -34,6 +34,7 @@ class KotlinFilePasteProvider : PasteProvider {
val ktFile = KtPsiFactory(project).createFile(text) val ktFile = KtPsiFactory(project).createFile(text)
val fileName = (ktFile.declarations.firstOrNull()?.name ?: return) + ".kt" val fileName = (ktFile.declarations.firstOrNull()?.name ?: return) + ".kt"
@Suppress("UsePropertyAccessSyntax")
val directory = ideView.getOrChooseDirectory() ?: return val directory = ideView.getOrChooseDirectory() ?: return
project.executeWriteCommand("Create Kotlin file") { project.executeWriteCommand("Create Kotlin file") {
val file = try { val file = try {
@@ -47,9 +48,9 @@ class KotlinFilePasteProvider : PasteProvider {
if (document != null) { if (document != null) {
document.setText(text) document.setText(text)
documentManager.commitDocument(document) documentManager.commitDocument(document)
val qualName = JavaDirectoryService.getInstance()?.getPackage(directory)?.qualifiedName val qualifiedName = JavaDirectoryService.getInstance()?.getPackage(directory)?.qualifiedName
if (qualName != null && file is KtFile) { if (qualifiedName != null && file is KtFile) {
file.packageFqName = FqName(qualName) file.packageFqName = FqName(qualifiedName)
} }
OpenFileDescriptor(project, file.virtualFile).navigate(true) OpenFileDescriptor(project, file.virtualFile).navigate(true)
} }
@@ -64,7 +65,8 @@ class KotlinFilePasteProvider : PasteProvider {
//todo: KT-25329, to remove these heuristics //todo: KT-25329, to remove these heuristics
if (text.contains(";\n") || if (text.contains(";\n") ||
((text.contains("public interface") || text.contains("public class")) && ((text.contains("public interface") || text.contains("public class")) &&
!text.contains("fun "))) return false //Optimisation for Java. Kotlin doesn't need that... !text.contains("fun "))
) return false //Optimisation for Java. Kotlin doesn't need that...
val file = KtPsiFactory(project).createFile(text) val file = KtPsiFactory(project).createFile(text)
return !PsiTreeUtil.hasErrorElements(file) return !PsiTreeUtil.hasErrorElements(file)
} }
@@ -37,8 +37,7 @@ import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
import java.util.* import java.util.*
class PlainTextPasteImportResolver(val dataForConversion: DataForConversion, val targetFile: KtFile) { class PlainTextPasteImportResolver(private val dataForConversion: DataForConversion, val targetFile: KtFile) {
private val file = dataForConversion.file private val file = dataForConversion.file
private val project = targetFile.project private val project = targetFile.project
@@ -59,8 +58,8 @@ class PlainTextPasteImportResolver(val dataForConversion: DataForConversion, val
private fun canBeImported(descriptor: DeclarationDescriptorWithVisibility?): Boolean { private fun canBeImported(descriptor: DeclarationDescriptorWithVisibility?): Boolean {
return descriptor != null return descriptor != null
&& descriptor.canBeReferencedViaImport() && descriptor.canBeReferencedViaImport()
&& descriptor.isVisible(targetFile, null, bindingContext, resolutionFacade) && descriptor.isVisible(targetFile, null, bindingContext, resolutionFacade)
} }
private fun addImport(importStatement: PsiImportStatementBase, shouldAddToTarget: Boolean = false) { private fun addImport(importStatement: PsiImportStatementBase, shouldAddToTarget: Boolean = false) {
@@ -76,46 +75,54 @@ class PlainTextPasteImportResolver(val dataForConversion: DataForConversion, val
val importedReference = importDirective.importedReference val importedReference = importDirective.importedReference
if (importPath != null && !importPath.hasAlias() && importedReference is KtDotQualifiedExpression) { if (importPath != null && !importPath.hasAlias() && importedReference is KtDotQualifiedExpression) {
val receiver = importedReference val receiver = importedReference
.receiverExpression .receiverExpression
.referenceExpression() .referenceExpression()
?.mainReference ?.mainReference
?.resolve() ?.resolve()
val selector = importedReference val selector = importedReference
.selectorExpression .selectorExpression
?.referenceExpression() ?.referenceExpression()
?.mainReference ?.mainReference
?.resolve() ?.resolve()
val isPackageReceiver = receiver is PsiPackage val isPackageReceiver = receiver is PsiPackage
val isClassReceiver = receiver is PsiClass val isClassReceiver = receiver is PsiClass
val isClassSelector = selector is PsiClass val isClassSelector = selector is PsiClass
if (importPath.isAllUnder) { if (importPath.isAllUnder) {
if (isClassReceiver) when {
addImport(psiElementFactory.createImportStaticStatement(receiver as PsiClass, "*")) isClassReceiver ->
else if (isPackageReceiver) addImport(psiElementFactory.createImportStaticStatement(receiver as PsiClass, "*"))
addImport(psiElementFactory.createImportStatementOnDemand((receiver as PsiPackage).qualifiedName)) isPackageReceiver ->
} addImport(psiElementFactory.createImportStatementOnDemand((receiver as PsiPackage).qualifiedName))
else { }
if (isClassSelector) } else {
addImport(psiElementFactory.createImportStatement(selector as PsiClass)) when {
else if (isClassReceiver) isClassSelector ->
addImport(psiElementFactory.createImportStaticStatement(receiver as PsiClass, importPath.importedName!!.asString())) addImport(psiElementFactory.createImportStatement(selector as PsiClass))
isClassReceiver ->
addImport(
psiElementFactory.createImportStaticStatement(
receiver as PsiClass,
importPath.importedName!!.asString()
)
)
}
} }
} }
} }
if (importList !in dataForConversion.elementsAndTexts.toList()) if (importList !in dataForConversion.elementsAndTexts.toList()) {
runWriteAction { runWriteAction {
targetFile.importDirectives.forEach(::tryConvertKotlinImport) targetFile.importDirectives.forEach(::tryConvertKotlinImport)
} }
}
} }
fun tryResolveReferences() { fun tryResolveReferences() {
val elementsWithUnresolvedRef = PsiTreeUtil.collectElements(file) { element ->
val elementsWithUnresolvedRef = PsiTreeUtil.collectElements(file) { element.reference != null
it.reference != null && element.reference is PsiQualifiedReference
&& it.reference is PsiQualifiedReference && element.reference?.resolve() == null
&& it.reference?.resolve() == null
} }
fun tryResolveReference(reference: PsiQualifiedReference): Boolean { fun tryResolveReference(reference: PsiQualifiedReference): Boolean {
@@ -123,28 +130,27 @@ class PlainTextPasteImportResolver(val dataForConversion: DataForConversion, val
val referenceName = reference.referenceName ?: return false val referenceName = reference.referenceName ?: return false
if (referenceName in failedToResolveReferenceNames) return false if (referenceName in failedToResolveReferenceNames) return false
val classes = shortNameCache.getClassesByName(referenceName, scope) val classes = shortNameCache.getClassesByName(referenceName, scope)
.mapNotNull { psiClass -> .mapNotNull { psiClass ->
val containingFile = psiClass.containingFile val containingFile = psiClass.containingFile
if (ProjectRootsUtil.isInProjectOrLibraryContent(containingFile)) { if (ProjectRootsUtil.isInProjectOrLibraryContent(containingFile)) {
psiClass to psiClass.getJavaMemberDescriptor() as? ClassDescriptor psiClass to psiClass.getJavaMemberDescriptor() as? ClassDescriptor
} } else null
else { }.filter { canBeImported(it.second) }
null
}
}
.filter { canBeImported(it.second) }
classes.find { (_, descriptor) -> JavaToKotlinClassMap.mapPlatformClass(descriptor!!).isNotEmpty() } classes.find { (_, descriptor) ->
?.let { (psiClass, _) -> addImport(psiElementFactory.createImportStatement(psiClass)) } JavaToKotlinClassMap.mapPlatformClass(descriptor!!).isNotEmpty()
}?.let { (psiClass, _) ->
addImport(psiElementFactory.createImportStatement(psiClass))
}
if (reference.resolve() != null) return true if (reference.resolve() != null) return true
classes.singleOrNull()?.let { (psiClass, _) -> classes.singleOrNull()?.let { (psiClass, _) ->
addImport(psiElementFactory.createImportStatement(psiClass), true) addImport(psiElementFactory.createImportStatement(psiClass), true)
} }
if (reference.resolve() != null) return true when {
else { reference.resolve() != null -> return true
if (classes.isNotEmpty()) { classes.isNotEmpty() -> {
ambiguityInResolution = true ambiguityInResolution = true
return false return false
} }
@@ -163,14 +169,10 @@ class PlainTextPasteImportResolver(val dataForConversion: DataForConversion, val
addImport(psiElementFactory.createImportStaticStatement(psiMember.containingClass!!, psiMember.name!!), true) addImport(psiElementFactory.createImportStaticStatement(psiMember.containingClass!!, psiMember.name!!), true)
} }
if (reference.resolve() != null) return false when {
else { reference.resolve() != null -> return false
if (members.isNotEmpty()) { members.isNotEmpty() -> ambiguityInResolution = true
ambiguityInResolution = true else -> couldNotResolve = true
}
else {
couldNotResolve = true
}
} }
return false return false
} }