From c6380e6910e6e1a0142e5ee30ecbaf18134d5229 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Thu, 26 Sep 2024 16:20:15 +0200 Subject: [PATCH] [debugger, kotlin] Smart step into: rewrite with debugger suspend command GitOrigin-RevId: ba8e3f7a57e081c7c57d569aad3b887cbe7de67c --- .../engine/DebuggerManagerThreadImpl.kt | 16 +++- .../KotlinSmartStepIntoHandler.kt | 88 +++++++------------ .../KotlinSmartStepTargetFilterer.kt | 43 +++++---- .../KotlinSmartStepTargetFiltererAdapter.kt | 71 ++++++++++++--- .../KotlinDescriptorTestCaseWithStepping.kt | 46 ++++++---- 5 files changed, 158 insertions(+), 106 deletions(-) diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/DebuggerManagerThreadImpl.kt b/java/debugger/impl/src/com/intellij/debugger/engine/DebuggerManagerThreadImpl.kt index d1017b0b27ee..6696add2653b 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/DebuggerManagerThreadImpl.kt +++ b/java/debugger/impl/src/com/intellij/debugger/engine/DebuggerManagerThreadImpl.kt @@ -296,6 +296,18 @@ class DebuggerManagerThreadImpl(parent: Disposable, private val parentScope: Cor @ApiStatus.Experimental fun invokeCommandAsCompletableFuture(action: suspend () -> T): CompletableFuture { DebuggerManagerThreadImpl.assertIsManagerThread() + val managerThread = InvokeThread.currentThread() as DebuggerManagerThreadImpl + val command = DebuggerManagerThreadImpl.getCurrentCommand() + val priority = command?.priority ?: PrioritizedTask.Priority.LOW + val suspendContext = (command as? SuspendContextCommandImpl)?.suspendContext + return invokeCommandAsCompletableFuture(managerThread, priority, suspendContext, action) +} + +@ApiStatus.Experimental +fun invokeCommandAsCompletableFuture(managerThread: DebuggerManagerThreadImpl, + priority: PrioritizedTask.Priority = PrioritizedTask.Priority.LOW, + suspendContext: SuspendContextImpl? = null, + action: suspend () -> T): CompletableFuture { val res = DebuggerCompletableFuture() suspend fun doRun() { @@ -307,10 +319,6 @@ fun invokeCommandAsCompletableFuture(action: suspend () -> T): CompletableFu } } - val managerThread = InvokeThread.currentThread() as DebuggerManagerThreadImpl - val command = DebuggerManagerThreadImpl.getCurrentCommand() - val priority = command?.priority ?: PrioritizedTask.Priority.LOW - val suspendContext = (command as? SuspendContextCommandImpl)?.suspendContext if (suspendContext != null) { managerThread.invoke(object : SuspendContextCommandImpl(suspendContext) { override suspend fun contextActionSuspend(suspendContext: SuspendContextImpl) = doRun() diff --git a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepIntoHandler.kt b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepIntoHandler.kt index 500b0cd53a62..7b0b1e6bf4e3 100644 --- a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepIntoHandler.kt +++ b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepIntoHandler.kt @@ -10,23 +10,23 @@ import com.intellij.debugger.actions.SmartStepTarget import com.intellij.debugger.engine.DebugProcessImpl import com.intellij.debugger.engine.DebuggerManagerThreadImpl import com.intellij.debugger.engine.MethodFilter -import com.intellij.debugger.engine.events.DebuggerCommandImpl +import com.intellij.debugger.engine.invokeCommandAsCompletableFuture import com.intellij.debugger.impl.DebuggerSession import com.intellij.debugger.impl.PrioritizedTask import com.intellij.debugger.jdi.MethodBytecodeUtil import com.intellij.debugger.statistics.DebuggerStatistics import com.intellij.debugger.statistics.Engine -import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.readAction import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.util.registry.Registry import com.intellij.psi.PsiDocumentManager import com.intellij.psi.util.parents import com.intellij.util.Range import com.intellij.util.containers.OrderedSet import com.sun.jdi.Location -import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.concurrency.Promise -import org.jetbrains.concurrency.compute +import org.jetbrains.concurrency.asPromise import org.jetbrains.kotlin.idea.base.psi.getTopmostElementAtOffset import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerSettings import org.jetbrains.kotlin.idea.debugger.base.util.* @@ -48,28 +48,15 @@ class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() { super.findStepIntoTargets(position, session) } - override fun findSmartStepTargetsAsync(position: SourcePosition, session: DebuggerSession): Promise> { - val result = AsyncPromise>() - val command = - object : DebuggerCommandImpl(PrioritizedTask.Priority.NORMAL) { - override fun action() = - result.compute { findSmartStepTargetsInReadAction(position, session) } - - override fun commandCancelled() { - result.setError("Cancelled") - } - } - val managerThread = session.process.managerThread - if (DebuggerManagerThreadImpl.isManagerThread()) { - managerThread.invoke(command) - } else { - managerThread.schedule(command) - } - return result - } + override fun findSmartStepTargetsAsync(position: SourcePosition, session: DebuggerSession): Promise> = + invokeCommandAsCompletableFuture(session.process.managerThread, PrioritizedTask.Priority.NORMAL) { + findSmartStepTargetsInternal(position, session) + }.asPromise() override fun findSmartStepTargets(position: SourcePosition): List = - findSmartStepTargetsInReadAction(position, null) + runBlockingCancellable { + findSmartStepTargetsInternal(position, null) + } override fun createMethodFilter(stepTarget: SmartStepTarget?): MethodFilter? = when (stepTarget) { @@ -77,26 +64,24 @@ class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() { else -> super.createMethodFilter(stepTarget) } - private fun findSmartStepTargetsInReadAction(position: SourcePosition, session: DebuggerSession?) = - ReadAction.nonBlocking> { - try { - findSmartStepTargets(position, session) - } catch (e: Exception) { - DebuggerStatistics.logSmartStepIntoTargetsDetection(session?.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.INTERNAL_ERROR) - throw e - } - }.executeSynchronously() + internal suspend fun findSmartStepTargetsInternal(position: SourcePosition, session: DebuggerSession?): List = + try { + findSmartStepTargets(position, session) + } catch (e: Exception) { + DebuggerStatistics.logSmartStepIntoTargetsDetection(session?.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.INTERNAL_ERROR) + throw e + } - private fun findSmartStepTargets(position: SourcePosition, session: DebuggerSession?): List { - val expression = position.getContainingExpression() ?: run { + private suspend fun findSmartStepTargets(position: SourcePosition, session: DebuggerSession?): List { + val expression = readAction { position.getContainingExpression() } ?: run { DebuggerStatistics.logSmartStepIntoTargetsDetection(session?.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.INVALID_POSITION) return emptyList() } - val lines = expression.getLines()?.coerceAtLeast(position.line) ?: run { + val lines = readAction { expression.getLines()?.coerceAtLeast(position.line) } ?: run { DebuggerStatistics.logSmartStepIntoTargetsDetection(session?.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.INVALID_POSITION) return emptyList() } - var targets = findSmartStepTargets(expression, lines) + var targets = readAction { findSmartStepTargets(expression, lines) } if (session != null) { val currentMethodName = session.process.suspendManager.pausedContext?.frameProxy?.safeLocation()?.safeMethod()?.name() // Cannot analyze method calls in the default method body, as they are located in a different method in bytecode @@ -109,8 +94,8 @@ class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() { } else { DebuggerStatistics.logSmartStepIntoTargetsDetection(position.file.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.BYTECODE_NOT_AVAILABLE) } - targets = targets.sortedBy { it.highlightElement?.textOffset ?: 0 } - return reorderWithSteppingFilters(targets) + targets = readAction { targets.sortedBy { it.highlightElement?.textOffset ?: 0 } } + return readAction { reorderWithSteppingFilters(targets) } } } @@ -121,16 +106,16 @@ private fun findSmartStepTargets(element: KtElement, lines: Range): List, context: SmartStepIntoContext): List { +private suspend fun calculateSmartStepTargetsToShow(targets: List, context: SmartStepIntoContext): List { val methodTargets = targets.filterIsInstance() val notYetExecutedMethodTargets = methodTargets.filterAlreadyExecuted(context).toHashSet() val targetsToShow = targets.filter { it !is KotlinMethodSmartStepTarget || it in notYetExecutedMethodTargets } val removed = methodTargets.toHashSet() - notYetExecutedMethodTargets - fixOrdinalsAfterFiltering(targets, removed) + readAction { fixOrdinalsAfterFiltering(targets, removed) } return targetsToShow } -private fun List.filterAlreadyExecuted(context: SmartStepIntoContext): List { +private suspend fun List.filterAlreadyExecuted(context: SmartStepIntoContext): List { DebuggerManagerThreadImpl.assertIsManagerThread() val debugProcess = context.debugProcess if (isEmpty()) { @@ -216,7 +201,7 @@ private fun KtElement.getLines(): Range? { private fun Range.coerceAtLeast(value: Int): Range = if (from >= value && to >= value) this else Range(max(value, from), max(value, to)) -private fun filterSmartStepTargets( +private suspend fun filterSmartStepTargets( location: Location, targets: List, context: SmartStepIntoContext, @@ -226,21 +211,11 @@ private fun filterSmartStepTargets( DebuggerStatistics.logSmartStepIntoTargetsDetection(debugProcess.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.BYTECODE_NOT_AVAILABLE) return targets } - val targetFilterer = KotlinSmartStepTargetFilterer(targets, debugProcess) - val targetFiltererAdapter = KotlinSmartStepTargetFiltererAdapter( - lines, location, debugProcess.positionManager, targetFilterer - ) + val targetFiltererAdapter = KotlinSmartStepTargetFiltererAdapter(lines, location) - var unvisitedTargets: List? = null MethodBytecodeUtil.visit(method, Long.MAX_VALUE, object : OpcodeReportingMethodVisitor(targetFiltererAdapter), MethodBytecodeUtil.InstructionOffsetReader { - private var stopCollectingVisitedTargets = false - override fun readBytecodeInstructionOffset(offset: Int) { targetFiltererAdapter.currentOffset = offset.toLong() - if (!stopCollectingVisitedTargets && offset >= location.codeIndex()) { - unvisitedTargets = targetFilterer.getUnvisitedTargets() - stopCollectingVisitedTargets = true - } } override fun reportOpcode(opcode: Int) { @@ -253,7 +228,8 @@ private fun filterSmartStepTargets( } }, true) - val unvisitedAtTheEnd = targetFilterer.getUnvisitedTargets() + val targetFilterer = KotlinSmartStepTargetFilterer(targets, debugProcess) + val (unvisitedTargets, unvisitedAtTheEnd) = targetFiltererAdapter.visitTrace(targetFilterer, debugProcess.positionManager) if (unvisitedAtTheEnd.isNotEmpty()) { val targetStrings = unvisitedAtTheEnd.map { "Target(name=${it.methodInfo.name}, ordinal=${it.ordinal})" } val session = debugProcess.session @@ -267,7 +243,7 @@ private fun filterSmartStepTargets( } else { DebuggerStatistics.logSmartStepIntoTargetsDetection(debugProcess.project, Engine.KOTLIN, SmartStepIntoDetectionStatus.SUCCESS) } - return unvisitedTargets!! + return unvisitedTargets } private fun Range.toClosedRange() = from..to diff --git a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFilterer.kt b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFilterer.kt index 1af0d7205373..b72511cccae7 100644 --- a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFilterer.kt +++ b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFilterer.kt @@ -3,6 +3,8 @@ package org.jetbrains.kotlin.idea.debugger.stepping.smartStepInto import com.intellij.debugger.engine.DebugProcessImpl import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.runReadAction import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.psi.PsiPrimitiveType @@ -36,10 +38,12 @@ class KotlinSmartStepTargetFilterer( private val functionCounter = mutableMapOf() private val targetWasVisited = BooleanArray(targets.size) { false } - fun visitInlineFunction(function: KtNamedFunction) { - val label = analyze(function) { - val symbol = function.symbol - KotlinMethodSmartStepTarget.calcLabel(symbol) + suspend fun visitInlineFunction(function: KtNamedFunction) { + val label = readAction { + analyze(function) { + val symbol = function.symbol + KotlinMethodSmartStepTarget.calcLabel(symbol) + } } val currentCount = functionCounter.increment(label) - 1 val matchedSteppingTargetIndex = targets.indexOfFirst { @@ -49,7 +53,7 @@ class KotlinSmartStepTargetFilterer( targetWasVisited[matchedSteppingTargetIndex] = true } - fun visitOrdinaryFunction(owner: String, name: String, signature: String) { + suspend fun visitOrdinaryFunction(owner: String, name: String, signature: String) { val currentCount = functionCounter.increment("$owner.$name$signature") - 1 for ((i, target) in targets.withIndex()) { if (targetWasVisited[i]) continue @@ -60,7 +64,7 @@ class KotlinSmartStepTargetFilterer( } } - private fun KotlinMethodSmartStepTarget.shouldBeVisited(owner: String, name: String, signature: String, currentCount: Int): Boolean { + private suspend fun KotlinMethodSmartStepTarget.shouldBeVisited(owner: String, name: String, signature: String, currentCount: Int): Boolean { val (updatedOwner, updatedName, updatedSignature) = BytecodeSignature(owner, name, signature) .handleMangling(methodInfo) .handleValueClassMethods(methodInfo) @@ -71,7 +75,7 @@ class KotlinSmartStepTargetFilterer( return matches(updatedOwner, updatedName, updatedSignature, currentCount) } - private fun KotlinMethodSmartStepTarget.matches(owner: String, name: String, signature: String, currentCount: Int): Boolean { + private suspend fun KotlinMethodSmartStepTarget.matches(owner: String, name: String, signature: String, currentCount: Int): Boolean { if (ordinal != currentCount) return false val nameMatches = methodNameMatches(methodInfo, name) if (!nameMatches) return false @@ -86,16 +90,18 @@ class KotlinSmartStepTargetFilterer( "equals" -> return signature == "(Ljava/lang/Object;)Z" } // it means the method is, in fact, the implicit primary constructor - analyze(declaration) { - return primaryConstructorMatches(declaration, owner, name, signature) + return readAction { + analyze(declaration) { + primaryConstructorMatches(declaration, owner, name, signature) + } } } if (!methodInfo.isInlineClassMember) { // Cannot create light class for functions with inline classes - val lightMethod = declaration.getLightClassMethod() + val lightMethod = readAction { declaration.getLightClassMethod() } // Do not match by name, as it was already checked - val lightMethodMatch = lightMethod?.matches(owner, signature, debugProcess) + val lightMethodMatch = runReadAction { lightMethod?.matches(owner, signature, debugProcess) } // Light method match still can fail in some Kotlin-specific cases (e.g., setter/getter signature) if (lightMethodMatch == true) { return true @@ -112,14 +118,15 @@ class KotlinSmartStepTargetFilterer( return owner == internalClassName } - private fun matchesBySignature(declaration: KtDeclaration, owner: String, signature: String): Boolean { - analyze(declaration) { - val symbol = declaration.symbol as? KaCallableSymbol ?: return false - val declarationSignature = symbol.getJvmSignature() - val declarationInternalName = symbol.getJvmInternalClassName() - return signature == declarationSignature && owner.isSubClassOf(declarationInternalName) + private suspend fun matchesBySignature(declaration: KtDeclaration, owner: String, signature: String): Boolean = + readAction { + analyze(declaration) { + val symbol = declaration.symbol as? KaCallableSymbol ?: return@analyze false + val declarationSignature = symbol.getJvmSignature() + val declarationInternalName = symbol.getJvmInternalClassName() + signature == declarationSignature && owner.isSubClassOf(declarationInternalName) + } } - } fun getUnvisitedTargets(): List = targets.filterIndexed { i, _ -> diff --git a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFiltererAdapter.kt b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFiltererAdapter.kt index 33aaa6f6f3fb..8967aaa23635 100644 --- a/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFiltererAdapter.kt +++ b/plugins/kotlin/jvm-debugger/core/src/org/jetbrains/kotlin/idea/debugger/stepping/smartStepInto/KotlinSmartStepTargetFiltererAdapter.kt @@ -1,24 +1,36 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.kotlin.idea.debugger.stepping.smartStepInto -import com.intellij.debugger.PositionManager +import com.intellij.debugger.engine.PositionManagerAsync +import com.intellij.openapi.application.readAction import com.intellij.psi.util.parentOfType import com.sun.jdi.Location +import kotlinx.coroutines.future.await import org.jetbrains.kotlin.idea.debugger.base.util.safeMethod import org.jetbrains.kotlin.idea.debugger.core.getInlineFunctionAndArgumentVariablesToBordersMap import org.jetbrains.kotlin.idea.debugger.core.isInlineFunctionMarkerVariableName import org.jetbrains.kotlin.psi.KtNamedFunction -class KotlinSmartStepTargetFiltererAdapter( +internal class KotlinSmartStepTargetFiltererAdapter( lines: ClosedRange, location: Location, - private val positionManager: PositionManager, - private val targetFilterer: KotlinSmartStepTargetFilterer ) : LineMatchingMethodVisitor(lines) { private val inlineCalls = extractInlineCalls(location) + private val targetOffset = location.codeIndex() private var inInline = false internal var currentOffset: Long = -1 + private val visitedTrace = mutableListOf() + private val unvisitedTrace = mutableListOf() + + private fun add(e: BytecodeTraceElement) { + if (currentOffset < targetOffset) { + visitedTrace += e + } else { + unvisitedTrace += e + } + } + public override fun reportOpcode(opcode: Int) { if (!lineEverMatched) return val inlineCall = inlineCalls.firstOrNull { currentOffset in it.bciRange } @@ -33,19 +45,54 @@ class KotlinSmartStepTargetFiltererAdapter( inInline = true if (!inlineCall.variableName.isInlineFunctionMarkerVariableName) return - - val calledInlineFunction = getCalledInlineFunction(positionManager, inlineCall.startLocation) ?: return - targetFilterer.visitInlineFunction(calledInlineFunction) + add(BytecodeTraceElement.InlineCall(inlineCall)) } override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) { if (lineMatches) { - targetFilterer.visitOrdinaryFunction(owner, name, descriptor) + add(BytecodeTraceElement.MethodCall(owner, name, descriptor)) + } + } + + suspend fun visitTrace( + targetFilterer: KotlinSmartStepTargetFilterer, + positionManager: PositionManagerAsync + ): Pair, List> { + for (element in visitedTrace) { + visitTraceElement(element, targetFilterer, positionManager) + } + val unvisitedTargets = targetFilterer.getUnvisitedTargets() + for (element in unvisitedTrace) { + visitTraceElement(element, targetFilterer, positionManager) + } + val unvisitedAtTheEnd = targetFilterer.getUnvisitedTargets() + return unvisitedTargets to unvisitedAtTheEnd + } + + private suspend fun visitTraceElement( + element: BytecodeTraceElement, + targetFilterer: KotlinSmartStepTargetFilterer, + positionManager: PositionManagerAsync + ) { + when (element) { + is BytecodeTraceElement.InlineCall -> { + val calledInlineFunction = getCalledInlineFunction(positionManager, element.callInfo.startLocation) ?: return + targetFilterer.visitInlineFunction(calledInlineFunction) + } + + is BytecodeTraceElement.MethodCall -> { + targetFilterer.visitOrdinaryFunction(element.owner, element.name, element.descriptor) + } } } } -private data class InlineCallInfo(val variableName: String, val bciRange: LongRange, val startLocation: Location) +private sealed class BytecodeTraceElement { + data class InlineCall(val callInfo: InlineCallInfo) : BytecodeTraceElement() + data class MethodCall(val owner: String, val name: String, val descriptor: String) : BytecodeTraceElement() +} + +internal data class InlineCallInfo(val variableName: String, val bciRange: LongRange, val startLocation: Location) private fun extractInlineCalls(location: Location): List = location.safeMethod() ?.getInlineFunctionAndArgumentVariablesToBordersMap() @@ -61,5 +108,7 @@ private fun extractInlineCalls(location: Location): List = locat // Filter already visible variable to support smart-step-into while inside an inline function .filterNot { location.codeIndex() in it.bciRange } -private fun getCalledInlineFunction(positionManager: PositionManager, location: Location): KtNamedFunction? = - positionManager.getSourcePosition(location)?.elementAt?.parentOfType() +private suspend fun getCalledInlineFunction(positionManager: PositionManagerAsync, location: Location): KtNamedFunction? { + val sourcePosition = positionManager.getSourcePositionAsync(location).await() + return readAction { sourcePosition?.elementAt?.parentOfType() } +} diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCaseWithStepping.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCaseWithStepping.kt index 79e3ad1ea2c9..6ddb6c83e710 100644 --- a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCaseWithStepping.kt +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCaseWithStepping.kt @@ -21,16 +21,22 @@ import com.intellij.execution.process.ProcessOutputTypes import com.intellij.jarRepository.JarRepositoryManager import com.intellij.jarRepository.RemoteRepositoryDescription import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.readAction import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.ComponentManagerEx import com.intellij.openapi.module.Module import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.libraries.ui.OrderRoot import com.intellij.psi.PsiElement import com.intellij.testFramework.runInEdtAndWait -import com.intellij.xdebugger.XDebuggerTestUtil import com.intellij.xdebugger.frame.XStackFrame import com.intellij.xdebugger.impl.XSourcePositionImpl import junit.framework.AssertionFailedError +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.concurrency.asDeferred import org.jetbrains.idea.maven.aether.ArtifactKind import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils.areLogErrorsIgnored @@ -183,6 +189,9 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase() dp.managerThread.schedule(stepOverCommand) } + private fun launch(block: suspend CoroutineScope.() -> Unit) = + (project as ComponentManagerEx).getCoroutineScope().launch(block = block) + private fun process(instruction: SteppingInstruction) { fun loop(count: Int, block: SuspendContextImpl.() -> Unit) { repeat(count) { @@ -197,18 +206,20 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase() SteppingInstructionKind.StepOut -> loop(instruction.arg) { doStepOut() } SteppingInstructionKind.StepOver -> loop(instruction.arg) { doStepOver() } SteppingInstructionKind.ForceStepOver -> loop(instruction.arg) { doStepOver(ignoreBreakpoints = true) } - SteppingInstructionKind.SmartStepInto -> loop(instruction.arg) { doSmartStepInto() } - SteppingInstructionKind.SmartStepIntoByIndex -> doOnBreakpoint { doSmartStepInto(instruction.arg) } + SteppingInstructionKind.SmartStepInto -> loop(instruction.arg) { launch { doSmartStepInto() } } + SteppingInstructionKind.SmartStepIntoByIndex -> doOnBreakpoint { launch { doSmartStepInto(instruction.arg) } } SteppingInstructionKind.Resume -> loop(instruction.arg) { resume(this) } SteppingInstructionKind.SmartStepTargetsExpectedNumber -> doOnBreakpoint { - checkNumberOfSmartStepTargets(instruction.arg) - resume(this) + launch { + checkNumberOfSmartStepTargets(instruction.arg) + this@KotlinDescriptorTestCaseWithStepping.resume(this@doOnBreakpoint) + } } } } - private fun checkNumberOfSmartStepTargets(expectedNumber: Int) { + private suspend fun checkNumberOfSmartStepTargets(expectedNumber: Int) { val smartStepFilters = createSmartStepIntoFilters() try { assertEquals( @@ -221,7 +232,7 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase() } } - private fun SuspendContextImpl.doSmartStepInto(chooseFromList: Int = 0) { + private suspend fun SuspendContextImpl.doSmartStepInto(chooseFromList: Int = 0) { this.doSmartStepInto(chooseFromList, false) } @@ -269,7 +280,7 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase() protected open fun extraPrintContext(context: SuspendContextImpl) {} - private fun SuspendContextImpl.doSmartStepInto(chooseFromList: Int, ignoreFilters: Boolean) { + private suspend fun SuspendContextImpl.doSmartStepInto(chooseFromList: Int, ignoreFilters: Boolean) { val filters = createSmartStepIntoFilters() if (chooseFromList == 0) { if (filters.isEmpty()) { @@ -292,25 +303,26 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase() elementAt.getElementTextWithContext() } - private fun createSmartStepIntoFilters(): List { + private suspend fun createSmartStepIntoFilters(): List { val position = debuggerContext.sourcePosition - val stepTargets = KotlinSmartStepIntoHandler() - .findStepIntoTargets(position, debuggerSession) - .blockingGet(XDebuggerTestUtil.TIMEOUT_MS) - ?: error("Couldn't calculate smart step targets") + val stepTargets = withContext(Dispatchers.Default) { + KotlinSmartStepIntoHandler() + .findStepIntoTargets(position, debuggerSession) + .asDeferred().await() + } // the resulting order is different from the order in code when stepping some methods are filtered // due to de-prioritisation in JvmSmartStepIntoHandler.reorderWithSteppingFilters - if (runReadAction { stepTargets.none { DebugProcessImpl.isClassFiltered(it.className)} }) { + if (readAction { stepTargets.none { DebugProcessImpl.isClassFiltered(it.className)} }) { try { assertEquals("Smart step targets are not sorted by position in tree", - stepTargets.sortedByPositionInTree().map { runReadAction { it.presentation } }, - stepTargets.map { runReadAction { it.presentation } }) + stepTargets.sortedByPositionInTree().map { readAction { it.presentation } }, + stepTargets.map { readAction { it.presentation } }) } catch (e: AssertionFailedError) { thrownExceptions.add(e) } } - return runReadAction { + return readAction { stepTargets.mapNotNull { stepTarget -> when (stepTarget) { is KotlinSmartStepTarget -> stepTarget.createMethodFilter()