[debugger-rd] IJPL-160146: Handle a quick evaluate remote dev scenario through XQuickEvaluateHandler

The only thing that is (and will) be kept in split is bridge between thin client and backend plugins

GitOrigin-RevId: 92e5e6889830d1e6681e9d87f44e9ab7e039f3f8
This commit is contained in:
Nikolay Rykunov
2024-09-26 12:07:10 +02:00
committed by intellij-monorepo-bot
parent 1a5deb0cca
commit 3c4285226d
21 changed files with 418 additions and 335 deletions

View File

@@ -238,7 +238,6 @@ c:com.intellij.xdebugger.impl.XDebuggerSupport
- getMarkObjectHandler():com.intellij.xdebugger.impl.actions.MarkObjectActionHandler
- getMuteBreakpointsHandler():com.intellij.xdebugger.impl.actions.DebuggerToggleActionHandler
- getPauseHandler():com.intellij.xdebugger.impl.actions.DebuggerActionHandler
- getQuickEvaluateHandler():com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
- getResumeActionHandler():com.intellij.xdebugger.impl.actions.DebuggerActionHandler
- getRunToCursorHandler():com.intellij.xdebugger.impl.actions.DebuggerActionHandler
- getShowExecutionPointHandler():com.intellij.xdebugger.impl.actions.DebuggerActionHandler
@@ -1022,16 +1021,6 @@ c:com.intellij.xdebugger.impl.evaluate.quick.XDebuggerTreeCreator
- createDescriptorByNode(java.lang.Object,com.intellij.concurrency.ResultConsumer):V
- createTree(com.intellij.openapi.util.Pair):com.intellij.ui.treeStructure.Tree
- getTitle(com.intellij.openapi.util.Pair):java.lang.String
f:com.intellij.xdebugger.impl.evaluate.quick.XQuickEvaluateHandler
- com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
- sf:Companion:com.intellij.xdebugger.impl.evaluate.quick.XQuickEvaluateHandler$Companion
- <init>():V
- canShowHint(com.intellij.openapi.project.Project):Z
- createValueHint(com.intellij.openapi.project.Project,com.intellij.openapi.editor.Editor,java.awt.Point,com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType):com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
- createValueHintAsync(com.intellij.openapi.project.Project,com.intellij.openapi.editor.Editor,java.awt.Point,com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType):com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler$CancellableHint
- getValueLookupDelay(com.intellij.openapi.project.Project):I
- isEnabled(com.intellij.openapi.project.Project):Z
f:com.intellij.xdebugger.impl.evaluate.quick.XQuickEvaluateHandler$Companion
c:com.intellij.xdebugger.impl.evaluate.quick.XValueHint
- com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
- <init>(com.intellij.openapi.project.Project,com.intellij.openapi.editor.Editor,java.awt.Point,com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType,com.intellij.xdebugger.evaluation.ExpressionInfo,com.intellij.xdebugger.evaluation.XDebuggerEvaluator,com.intellij.xdebugger.XDebugSession,Z):V

View File

@@ -49,5 +49,6 @@
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.debugger" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
</component>
</module>

View File

@@ -4,6 +4,6 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.backend.remoteApiProvider
implementation="com.intellij.platform.debugger.impl.backend.BackendDebuggerValueLookupHintsRemoteApiProvider"/>
implementation="com.intellij.platform.debugger.impl.backend.BackendXDebuggerRemoteApiProviders"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,14 @@
// Copyright 2000-2024 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.platform.rpc.backend.RemoteApiProvider
import com.intellij.xdebugger.impl.rpc.XDebuggerValueLookupHintsRemoteApi
import fleet.rpc.remoteApiDescriptor
private class BackendXDebuggerRemoteApiProviders : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<XDebuggerValueLookupHintsRemoteApi>()) {
BackendXDebuggerValueLookupHintsRemoteApi()
}
}
}

View File

