[debugger] review of text visualizers, IJ-CR-137328

GitOrigin-RevId: 42d1f4461648eb890ac98bffd3486ddf0668e71d
This commit is contained in:
Vladimir Parfinenko
2024-08-08 13:45:08 +02:00
committed by intellij-monorepo-bot
parent 2810dcfe6f
commit d751b630b1
7 changed files with 36 additions and 44 deletions

View File

@@ -41,7 +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.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopupUtil;
import com.sun.jdi.*;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -207,7 +207,7 @@ public class JavaValue extends XNamedValue implements NodeDescriptorProvider, XV
}
});
}
else if (VisualizedTextPopup.INSTANCE.isVisualizable(text)) {
else if (VisualizedTextPopupUtil.isVisualizable(text)) {
node.setFullValueEvaluator(new XFullValueEvaluator() {
@Override
public void startEvaluation(@NotNull XFullValueEvaluationCallback callback) {

View File

@@ -755,7 +755,6 @@ com.intellij.xdebugger.ui.DebuggerColors
- 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
- canVisualize(java.lang.String):Z
- a:visualize(java.lang.String):java.util.List
*:com.intellij.xdebugger.ui.VisualizedContentTab
- a:createComponent(com.intellij.openapi.project.Project,com.intellij.openapi.Disposable):javax.swing.JComponent

View File

@@ -15,19 +15,9 @@ import javax.swing.JComponent
*/
@ApiStatus.Experimental // until we consider collection visualizers
interface 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.
* Returns an empty list, if [canVisualize] returns false.
* Returns an empty list, if [value] cannot be visualized.
*/
fun visualize(value: @NlsSafe String): List<VisualizedContentTab>
}

View File

@@ -6,7 +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 com.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopupUtil
import org.jetbrains.annotations.ApiStatus
import java.awt.CardLayout
import java.awt.Font
@@ -32,7 +32,7 @@ abstract class CustomComponentEvaluator(name: String) : XFullValueEvaluator() {
editor: Editor?,
component: JComponent,
cancelCallback: Runnable?) {
VisualizedTextPopup.showValuePopup(event, project, editor, component, cancelCallback)
VisualizedTextPopupUtil.showValuePopup(event, project, editor, component, cancelCallback)
}
protected class EvaluationCallback(private val myPanel: JComponent,

View File

@@ -52,7 +52,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 com.intellij.xdebugger.impl.ui.visualizedtext.VisualizedTextPopupUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
@@ -137,7 +137,7 @@ public final class DebuggerUIUtil {
@NotNull MouseEvent event,
@NotNull Project project,
@Nullable Editor editor) {
WriteIntentReadAction.run((Runnable)() -> VisualizedTextPopup.INSTANCE.evaluateAndShowValuePopup(evaluator, event, project, editor));
WriteIntentReadAction.run((Runnable)() -> VisualizedTextPopupUtil.evaluateAndShowValuePopup(evaluator, event, project, editor));
}
/**

View File

@@ -8,13 +8,13 @@ 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 {
internal object RawTextVisualizer : TextValueVisualizer {
override fun visualize(value: @NlsSafe String): List<VisualizedContentTab> =
listOf(object : TextBasedContentTab(), VisualizedContentTabWithStats {
override val name
get() = XDebuggerBundle.message("xdebugger.visualized.text.name.raw")
override val id
get() = FallbackTextVisualizer::class.qualifiedName!!
get() = RawTextVisualizer::class.qualifiedName!!
override val contentTypeForStats
get() = TextVisualizerContentType.RAW
override fun formatText() =

View File

@@ -28,6 +28,7 @@ import java.awt.Font
import java.awt.Rectangle
import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import javax.swing.JComponent
import javax.swing.JPanel
import kotlin.math.max
@@ -36,11 +37,11 @@ import kotlin.math.max
* Provides tools to show a text-like value that might be formatted for better readability (JSON, XML, HTML, etc.).
*/
@ApiStatus.Internal
object VisualizedTextPopup {
object VisualizedTextPopupUtil {
private const val SELECTED_TAB_KEY_PREFIX = "DEBUGGER_VISUALIZED_TEXT_SELECTED_TAB#"
private val LOG = Logger.getInstance(VisualizedTextPopup.javaClass)
private val LOG = Logger.getInstance(VisualizedTextPopupUtil.javaClass)
private val extensionPoint: ExtensionPointName<TextValueVisualizer> =
ExtensionPointName.create("com.intellij.xdebugger.textValueVisualizer")
@@ -70,6 +71,7 @@ object VisualizedTextPopup {
return popup
}
@JvmStatic
fun evaluateAndShowValuePopup(evaluator: XFullValueEvaluator, event: MouseEvent, project: Project, editor: Editor?) {
if (evaluator is CustomComponentEvaluator) {
return evaluator.show(event, project, editor)
@@ -77,17 +79,12 @@ object VisualizedTextPopup {
val panel = PopupPanel(project)
val callback = EvaluationCallback(project, panel)
val popup = showValuePopup(event, project, editor, panel.component, callback::setObsolete)
val popup = showValuePopup(event, project, editor, panel, callback::setObsolete)
Disposer.register(popup, panel)
evaluator.startEvaluation(callback) // to make it really cancellable
}
private class PopupPanel(private val project: Project) : Disposable.Default {
private val base = JPanel(CardLayout())
val component: JComponent
get() = base
private class PopupPanel(private val project: Project) : JPanel(CardLayout()), Disposable.Default {
init {
showOnlyText(XDebuggerUIConstants.getEvaluatingExpressionMessage())
}
@@ -99,10 +96,10 @@ object VisualizedTextPopup {
}
fun showComponent(component: JComponent) {
base.removeAll()
base.add(component)
base.revalidate()
base.repaint()
removeAll()
add(component)
revalidate()
repaint()
}
}
@@ -110,7 +107,7 @@ object VisualizedTextPopup {
private class EvaluationCallback(private val project: Project, private val panel: PopupPanel) : XFullValueEvaluator.XFullValueEvaluationCallback {
private val obsolete = AtomicBoolean(false)
private var lastFullValueHashCode: Int? = null
private var lastFullValueHashCode = AtomicReference<Int?>()
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.
@@ -118,14 +115,16 @@ object VisualizedTextPopup {
// 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()
val hashCode = fullValue.hashCode()
if (hashCode == lastFullValueHashCode.get()) return
lastFullValueHashCode.set(hashCode)
AppUIUtil.invokeOnEdt {
try {
panel.showComponent(createComponent(fullValue))
}
catch (e: Exception) {
LOG.error(e)
errorOccurred(e.toString())
}
}
@@ -133,7 +132,7 @@ object VisualizedTextPopup {
override fun errorOccurred(errorMessage: String) {
AppUIUtil.invokeOnEdt {
panel.showOnlyText(errorMessage) {
panel.showOnlyText("ERROR OCCURRED: $errorMessage") {
it.foreground = XDebuggerUIConstants.ERROR_MESSAGE_ATTRIBUTES.fgColor
}
}
@@ -171,16 +170,18 @@ object VisualizedTextPopup {
val tabs = tabsAndComponents.map { it.first }
val tabIds = tabs.map { it.id }
if (tabIds.distinct().size != tabIds.size) {
LOG.error("non-unique tab ids: $tabIds")
}
// 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 selectedTabKey = SELECTED_TAB_KEY_PREFIX + tabIds.sorted().joinToString("#")
val alreadyShownTabs = mutableSetOf<VisualizedContentTab>()
fun onTabShown() {
val idx = panel.selectedIndex
if (idx < 0 || idx >= tabs.size) return
val selectedTab = tabs[idx]
val selectedTab = tabs.getOrNull(panel.selectedIndex) ?: return
PropertiesComponent.getInstance().setValue(selectedTabKey, selectedTab.id)
val firstTime = alreadyShownTabs.add(selectedTab)
@@ -198,6 +199,7 @@ object VisualizedTextPopup {
}
}
// We return pairs because it's easier to do all dangerous stuff and catch all errors in one place.
fun collectVisualizedTabs(project: Project, fullValue: String, parentDisposable: Disposable): List<Pair<VisualizedContentTab, JComponent>> {
val tabs = extensionPoint.extensionList
.flatMap { viz ->
@@ -210,7 +212,7 @@ object VisualizedTextPopup {
}
} +
// Explicitly add the fallback raw visualizer to make it the last one.
FallbackTextVisualizer.visualize(fullValue)
RawTextVisualizer.visualize(fullValue)
return tabs.mapNotNull { tab ->
try {
@@ -223,13 +225,14 @@ object VisualizedTextPopup {
}
}
@JvmStatic
fun isVisualizable(fullValue: String): Boolean {
// text with line breaks would be nicely rendered by the raw visualizer
return StringUtil.containsLineBreak(fullValue) ||
extensionPoint.extensionList
.any { viz ->
try {
viz.canVisualize(fullValue)
viz.visualize(fullValue).isNotEmpty()
}
catch (t: Throwable) {
LOG.error("failed to check visualization of value ($viz)", t, Attachment("value.txt", fullValue))