diff --git a/platform/xdebugger-impl/backend/src/com/intellij/platform/debugger/impl/backend/BackendXDebugSessionApi.kt b/platform/xdebugger-impl/backend/src/com/intellij/platform/debugger/impl/backend/BackendXDebugSessionApi.kt index 86a94360bbd0..e60f33de5f25 100644 --- a/platform/xdebugger-impl/backend/src/com/intellij/platform/debugger/impl/backend/BackendXDebugSessionApi.kt +++ b/platform/xdebugger-impl/backend/src/com/intellij/platform/debugger/impl/backend/BackendXDebugSessionApi.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.debugger.impl.backend import com.intellij.ide.rpc.FrontendDocumentId @@ -14,11 +14,9 @@ import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.project.Project -import com.intellij.openapi.util.NlsContexts import com.intellij.platform.debugger.impl.rpc.* import com.intellij.platform.project.ProjectId import com.intellij.platform.project.findProject -import com.intellij.platform.util.coroutines.attachAsChildTo import com.intellij.platform.util.coroutines.childScope import com.intellij.ui.FileColorManager import com.intellij.util.ThreeState @@ -29,12 +27,10 @@ import com.intellij.xdebugger.evaluation.EvaluationMode import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider import com.intellij.xdebugger.frame.XExecutionStack import com.intellij.xdebugger.frame.XStackFrame -import com.intellij.xdebugger.frame.XSuspendContext import com.intellij.xdebugger.impl.XDebugSessionImpl import com.intellij.xdebugger.impl.XSourceKind import com.intellij.xdebugger.impl.XSteppingSuspendContext -import com.intellij.xdebugger.impl.frame.XStackFrameWithCustomBackgroundColor -import com.intellij.xdebugger.impl.frame.XStackFrameWithSeparatorAbove +import com.intellij.xdebugger.impl.frame.* import com.intellij.xdebugger.impl.rpc.models.findValue import com.intellij.xdebugger.impl.rpc.models.getOrStoreGlobally import com.intellij.xdebugger.impl.rpc.models.storeGlobally @@ -44,15 +40,10 @@ import com.intellij.xdebugger.stepping.ForceSmartStepIntoSource import com.intellij.xdebugger.stepping.XSmartStepIntoHandler import com.intellij.xdebugger.stepping.XSmartStepIntoVariant import fleet.rpc.core.toRpc -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* import kotlinx.coroutines.future.asDeferred -import kotlinx.coroutines.withContext import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.await import org.jetbrains.concurrency.rejectedPromise @@ -200,57 +191,39 @@ internal class BackendXDebugSessionApi : XDebugSessionApi { } } - override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow { - val session = sessionId.findValue() ?: return emptyFlow() - val scope = session.coroutineScope.childScopeCancelledOnSessionEvents("RunningExecutionStacksScope", session) - return createExecutionStacksEventFlow(session, scope) { container -> - session.debugProcess.computeRunningExecutionStacks(container) + override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): TimeoutSafeResult { + val session = sessionId.findValue() + ?: return CompletableDeferred(value = XExecutionStacksResult.ExecutionStacks(listOf())) + return computeExecutionStacksImpl(session.coroutineScope, session) { + session.debugProcess.computeRunningExecutionStacksSuspend() } } - override suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): Flow { - val suspendContextModel = suspendContextId.findValue() ?: return emptyFlow() + override suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): TimeoutSafeResult { + val suspendContextModel = suspendContextId.findValue() + ?: return CompletableDeferred(value = XExecutionStacksResult.ExecutionStacks(listOf())) val session = suspendContextModel.session - return createExecutionStacksEventFlow(session, suspendContextModel.coroutineScope) { container -> - suspendContextModel.suspendContext.computeExecutionStacks(container) + return computeExecutionStacksImpl(suspendContextModel.coroutineScope, session) { + suspendContextModel.suspendContext.computeExecutionStacksSuspend() } } - private fun createExecutionStacksEventFlow( - session: XDebugSessionImpl, + private fun computeExecutionStacksImpl( scope: CoroutineScope, - computeExecutionStacks: (XSuspendContext.XExecutionStackContainer) -> Unit - ) : Flow { - return channelFlow { - attachAsChildTo(scope) - val container = object : XSuspendContext.XExecutionStackContainer { - @Volatile - var obsolete = false - - override fun isObsolete(): Boolean { - return obsolete - } - - override fun addExecutionStack(executionStacks: List, last: Boolean) { - val stacks = executionStacks.map { stack -> - stack.toRpc(scope, session) - } - trySend(XExecutionStacksEvent.NewExecutionStacks(stacks, last)) - if (last) { - this@channelFlow.close() - } - } - - override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) { - trySend(XExecutionStacksEvent.ErrorOccurred(errorMessage)) + session: XDebugSessionImpl, + computeExecutionStacks: suspend () -> List, + ): TimeoutSafeResult { + return scope.async { + try { + val stackDtos = computeExecutionStacks().map { stack -> + stack.toRpc(scope, session) } + XExecutionStacksResult.ExecutionStacks(stackDtos) } - computeExecutionStacks(container) - - awaitClose { - container.obsolete = true + catch (e: XExecutionStacksComputationException) { + XExecutionStacksResult.ErrorOccurred(e.message ?: "Error occurred while computing execution stacks") } - }.buffer(Channel.UNLIMITED) + } } private fun CoroutineScope.childScopeCancelledOnSessionEvents(name: String, session: XDebugSessionImpl): CoroutineScope = diff --git a/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/FrontendXDebuggerSession.kt b/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/FrontendXDebuggerSession.kt index 779917b621b9..dd1bd34d0829 100644 --- a/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/FrontendXDebuggerSession.kt +++ b/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/FrontendXDebuggerSession.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.debugger.impl.frontend import com.intellij.execution.RunContentDescriptorIdImpl @@ -437,7 +437,8 @@ class FrontendXDebuggerSession private constructor( coroutineScope.launch { XDebugSessionApi.getInstance() .computeRunningExecutionStacks(id) - .collectExecutionStackEvents(project, coroutineScope, container) + .await() + .handleResult(project, coroutineScope, container) } } diff --git a/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/frame/FrontendXSuspendContext.kt b/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/frame/FrontendXSuspendContext.kt index 49d8724d922d..817739c5291b 100644 --- a/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/frame/FrontendXSuspendContext.kt +++ b/platform/xdebugger-impl/frontend/src/com/intellij/platform/debugger/impl/frontend/frame/FrontendXSuspendContext.kt @@ -1,11 +1,11 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.debugger.impl.frontend.frame import com.intellij.openapi.project.Project import com.intellij.platform.debugger.impl.frontend.storage.FrontendXStackFramesStorage import com.intellij.platform.debugger.impl.frontend.storage.getOrCreateStackFrame import com.intellij.platform.debugger.impl.rpc.XDebugSessionApi -import com.intellij.platform.debugger.impl.rpc.XExecutionStacksEvent +import com.intellij.platform.debugger.impl.rpc.XExecutionStacksResult import com.intellij.platform.debugger.impl.rpc.XStackFrameDto import com.intellij.platform.debugger.impl.rpc.XSuspendContextDto import com.intellij.platform.util.coroutines.childScope @@ -14,7 +14,6 @@ import com.intellij.xdebugger.frame.XSuspendContext import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch internal class FrontendXSuspendContext( @@ -41,7 +40,8 @@ internal class FrontendXSuspendContext( lifetimeScope.launch { XDebugSessionApi.getInstance() .computeExecutionStacks(id) - .collectExecutionStackEvents(project, lifetimeScope, container) + .await() + .handleResult(project, lifetimeScope, container) } } @@ -54,24 +54,23 @@ internal class FrontendXSuspendContext( } } -internal suspend fun Flow.collectExecutionStackEvents( +internal fun XExecutionStacksResult.handleResult( project: Project, coroutineScope: CoroutineScope, - container: XSuspendContext.XExecutionStackContainer + container: XSuspendContext.XExecutionStackContainer, ) { - collect { executionStackEvent -> - when (executionStackEvent) { - is XExecutionStacksEvent.ErrorOccurred -> { - container.errorOccurred(executionStackEvent.errorMessage) - } - is XExecutionStacksEvent.NewExecutionStacks -> { - // TODO[IJPL-177087]: here we are binding FrontendXExecutionStack to the suspend context scope, - // which is the safest-narrowest scope in our possession. - // However, maybe it's possible to set up, for example, a scope that ends when another stack is selected from a combobox. - // But it requires further investigation. - val feStacks = executionStackEvent.stacks.map { FrontendXExecutionStack(it, project, coroutineScope) } - container.addExecutionStack(feStacks, executionStackEvent.last) - } + val executionStackEvent = this + when (executionStackEvent) { + is XExecutionStacksResult.ErrorOccurred -> { + container.errorOccurred(executionStackEvent.errorMessage) + } + is XExecutionStacksResult.ExecutionStacks -> { + // TODO[IJPL-177087]: here we are binding FrontendXExecutionStack to the suspend context scope, + // which is the safest-narrowest scope in our possession. + // However, maybe it's possible to set up, for example, a scope that ends when another stack is selected from a combobox. + // But it requires further investigation. + val feStacks = executionStackEvent.stacks.map { FrontendXExecutionStack(it, project, coroutineScope) } + container.addExecutionStack(feStacks, true) } } } \ No newline at end of file diff --git a/platform/xdebugger-impl/rpc/src/com/intellij/platform/debugger/impl/rpc/XDebugSessionApi.kt b/platform/xdebugger-impl/rpc/src/com/intellij/platform/debugger/impl/rpc/XDebugSessionApi.kt index 7e03edc30ccd..1b82a3962be7 100644 --- a/platform/xdebugger-impl/rpc/src/com/intellij/platform/debugger/impl/rpc/XDebugSessionApi.kt +++ b/platform/xdebugger-impl/rpc/src/com/intellij/platform/debugger/impl/rpc/XDebugSessionApi.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.debugger.impl.rpc import com.intellij.execution.RunContentDescriptorIdImpl @@ -58,9 +58,9 @@ interface XDebugSessionApi : RemoteApi { suspend fun setCurrentStackFrame(sessionId: XDebugSessionId, executionStackId: XExecutionStackId, frameId: XStackFrameId, isTopFrame: Boolean, changedByUser: Boolean = false) - suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): Flow + suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): TimeoutSafeResult - suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow + suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): TimeoutSafeResult suspend fun muteBreakpoints(sessionDataId: XDebugSessionDataId, muted: Boolean) @@ -98,12 +98,12 @@ data class XDebugSessionDto( @ApiStatus.Internal @Serializable -sealed interface XExecutionStacksEvent { +sealed interface XExecutionStacksResult { @Serializable - data class NewExecutionStacks(val stacks: List, val last: Boolean) : XExecutionStacksEvent + data class ExecutionStacks(val stacks: List) : XExecutionStacksResult @Serializable - data class ErrorOccurred(val errorMessage: @NlsContexts.DialogMessage String) : XExecutionStacksEvent + data class ErrorOccurred(val errorMessage: @NlsContexts.DialogMessage String) : XExecutionStacksResult } @ApiStatus.Internal diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/frame/XExecutionStackUtils.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/frame/XExecutionStackUtils.kt new file mode 100644 index 000000000000..07316bf2b6af --- /dev/null +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/frame/XExecutionStackUtils.kt @@ -0,0 +1,85 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.xdebugger.impl.frame + +import com.intellij.openapi.util.NlsContexts +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.frame.XSuspendContext +import kotlinx.coroutines.CompletableDeferred +import org.jetbrains.annotations.ApiStatus + +/** + * Computes the list of running execution stacks for this debug process. + * + * This is a suspend wrapper around [XDebugProcess.computeRunningExecutionStacks] that converts + * the callback-based API to a coroutine-based one. + * + * @return the list of currently running [XExecutionStack]s + * @throws XExecutionStacksComputationException if an error occurs during computation + */ +@ApiStatus.Internal +suspend fun XDebugProcess.computeRunningExecutionStacksSuspend(): List { + return computeExecutionStacksImpl { + computeRunningExecutionStacks(it) + } +} + +/** + * Computes all execution stacks for this suspend context. + * + * This is a suspend wrapper around [XSuspendContext.computeExecutionStacks] that converts + * the callback-based API to a coroutine-based one. + * + * @return the list of all [XExecutionStack]s in this suspend context + * @throws XExecutionStacksComputationException if an error occurs during computation + */ +@ApiStatus.Internal +suspend fun XSuspendContext.computeExecutionStacksSuspend(): List { + return computeExecutionStacksImpl { container -> + computeExecutionStacks(container) + } +} + +/** + * Exception thrown when an error occurs during the computation of execution stacks. + * + * This exception wraps error messages reported via [com.intellij.xdebugger.frame.XSuspendContext.XExecutionStackContainer.errorOccurred]. + * + * @param message the error message describing what went wrong during computation + */ +@ApiStatus.Internal +class XExecutionStacksComputationException(message: String) : Exception(message) + +private suspend fun computeExecutionStacksImpl( + computeExecutionStacks: (XSuspendContext.XExecutionStackContainer) -> Unit, +): List { + val result = mutableListOf() + val computationCompleted = CompletableDeferred() + var obsolete = false + val container = object : XSuspendContext.XExecutionStackContainer { + override fun isObsolete(): Boolean { + return obsolete + } + + override fun addExecutionStack(executionStacks: List, last: Boolean) { + result.addAll(executionStacks) + if (last) { + computationCompleted.complete(Unit) + } + } + + override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) { + computationCompleted.completeExceptionally(XExecutionStacksComputationException(errorMessage)) + } + } + + try { + computeExecutionStacks(container) + computationCompleted.await() + } + finally { + obsolete = true + } + + return result +} \ No newline at end of file