From a93db7099558d809f71732b6c288ba9c342d1323 Mon Sep 17 00:00:00 2001 From: Natalia Ukhorskaya Date: Tue, 29 Mar 2016 10:52:48 +0300 Subject: [PATCH] Debugger: Convert java expression to kotlin when open EE dialog from Variables View #KT-6805 Fixed --- ChangeLog.md | 1 + .../evaluate/KotlinCodeFragmentFactory.kt | 29 +++- .../tinyApp/outs/createExpressionSimple.out | 32 +++++ .../frame/createExpressionSimple.kt | 19 +++ .../AbstractKotlinEvaluateExpressionTest.kt | 133 ++++++++++++++---- ...KotlinEvaluateExpressionTestGenerated.java | 6 + 6 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 idea/testData/debugger/tinyApp/outs/createExpressionSimple.out create mode 100644 idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/createExpressionSimple.kt diff --git a/ChangeLog.md b/ChangeLog.md index b4d02cbff5f..60553741849 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -26,6 +26,7 @@ - [KT-11638](https://youtrack.jetbrains.com/issue/KT-11638) Fixed hashCode() implementation in "Generate equals/hashCode" action - Configure Kotlin: show only changed files in the notification "Kotlin not configured" - Configure Kotlin: restore all changed files in undo action +- Convert java expression to kotlin when open Evaluate Expression dialog from Variables View ### Java to Kotlin converter diff --git a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt index f5e125ec980..cbb4da9dfec 100644 --- a/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinCodeFragmentFactory.kt @@ -28,6 +28,7 @@ import com.intellij.openapi.util.Key import com.intellij.psi.* import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.IncorrectOperationException import com.intellij.util.concurrency.Semaphore import com.intellij.xdebugger.XDebuggerManager import com.intellij.xdebugger.impl.XDebugSessionImpl @@ -40,9 +41,9 @@ import org.jetbrains.eval4j.jdi.asValue import org.jetbrains.kotlin.asJava.KtLightClass import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils +import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider import org.jetbrains.kotlin.idea.refactoring.j2kText import org.jetbrains.kotlin.idea.refactoring.quoteIfNeeded -import org.jetbrains.kotlin.idea.debugger.KotlinEditorTextProvider import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* @@ -117,7 +118,31 @@ class KotlinCodeFragmentFactory: CodeFragmentFactory() { = wrapContextIfNeeded(project, getContextElement(context)) override fun createPresentationCodeFragment(item: TextWithImports, context: PsiElement?, project: Project): JavaCodeFragment { - return createCodeFragment(item, context, project) + val kotlinCodeFragment = createCodeFragment(item, context, project) + if (PsiTreeUtil.hasErrorElements(kotlinCodeFragment) && kotlinCodeFragment is KtExpressionCodeFragment) { + val javaExpression = try { + PsiElementFactory.SERVICE.getInstance(project).createExpressionFromText(item.text, context) + } + catch(e: IncorrectOperationException) { + null + } + + if (javaExpression != null) { + val newText = javaExpression.j2kText() + if (newText != null) { + val convertedFragment = KtExpressionCodeFragment( + project, + kotlinCodeFragment.name, + newText, + kotlinCodeFragment.importsToString(), + kotlinCodeFragment.context + ) + + return convertedFragment + } + } + } + return kotlinCodeFragment } override fun isContextAccepted(contextElement: PsiElement?): Boolean { diff --git a/idea/testData/debugger/tinyApp/outs/createExpressionSimple.out b/idea/testData/debugger/tinyApp/outs/createExpressionSimple.out new file mode 100644 index 00000000000..bfaf72d4984 --- /dev/null +++ b/idea/testData/debugger/tinyApp/outs/createExpressionSimple.out @@ -0,0 +1,32 @@ +LineBreakpoint created at createExpressionSimple.kt:14 +!JDK_HOME!\bin\java -agentlib:jdwp=transport=dt_socket,address=!HOST_NAME!:!HOST_PORT!,suspend=y,server=n -Dfile.encoding=!FILE_ENCODING! -classpath !OUTPUT_PATH!;!KOTLIN_RUNTIME!;!CUSTOM_LIBRARY!;!RT_JAR! createExpressionSimple.CreateExpressionSimpleKt +Connected to the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' +createExpressionSimple.kt:14 +package createExpressionSimple + +class MyClass: Base() { + val i = 1 +} + +open class Base { + val baseI = 2 +} + +fun main(args: Array) { + val myClass: Base = MyClass() + //Breakpoint! + val a = 1 +} + +// PRINT_FRAME +// DESCRIPTOR_VIEW_OPTIONS: NAME_AND_EXPRESSION + + + frame = main + local = args (expression = args) + local = myClass (expression = myClass) + field = i (expression = (myClass as MyClass).i) + field = baseI (expression = (myClass as MyClass).baseI) +Disconnected from the target VM, address: '!HOST_NAME!:PORT_NAME!', transport: 'socket' + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/createExpressionSimple.kt b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/createExpressionSimple.kt new file mode 100644 index 00000000000..5f4cdb93fda --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/createExpressionSimple.kt @@ -0,0 +1,19 @@ +package createExpressionSimple + +class MyClass: Base() { + val i = 1 +} + +open class Base { + val baseI = 2 +} + +fun main(args: Array) { + val myClass: Base = MyClass() + //Breakpoint! + val a = 1 +} + +// PRINT_FRAME +// DESCRIPTOR_VIEW_OPTIONS: NAME_AND_EXPRESSION + diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt index 3bdd13d6b70..6c387a6e9f2 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/AbstractKotlinEvaluateExpressionTest.kt @@ -23,7 +23,7 @@ import com.intellij.debugger.engine.evaluation.EvaluateException import com.intellij.debugger.engine.evaluation.TextWithImports import com.intellij.debugger.engine.evaluation.TextWithImportsImpl import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilderImpl -import com.intellij.debugger.impl.DescriptorTestCase +import com.intellij.debugger.engine.events.DebuggerCommandImpl import com.intellij.debugger.settings.NodeRendererSettings import com.intellij.debugger.ui.impl.watch.* import com.intellij.debugger.ui.tree.FieldDescriptor @@ -34,6 +34,7 @@ import com.intellij.execution.process.ProcessOutputTypes import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.FileUtil import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression import com.intellij.xdebugger.impl.XDebugSessionImpl import com.intellij.xdebugger.impl.breakpoints.XExpressionImpl import com.intellij.xdebugger.impl.frame.XDebugViewSessionListener @@ -54,8 +55,9 @@ import org.jetbrains.eval4j.jdi.asValue import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerTestBase import org.jetbrains.kotlin.idea.debugger.KotlinFrameExtraVariablesProvider +import org.jetbrains.kotlin.idea.debugger.evaluate.AbstractKotlinEvaluateExpressionTest.PrinterConfig.DescriptorViewOptions import org.jetbrains.kotlin.idea.util.application.runReadAction -import org.jetbrains.kotlin.test.InTextDirectivesUtils +import org.jetbrains.kotlin.test.InTextDirectivesUtils.* import org.junit.Assert import java.io.File import java.util.* @@ -109,13 +111,14 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { configureSettings(fileText) createAdditionalBreakpoints(fileText) - val shouldPrintFrame = InTextDirectivesUtils.isDirectiveDefined(fileText, "// PRINT_FRAME") - val skipInPrintFrame = if (shouldPrintFrame) InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// SKIP: ") else emptyList() + val shouldPrintFrame = isDirectiveDefined(fileText, "// PRINT_FRAME") + val skipInPrintFrame = if (shouldPrintFrame) findLinesWithPrefixesRemoved(fileText, "// SKIP: ") else emptyList() + val descriptorViewOptions = DescriptorViewOptions.valueOf(findStringWithPrefixes(fileText, "// DESCRIPTOR_VIEW_OPTIONS: ") ?: "FULL") val expressions = loadTestDirectivesPairs(fileText, "// EXPRESSION: ", "// RESULT: ") val blocks = findFilesWithBlocks(file).map { FileUtil.loadFile(it, true) } - val expectedBlockResults = blocks.map { InTextDirectivesUtils.findLinesWithPrefixesRemoved(it, "// RESULT: ").joinToString("\n") } + val expectedBlockResults = blocks.map { findLinesWithPrefixesRemoved(it, "// RESULT: ").joinToString("\n") } createDebugProcess(path) @@ -141,7 +144,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { } finally { if (shouldPrintFrame) { - printFrame(variablesView, watchesView, PrinterConfig(skipInPrintFrame)) + printFrame(variablesView, watchesView, PrinterConfig(skipInPrintFrame, descriptorViewOptions)) println(fileText, ProcessOutputTypes.SYSTEM) } else { @@ -203,7 +206,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { val tree = variablesView.tree!! expandAll( tree, - Runnable { + { try { Printer(config).printTree(tree) @@ -218,7 +221,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { }, hashSetOf(), // TODO why this is needed? Otherwise some tests are never ended - DescriptorTestCase.NodeFilter { it !is XValueNodeImpl || it.name != "cause" }, + { it !is XValueNodeImpl || it.name != "cause" }, this ) } @@ -227,9 +230,37 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { return KotlinFrameExtraVariablesProvider().collectVariables(debuggerContext.sourcePosition, evaluationContext, hashSetOf()) } - private class PrinterConfig( - val variablesToSkipInPrintFrame: List = emptyList() - ) + internal class PrinterConfig( + val variablesToSkipInPrintFrame: List = emptyList(), + val descriptorOptionsOptions: DescriptorViewOptions = DescriptorViewOptions.FULL + ) { + enum class DescriptorViewOptions { + FULL, + NAME_AND_EXPRESSION + } + + fun shouldRenderSourcesPosition(): Boolean { + return when(descriptorOptionsOptions) { + DescriptorViewOptions.FULL -> true + else -> false + } + } + + fun shouldRenderExpression(): Boolean { + return when(descriptorOptionsOptions) { + DescriptorViewOptions.NAME_AND_EXPRESSION -> true + else -> false + } + } + + fun renderLabel(descriptor: NodeDescriptorImpl): String { + return when { + descriptor is WatchItemDescriptor -> descriptor.calcValueName() + descriptorOptionsOptions == DescriptorViewOptions.NAME_AND_EXPRESSION -> descriptor.name ?: descriptor.label + else -> descriptor.label + } + } + } private inner class Printer(private val config: PrinterConfig) { fun printTree(tree: XDebuggerTree) { @@ -237,7 +268,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { printNode(root, 0) } - private fun printNode(node: TreeNode, indent: Int) { + private fun printNode(node: TreeNode, indent: Int) { val descriptor = when { node is DebuggerTreeNodeImpl -> node.descriptor node is XValueNodeImpl -> (node.valueContainer as? JavaValue)?.descriptor ?: MessageDescriptor(node.text.toString()) @@ -249,38 +280,80 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { else -> MessageDescriptor(node.toString()) } - if (descriptor != null && printDescriptor(descriptor, indent)) return + if (descriptor != null && printDescriptor(node, descriptor, indent)) return printChildren(node, indent + 2) } - fun printDescriptor(descriptor: NodeDescriptorImpl, indent: Int): Boolean { + fun printDescriptor(node: TreeNode, descriptor: NodeDescriptorImpl, indent: Int): Boolean { if (descriptor is DefaultNodeDescriptor) return true if (config.variablesToSkipInPrintFrame.contains(descriptor.name)) return true - var label = descriptor.label + var label = config.renderLabel(descriptor) + // TODO: update presentation before calc label if (label == NodeDescriptorImpl.UNKNOWN_VALUE_MESSAGE && descriptor is StaticDescriptor) { label = "static = " + NodeRendererSettings.getInstance().classRenderer.renderTypeName(descriptor.type.name()) } if (label.endsWith(XDebuggerUIConstants.COLLECTING_DATA_MESSAGE)) return true - val curIndent = " ".repeat(indent) - when (descriptor) { - is StackFrameDescriptor -> logDescriptor(descriptor, "$curIndent frame = $label\n") - is WatchItemDescriptor -> logDescriptor(descriptor, "$curIndent extra = ${descriptor.calcValueName()}\n") - is LocalVariableDescriptor -> logDescriptor(descriptor, "$curIndent local = $label" - + " (sp = ${render(SourcePositionProvider.getSourcePosition(descriptor, myProject, debuggerContext))})\n") - is StaticDescriptor -> logDescriptor(descriptor, "$curIndent static = $label\n") - is ThisDescriptorImpl -> logDescriptor(descriptor, "$curIndent this = $label\n") - is FieldDescriptor -> logDescriptor(descriptor, "$curIndent field = $label" - + " (sp = ${render(SourcePositionProvider.getSourcePosition(descriptor, myProject, debuggerContext))})\n") - is MessageDescriptor -> logDescriptor(descriptor, "$curIndent - $label\n") - else -> logDescriptor(descriptor, "$curIndent unknown = $label\n") + val builder = StringBuilder() + + with(builder) { + append(" ".repeat(indent + 1)) + append(getPrefix(descriptor)) + append(label) + if (config.shouldRenderSourcesPosition() && hasSourcePosition(descriptor)) { + val sp = SourcePositionProvider.getSourcePosition(descriptor, myProject, debuggerContext) + append(" (sp = ${render(sp)})") + } + + if (config.shouldRenderExpression() && descriptor is ValueDescriptorImpl) { + var expression: PsiExpression? = null + debuggerContext.debugProcess!!.managerThread.invokeAndWait(object : DebuggerCommandImpl() { + override fun action() { + expression = runReadAction { + descriptor.getTreeEvaluation((node as XValueNodeImpl).valueContainer as JavaValue, debuggerContext) as? PsiExpression + } + } + }) + if (expression != null) { + val text = KotlinCodeFragmentFactory().createPresentationCodeFragment( + TextWithImportsImpl(expression!!), debuggerContext.sourcePosition.elementAt, project + ).text + append(" (expression = $text)") + } + } + append("\n") } + + logDescriptor(descriptor, builder.toString()) + return false } + private fun getPrefix(descriptor: NodeDescriptorImpl): String { + val prefix = when (descriptor) { + is StackFrameDescriptor -> "frame" + is WatchItemDescriptor -> "extra" + is LocalVariableDescriptor -> "local" + is StaticDescriptor -> "static" + is ThisDescriptorImpl -> "this" + is FieldDescriptor -> "field" + is MessageDescriptor -> "" + else -> "unknown" + } + return prefix + " ".repeat("unknown ".length - prefix.length) + if (descriptor is MessageDescriptor) " - " else " = " + } + + private fun hasSourcePosition(descriptor: NodeDescriptorImpl): Boolean { + return when (descriptor) { + is LocalVariableDescriptor, + is FieldDescriptor -> true + else -> false + } + } + private fun printChildren(node: TreeNode, indent: Int) { val e = node.children() while (e.hasMoreElements()) { @@ -312,8 +385,8 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { } private fun loadTestDirectivesPairs(fileContent: String, directivePrefix: String, expectedPrefix: String): List> { - val directives = InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileContent, directivePrefix) - val expected = InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileContent, expectedPrefix) + val directives = findLinesWithPrefixesRemoved(fileContent, directivePrefix) + val expected = findLinesWithPrefixesRemoved(fileContent, expectedPrefix) assert(directives.size == expected.size) { "Sizes of test directives are different" } return directives.zip(expected) } @@ -328,7 +401,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() { Assert.assertTrue("KotlinCodeFragmentFactory should be accepted for context element otherwise default evaluator will be called. ContextElement = ${contextElement.text}", KotlinCodeFragmentFactory().isContextAccepted(contextElement)) - val labelsAsText = InTextDirectivesUtils.findLinesWithPrefixesRemoved(contextElement.containingFile.text, "// DEBUG_LABEL: ") + val labelsAsText = findLinesWithPrefixesRemoved(contextElement.containingFile.text, "// DEBUG_LABEL: ") if (labelsAsText.isEmpty()) return contextElement val markupMap = hashMapOf() diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java index e81bdf6d70a..b366910024a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/evaluate/KotlinEvaluateExpressionTestGenerated.java @@ -495,6 +495,12 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame"), Pattern.compile("^(.+)\\.kt$"), true); } + @TestMetadata("createExpressionSimple.kt") + public void testCreateExpressionSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/createExpressionSimple.kt"); + doSingleBreakpointTest(fileName); + } + @TestMetadata("delegatedPropertyInClass.kt") public void testDelegatedPropertyInClass() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/delegatedPropertyInClass.kt");