Watches: Add Kotlin/JVM views to watches window (KT-28134, KT-28087, KT-22250)

Kotlin mode: show only Kotlin variables and captured values. The variable names are Kotlin-friendly.
JVM mode: show all variables available in the current stack position, including synthetic ones.
This commit is contained in:
Yan Zhulanow
2018-12-07 19:58:18 +09:00
parent 5c419fc629
commit 4c681c787d
16 changed files with 328 additions and 9 deletions
@@ -17,15 +17,93 @@
package org.jetbrains.kotlin.idea.debugger
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.LocalVariableProxyImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.xdebugger.frame.XValueChildrenList
import com.sun.jdi.Value
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_PARAMETER_NAME
import org.jetbrains.kotlin.codegen.inline.INLINE_FUN_VAR_SUFFIX
import org.jetbrains.kotlin.codegen.inline.isFakeLocalVariableForInline
import org.jetbrains.kotlin.idea.debugger.evaluate.THIS_NAME
class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDescriptorImpl(frame, MethodsTracker()), true) {
override fun getVisibleVariables(): List<LocalVariableProxyImpl>? {
return super.getStackFrameProxy().visibleVariablesSafe()
.filter { !isFakeLocalVariableForInline(it.name()) }
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun superBuildVariables(evaluationContext: EvaluationContextImpl, children: XValueChildrenList) {
if (!kotlinVariableViewService.kotlinVariableView) {
return super.superBuildVariables(evaluationContext, children)
}
val nodeManager = evaluationContext.debugProcess.xdebugProcess!!.nodeManager
fun addItem(variable: LocalVariableProxyImpl) {
val variableDescriptor = nodeManager.getLocalVariableDescriptor(null, variable)
children.add(JavaValue.create(null, variableDescriptor, evaluationContext, nodeManager, false))
}
val (thisReferences, otherVariables) = visibleVariables
.partition { it.name() == THIS_NAME || it.name().startsWith("$THIS_NAME ") }
thisReferences.forEach(::addItem)
otherVariables.forEach(::addItem)
}
override fun getVisibleVariables(): List<LocalVariableProxyImpl> {
val allVisibleVariables = super.getStackFrameProxy().safeVisibleVariables()
if (!kotlinVariableViewService.kotlinVariableView) {
return allVisibleVariables.map { variable ->
if (isFakeLocalVariableForInline(variable.name())) variable.wrapSyntheticInlineVariable() else variable
}
}
return allVisibleVariables.asSequence()
.filter { !isHidden(it) }
.map { remapVariableName(it) }
.distinctBy { it.name() }
.toList()
.sortedBy { it.variable }
}
private fun isHidden(variable: LocalVariableProxyImpl): Boolean {
val name = variable.name()
return isFakeLocalVariableForInline(name)
|| name.endsWith(INLINE_FUN_VAR_SUFFIX)
|| name == CONTINUATION_PARAMETER_NAME.asString()
}
private fun remapVariableName(variable: LocalVariableProxyImpl) = with(variable) {
when {
isLabeledThisReference() -> {
val label = variable.name().drop(AsmUtil.LABELED_THIS_PARAMETER.length)
clone(withName = "$THIS_NAME (@$label)")
}
else -> variable
}
}
private fun LocalVariableProxyImpl.isLabeledThisReference(): Boolean {
@Suppress("ConvertToStringTemplate")
return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
}
}
private fun LocalVariableProxyImpl.clone(withName: String): LocalVariableProxyImpl {
return object : LocalVariableProxyImpl(frame, variable) {
override fun name() = withName
}
}
private fun LocalVariableProxyImpl.wrapSyntheticInlineVariable(): LocalVariableProxyImpl {
val proxyWrapper = object : StackFrameProxyImpl(frame.threadProxy(), frame.stackFrame, frame.indexFromBottom) {
override fun getValue(localVariable: LocalVariableProxyImpl): Value {
return frame.virtualMachine.mirrorOfVoid()
}
}
return LocalVariableProxyImpl(proxyWrapper, variable)
}
@@ -0,0 +1,52 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.debugger
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.components.ServiceManager
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.impl.XDebuggerUtilImpl
import org.jetbrains.kotlin.idea.KotlinFileTypeFactory
class ToggleKotlinVariablesState {
companion object {
private const val KOTLIN_VARIABLE_VIEW = "debugger.kotlin.variable.view"
fun getService(): ToggleKotlinVariablesState {
return ServiceManager.getService(ToggleKotlinVariablesState::class.java)
}
}
var kotlinVariableView = PropertiesComponent.getInstance().getBoolean(KOTLIN_VARIABLE_VIEW, true)
set(newValue) {
field = newValue
PropertiesComponent.getInstance().setValue(KOTLIN_VARIABLE_VIEW, newValue)
}
}
class ToggleKotlinVariablesView : ToggleAction() {
private val kotlinVariableViewService = ToggleKotlinVariablesState.getService()
override fun update(e: AnActionEvent) {
super.update(e)
val session = XDebugSession.DATA_KEY.getData(e.dataContext)
e.presentation.isEnabledAndVisible = session != null && session.isInKotlinFile()
}
private fun XDebugSession.isInKotlinFile(): Boolean {
val fileExtension = currentPosition?.file?.extension ?: return false
return fileExtension in KotlinFileTypeFactory.KOTLIN_EXTENSIONS
}
override fun isSelected(e: AnActionEvent) = kotlinVariableViewService.kotlinVariableView
override fun setSelected(e: AnActionEvent, state: Boolean) {
kotlinVariableViewService.kotlinVariableView = state
XDebuggerUtilImpl.rebuildAllSessionsViews(e.project)
}
}
@@ -470,15 +470,12 @@ class KotlinEvaluator(val codeFragment: KtCodeFragment, val sourcePosition: Sour
val parameters = mutableListOf<Parameter>()
val receiver = config.descriptor.receiverParameter
if (receiver != null) {
parameters += Parameter(THIS_NAME, receiver.getParameterType(true))
parameters += Parameter(THIS_NAME + "@" + config.descriptor.name, receiver.getParameterType(true))
}
for (param in config.descriptor.parameters) {
val paramName = when {
param.argumentText.contains("@") -> param.argumentText.substringBefore("@")
param.argumentText.startsWith("::") -> param.argumentText.substring(2)
else -> param.argumentText
}
val argument = param.argumentText
val paramName = if (argument.startsWith("::")) argument.substring(2) else argument
val paramDescriptor = param.originalDescriptor
if (paramDescriptor is SyntheticFieldDescriptor) {
+13
View File
@@ -61,6 +61,17 @@
<action id="AddToProblemApiInspection" class="org.jetbrains.kotlin.idea.inspections.api.AddIncompatibleApiAction"
text="Report as incompatible API">
</action>
<group id="Kotlin.XDebugger.Actions">
<action id="Kotlin.XDebugger.ToggleKotlinVariableView"
class="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesView"
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
</group>
<group id="Kotlin.XDebugger.Watches.Tree.Toolbar">
<reference ref="Kotlin.XDebugger.ToggleKotlinVariableView"/>
<add-to-group group-id="XDebugger.Watches.Tree.Toolbar" relative-to-action="XDebugger.SwitchWatchesInVariables" anchor="after"/>
</group>
</actions>
<extensions defaultExtensionNs="com.intellij">
@@ -77,6 +88,8 @@
<projectService serviceImplementation="org.jetbrains.kotlin.idea.versions.SuppressNotificationState"/>
<applicationService serviceImplementation="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSmartStepIntoHandler"/>
<debugger.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>
+13
View File
@@ -61,6 +61,17 @@
<action id="AddToProblemApiInspection" class="org.jetbrains.kotlin.idea.inspections.api.AddIncompatibleApiAction"
text="Report as incompatible API">
</action>
<group id="Kotlin.XDebugger.Actions">
<action id="Kotlin.XDebugger.ToggleKotlinVariableView"
class="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesView"
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
</group>
<group id="Kotlin.XDebugger.Watches.Tree.Toolbar">
<reference ref="Kotlin.XDebugger.ToggleKotlinVariableView"/>
<add-to-group group-id="XDebugger.Watches.Tree.Toolbar" relative-to-action="XDebugger.SwitchWatchesInVariables" anchor="after"/>
</group>
</actions>
<extensions defaultExtensionNs="com.intellij">
@@ -77,6 +88,8 @@
<projectService serviceImplementation="org.jetbrains.kotlin.idea.versions.SuppressNotificationState"/>
<applicationService serviceImplementation="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSmartStepIntoHandler"/>
<debugger.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>
@@ -33,7 +33,9 @@ class A {
local = element: int = 1 (sp = frameInlineArgument.kt, 4)
local = this_$iv: frameInlineArgument.A = {frameInlineArgument.A@uniqueID} (sp = null)
field = prop: int = 1 (sp = frameInlineArgument.kt, 17)
local = $i$f$inlineFun: int = undefined (sp = null)
local = element$iv: double = 1.0 (sp = frameInlineArgument.kt, 4)
local = $i$a$-inlineFun-FrameInlineArgumentKt$main$1: int = undefined (sp = null)
Disconnected from the target VM
Process finished with exit code 0
@@ -47,11 +47,14 @@ fun main(args: Array<String>) {
local = element: float = 1.0 (sp = frameInlineArgumentInsideInlineFun.kt, 13)
local = this_$iv: frameInlineArgumentInsideInlineFun.B = {frameInlineArgumentInsideInlineFun.B@uniqueID} (sp = null)
- Class has no fields
local = $i$f$foo: int = undefined (sp = null)
local = element$iv: int = 1 (sp = frameInlineArgumentInsideInlineFun.kt, 13)
local = this_$iv$iv: frameInlineArgumentInsideInlineFun.A = {frameInlineArgumentInsideInlineFun.A@uniqueID} (sp = null)
- Class has no fields
local = $i$f$inlineFun: int = undefined (sp = null)
local = element$iv$iv: double = 1.0 (sp = frameInlineArgumentInsideInlineFun.kt, 13)
local = it$iv: int = 1 (sp = null)
local = $i$a$-inlineFun-B$foo$1: int = undefined (sp = null)
Disconnected from the target VM
Process finished with exit code 0
@@ -37,6 +37,7 @@ class A {
local = element: int = 1 (sp = frameInlineFun.kt, 12)
local = this_$iv: frameInlineFun.A = {frameInlineFun.A@uniqueID} (sp = null)
field = prop: int = 1 (sp = frameInlineFun.kt, 17)
local = $i$f$inlineFun: int = undefined (sp = null)
local = element$iv: double = 1.0 (sp = frameInlineFun.kt, 12)
Disconnected from the target VM
@@ -60,11 +60,13 @@ fun main(args: Array<String>) {
local = element: float = 1.0 (sp = frameInlineFunCallInsideInlineFun.kt, 5)
local = this_$iv: frameInlineFunCallInsideInlineFun.B = {frameInlineFunCallInsideInlineFun.B@uniqueID} (sp = null)
- Class has no fields
local = $i$f$foo: int = undefined (sp = null)
local = element$iv: int = 2 (sp = frameInlineFunCallInsideInlineFun.kt, 5)
local = a$iv: frameInlineFunCallInsideInlineFun.A = {frameInlineFunCallInsideInlineFun.A@uniqueID} (sp = null)
field = prop: int = 1 (sp = frameInlineFunCallInsideInlineFun.kt, 10)
local = this_$iv$iv: frameInlineFunCallInsideInlineFun.A = {frameInlineFunCallInsideInlineFun.A@uniqueID} (sp = null)
field = prop: int = 1 (sp = frameInlineFunCallInsideInlineFun.kt, 10)
local = $i$f$inlineFun: int = undefined (sp = null)
local = element$iv$iv: double = 1.0 (sp = frameInlineFunCallInsideInlineFun.kt, 5)
Disconnected from the target VM
@@ -0,0 +1,49 @@
// SHOW_KOTLIN_VARIABLES
package frameInlineFunCallInsideInlineFunKotlinVariables
class A {
inline fun inlineFun(s: (Int) -> Unit) {
val element = 1.0
//TODO breakpoint here doesn't work (not only in tests)
s(1)
}
val prop = 1
}
class B {
inline fun foo(s: (Int) -> Unit) {
val element = 2
val a = A()
// STEP_INTO: 1
// STEP_OVER: 1
//Breakpoint!
a.inlineFun {
val e = element
}
s(1)
}
}
class C {
fun bar() {
val element = 1f
B().foo {
val e = element
}
}
}
fun main(args: Array<String>) {
C().bar()
}
// PRINT_FRAME
// EXPRESSION: element
// RESULT: 1.0: D
// EXPRESSION: this.prop
// RESULT: 1: I
@@ -0,0 +1,65 @@
LineBreakpoint created at frameInlineFunCallInsideInlineFunKotlinVariables.kt:22
Run Java
Connected to the target VM
frameInlineFunCallInsideInlineFunKotlinVariables.kt:22
frameInlineFunCallInsideInlineFunKotlinVariables.kt:7
frameInlineFunCallInsideInlineFunKotlinVariables.kt:9
Compile bytecode for element
Compile bytecode for this.prop
// SHOW_KOTLIN_VARIABLES
package frameInlineFunCallInsideInlineFunKotlinVariables
class A {
inline fun inlineFun(s: (Int) -> Unit) {
val element = 1.0
//TODO breakpoint here doesn't work (not only in tests)
s(1)
}
val prop = 1
}
class B {
inline fun foo(s: (Int) -> Unit) {
val element = 2
val a = A()
// STEP_INTO: 1
// STEP_OVER: 1
//Breakpoint!
a.inlineFun {
val e = element
}
s(1)
}
}
class C {
fun bar() {
val element = 1f
B().foo {
val e = element
}
}
}
fun main(args: Array<String>) {
C().bar()
}
// PRINT_FRAME
// EXPRESSION: element
// RESULT: 1.0: D
// EXPRESSION: this.prop
// RESULT: 1: I
frame = bar:9, C {frameInlineFunCallInsideInlineFunKotlinVariables}
this = this = {frameInlineFunCallInsideInlineFunKotlinVariables.C@uniqueID}
- Class has no fields
local = element: float = 1.0 (sp = frameInlineFunCallInsideInlineFunKotlinVariables.kt, 7)
Disconnected from the target VM
Process finished with exit code 0
@@ -24,6 +24,8 @@ inline fun foo(f: () -> Unit) {
frame = main:7, FrameSharedVarLocalVarKt {frameSharedVarLocalVar}
local = args: java.lang.String[] = {java.lang.String[0]@uniqueID} (sp = frameSharedVarLocalVar.kt, 3)
local = var1: int = 1 (sp = frameSharedVarLocalVar.kt, 4)
local = $i$f$foo: int = undefined (sp = null)
local = $i$a$-foo-FrameSharedVarLocalVarKt$main$1: int = undefined (sp = null)
Disconnected from the target VM
Process finished with exit code 0
@@ -0,0 +1,16 @@
package kt28087
fun main() {
"a".indexed { index, c ->
//Breakpoint!
val a = 5
}
}
private inline fun CharSequence.indexed(action: (index: Int, Char) -> Unit): Unit {
var index = 0
for (item in this) action(index++, item)
}
// EXPRESSION: index
// RESULT: 0: I
@@ -0,0 +1,8 @@
LineBreakpoint created at kt28087.kt:6
Run Java
Connected to the target VM
kt28087.kt:6
Compile bytecode for index
Disconnected from the target VM
Process finished with exit code 0
@@ -42,6 +42,7 @@ 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.ToggleKotlinVariablesState
import org.jetbrains.kotlin.idea.debugger.evaluate.AbstractKotlinEvaluateExpressionTest.PrinterConfig.DescriptorViewOptions
import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread
import org.jetbrains.kotlin.idea.util.application.runReadAction
@@ -57,6 +58,7 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
private var appender: AppenderSkeleton? = null
private var oldLogLevel: Level? = null
private var oldShowKotlinVariables: Boolean = false
private var oldShowFqTypeNames = false
override fun setUp() {
@@ -66,6 +68,8 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
oldShowFqTypeNames = classRenderer.SHOW_FQ_TYPE_NAMES
classRenderer.SHOW_FQ_TYPE_NAMES = true
oldShowKotlinVariables = ToggleKotlinVariablesState.getService().kotlinVariableView
oldLogLevel = logger.level
logger.level = Level.DEBUG
@@ -82,6 +86,8 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
}
override fun tearDown() {
ToggleKotlinVariablesState.getService().kotlinVariableView = oldShowKotlinVariables
logger.level = oldLogLevel
logger.removeAppender(appender)
@@ -104,6 +110,8 @@ abstract class AbstractKotlinEvaluateExpressionTest : KotlinDebuggerTestBase() {
val skipInPrintFrame = if (shouldPrintFrame) findListWithPrefixes(fileText, "// SKIP: ") else emptyList()
val descriptorViewOptions = DescriptorViewOptions.valueOf(findStringWithPrefixes(fileText, "// DESCRIPTOR_VIEW_OPTIONS: ") ?: "FULL")
ToggleKotlinVariablesState.getService().kotlinVariableView = isDirectiveDefined(fileText, "// SHOW_KOTLIN_VARIABLES")
val expressions = loadTestDirectivesPairs(fileText, "// EXPRESSION: ", "// RESULT: ")
val blocks = findFilesWithBlocks(file).map { FileUtil.loadFile(it, true) }
@@ -231,6 +231,11 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/kt22366.kt");
}
@TestMetadata("kt28087.kt")
public void testKt28087() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/kt28087.kt");
}
@TestMetadata("kt5554OnlyIntsShouldBeCoerced.kt")
public void testKt5554OnlyIntsShouldBeCoerced() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/kt5554OnlyIntsShouldBeCoerced.kt");
@@ -655,6 +660,11 @@ public class KotlinEvaluateExpressionTestGenerated extends AbstractKotlinEvaluat
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/frameInlineFunCallInsideInlineFun.kt");
}
@TestMetadata("frameInlineFunCallInsideInlineFunKotlinVariables.kt")
public void testFrameInlineFunCallInsideInlineFunKotlinVariables() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/frameInlineFunCallInsideInlineFunKotlinVariables.kt");
}
@TestMetadata("frameInnerClass.kt")
public void testFrameInnerClass() throws Exception {
runTest("idea/testData/debugger/tinyApp/src/evaluate/singleBreakpoint/frame/frameInnerClass.kt");