mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-02-04 23:39:07 +07:00
[debugger] API to visualize text values, IDEA-135082
GitOrigin-RevId: 1da6fc297d89596852cafd9112c48d26e9f55fec
This commit is contained in:
committed by
intellij-monorepo-bot
parent
776be1535f
commit
8d6375c6c2
@@ -41,6 +41,7 @@ import com.intellij.xdebugger.impl.pinned.items.PinToTopMemberValue;
|
||||
import com.intellij.xdebugger.impl.pinned.items.PinToTopParentValue;
|
||||
import com.intellij.xdebugger.impl.ui.XValueTextProvider;
|
||||
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl;
|
||||
import com.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopup;
|
||||
import com.sun.jdi.*;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -204,7 +205,7 @@ public class JavaValue extends XNamedValue implements NodeDescriptorProvider, XV
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (StringUtil.containsLineBreak(text)) {
|
||||
else if (VisualizedTextPopup.INSTANCE.isVisualizable(text)) {
|
||||
node.setFullValueEvaluator(new XFullValueEvaluator() {
|
||||
@Override
|
||||
public void startEvaluation(@NotNull XFullValueEvaluationCallback callback) {
|
||||
|
||||
@@ -749,6 +749,17 @@ com.intellij.xdebugger.ui.DebuggerColors
|
||||
- sf:NOT_TOP_FRAME_ATTRIBUTES:com.intellij.openapi.editor.colors.TextAttributesKey
|
||||
- sf:SMART_STEP_INTO_SELECTION:com.intellij.openapi.editor.colors.TextAttributesKey
|
||||
- sf:SMART_STEP_INTO_TARGET:com.intellij.openapi.editor.colors.TextAttributesKey
|
||||
*:com.intellij.xdebugger.ui.TextValueVisualizer
|
||||
- *sf:Companion:com.intellij.xdebugger.ui.TextValueVisualizer$Companion
|
||||
- canVisualize(java.lang.String):Z
|
||||
- a:visualize(java.lang.String):java.util.List
|
||||
*f:com.intellij.xdebugger.ui.TextValueVisualizer$Companion
|
||||
- f:getEP():com.intellij.openapi.extensions.ExtensionPointName
|
||||
*:com.intellij.xdebugger.ui.VisualizedContentTab
|
||||
- a:createComponent(com.intellij.openapi.project.Project):javax.swing.JComponent
|
||||
- a:getId():java.lang.String
|
||||
- a:getName():java.lang.String
|
||||
- onShown(com.intellij.openapi.project.Project,Z):V
|
||||
c:com.intellij.xdebugger.ui.XDebugTabLayouter
|
||||
- <init>():V
|
||||
- registerAdditionalContent(com.intellij.execution.ui.RunnerLayoutUi):V
|
||||
|
||||
@@ -324,3 +324,5 @@ debugger.frame.list.help.description=Go back to the previous frame. This will re
|
||||
|
||||
action.TurnOffDfaAssist.text=Turn Off Data Flow Assist for This Session
|
||||
action.TurnOffDfaAssist.description=Switch off data flow\u2013aided debugging for this session
|
||||
|
||||
xdebugger.visualized.text.name.raw=Raw
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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.ui
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Extension point for visualization or formatting of plain text values obtained from debuggee's string-like entities.
|
||||
* E.g., JSON might be pretty-printed with syntax highlighting, HTML might be rendered in a browser and can be pretty-printed as XML.
|
||||
*/
|
||||
@ApiStatus.Experimental // until we consider collection visualizers
|
||||
interface TextValueVisualizer {
|
||||
|
||||
companion object {
|
||||
val EP: ExtensionPointName<TextValueVisualizer> = ExtensionPointName.create("com.intellij.xdebugger.textValueVisualizer")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this extension can visualize the given value.
|
||||
* If `true`, then [visualize] returns a non-empty list.
|
||||
*/
|
||||
fun canVisualize(value: String): Boolean {
|
||||
// Default implementation, feel free to override it if there is a more efficient way to check this.
|
||||
return visualize(value).isNotEmpty()
|
||||
}
|
||||
|
||||
/** Visualizes the given value, possibly in several ways. */
|
||||
fun visualize(value: @NlsSafe String): List<VisualizedContentTab>
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental // until we consider collection visualizers
|
||||
interface VisualizedContentTab {
|
||||
/** Title of the tab with the content. */
|
||||
val name: @Nls String
|
||||
|
||||
/** Internal ID of the tab used to remember the last used visualizer. */
|
||||
val id: String
|
||||
|
||||
/** Create the visualized content component. */
|
||||
fun createComponent(project: Project): JComponent
|
||||
|
||||
/**
|
||||
* This callback is called when the tab is shown.
|
||||
* If user switches between tabs, `firstTime` is `true` only when the tab is shown for the first time.
|
||||
*/
|
||||
fun onShown(project: Project, firstTime: Boolean) {
|
||||
}
|
||||
}
|
||||
@@ -3631,6 +3631,12 @@ c:com.intellij.xdebugger.impl.ui.tree.nodes.XValueTextRendererImpl
|
||||
- renderSpecialSymbol(java.lang.String):V
|
||||
- renderStringValue(java.lang.String,java.lang.String,I):V
|
||||
- renderValue(java.lang.String):V
|
||||
a:com.intellij.xdebugger.impl.ui.visualizedtext.TextBasedContentTab
|
||||
- com.intellij.xdebugger.ui.VisualizedContentTab
|
||||
- <init>():V
|
||||
- createComponent(com.intellij.openapi.project.Project):javax.swing.JComponent
|
||||
- pa:formatText():java.lang.String
|
||||
- pa:getFileType():com.intellij.openapi.fileTypes.FileType
|
||||
c:com.intellij.xdebugger.memory.action.ShowClassesWithDiffAction
|
||||
- com.intellij.openapi.actionSystem.ToggleAction
|
||||
- <init>():V
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<extensionPoint name="xdebugger.dialog.process.view.provider" interface="com.intellij.xdebugger.impl.ui.attach.dialog.extensions.XAttachToProcessViewProvider" dynamic="true"/>
|
||||
|
||||
<extensionPoint name="xdebugger.inlineBreakpointsDisabler" interface="com.intellij.xdebugger.breakpoints.InlineBreakpointsDisabler" dynamic="true"/>
|
||||
|
||||
<extensionPoint name="xdebugger.textValueVisualizer" interface="com.intellij.xdebugger.ui.TextValueVisualizer" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.AppUIUtil.invokeOnEdt
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.xdebugger.frame.XFullValueEvaluator
|
||||
import com.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopup
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Font
|
||||
@@ -31,7 +32,7 @@ abstract class CustomComponentEvaluator(name: String) : XFullValueEvaluator() {
|
||||
editor: Editor?,
|
||||
component: JComponent,
|
||||
cancelCallback: Runnable?) {
|
||||
DebuggerUIUtil.showValuePopup(event, project, editor, component, cancelCallback)
|
||||
VisualizedTextPopup.showValuePopup(event, project, editor, component, cancelCallback)
|
||||
}
|
||||
|
||||
protected class EvaluationCallback(private val myPanel: JComponent,
|
||||
|
||||
@@ -22,17 +22,14 @@ import com.intellij.openapi.fileTypes.FileTypes;
|
||||
import com.intellij.openapi.keymap.KeymapUtil;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.popup.*;
|
||||
import com.intellij.openapi.util.DimensionService;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.util.NlsContexts;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.openapi.util.registry.Registry;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.wm.IdeFocusManager;
|
||||
import com.intellij.openapi.wm.WindowManager;
|
||||
import com.intellij.ui.AppUIUtil;
|
||||
import com.intellij.ui.ComponentUtil;
|
||||
import com.intellij.ui.EditorTextField;
|
||||
import com.intellij.ui.ScreenUtil;
|
||||
import com.intellij.ui.awt.RelativePoint;
|
||||
import com.intellij.ui.popup.list.ListPopupImpl;
|
||||
@@ -53,6 +50,7 @@ import com.intellij.xdebugger.impl.frame.XWatchesView;
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree;
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState;
|
||||
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl;
|
||||
import com.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopup;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
@@ -62,7 +60,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static com.intellij.openapi.wm.IdeFocusManager.getGlobalInstance;
|
||||
@@ -138,44 +135,7 @@ public final class DebuggerUIUtil {
|
||||
@NotNull MouseEvent event,
|
||||
@NotNull Project project,
|
||||
@Nullable Editor editor) {
|
||||
if (evaluator instanceof CustomComponentEvaluator customComponentEvaluator) {
|
||||
customComponentEvaluator.show(event, project, editor);
|
||||
}
|
||||
else {
|
||||
EditorTextField textArea = createTextViewer(XDebuggerUIConstants.getEvaluatingExpressionMessage(), project);
|
||||
final FullValueEvaluationCallbackImpl callback = new FullValueEvaluationCallbackImpl(textArea);
|
||||
showValuePopup(event, project, editor, textArea, callback::setObsolete);
|
||||
evaluator.startEvaluation(callback); /*to make it really cancellable*/
|
||||
}
|
||||
}
|
||||
|
||||
static void showValuePopup(@NotNull MouseEvent event,
|
||||
@NotNull Project project,
|
||||
@Nullable Editor editor,
|
||||
JComponent component,
|
||||
@Nullable Runnable cancelCallback) {
|
||||
|
||||
Dimension size = DimensionService.getInstance().getSize(FULL_VALUE_POPUP_DIMENSION_KEY, project);
|
||||
if (size == null) {
|
||||
Dimension frameSize = Objects.requireNonNull(WindowManager.getInstance().getFrame(project)).getSize();
|
||||
size = new Dimension(frameSize.width / 2, frameSize.height / 2);
|
||||
}
|
||||
|
||||
component.setPreferredSize(size);
|
||||
|
||||
JBPopup popup = createValuePopup(project, component, cancelCallback);
|
||||
if (editor == null) {
|
||||
Rectangle bounds = new Rectangle(event.getLocationOnScreen(), size);
|
||||
ScreenUtil.fitToScreenVertical(bounds, 5, 5, true);
|
||||
if (size.width != bounds.width || size.height != bounds.height) {
|
||||
size = bounds.getSize();
|
||||
component.setPreferredSize(size);
|
||||
}
|
||||
popup.showInScreenCoordinates(event.getComponent(), bounds.getLocation());
|
||||
}
|
||||
else {
|
||||
popup.showInBestPositionFor(editor);
|
||||
}
|
||||
VisualizedTextPopup.INSTANCE.evaluateAndShowValuePopup(evaluator, event, project, editor);
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
@@ -196,25 +156,6 @@ public final class DebuggerUIUtil {
|
||||
return textArea;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static FullValueEvaluationCallbackImpl startEvaluation(@NotNull TextViewer textViewer,
|
||||
@NotNull XFullValueEvaluator evaluator,
|
||||
@Nullable Runnable afterFullValueEvaluation) {
|
||||
FullValueEvaluationCallbackImpl callback = new FullValueEvaluationCallbackImpl(textViewer) {
|
||||
@Override
|
||||
public void evaluated(@NotNull String fullValue, @Nullable Font font) {
|
||||
super.evaluated(fullValue, font);
|
||||
AppUIUtil.invokeOnEdt(() -> {
|
||||
if (afterFullValueEvaluation != null) {
|
||||
afterFullValueEvaluation.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
evaluator.startEvaluation(callback);
|
||||
return callback;
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
public static ComponentPopupBuilder createTextViewerPopupBuilder(@NotNull JComponent popupContent,
|
||||
@NotNull TextViewer textViewer,
|
||||
@@ -222,15 +163,44 @@ public final class DebuggerUIUtil {
|
||||
@NotNull Project project,
|
||||
@Nullable Runnable afterFullValueEvaluation,
|
||||
@Nullable Runnable hideRunnable) {
|
||||
final @NotNull FullValueEvaluationCallbackImpl callback = startEvaluation(textViewer, evaluator, afterFullValueEvaluation);
|
||||
|
||||
AtomicBoolean evaluationObsolete = new AtomicBoolean(false);
|
||||
var callback = new XFullValueEvaluator.XFullValueEvaluationCallback() {
|
||||
@Override
|
||||
public void evaluated(@NotNull final String fullValue, @Nullable final Font font) {
|
||||
AppUIUtil.invokeOnEdt(() -> {
|
||||
textViewer.setText(fullValue);
|
||||
if (font != null) {
|
||||
textViewer.setFont(font);
|
||||
}
|
||||
if (afterFullValueEvaluation != null) {
|
||||
afterFullValueEvaluation.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(@NotNull final String errorMessage) {
|
||||
AppUIUtil.invokeOnEdt(() -> {
|
||||
textViewer.setForeground(XDebuggerUIConstants.ERROR_MESSAGE_ATTRIBUTES.getFgColor());
|
||||
textViewer.setText(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObsolete() {
|
||||
return evaluationObsolete.get();
|
||||
}
|
||||
};
|
||||
|
||||
Runnable cancelCallback = () -> {
|
||||
callback.setObsolete();
|
||||
evaluationObsolete.set(true);
|
||||
if (hideRunnable != null) {
|
||||
hideRunnable.run();
|
||||
}
|
||||
};
|
||||
|
||||
evaluator.startEvaluation(callback);
|
||||
return createCancelablePopupBuilder(project, popupContent, textViewer, cancelCallback, null);
|
||||
}
|
||||
|
||||
@@ -414,42 +384,6 @@ public final class DebuggerUIUtil {
|
||||
return EditorColorsUtil.getColorSchemeForComponent(component);
|
||||
}
|
||||
|
||||
private static class FullValueEvaluationCallbackImpl implements XFullValueEvaluator.XFullValueEvaluationCallback {
|
||||
private final AtomicBoolean myObsolete = new AtomicBoolean(false);
|
||||
private final EditorTextField myTextArea;
|
||||
|
||||
FullValueEvaluationCallbackImpl(final EditorTextField textArea) {
|
||||
myTextArea = textArea;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluated(@NotNull final String fullValue, @Nullable final Font font) {
|
||||
AppUIUtil.invokeOnEdt(() -> {
|
||||
myTextArea.setText(fullValue);
|
||||
if (font != null) {
|
||||
myTextArea.setFont(font);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(@NotNull final String errorMessage) {
|
||||
AppUIUtil.invokeOnEdt(() -> {
|
||||
myTextArea.setForeground(XDebuggerUIConstants.ERROR_MESSAGE_ATTRIBUTES.getFgColor());
|
||||
myTextArea.setText(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
private void setObsolete() {
|
||||
myObsolete.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObsolete() {
|
||||
return myObsolete.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getNodeRawValue(@NotNull XValueNodeImpl valueNode) {
|
||||
String res = null;
|
||||
|
||||
@@ -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.xdebugger.impl.ui.visualizedtext
|
||||
|
||||
import com.intellij.openapi.fileTypes.FileTypes
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.xdebugger.XDebuggerBundle
|
||||
import com.intellij.xdebugger.ui.TextValueVisualizer
|
||||
import com.intellij.xdebugger.ui.VisualizedContentTab
|
||||
|
||||
// It's not registered as an extension, added explicitly as the last visualizer.
|
||||
internal object FallbackTextVisualizer : TextValueVisualizer {
|
||||
override fun visualize(value: @NlsSafe String): List<VisualizedContentTab> =
|
||||
listOf(object : TextBasedContentTab() {
|
||||
override val name
|
||||
get() = XDebuggerBundle.message("xdebugger.visualized.text.name.raw")
|
||||
override val id
|
||||
get() = FallbackTextVisualizer::class.qualifiedName!!
|
||||
override fun formatText() =
|
||||
value
|
||||
override val fileType
|
||||
get() = FileTypes.PLAIN_TEXT
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.ui.visualizedtext
|
||||
|
||||
import com.intellij.openapi.fileTypes.FileType
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
|
||||
import com.intellij.xdebugger.ui.VisualizedContentTab
|
||||
|
||||
/**
|
||||
* Simple tab that displays the optionally [formatted][formatText] text
|
||||
* using [fileType]-based highlighting.
|
||||
*/
|
||||
abstract class TextBasedContentTab : VisualizedContentTab {
|
||||
protected abstract fun formatText(): String
|
||||
|
||||
protected abstract val fileType: FileType
|
||||
|
||||
override fun createComponent(project: Project) =
|
||||
DebuggerUIUtil.createTextViewer(formatText(), project, fileType).component
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// 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.ui.visualizedtext
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.diagnostic.Attachment
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.DimensionService
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import com.intellij.ui.AppUIUtil
|
||||
import com.intellij.ui.ScreenUtil
|
||||
import com.intellij.ui.components.JBTabbedPane
|
||||
import com.intellij.xdebugger.frame.XFullValueEvaluator
|
||||
import com.intellij.xdebugger.impl.ui.*
|
||||
import com.intellij.xdebugger.ui.TextValueVisualizer
|
||||
import com.intellij.xdebugger.ui.VisualizedContentTab
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Font
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import kotlin.math.max
|
||||
|
||||
private const val SELECTED_TAB_KEY_PREFIX = "DEBUGGER_VISUALIZED_TEXT_SELECTED_TAB#"
|
||||
private val logger = Logger.getInstance(VisualizedTextPopup.javaClass)
|
||||
|
||||
/**
|
||||
* Provides tools to show a text-like value that might be formatted for better readability (JSON, XML, HTML, etc.).
|
||||
*/
|
||||
internal object VisualizedTextPopup {
|
||||
|
||||
fun showValuePopup(event: MouseEvent, project: Project, editor: Editor?, component: JComponent, cancelCallback: Runnable?) {
|
||||
var size = DimensionService.getInstance().getSize(DebuggerUIUtil.FULL_VALUE_POPUP_DIMENSION_KEY, project)
|
||||
if (size == null) {
|
||||
val frameSize = WindowManager.getInstance().getFrame(project)!!.size
|
||||
size = Dimension(frameSize.width / 2, frameSize.height / 2)
|
||||
}
|
||||
|
||||
component.preferredSize = size
|
||||
|
||||
val popup = DebuggerUIUtil.createValuePopup(project, component, cancelCallback)
|
||||
if (editor == null) {
|
||||
val bounds = Rectangle(event.locationOnScreen, size)
|
||||
ScreenUtil.fitToScreenVertical(bounds, 5, 5, true)
|
||||
if (size.width != bounds.width || size.height != bounds.height) {
|
||||
size = bounds.size
|
||||
component.preferredSize = size
|
||||
}
|
||||
popup.showInScreenCoordinates(event.component, bounds.location)
|
||||
}
|
||||
else {
|
||||
popup.showInBestPositionFor(editor)
|
||||
}
|
||||
}
|
||||
|
||||
fun evaluateAndShowValuePopup(evaluator: XFullValueEvaluator, event: MouseEvent, project: Project, editor: Editor?) {
|
||||
if (evaluator is CustomComponentEvaluator) {
|
||||
return evaluator.show(event, project, editor)
|
||||
}
|
||||
|
||||
val panel = TextPanel(project)
|
||||
val callback = EvaluationCallback(project, panel)
|
||||
showValuePopup(event, project, editor, panel, callback::setObsolete)
|
||||
evaluator.startEvaluation(callback) // to make it really cancellable
|
||||
}
|
||||
|
||||
private class TextPanel(private val project: Project) : JPanel(CardLayout()) {
|
||||
init {
|
||||
showOnlyText(XDebuggerUIConstants.getEvaluatingExpressionMessage())
|
||||
}
|
||||
|
||||
fun showOnlyText(value: String, format: (TextViewer) -> Unit = {}) {
|
||||
val textArea = DebuggerUIUtil.createTextViewer(value, project)
|
||||
format(textArea)
|
||||
|
||||
removeAll()
|
||||
add(textArea)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class EvaluationCallback(private val project: Project, private val panel: TextPanel) : XFullValueEvaluator.XFullValueEvaluationCallback {
|
||||
private val obsolete = AtomicBoolean(false)
|
||||
|
||||
private var lastFullValueHashCode: Int? = null
|
||||
|
||||
override fun evaluated(fullValue: String, font: Font?) {
|
||||
// This code is not expected to be called multiple times, but it is actually called in the case of huge Java string.
|
||||
// 1. NodeDescriptorImpl.updateRepresentation() calls ValueDescriptorImpl.calcRepresentation() and it calls labelChanged()
|
||||
// 2. NodeDescriptorImpl.updateRepresentation() also directly calls labelChanged()
|
||||
// Double visualization spoils statistics and wastes the resources.
|
||||
// Try to prevent it by a simple hash code check.
|
||||
if (fullValue.hashCode() == lastFullValueHashCode) return
|
||||
lastFullValueHashCode = fullValue.hashCode()
|
||||
|
||||
AppUIUtil.invokeOnEdt {
|
||||
try {
|
||||
panel.removeAll()
|
||||
panel.add(createComponent(fullValue))
|
||||
panel.revalidate()
|
||||
panel.repaint()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
errorOccurred(e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun errorOccurred(errorMessage: String) {
|
||||
AppUIUtil.invokeOnEdt {
|
||||
panel.showOnlyText(errorMessage) {
|
||||
it.foreground = XDebuggerUIConstants.ERROR_MESSAGE_ATTRIBUTES.fgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setObsolete() {
|
||||
obsolete.set(true)
|
||||
}
|
||||
|
||||
override fun isObsolete(): Boolean {
|
||||
return obsolete.get()
|
||||
}
|
||||
|
||||
private fun createComponent(fullValue: String): JComponent {
|
||||
val tabs = collectVisualizedTabs(fullValue)
|
||||
assert(tabs.isNotEmpty()) { "at least one raw tab is expected to be provided by fallback visualizer" }
|
||||
if (tabs.size > 1) {
|
||||
try {
|
||||
return createTabbedPane(tabs, fullValue)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger.error("failed to visualize value", e, Attachment("value.txt", fullValue))
|
||||
// Fallback to the default visualizer, which provided the last tab.
|
||||
}
|
||||
}
|
||||
|
||||
return tabs.last()
|
||||
.also { it.onShown(project, firstTime = true) }
|
||||
.createComponent(project)
|
||||
}
|
||||
|
||||
private fun createTabbedPane(tabs: List<VisualizedContentTab>, fullValue: String): JComponent {
|
||||
assert(tabs.isNotEmpty())
|
||||
|
||||
val panel = JBTabbedPane()
|
||||
for (tab in tabs) {
|
||||
val component = try {
|
||||
tab.createComponent(project)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
// It's not easy to recover after missing a tab, so we throw and catch above.
|
||||
throw Exception("failed to create visualized component (${tab.id})", e)
|
||||
}
|
||||
panel.addTab(tab.name, component)
|
||||
}
|
||||
|
||||
// We try to make it content-specific by remembering separate value for every set of tabs.
|
||||
// E.g., it allows remembering that in the group HTML+XML+RAW user prefers HTML, and in the group HTML+MARKDOWN+RAW -- MARKDOWN.
|
||||
val selectedTabKey = SELECTED_TAB_KEY_PREFIX + tabs.map { it.id }.sorted().joinToString("#")
|
||||
|
||||
val alreadyShownTabs = mutableSetOf<VisualizedContentTab>()
|
||||
fun onTabShown() {
|
||||
val idx = panel.selectedIndex
|
||||
if (idx < 0 || idx >= tabs.size) return
|
||||
|
||||
val selectedTab = tabs[idx]
|
||||
PropertiesComponent.getInstance().setValue(selectedTabKey, selectedTab.id)
|
||||
|
||||
val firstTime = alreadyShownTabs.add(selectedTab)
|
||||
selectedTab.onShown(project, firstTime)
|
||||
}
|
||||
|
||||
val savedSelectedTabId = PropertiesComponent.getInstance().getValue(selectedTabKey)
|
||||
val selectedIndex = max(0, tabs.indexOfFirst { it.id == savedSelectedTabId })
|
||||
panel.selectedIndex = selectedIndex
|
||||
onTabShown() // call it manually, because change listener is triggered only if selectedIndex > 0
|
||||
|
||||
panel.model.addChangeListener { onTabShown() }
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectVisualizedTabs(fullValue: String): List<VisualizedContentTab> {
|
||||
return TextValueVisualizer.EP.extensionList.flatMap { viz ->
|
||||
try {
|
||||
viz.visualize(fullValue)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
logger.error("failed to visualize value ($viz)", t, Attachment("value.txt", fullValue))
|
||||
emptyList()
|
||||
}
|
||||
} +
|
||||
// Explicitly add the fallback raw visualizer to make it the last one.
|
||||
FallbackTextVisualizer.visualize(fullValue)
|
||||
}
|
||||
|
||||
fun isVisualizable(fullValue: String): Boolean {
|
||||
// text with line breaks would be nicely rendered by the raw visualizer
|
||||
return StringUtil.containsLineBreak(fullValue) ||
|
||||
TextValueVisualizer.EP.extensionList.any { viz ->
|
||||
try {
|
||||
viz.canVisualize(fullValue)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
logger.error("failed to check visualization of value ($viz)", t, Attachment("value.txt", fullValue))
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user