@@ -1,25 +1,28 @@
// Copyright 2000-2024 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.codeInsight.TargetElementUtil
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.EditorId
import com.intellij.openapi.editor.impl.findEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProject
import com.intellij.platform.rpc.backend.RemoteApiProvider
import com.intellij.psi.PsiDocumentManager
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.evaluation.ExpressionInfo
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator
import com.intellij.xdebugger.impl.DebuggerSupport
import com.intellij.xdebugger.impl.evaluate.quick.XQuickEvaluateHandler
import com.intellij.xdebugger.impl.evaluate.quick.XValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import com.intellij.xdebugger.impl.rpc.RemoteValueHint
import com.intellij.xdebugger.impl.rpc.RemoteValueHintId
import com.intellij.xdebugger.impl.rpc.XDebuggerValueLookupHintsRemoteApi
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -32,26 +35,49 @@ import java.awt.Point
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
private class BackendDebuggerValueLookupHintsRemoteApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<XDebuggerValueLookupHintsRemoteApi>()) {
BackendDebuggerValueLookupHintsRemoteApi()
}
}
}
private class BackendDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHintsRemoteApi {
internal class BackendXDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHintsRemoteApi {
override suspend fun getExpressionInfo(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType): ExpressionInfo? {
return withContext(Dispatchers.EDT) {
val project = projectId.findProject()
val editor = editorId.findEditor()
val evaluator = XDebuggerManager.getInstance(project).getCurrentSession()?.debugProcess?.evaluator ?: return@withContext null
val expressionInfo = XQuickEvaluateHandler.getExpressionInfo(evaluator, project, hintType, editor, offset)
val expressionInfo = getExpressionInfo(evaluator, project, hintType, editor, offset)
return@withContext expressionInfo
}
}
private data class EditorEvaluateExpressionData(
val adjustedOffset: Int,
val hasSelection: Boolean,
val selectionStart: Int,
val selectionEnd: Int,
)
private suspend fun getExpressionInfo(
evaluator: XDebuggerEvaluator, project: Project,
type: ValueHintType?, editor: Editor, offset: Int,
): ExpressionInfo? {
val document = editor.getDocument()
val evaluateExpressionData = readAction {
// adjust offset to match with other actions, like go to declaration
val adjustedOffset = TargetElementUtil.adjustOffset(PsiDocumentManager.getInstance(project).getPsiFile(document), document, offset)
val selectionModel = editor.getSelectionModel()
val selectionStart = selectionModel.selectionStart
val selectionEnd = selectionModel.selectionEnd
val hasSelection = selectionModel.hasSelection()
EditorEvaluateExpressionData(adjustedOffset, hasSelection, selectionStart, selectionEnd)
}
if ((type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT) && evaluateExpressionData.hasSelection
&& evaluateExpressionData.adjustedOffset in evaluateExpressionData.selectionStart..evaluateExpressionData.selectionEnd
) {
return ExpressionInfo(TextRange(evaluateExpressionData.selectionStart, evaluateExpressionData.selectionEnd))
}
val expressionInfo = evaluator.getExpressionInfoAtOffsetAsync(project, document, evaluateExpressionData.adjustedOffset,
type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT).await()
return expressionInfo
}
override suspend fun canShowHint(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType): Boolean {
return withContext(Dispatchers.EDT) {
@@ -59,24 +85,44 @@ private class BackendDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHin
val editor = editorId.findEditor()
val point = editor.offsetToXY(offset)
val canShowHint = getValueHintFromDebuggers(project, editor, point, hintType) != null
val canShowHint = getValueHintFromDebuggerPlugins(project, editor, point, hintType) != null
return@withContext canShowHint
}
}
override suspend fun createHint(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType): RemoteValueHint? {
override suspend fun createHint(
projectId: ProjectId,
editorId: EditorId,
offset: Int,
hintType: ValueHintType,
fromPlugins: Boolean,
): RemoteValueHintId? {
return withContext(Dispatchers.EDT) {
val project = projectId.findProject()
val editor = editorId.findEditor()
val point = editor.offsetToXY(offset)
val hint = getValueHintFromDebuggers(project, editor, point, hintType) ?: return@withContext null
val hint = if (fromPlugins) {
getValueHintFromDebuggerPlugins(project, editor, point, hintType) ?: return@withContext null
}
else {
val session = XDebuggerManager.getInstance(project).getCurrentSession()
if (session == null) {
return@withContext null
}
val evaluator = session.getDebugProcess().getEvaluator()
if (evaluator == null) {
return@withContext null
}
val expressionInfo = getExpressionInfo(evaluator, project, hintType, editor, offset) ?: return@withContext null
XValueHint(project, editor, point, hintType, expressionInfo, evaluator, session, false)
}
val hintId = BackendDebuggerValueLookupHintsHolder.getInstance(project).registerNewHint(hint)
RemoteValueHint(hintId)
RemoteValueHintId(hintId)
}
}
private suspend fun getValueHintFromDebuggers(
private suspend fun getValueHintFromDebuggerPlugins(
project: Project,
editor: Editor,
point: Point,
@@ -94,9 +140,9 @@ private class BackendDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHin
return null
}
override suspend fun showHint(projectId: ProjectId, hintId: Int): Flow<Unit> {
override suspend fun showHint(projectId: ProjectId, hintId: RemoteValueHintId): Flow<Unit> {
val project = projectId.findProject()
val hint = BackendDebuggerValueLookupHintsHolder.getInstance(project).getHintById(hintId) ?: return emptyFlow()
val hint = BackendDebuggerValueLookupHintsHolder.getInstance(project).getHintById(hintId.id) ?: return emptyFlow()
return callbackFlow {
withContext(Dispatchers.EDT) {
hint.invokeHint {
@@ -108,10 +154,10 @@ private class BackendDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHin
}
}
override suspend fun removeHint(projectId: ProjectId, hintId: Int) {
override suspend fun removeHint(projectId: ProjectId, hintId: RemoteValueHintId) {
val project = projectId.findProject()
val hint = BackendDebuggerValueLookupHintsHolder.getInstance(project).getHintById(hintId) ?: return
BackendDebuggerValueLookupHintsHolder.getInstance(project).removeHint(hintId)
val hint = BackendDebuggerValueLookupHintsHolder.getInstance(project).getHintById(hintId.id) ?: return
BackendDebuggerValueLookupHintsHolder.getInstance(project).removeHint(hintId.id)
withContext(Dispatchers.EDT) {
hint.hideHint()
}

View File

@@ -46,5 +46,6 @@
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.frontend" />
</component>
</module>

View File

@@ -1,6 +1,7 @@
<idea-plugin package="com.intellij.platform.debugger.impl.frontend">
<dependencies>
<plugin id="com.intellij.platform.experimental.frontend"/>
<module name="intellij.platform.frontend"/>
</dependencies>
<actions>
<action id="QuickEvaluateExpression" class="com.intellij.platform.debugger.impl.frontend.actions.QuickEvaluateAction"/>

View File

@@ -7,6 +7,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorGutter;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.project.Project;
import com.intellij.platform.debugger.impl.frontend.evaluate.quick.XQuickEvaluateHandler;
import com.intellij.platform.debugger.impl.frontend.evaluate.quick.common.ValueLookupManager;
import com.intellij.xdebugger.impl.DebuggerSupport;
import com.intellij.xdebugger.impl.actions.DebuggerActionHandler;
@@ -28,6 +29,7 @@ public class QuickEvaluateAction extends XDebuggerActionBase {
private static class QuickEvaluateHandlerWrapper extends DebuggerActionHandler {
private final QuickEvaluateHandler myHandler;
private final QuickEvaluateHandler myXQuickEvaluateHandler = new XQuickEvaluateHandler();
QuickEvaluateHandlerWrapper(final QuickEvaluateHandler handler) {
myHandler = handler;
@@ -38,14 +40,22 @@ public class QuickEvaluateAction extends XDebuggerActionBase {
Editor editor = event.getData(CommonDataKeys.EDITOR);
if (editor != null) {
LogicalPosition logicalPosition = editor.getCaretModel().getLogicalPosition();
QuickEvaluateHandler handler;
// first try to use platform's evaluate handler
if (myXQuickEvaluateHandler.isEnabled(project, event)) {
handler = myXQuickEvaluateHandler;
}
else {
handler = myHandler;
}
ValueLookupManager.getInstance(project).
showHint(myHandler, editor, editor.logicalPositionToXY(logicalPosition), null, ValueHintType.MOUSE_CLICK_HINT);
showHint(handler, editor, editor.logicalPositionToXY(logicalPosition), null, ValueHintType.MOUSE_CLICK_HINT);
}
}
@Override
public boolean isEnabled(@NotNull Project project, @NotNull AnActionEvent event) {
if (!myHandler.isEnabled(project, event)) {
if (!myHandler.isEnabled(project, event) && !myXQuickEvaluateHandler.isEnabled(project, event)) {
return false;
}

View File

@@ -0,0 +1,23 @@
// Copyright 2000-2024 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.platform.kernel.KernelService
import com.jetbrains.rhizomedb.asOf
import fleet.kernel.rete.Rete
import kotlinx.coroutines.ExperimentalCoroutinesApi
/**
* Allows reading from the current DB state.
* It may be useful when you don't know whether function will be called in EDT context (where DB state is propagated)
* or in some background thread, where DB state is not propagated by default.
*
* Avoid using this API if your code is called in EDT.
*
* NB: This is a delicate API! Use with caution, since using [f] may be called on the "old" DB state where some latest changes are not applied
*/
@OptIn(ExperimentalCoroutinesApi::class)
internal fun <T> withCurrentDb(f: () -> T): T {
return asOf(KernelService.instance.kernelCoroutineScope.getCompleted().coroutineContext[Rete]!!.lastKnownDb.value) {
f()
}
}

View File

@@ -0,0 +1,102 @@
// Copyright 2000-2024 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.evaluate.quick
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.editorId
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.debugger.impl.frontend.evaluate.quick.common.RemoteValueHint
import com.intellij.platform.debugger.impl.frontend.withCurrentDb
import com.intellij.platform.frontend.FrontendApplicationInfo
import com.intellij.platform.frontend.FrontendType
import com.intellij.platform.kernel.withKernel
import com.intellij.platform.project.asEntity
import com.intellij.platform.project.projectId
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.impl.XDebuggerActiveSessionEntity
import com.intellij.xdebugger.impl.evaluate.childCoroutineScope
import com.intellij.xdebugger.impl.evaluate.quick.XValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import com.intellij.xdebugger.impl.rpc.XDebuggerValueLookupHintsRemoteApi
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import org.jetbrains.concurrency.asPromise
import java.awt.Point
private val LOG = Logger.getInstance(XQuickEvaluateHandler::class.java)
internal class XQuickEvaluateHandler : QuickEvaluateHandler() {
override fun isEnabled(project: Project): Boolean {
val frontendType = FrontendApplicationInfo.getFrontendType()
if (frontendType is FrontendType.RemoteDev && !frontendType.isLuxSupported) {
return false
}
val hasActiveSession = withCurrentDb {
val projectEntity = project.asEntity()
XDebuggerActiveSessionEntity.all().any { it.projectEntity == projectEntity }
}
return hasActiveSession
}
override fun createValueHint(project: Project, editor: Editor, point: Point, type: ValueHintType?): AbstractValueHint? {
return null
}
@OptIn(DelicateCoroutinesApi::class)
override fun createValueHintAsync(project: Project, editor: Editor, point: Point, type: ValueHintType): CancellableHint {
val offset = AbstractValueHint.calculateOffset(editor, point)
val document = editor.getDocument()
val documentCoroutineScope = editor.childCoroutineScope("XQuickEvaluateHandler#valueHint")
val projectId = project.projectId()
val editorId = editor.editorId()
val expressionInfoDeferred = documentCoroutineScope.async(Dispatchers.IO) {
withKernel {
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.getExpressionInfo(projectId, editorId, offset, type)
}
}
val hintDeferred: Deferred<AbstractValueHint?> = documentCoroutineScope.async(Dispatchers.IO) {
val expressionInfo = expressionInfoDeferred.await()
val textLength = document.textLength
if (expressionInfo == null) {
return@async null
}
val range = expressionInfo.textRange
if (range.startOffset > range.endOffset || range.startOffset < 0 || range.endOffset > textLength) {
LOG.error("invalid range: $range, text length = $textLength")
return@async null
}
if (Registry.`is`("debugger.valueLookupFrontendBackend") || FrontendApplicationInfo.getFrontendType() is FrontendType.RemoteDev) {
RemoteValueHint(project, projectId, editor, point, type, offset, expressionInfo, fromPlugins = false)
}
else {
val session = XDebuggerManager.getInstance(project).getCurrentSession()
if (session == null) {
return@async null
}
val evaluator = session.getDebugProcess().getEvaluator()
if (evaluator == null) {
return@async null
}
XValueHint(project, editor, point, type, expressionInfo, evaluator, session, false)
}
}
hintDeferred.invokeOnCompletion {
documentCoroutineScope.cancel()
}
return CancellableHint(hintDeferred.asCompletableFuture().asPromise(), expressionInfoDeferred)
}
override fun canShowHint(project: Project): Boolean {
return isEnabled(project)
}
override fun getValueLookupDelay(project: Project?): Int {
return XDebuggerSettingsManager.getInstance().getDataViewSettings().getValueLookupDelay()
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2000-2024 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.evaluate.quick.common
import com.intellij.openapi.application.EDT
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.editorId
import com.intellij.openapi.project.Project
import com.intellij.platform.kernel.withKernel
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.projectId
import com.intellij.xdebugger.evaluation.ExpressionInfo
import com.intellij.xdebugger.impl.evaluate.childCoroutineScope
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import com.intellij.xdebugger.impl.rpc.RemoteValueHintId
import com.intellij.xdebugger.impl.rpc.XDebuggerValueLookupHintsRemoteApi
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import fleet.util.logging.logger
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.concurrency.asPromise
import org.jetbrains.concurrency.resolvedPromise
import java.awt.Point
@ApiStatus.Internal
class RemoteValueHint(
project: Project, private val projectId: ProjectId,
private val editor: Editor,
point: Point,
private val type: ValueHintType,
private val offset: Int,
expressionInfo: ExpressionInfo,
private val fromPlugins: Boolean,
) : AbstractValueHint(project, editor, point, type, expressionInfo.textRange) {
private var remoteHint: Deferred<RemoteValueHintId?>? = null
private var hintCoroutineScope: CoroutineScope? = null
@OptIn(DelicateCoroutinesApi::class)
override fun evaluateAndShowHint() {
hintCoroutineScope = editor.childCoroutineScope("RemoteValueHintScope")
val remoteHint = hintCoroutineScope!!.async(Dispatchers.IO) {
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.createHint(projectId, editor.editorId(), offset, type, fromPlugins)
}
this@RemoteValueHint.remoteHint = remoteHint
hintCoroutineScope!!.launch(Dispatchers.IO) {
val hint = remoteHint.await() ?: return@launch
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
val closedEvent = remoteApi.showHint(projectId, hint)
withContext(Dispatchers.EDT) {
closedEvent.collect {
hideHint()
processHintHidden()
}
}
}
}
override fun hideHint() {
hintCoroutineScope?.launch(Dispatchers.IO) {
val hint = remoteHint?.await()
if (hint == null) {
hintCoroutineScope?.cancel()
return@launch
}
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.removeHint(projectId, hint)
hintCoroutineScope?.cancel()
}
super.hideHint()
}
}

View File

@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.event.EditorMouseMotionListener;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.platform.debugger.impl.frontend.evaluate.quick.XQuickEvaluateHandler;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.util.Alarm;
import com.intellij.util.ui.UIUtil;
@@ -39,6 +40,7 @@ public class ValueLookupManager implements EditorMouseMotionListener, EditorMous
private final Alarm myAlarm;
private HintRequest myHintRequest = null;
private AbstractValueHint myCurrentHint = null;
private final @NotNull QuickEvaluateHandler myXQuickEvaluateHandler = new XQuickEvaluateHandler();
private boolean myListening;
public ValueLookupManager(@NotNull Project project) {
@@ -100,22 +102,23 @@ public class ValueLookupManager implements EditorMouseMotionListener, EditorMous
Point point = e.getMouseEvent().getPoint();
if (!Registry.is("debugger.valueLookupFrontendBackend")) {
for (DebuggerSupport support : DebuggerSupport.getDebuggerSupports()) {
QuickEvaluateHandler handler = support.getQuickEvaluateHandler();
if (handler.isEnabled(myProject)) {
requestHint(handler, editor, point, e, type);
return;
}
// handle platform handler first
if (myXQuickEvaluateHandler.isEnabled(myProject)) {
requestHint(myXQuickEvaluateHandler, editor, point, e, type);
return;
}
// otherwise, handle plugin handlers
// for remote dev: specific DebuggerSupport with remote bridge will be used
for (DebuggerSupport support : DebuggerSupport.getDebuggerSupports()) {
QuickEvaluateHandler handler = support.getQuickEvaluateHandler();
if (handler.isEnabled(myProject)) {
requestHint(handler, editor, point, e, type);
return;
}
}
// if no providers were triggered - hide
hideHint();
}
else {
QuickEvaluateHandler handler = new ValueLookupManagerQuickEvaluateHandler();
requestHint(handler, editor, point, e, type);
}
// if no providers were triggered - hide
hideHint();
}
private void requestHint(final QuickEvaluateHandler handler,

View File

@@ -1,148 +0,0 @@
// Copyright 2000-2024 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.evaluate.quick.common
import com.intellij.openapi.application.EDT
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.editorId
import com.intellij.openapi.project.Project
import com.intellij.platform.kernel.withKernel
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.projectId
import com.intellij.xdebugger.evaluation.ExpressionInfo
import com.intellij.xdebugger.impl.evaluate.childCoroutineScope
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import com.intellij.xdebugger.impl.rpc.RemoteValueHint
import com.intellij.xdebugger.impl.rpc.XDebuggerValueLookupHintsRemoteApi
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import fleet.util.logging.logger
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.concurrency.asPromise
import org.jetbrains.concurrency.resolvedPromise
import java.awt.Point
private val LOG = logger<ValueLookupManagerQuickEvaluateHandler>()
/**
* Bridge between [QuickEvaluateHandler]s provided by [DebuggerSupport]s and [ValueLookupManager]
*/
// TODO: QuickEvaluateHandler should be converted to frontend EP and this one should be just XQuickEvaluateHandler
@ApiStatus.Internal
open class ValueLookupManagerQuickEvaluateHandler : QuickEvaluateHandler() {
override fun isEnabled(project: Project): Boolean {
return true
}
override fun createValueHint(project: Project, editor: Editor, point: Point, type: ValueHintType?): AbstractValueHint? {
return null
}
@OptIn(DelicateCoroutinesApi::class)
override fun createValueHintAsync(project: Project, editor: Editor, point: Point, type: ValueHintType?): CancellableHint {
if (type == null) {
return CancellableHint(resolvedPromise(), null)
}
val offset = AbstractValueHint.calculateOffset(editor, point)
val document = editor.getDocument()
val documentCoroutineScope = editor.childCoroutineScope("ValueLookupManagerQuickEvaluateHandler#valueHint")
val projectId = project.projectId()
val editorId = editor.editorId()
val canShowHint = documentCoroutineScope.async(Dispatchers.IO) {
withKernel {
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.canShowHint(projectId, editorId, offset, type)
}
}
val expressionInfoDeferred = documentCoroutineScope.async(Dispatchers.IO) {
if (!canShowHint.await()) {
return@async null
}
withKernel {
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
val projectId = project.projectId()
val editorId = editor.editorId()
remoteApi.getExpressionInfo(projectId, editorId, offset, type)
}
}
val hintDeferred: Deferred<AbstractValueHint?> = documentCoroutineScope.async(Dispatchers.IO) {
if (!canShowHint.await()) {
return@async null
}
val expressionInfo = expressionInfoDeferred.await()
val textLength = document.textLength
if (expressionInfo == null) {
return@async null
}
val range = expressionInfo.textRange
if (range.startOffset > range.endOffset || range.startOffset < 0 || range.endOffset > textLength) {
LOG.error("invalid range: $range, text length = $textLength")
return@async null
}
val hint = ValueLookupManagerValueHint(project, projectId, editor, point, type, offset, expressionInfo)
return@async hint
}
hintDeferred.invokeOnCompletion {
documentCoroutineScope.cancel()
}
return CancellableHint(hintDeferred.asCompletableFuture().asPromise(), expressionInfoDeferred)
}
override fun canShowHint(project: Project): Boolean {
return true
}
override fun getValueLookupDelay(project: Project?): Int {
return XDebuggerSettingsManager.getInstance().getDataViewSettings().getValueLookupDelay()
}
}
private class ValueLookupManagerValueHint(
project: Project, private val projectId: ProjectId,
private val editor: Editor,
point: Point,
private val type: ValueHintType,
private val offset: Int,
expressionInfo: ExpressionInfo,
) : AbstractValueHint(project, editor, point, type, expressionInfo.textRange) {
private var remoteHint: Deferred<RemoteValueHint?>? = null
private var hintCoroutineScope: CoroutineScope? = null
@OptIn(DelicateCoroutinesApi::class)
override fun evaluateAndShowHint() {
hintCoroutineScope = editor.childCoroutineScope("ValueLookupManagerValueHintScope")
val remoteHint = hintCoroutineScope!!.async(Dispatchers.IO) {
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.createHint(projectId, editor.editorId(), offset, type)
}
this@ValueLookupManagerValueHint.remoteHint = remoteHint
hintCoroutineScope!!.launch(Dispatchers.IO) {
val hint = remoteHint.await() ?: return@launch
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
val closedEvent = remoteApi.showHint(projectId, hint.id)
withContext(Dispatchers.EDT) {
closedEvent.collect {
hideHint()
processHintHidden()
}
}
}
}
override fun hideHint() {
hintCoroutineScope?.launch(Dispatchers.IO) {
val hint = remoteHint?.await()
if (hint == null) {
hintCoroutineScope?.cancel()
return@launch
}
val remoteApi = XDebuggerValueLookupHintsRemoteApi.getInstance()
remoteApi.removeHint(projectId, hint.id)
hintCoroutineScope?.cancel()
}
super.hideHint()
}
}

View File

@@ -145,6 +145,7 @@
<registryKey defaultValue="false" key="debugger.valueLookupFrontendBackend"
description="Provides a way to use frontend-backend implementation of debugger's evaluation popup"/>
<platform.entityTypes implementation="com.intellij.xdebugger.impl.evaluate.XDebuggerValueLookupEntityTypesProvider"/>
<platform.entityTypes implementation="com.intellij.xdebugger.impl.XDebuggerActiveSessionEntityTypesProvider"/>
<intentionAction>
<language>UAST</language>

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 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.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl;
import com.intellij.openapi.Disposable;
@@ -145,6 +145,7 @@ public abstract class DebuggerSupport {
}
public @NotNull QuickEvaluateHandler getQuickEvaluateHandler() {
// See [XQuickEvaluateHandler] which is provided in frontend
return DISABLED_QUICK_EVALUATE;
}

View File

@@ -0,0 +1,67 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl
import com.intellij.openapi.project.Project
import com.intellij.platform.kernel.EntityTypeProvider
import com.intellij.platform.kernel.withKernel
import com.intellij.platform.project.ProjectEntity
import com.intellij.platform.project.asEntity
import com.jetbrains.rhizomedb.EID
import com.jetbrains.rhizomedb.Entity
import com.jetbrains.rhizomedb.EntityType
import com.jetbrains.rhizomedb.RefFlags
import com.jetbrains.rhizomedb.entity
import fleet.kernel.DurableEntityType
import fleet.kernel.change
import fleet.kernel.shared
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
private class XDebuggerActiveSessionEntityTypesProvider : EntityTypeProvider {
override fun entityTypes(): List<EntityType<*>> {
return listOf(
XDebuggerActiveSessionEntity,
)
}
}
@ApiStatus.Internal
data class XDebuggerActiveSessionEntity(override val eid: EID) : Entity {
val projectEntity by Project
@ApiStatus.Internal
companion object : DurableEntityType<XDebuggerActiveSessionEntity>(
XDebuggerActiveSessionEntity::class.java.name,
"com.intellij",
::XDebuggerActiveSessionEntity
) {
val Project = requiredRef<ProjectEntity>("project", RefFlags.UNIQUE)
}
}
internal fun synchronizeActiveSessionWithDb(coroutineScope: CoroutineScope, project: Project, activeSessionState: Flow<XDebugSessionImpl?>) {
coroutineScope.launch(Dispatchers.IO) {
activeSessionState.collect { newActiveSession ->
withKernel {
change {
shared {
val projectEntity = project.asEntity()
// if there is no active session -> remove entity from DB
if (newActiveSession == null) {
entity(XDebuggerActiveSessionEntity.Project, projectEntity)?.delete()
}
else {
// insert entity if active session is added
XDebuggerActiveSessionEntity.upsert(XDebuggerActiveSessionEntity.Project, projectEntity) {
it[XDebuggerActiveSessionEntity.Project] = projectEntity
}
}
}
}
}
}
}
}

View File

@@ -64,6 +64,9 @@ import com.intellij.xdebugger.impl.ui.XDebugSessionTab;
import com.intellij.xdebugger.ui.DebuggerColors;
import kotlin.Unit;
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.flow.MutableStateFlow;
import kotlinx.coroutines.flow.StateFlow;
import kotlinx.coroutines.flow.StateFlowKt;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nls;
@@ -76,7 +79,9 @@ import java.awt.event.MouseEvent;
import java.util.List;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import static com.intellij.xdebugger.impl.CoroutineUtilsKt.createMutableStateFlow;
import static com.intellij.xdebugger.impl.XDebuggerActiveSessionControllerKt.synchronizeActiveSessionWithDb;
@ApiStatus.Internal
@State(name = "XDebuggerManager", storages = @Storage(StoragePathMacros.WORKSPACE_FILE))
@@ -92,7 +97,7 @@ public final class XDebuggerManagerImpl extends XDebuggerManager implements Pers
private final XDebuggerPinToTopManager myPinToTopManager;
private final XDebuggerExecutionPointManager myExecutionPointManager;
private final Map<ProcessHandler, XDebugSessionImpl> mySessions = Collections.synchronizedMap(new LinkedHashMap<>());
private final AtomicReference<XDebugSessionImpl> myActiveSession = new AtomicReference<>();
private final MutableStateFlow<@Nullable XDebugSessionImpl> myActiveSession = createMutableStateFlow(null);
private XDebuggerState myState = new XDebuggerState();
@@ -109,6 +114,8 @@ public final class XDebuggerManagerImpl extends XDebuggerManager implements Pers
myPinToTopManager = new XDebuggerPinToTopManager(coroutineScope);
myExecutionPointManager = new XDebuggerExecutionPointManager(project, coroutineScope);
synchronizeActiveSessionWithDb(coroutineScope, project, myActiveSession);
messageBusConnection.subscribe(FileDocumentManagerListener.TOPIC, new FileDocumentManagerListener() {
@Override
public void fileContentLoaded(@NotNull VirtualFile file, @NotNull Document document) {
@@ -383,11 +390,18 @@ public final class XDebuggerManagerImpl extends XDebuggerManager implements Pers
@Override
public @Nullable XDebugSessionImpl getCurrentSession() {
return myActiveSession.get();
return myActiveSession.getValue();
}
@ApiStatus.Internal
public StateFlow<@Nullable XDebugSessionImpl> getCurrentSessionFlow() {
return myActiveSession;
}
boolean setCurrentSession(@Nullable XDebugSessionImpl session) {
XDebugSessionImpl previousSession = myActiveSession.getAndSet(session);
XDebugSessionImpl previousSession = StateFlowKt.getAndUpdate(myActiveSession, (currentValue) -> {
return session;
});
boolean sessionChanged = previousSession != session;
if (sessionChanged) {
if (session != null) {

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl;
import com.intellij.openapi.actionSystem.DataContext;
@@ -7,8 +7,6 @@ import com.intellij.xdebugger.impl.actions.*;
import com.intellij.xdebugger.impl.actions.handlers.*;
import com.intellij.xdebugger.impl.breakpoints.XBreakpointPanelProvider;
import com.intellij.xdebugger.impl.breakpoints.ui.BreakpointPanelProvider;
import com.intellij.xdebugger.impl.evaluate.quick.XQuickEvaluateHandler;
import com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler;
import org.jetbrains.annotations.NotNull;
public class XDebuggerSupport extends DebuggerSupport {
@@ -26,7 +24,6 @@ public class XDebuggerSupport extends DebuggerSupport {
private final XDebuggerPauseActionHandler myPauseHandler;
private final XDebuggerSuspendedActionHandler myShowExecutionPointHandler;
private final XDebuggerEvaluateActionHandler myEvaluateHandler;
private final XQuickEvaluateHandler myQuickEvaluateHandler;
private final XAddToWatchesFromEditorActionHandler myAddToWatchesActionHandler;
private final XAddToInlineWatchesFromEditorActionHandler myAddToInlineWatchesActionHandler;
@@ -91,7 +88,6 @@ public class XDebuggerSupport extends DebuggerSupport {
};
myMuteBreakpointsHandler = new XDebuggerMuteBreakpointsHandler();
myEvaluateHandler = new XDebuggerEvaluateActionHandler();
myQuickEvaluateHandler = new XQuickEvaluateHandler();
myMarkObjectActionHandler = new XMarkObjectActionHandler();
myEditBreakpointActionHandler = new XDebuggerEditBreakpointActionHandler();
}
@@ -186,12 +182,6 @@ public class XDebuggerSupport extends DebuggerSupport {
return myEvaluateHandler;
}
@Override
@NotNull
public QuickEvaluateHandler getQuickEvaluateHandler() {
return myQuickEvaluateHandler;
}
@NotNull
@Override
public DebuggerActionHandler getAddToWatchesActionHandler() {

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl
import kotlinx.coroutines.flow.MutableStateFlow
// Used only for Java code, since MutableStateFlow function cannot be called there.
internal fun <T> createMutableStateFlow(initialValue: T): MutableStateFlow<T> {
return MutableStateFlow(initialValue)
}

View File

@@ -1,114 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl.evaluate.quick
import com.intellij.codeInsight.TargetElementUtil
import com.intellij.openapi.application.readAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.evaluation.ExpressionInfo
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator
import com.intellij.xdebugger.impl.evaluate.childCoroutineScope
import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint
import com.intellij.xdebugger.impl.evaluate.quick.common.QuickEvaluateHandler
import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import com.intellij.xdebugger.settings.XDebuggerSettingsManager
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.concurrency.asPromise
import org.jetbrains.concurrency.await
import java.awt.Point
private val LOG = Logger.getInstance(XQuickEvaluateHandler::class.java)
class XQuickEvaluateHandler : QuickEvaluateHandler() {
override fun isEnabled(project: Project): Boolean {
val session = XDebuggerManager.getInstance(project).getCurrentSession()
return session != null && session.getDebugProcess().getEvaluator() != null
}
override fun createValueHint(project: Project, editor: Editor, point: Point, type: ValueHintType?): AbstractValueHint? {
return null
}
@OptIn(DelicateCoroutinesApi::class)
override fun createValueHintAsync(project: Project, editor: Editor, point: Point, type: ValueHintType): CancellableHint {
val session = XDebuggerManager.getInstance(project).getCurrentSession()
if (session == null) {
return CancellableHint.resolved(null)
}
val evaluator = session.getDebugProcess().getEvaluator()
if (evaluator == null) {
return CancellableHint.resolved(null)
}
val offset = AbstractValueHint.calculateOffset(editor, point)
val document = editor.getDocument()
val documentCoroutineScope = editor.childCoroutineScope("XQuickEvaluateHandler#valueHint")
val expressionInfoDeferred = documentCoroutineScope.async(Dispatchers.IO) {
getExpressionInfo(evaluator, project, type, editor, offset)
}
val hintDeferred: Deferred<AbstractValueHint?> = documentCoroutineScope.async(Dispatchers.IO) {
val expressionInfo = expressionInfoDeferred.await()
val textLength = document.textLength
if (expressionInfo == null) {
return@async null
}
val range = expressionInfo.textRange
if (range.startOffset > range.endOffset || range.startOffset < 0 || range.endOffset > textLength) {
LOG.error("invalid range: $range, text length = $textLength, evaluator: $evaluator")
return@async null
}
XValueHint(project, editor, point, type, expressionInfo, evaluator, session, false)
}
hintDeferred.invokeOnCompletion {
documentCoroutineScope.cancel()
}
return CancellableHint(hintDeferred.asCompletableFuture().asPromise(), expressionInfoDeferred)
}
override fun canShowHint(project: Project): Boolean {
return isEnabled(project)
}
override fun getValueLookupDelay(project: Project?): Int {
return XDebuggerSettingsManager.getInstance().getDataViewSettings().getValueLookupDelay()
}
companion object {
private data class EditorEvaluateExpressionData(val adjustedOffset: Int, val hasSelection: Boolean, val selectionStart: Int, val selectionEnd: Int)
@ApiStatus.Internal
suspend fun getExpressionInfo(
evaluator: XDebuggerEvaluator, project: Project,
type: ValueHintType?, editor: Editor, offset: Int,
): ExpressionInfo? {
val document = editor.getDocument()
val evaluateExpressionData = readAction {
// adjust offset to match with other actions, like go to declaration
val adjustedOffset = TargetElementUtil.adjustOffset(PsiDocumentManager.getInstance(project).getPsiFile(document), document, offset)
val selectionModel = editor.getSelectionModel()
val selectionStart = selectionModel.selectionStart
val selectionEnd = selectionModel.selectionEnd
val hasSelection = selectionModel.hasSelection()
EditorEvaluateExpressionData(adjustedOffset, hasSelection, selectionStart, selectionEnd)
}
if ((type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT) && evaluateExpressionData.hasSelection
&& evaluateExpressionData.adjustedOffset in evaluateExpressionData.selectionStart..evaluateExpressionData.selectionEnd
) {
return ExpressionInfo(TextRange(evaluateExpressionData.selectionStart, evaluateExpressionData.selectionEnd))
}
val expressionInfo = evaluator.getExpressionInfoAtOffsetAsync(project, document, evaluateExpressionData.adjustedOffset,
type == ValueHintType.MOUSE_CLICK_HINT || type == ValueHintType.MOUSE_ALT_OVER_HINT).await()
return expressionInfo
}
}
}

View File

@@ -10,7 +10,6 @@ import com.intellij.xdebugger.impl.evaluate.quick.common.ValueHintType
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import fleet.util.UID
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
@@ -22,11 +21,11 @@ interface XDebuggerValueLookupHintsRemoteApi : RemoteApi<Unit> {
suspend fun canShowHint(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType): Boolean
suspend fun createHint(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType): RemoteValueHint?
suspend fun createHint(projectId: ProjectId, editorId: EditorId, offset: Int, hintType: ValueHintType, fromPlugins: Boolean): RemoteValueHintId?
suspend fun showHint(projectId: ProjectId, hintId: Int): Flow<Unit>
suspend fun showHint(projectId: ProjectId, hintId: RemoteValueHintId): Flow<Unit>
suspend fun removeHint(projectId: ProjectId, hintId: Int)
suspend fun removeHint(projectId: ProjectId, hintId: RemoteValueHintId)
companion object {
@JvmStatic
@@ -40,4 +39,4 @@ interface XDebuggerValueLookupHintsRemoteApi : RemoteApi<Unit> {
@ApiStatus.Internal
@Serializable
data class RemoteValueHint(val id: Int)
data class RemoteValueHintId(val id: Int)