diff --git a/java/debugger/frontend/resources/intellij.java.debugger.impl.frontend.xml b/java/debugger/frontend/resources/intellij.java.debugger.impl.frontend.xml index c5d67e65dd9b..afd43323530c 100644 --- a/java/debugger/frontend/resources/intellij.java.debugger.impl.frontend.xml +++ b/java/debugger/frontend/resources/intellij.java.debugger.impl.frontend.xml @@ -5,4 +5,7 @@ + + + \ No newline at end of file diff --git a/java/debugger/frontend/src/com/intellij/java/debugger/impl/frontend/JavaValueDescriptorStateExtension.kt b/java/debugger/frontend/src/com/intellij/java/debugger/impl/frontend/JavaValueDescriptorStateExtension.kt new file mode 100644 index 000000000000..641a27a324ac --- /dev/null +++ b/java/debugger/frontend/src/com/intellij/java/debugger/impl/frontend/JavaValueDescriptorStateExtension.kt @@ -0,0 +1,15 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.debugger.impl.frontend + +import com.intellij.java.debugger.impl.shared.engine.JavaValueDescriptor +import com.intellij.java.debugger.impl.shared.engine.JavaValueDescriptorState +import com.intellij.platform.debugger.impl.frontend.FrontendDescriptorStateManagerExtension +import com.intellij.xdebugger.frame.XDescriptor +import kotlinx.coroutines.CoroutineScope + +private class JavaValueDescriptorStateExtension : FrontendDescriptorStateManagerExtension { + override fun createState(descriptor: XDescriptor, cs: CoroutineScope): Any? { + if (descriptor !is JavaValueDescriptor) return null + return JavaValueDescriptorState(descriptor, cs) + } +} diff --git a/java/debugger/impl/BUILD.bazel b/java/debugger/impl/BUILD.bazel index f5f067be7b5e..f21859397380 100644 --- a/java/debugger/impl/BUILD.bazel +++ b/java/debugger/impl/BUILD.bazel @@ -65,6 +65,7 @@ jvm_library( "//xml/xml-psi-api:psi", "//platform/xdebugger-impl/shared", "//platform/xdebugger-impl/rpc", + "//fleet/rpc", ], runtime_deps = [":impl_resources"] ) diff --git a/java/debugger/impl/intellij.java.debugger.impl.iml b/java/debugger/impl/intellij.java.debugger.impl.iml index cfe0a6a210e8..c68db03b0f45 100644 --- a/java/debugger/impl/intellij.java.debugger.impl.iml +++ b/java/debugger/impl/intellij.java.debugger.impl.iml @@ -98,6 +98,7 @@ + diff --git a/java/debugger/impl/src/com/intellij/debugger/actions/ViewAsGroup.java b/java/debugger/impl/src/com/intellij/debugger/actions/ViewAsGroup.java index d9574962abc5..d4619a781b78 100644 --- a/java/debugger/impl/src/com/intellij/debugger/actions/ViewAsGroup.java +++ b/java/debugger/impl/src/com/intellij/debugger/actions/ViewAsGroup.java @@ -20,6 +20,7 @@ import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree; import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase; import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; import one.util.streamex.StreamEx; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -89,8 +90,8 @@ public class ViewAsGroup extends ActionGroup implements DumbAware { public void threadAction(@NotNull SuspendContextImpl suspendContext) { for (XValueNodeImpl node : selectedNodes) { XValue container = node.getValueContainer(); - if (container instanceof JavaValue) { - ((JavaValue)container).setRenderer(myNodeRenderer, node); + if (container instanceof JavaValue javaValue) { + javaValue.setRenderer(myNodeRenderer, node); } } } @@ -158,14 +159,8 @@ public class ViewAsGroup extends ActionGroup implements DumbAware { private static @NotNull CompletableFuture> getApplicableRenderers(List values, DebugProcessImpl process) { List>> futures = new ArrayList<>(values.size()); for (JavaValue value : values) { - if (value instanceof JavaReferringObjectsValue) { // disable for any referrers at all - return CompletableFuture.completedFuture(Collections.emptyList()); - } - ValueDescriptorImpl valueDescriptor = value.getDescriptor(); - if (!valueDescriptor.isValueValid()) { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - futures.add(process.getApplicableRenderers(valueDescriptor.getType())); + CompletableFuture> completedFuture = getApplicableNodeRenderers(value); + futures.add(completedFuture); } return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(__ -> { @@ -183,6 +178,19 @@ public class ViewAsGroup extends ActionGroup implements DumbAware { }); } + @ApiStatus.Internal + public static CompletableFuture> getApplicableNodeRenderers(JavaValue value) { + if (value instanceof JavaReferringObjectsValue) { // disable for any referrers at all + return CompletableFuture.completedFuture(Collections.emptyList()); + } + ValueDescriptorImpl valueDescriptor = value.getDescriptor(); + if (!valueDescriptor.isValueValid()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + DebugProcessImpl process = value.getEvaluationContext().getDebugProcess(); + return process.getApplicableRenderers(valueDescriptor.getType()); + } + @Override public void update(final @NotNull AnActionEvent event) { DebuggerContextImpl debuggerContext = DebuggerAction.getDebuggerContext(event.getDataContext()); diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java index 67f8a4103a7c..f704078c2e00 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java @@ -81,6 +81,7 @@ import com.intellij.xdebugger.XDebuggerBundle; import com.intellij.xdebugger.XDebuggerManager; import com.intellij.xdebugger.XSourcePosition; import com.intellij.xdebugger.frame.XExecutionStack; +import com.intellij.xdebugger.impl.CoroutineUtilsKt; import com.intellij.xdebugger.impl.XDebugSessionImpl; import com.intellij.xdebugger.impl.XDebuggerManagerImpl; import com.intellij.xdebugger.impl.actions.XDebuggerActions; @@ -94,10 +95,13 @@ import com.sun.jdi.event.LocatableEvent; import com.sun.jdi.request.EventRequest; import com.sun.jdi.request.EventRequestManager; import com.sun.jdi.request.StepRequest; +import kotlin.Unit; import kotlin.coroutines.EmptyCoroutineContext; import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.CoroutineScopeKt; import kotlinx.coroutines.Job; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableSharedFlow; import one.util.streamex.StreamEx; import org.jetbrains.annotations.*; @@ -146,6 +150,7 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb private volatile Map myArguments; private final List myRenderers = new ArrayList<>(); + private final MutableSharedFlow myRenderersUpdated = CoroutineUtilsKt.createMutableSharedFlow(1, 1); // we use null key here private final Map myNodeRenderersMap = Collections.synchronizedMap(new HashMap<>()); @@ -220,6 +225,11 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb return new DebuggerManagerThreadImpl(disposable, projectScope, this); } + @ApiStatus.Internal + public Flow getRenderersUpdatedFlow() { + return myRenderersUpdated; + } + private void reloadRenderers() { getManagerThread().schedule(new DebuggerCommandImpl(PrioritizedTask.Priority.HIGH) { @Override @@ -230,6 +240,7 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb myRenderers.addAll(NodeRendererSettings.getInstance().getAllRenderers(project)); } finally { + myRenderersUpdated.tryEmit(Unit.INSTANCE); DebuggerInvocationUtil.invokeLaterAnyModality(project, () -> { final DebuggerSession session = mySession; if (session != null && session.isAttached()) { diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/JavaValue.java b/java/debugger/impl/src/com/intellij/debugger/engine/JavaValue.java index a206456b0aac..24832632b66b 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/JavaValue.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/JavaValue.java @@ -523,17 +523,7 @@ public class JavaValue extends XNamedValue implements NodeDescriptorProvider, XV @Override public @Nullable CompletableFuture getXValueDescriptorAsync() { - return myValueDescriptor.getInitFuture().thenApply(ignored -> { - Value value = myValueDescriptor.getValue(); - JavaValueObjectReferenceInfo objectReferenceInfo = null; - if (value instanceof ObjectReference ref) { - objectReferenceInfo = new JavaValueObjectReferenceInfo(ref.referenceType().name(), ref.virtualMachine().canGetInstanceInfo()); - } - return new JavaValueDescriptor( - myValueDescriptor.isString(), - objectReferenceInfo - ); - }); + return myValueDescriptor.getInitFuture().thenCompose(ignored -> JavaValueUtilsKt.getJavaValueXDescriptor(this)); } @Override diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/JavaValueUtils.kt b/java/debugger/impl/src/com/intellij/debugger/engine/JavaValueUtils.kt index fd7fb670ad7b..790f1f5b34c0 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/JavaValueUtils.kt +++ b/java/debugger/impl/src/com/intellij/debugger/engine/JavaValueUtils.kt @@ -1,13 +1,29 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.debugger.engine +import com.intellij.debugger.actions.ViewAsGroup import com.intellij.debugger.engine.evaluation.EvaluationContextImpl import com.intellij.debugger.impl.DebuggerUtilsEx import com.intellij.debugger.impl.PrioritizedTask import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl +import com.intellij.debugger.ui.tree.render.NodeRenderer +import com.intellij.debugger.ui.tree.render.Renderer +import com.intellij.java.debugger.impl.shared.engine.JavaValueDescriptor +import com.intellij.java.debugger.impl.shared.engine.JavaValueObjectReferenceInfo +import com.intellij.java.debugger.impl.shared.engine.NodeRendererDto import com.intellij.openapi.application.readAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.XDescriptor +import com.sun.jdi.ObjectReference +import fleet.rpc.core.toRpc +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.future.await +import kotlinx.coroutines.future.future +import java.util.concurrent.CompletableFuture import kotlin.coroutines.cancellation.CancellationException @OptIn(InternalCoroutinesApi::class) @@ -34,4 +50,37 @@ internal fun scheduleSourcePositionCompute( positionCallback(null) } } -} \ No newline at end of file +} + +internal fun getJavaValueXDescriptor(javaValue: JavaValue): CompletableFuture { + val valueDescriptor = javaValue.descriptor + val value = valueDescriptor.getValue() + var objectReferenceInfo: JavaValueObjectReferenceInfo? = null + if (value is ObjectReference) { + objectReferenceInfo = JavaValueObjectReferenceInfo(value.referenceType().name(), value.virtualMachine().canGetInstanceInfo()) + } + val cs = valueDescriptor.project.service().cs + return cs.future { + val renderersUpdatedFlow = javaValue.evaluationContext.debugProcess.renderersUpdatedFlow + JavaValueDescriptor( + valueDescriptor.isString(), + objectReferenceInfo, + valueDescriptor.lastRenderer?.toRpc(), + valueDescriptor.lastRendererFlow.map { it.toRpc() }.toRpc(), + renderersUpdatedFlow.map { fetchApplicableNodeRenderers(javaValue).map { it.toRpc() } }.toRpc() + ) + } +} + +private suspend fun fetchApplicableNodeRenderers(javaValue: JavaValue): List { + val renderersFuture = withDebugContext(javaValue.evaluationContext.suspendContext) { + ViewAsGroup.getApplicableNodeRenderers(javaValue) + } + return renderersFuture.await() +} + +private fun Renderer.toRpc() = (this as? NodeRenderer)?.toRpc() +private fun NodeRenderer.toRpc() = NodeRendererDto(uniqueId, name) + +@Service(Service.Level.PROJECT) +private class JavaValueDescriptorCreationService(val cs: CoroutineScope) diff --git a/java/debugger/impl/src/com/intellij/debugger/ui/impl/watch/ValueDescriptorImpl.java b/java/debugger/impl/src/com/intellij/debugger/ui/impl/watch/ValueDescriptorImpl.java index 0d6f21d999ca..d4cc62749751 100644 --- a/java/debugger/impl/src/com/intellij/debugger/ui/impl/watch/ValueDescriptorImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/ui/impl/watch/ValueDescriptorImpl.java @@ -39,9 +39,13 @@ import com.intellij.util.concurrency.Semaphore; import com.intellij.xdebugger.frame.XValueModifier; import com.intellij.xdebugger.frame.XValueNode; import com.intellij.xdebugger.frame.presentation.XRegularValuePresentation; +import com.intellij.xdebugger.impl.CoroutineUtilsKt; import com.intellij.xdebugger.impl.frame.XValueMarkers; import com.intellij.xdebugger.impl.ui.tree.ValueMarkup; import com.sun.jdi.*; +import kotlin.Unit; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableSharedFlow; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,8 +64,8 @@ public abstract class ValueDescriptorImpl extends NodeDescriptorImpl implements private final CompletableFuture myInitFuture; NodeRenderer myRenderer = null; - NodeRenderer myAutoRenderer = null; + private final MutableSharedFlow myRenderersChangedFlow = CoroutineUtilsKt.createMutableSharedFlow(0, 1); private Value myValue; @@ -497,10 +501,16 @@ public abstract class ValueDescriptorImpl extends NodeDescriptorImpl implements public void displayAs(NodeDescriptor descriptor) { if (descriptor instanceof ValueDescriptorImpl valueDescriptor) { myRenderer = valueDescriptor.myRenderer; + myRenderersChangedFlow.tryEmit(Unit.INSTANCE); } super.displayAs(descriptor); } + @ApiStatus.Internal + public Flow getLastRendererFlow() { + return CoroutineUtilsKt.mapFlow(myRenderersChangedFlow, __ -> getLastRenderer()); + } + public Renderer getLastRenderer() { return myRenderer != null ? myRenderer : myAutoRenderer; } @@ -539,7 +549,11 @@ public abstract class ValueDescriptorImpl extends NodeDescriptorImpl implements } else { return DebuggerUtilsAsync.reschedule(debugProcess.getAutoRendererAsync(type)) - .thenApply(r -> myAutoRenderer = r); + .thenApply(r -> { + myAutoRenderer = r; + myRenderersChangedFlow.tryEmit(Unit.INSTANCE); + return r; + }); } }); } @@ -548,6 +562,7 @@ public abstract class ValueDescriptorImpl extends NodeDescriptorImpl implements DebuggerManagerThreadImpl.assertIsManagerThread(); myRenderer = renderer; myAutoRenderer = null; + myRenderersChangedFlow.tryEmit(Unit.INSTANCE); } diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/engine/JavaValueDescriptor.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/engine/JavaValueDescriptor.kt index bc211caa6f7b..ed702015339e 100644 --- a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/engine/JavaValueDescriptor.kt +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/engine/JavaValueDescriptor.kt @@ -3,6 +3,11 @@ package com.intellij.java.debugger.impl.shared.engine import com.intellij.xdebugger.frame.CustomXDescriptorSerializerProvider import com.intellij.xdebugger.frame.XDescriptor +import fleet.rpc.core.RpcFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import org.jetbrains.annotations.ApiStatus @@ -17,10 +22,17 @@ const val JAVA_VALUE_KIND: String = "JavaValue" data class JavaValueDescriptor( val isString: Boolean, val objectReferenceInfo: JavaValueObjectReferenceInfo?, + val initialRenderer: NodeRendererDto?, + val rendererFlow: RpcFlow, + val applicableRenderersFlow: RpcFlow>, ) : XDescriptor { override val kind: String = JAVA_VALUE_KIND } +@ApiStatus.Internal +@Serializable +data class NodeRendererDto(val id: String, val name: String) + @ApiStatus.Internal @Serializable data class JavaValueObjectReferenceInfo( @@ -35,4 +47,26 @@ private class JavaValueDescriptorSerializerProvider : CustomXDescriptorSerialize } return null } +} + +@ApiStatus.Internal +class JavaValueDescriptorState(val descriptor: JavaValueDescriptor, cs: CoroutineScope) { + private val _lastRenderer = MutableStateFlow(descriptor.initialRenderer) + private val _applicableRenderers = MutableStateFlow(emptyList()) + + init { + cs.launch { + descriptor.rendererFlow.toFlow().collectLatest { + _lastRenderer.value = it + } + } + cs.launch { + descriptor.applicableRenderersFlow.toFlow().collectLatest { + _applicableRenderers.value = it + } + } + } + + val lastRenderer: NodeRendererDto? get() = _lastRenderer.value + val applicableRenderers: List get() = _applicableRenderers.value } \ No newline at end of file