[PyCharm Data Viewer] Refactoring and PY-44817. Extract the common part from the old and new tables. Added an ability to save format for all tables #PY-44817 Fixed

* Added checkbox to allow remembering an inserted format for all tables
* Added an ability to save format between ide sessions
* Extracted the common part of tables in a separate class.


Merge-request: IJ-MR-158219
Merged-by: Natalia Murycheva <natalia.murycheva@jetbrains.com>

GitOrigin-RevId: a33ea8ed7174e45b151ade0cd36717ab61d97427
This commit is contained in:
Natalia Murycheva
2025-04-01 20:49:47 +00:00
committed by intellij-monorepo-bot
parent dff61fdd49
commit 3757fcb978
15 changed files with 754 additions and 441 deletions

View File

@@ -823,14 +823,14 @@ debugger.test.failed.caption=Test failed
debugger.error.in.test.setup.or.teardown.caption=Error in test set up or tear down
debugger.remote.port.out.of.boundaries=The port number is out of boundaries
debugger.dataviewer.action.export.name=Export\u2026
debugger.dataviewer.action.export.dialog.description=Save to
debugger.dataviewer.action.export.dialog.label=Export files
debugger.dataviewer.export.error.title=Table export failed
debugger.dataviewer.export.error.invalid.filepath=Invalid filepath
debugger.dataviewer.export.error.invalid.filepath.content=Filepath ''{0}'' is invalid
debugger.dataviewer.export.error.unhandled=Unhandled error
debugger.dataviewer.export.error.unhandled.content=Couldn''t export to ''{0}'':\n{1}
debugger.dataViewer.action.export.name=Export\u2026
debugger.dataViewer.action.export.dialog.description=Save to
debugger.dataViewer.action.export.dialog.label=Export files
debugger.dataViewer.export.error.title=Table export failed
debugger.dataViewer.export.error.invalid.filepath=Invalid filepath
debugger.dataViewer.export.error.invalid.filepath.content=Filepath ''{0}'' is invalid
debugger.dataViewer.export.error.unhandled=Unhandled error
debugger.dataViewer.export.error.unhandled.content=Couldn''t export to ''{0}'':\n{1}
debugger.dataviewer.notification.group.title=Data Viewer error
debugger.dataviewer.action.copy.name=Copy
debugger.dataviewer.action.set.filter.name=Set Filter\u2026
@@ -850,11 +850,14 @@ debugger.dataviewer.action.copy.properties.include.indices=Include indices
debugger.dataviewer.action.copy.properties.separator=Separator:
debugger.dataviewer.action.copy.update.message=Copy to clipboard
debugger.dataviewer.action.remove.filter.name=Remove Filter
debugger.dataviewer.modifier.error=Modifier error: {0}
debugger.dataViewer.modifier.error=Modifier error: {0}
debugger.dataviewer.header.filter.hint={0}: {1}
debugger.dataviewer.header.filter.hint.mode.expression=Expression
debugger.dataviewer.header.filter.hint.mode.regex=RegEx
debugger.dataviewer.header.filter.hint.mode.substring=Substring
debugger.dataViewer.dataframes.unsupported=<html>The old view does not support this dataframe type.</html>
debugger.dataViewer.switch.between.tables.gotIt.text=Here you can switch between new and old table modes for viewing data.
debugger.dataViewer.switch.between.tables.gotIt.link=Switch mode
debugger.remote.waiting.for.process.connection=Waiting for process connection\u2026
debugger.remote.waiting.for.connection=Waiting for connection

View File

@@ -6,7 +6,7 @@ import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.containerview.ColoredCellRenderer;
import com.jetbrains.python.debugger.containerview.ColumnFilter;
import com.jetbrains.python.debugger.containerview.DataViewStrategy;
import com.jetbrains.python.debugger.containerview.PyDataViewerPanel;
import com.jetbrains.python.debugger.containerview.PyDataViewerCommunityPanel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -46,7 +46,7 @@ public class ArrayViewStrategy extends DataViewStrategy {
@Override
public AsyncArrayTableModel createTableModel(int rowCount,
int columnCount,
@NotNull PyDataViewerPanel panel,
@NotNull PyDataViewerCommunityPanel panel,
@NotNull PyDebugValue debugValue) {
return new AsyncArrayTableModel(rowCount, columnCount, panel, debugValue, this);
}

View File

@@ -21,7 +21,7 @@ import com.jetbrains.python.debugger.ArrayChunkBuilder;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.containerview.DataViewStrategy;
import com.jetbrains.python.debugger.containerview.PyDataViewerPanel;
import com.jetbrains.python.debugger.containerview.PyDataViewerCommunityPanel;
import org.jetbrains.annotations.NotNull;
import javax.swing.event.TableModelEvent;
@@ -37,7 +37,7 @@ public class AsyncArrayTableModel extends AbstractTableModel {
private final int myRows;
private final int myColumns;
private final PyDataViewerPanel myDataProvider;
private final PyDataViewerCommunityPanel myDataProvider;
private final ExecutorService myExecutorService = ConcurrencyUtil.newSingleThreadExecutor("Python async table");
@@ -53,7 +53,7 @@ public class AsyncArrayTableModel extends AbstractTableModel {
return ListenableFutureTask.create(() -> {
ArrayChunk chunk = myDebugValue.getFrameAccessor()
.getArrayItems(myDebugValue, key.first, key.second, Math.min(CHUNK_ROW_SIZE, getRowCount() - key.first),
Math.min(CHUNK_COL_SIZE, getColumnCount() - key.second), myDataProvider.getFormat());
Math.min(CHUNK_COL_SIZE, getColumnCount() - key.second), myDataProvider.getDataViewerModel().getFormat());
handleChunkAdded(key.first, key.second, chunk);
return chunk;
});
@@ -62,7 +62,7 @@ public class AsyncArrayTableModel extends AbstractTableModel {
public AsyncArrayTableModel(int rows,
int columns,
PyDataViewerPanel provider,
PyDataViewerCommunityPanel provider,
PyDebugValue debugValue,
DataViewStrategy strategy) {
myRows = rows;
@@ -131,7 +131,7 @@ public class AsyncArrayTableModel extends AbstractTableModel {
try {
ArrayChunk chunk = myDebugValue.getFrameAccessor()
.getArrayItems(myDebugValue, fromRow, fromCol, toRow - fromRow + 1, toCol - fromCol + 1,
myDataProvider.getFormat());
myDataProvider.getDataViewerModel().getFormat());
if (chunk != null) {
whenLoaded.accept(chunk);

View File

@@ -34,7 +34,7 @@ public abstract class DataViewStrategy {
public abstract AsyncArrayTableModel createTableModel(int rowCount,
int columnCount,
@NotNull PyDataViewerPanel panel,
@NotNull PyDataViewerCommunityPanel panel,
@NotNull PyDebugValue debugValue);
public abstract ColoredCellRenderer createCellRenderer(double minValue, double maxValue, @NotNull ArrayChunk arrayChunk);

View File

@@ -91,7 +91,7 @@ class PyDataView(private val project: Project) : DumbAware {
window.contentManager.getReady(this).doWhenDone {
val selectedInfo = addTab(value.frameAccessor)
val dataViewerPanel = selectedInfo.component as PyDataViewerPanel
dataViewerPanel.apply(value, false)
dataViewerPanel.component.apply(value, false)
window.show {
window.component.requestFocusInWindow()
dataViewerPanel.requestFocusInWindow()
@@ -103,7 +103,7 @@ class PyDataView(private val project: Project) : DumbAware {
val tabsToRemove: MutableList<Content> = ArrayList()
contentManager.contents.forEach {
if (ifClose.test(getPanel(it.component).frameAccessor)) {
if (ifClose.test(getPanel(it.component).dataViewerModel.frameAccessor)) {
tabsToRemove.add(it)
}
}
@@ -111,7 +111,6 @@ class PyDataView(private val project: Project) : DumbAware {
ApplicationManager.getApplication().invokeLater {
tabsToRemove.forEach {
contentManager.removeContent(it, true)
getPanel(it.component).closeEditorTabs()
}
}
}
@@ -119,8 +118,8 @@ class PyDataView(private val project: Project) : DumbAware {
fun updateTabs(handler: ProcessHandler) {
saveSelectedInfo()
contentManager.contents.forEach { content ->
val panel: PyDataViewerPanel = getPanel(content.component)
val accessor = panel.frameAccessor
val panel: PyDataViewerCommunityPanel = getPanel(content.component)
val accessor = panel.dataViewerModel.frameAccessor
if (accessor !is PyDebugProcess) {
return@forEach
}
@@ -145,7 +144,7 @@ class PyDataView(private val project: Project) : DumbAware {
private fun saveSelectedInfo() {
val selectedInfo = contentManager.selectedContent
if (!hasOnlyEmptyTab() && selectedInfo != null) {
val accessor: PyFrameAccessor = getPanel(selectedInfo.component).frameAccessor
val accessor: PyFrameAccessor = getPanel(selectedInfo.component).dataViewerModel.frameAccessor
if (accessor is PyDebugProcess) {
selectedInfos[accessor.processHandler] = selectedInfo
}
@@ -173,14 +172,7 @@ class PyDataView(private val project: Project) : DumbAware {
contentManager.removeAllContents(true)
}
var panel: PyDataViewerPanel? = null
for (factory in PyDataViewPanelFactory.EP_NAME.extensionList) {
panel = factory.createDataViewPanel(project, frameAccessor)
if (panel != null) break
}
if (panel == null) {
panel = PyDataViewerPanel(project, frameAccessor)
}
val panel = PyDataViewerPanel(project, frameAccessor)
val content = ContentFactory.getInstance().createContent(panel, null, false)
content.isCloseable = true
@@ -198,7 +190,7 @@ class PyDataView(private val project: Project) : DumbAware {
if (window is ToolWindowEx) {
window.setTabActions(NewViewerAction(frameAccessor))
}
panel.addListener(PyDataViewerPanel.Listener {
panel.addListener( PyDataViewerAbstractPanel.OnNameChangedListener {
content.displayName = it
})
Disposer.register(content, panel)
@@ -233,15 +225,15 @@ class PyDataView(private val project: Project) : DumbAware {
}
}
fun getPanel(component: JComponent): PyDataViewerPanel {
return component as PyDataViewerPanel
fun getPanel(component: JComponent): PyDataViewerCommunityPanel {
return component as PyDataViewerCommunityPanel
}
companion object {
private const val DATA_VIEWER_ID = "SciView"
const val COLORED_BY_DEFAULT = "python.debugger.dataview.coloredbydefault"
const val AUTO_RESIZE = "python.debugger.dataview.autoresize"
const val COLORED_BY_DEFAULT: String = "python.debugger.dataView.coloredByDefault"
const val AUTO_RESIZE: String = "python.debugger.dataView.autoresize"
private const val HELP_ID = "reference.toolWindows.PyDataView"
@JvmStatic

View File

@@ -10,6 +10,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.installAndEnable
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.components.JBCheckBox
import com.intellij.util.PlatformUtils
@@ -21,15 +22,13 @@ import com.jetbrains.python.debugger.containerview.PyDataView.Companion.isColori
import com.jetbrains.python.debugger.containerview.PyDataView.Companion.setAutoResizeEnabled
import com.jetbrains.python.debugger.containerview.PyDataView.Companion.setColoringEnabled
import java.awt.BorderLayout
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.Action
import javax.swing.BoxLayout
import javax.swing.JPanel
class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : DialogWrapper(myProject, false) {
private val mainPanel: JPanel
private val dataViewerPanel: PyDataViewerPanel
private val dataViewerPanel: PyDataViewerCommunityPanel
private var jupyterSuggestionPanel: JPanel? = null
init {
@@ -37,7 +36,7 @@ class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : Di
setCancelButtonText(PyBundle.message("debugger.data.view.close"))
setCrossClosesWindow(true)
dataViewerPanel = PyDataViewerPanel(myProject, value.frameAccessor)
dataViewerPanel = PyDataViewerCommunityPanel(PyDataViewerModel(myProject, value.frameAccessor))
dataViewerPanel.apply(value, modifier = false, commandSource = null)
dataViewerPanel.preferredSize = JBUI.size(TABLE_DEFAULT_WIDTH, TABLE_DEFAULT_HEIGHT)
@@ -52,8 +51,8 @@ class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : Di
mainPanel.add(dataViewerPanel, BorderLayout.CENTER)
dataViewerPanel.addListener(object : PyDataViewerPanel.Listener {
override fun onNameChanged(name: @NlsContexts.TabTitle String) {
dataViewerPanel.addListener(object : PyDataViewerAbstractPanel.OnNameChangedListener {
override fun onNameChanged(@NlsSafe name: @NlsContexts.TabTitle String) {
title = name
}
})
@@ -64,27 +63,23 @@ class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : Di
init()
}
override fun getDimensionServiceKey() = "#com.jetbrains.python.debugger.containerview.PyDataViewDialog"
override fun getDimensionServiceKey(): String = "#com.jetbrains.python.debugger.containerview.PyDataViewDialog"
private fun createBottomElements(): JPanel {
val colored = JBCheckBox(PyBundle.message("debugger.data.view.colored.cells"))
colored.setSelected(isColoringEnabled(myProject))
colored.addActionListener(object : ActionListener {
override fun actionPerformed(e: ActionEvent?) {
setColoringEnabled(myProject, colored.isSelected)
dataViewerPanel.isColored = colored.isSelected
}
})
colored.addActionListener {
setColoringEnabled(myProject, colored.isSelected)
dataViewerPanel.dataViewerModel.isColored = colored.isSelected
}
val resize = JBCheckBox(PyBundle.message("debugger.data.view.resize.automatically"))
resize.setSelected(isAutoResizeEnabled(myProject))
resize.addActionListener(object : ActionListener {
override fun actionPerformed(e: ActionEvent?) {
setAutoResizeEnabled(myProject, resize.isSelected)
dataViewerPanel.resize(resize.isSelected)
dataViewerPanel.updateUI()
}
})
resize.addActionListener {
setAutoResizeEnabled(myProject, resize.isSelected)
dataViewerPanel.resize(resize.isSelected)
dataViewerPanel.updateUI()
}
return JPanel().apply {
layout = BoxLayout(this, BoxLayout.Y_AXIS)
@@ -93,9 +88,9 @@ class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : Di
}
}
override fun createActions() = arrayOf<Action>(cancelAction)
override fun createActions(): Array<Action> = arrayOf(cancelAction)
override fun createCenterPanel() = mainPanel
override fun createCenterPanel(): JPanel = mainPanel
private fun createJupyterSuggestionPanel(): JPanel? {
if (PlatformUtils.isCommunityEdition()) return null
@@ -166,7 +161,7 @@ class PyDataViewDialog(private val myProject: Project, value: PyDebugValue) : Di
private const val TABLE_DEFAULT_WIDTH = 700
private const val TABLE_DEFAULT_HEIGHT = 500
private const val JUPYTER_SUGGESTION_ENABLED_PROPERTY_KEY = "python.debugger.dataview.jupyter.suggestion.enabled"
private const val JUPYTER_SUGGESTION_ENABLED_PROPERTY_KEY = "python.debugger.dataView.jupyter.suggestion.enabled"
private fun isJupyterSuggestionEnabled(project: Project) = PropertiesComponent.getInstance(project).getBoolean(JUPYTER_SUGGESTION_ENABLED_PROPERTY_KEY, true)

View File

@@ -2,11 +2,9 @@
package com.jetbrains.python.debugger.containerview;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.debugger.PyFrameAccessor;
public abstract class PyDataViewPanelFactory {
public static final ExtensionPointName<PyDataViewPanelFactory> EP_NAME = ExtensionPointName.create("Pythonid.dataViewPanelFactory");
public abstract PyDataViewerPanel createDataViewPanel(Project project, PyFrameAccessor frameAccessor);
public abstract PyDataViewerAbstractPanel createDataViewerPanel(PyDataViewerModel state);
}

View File

@@ -0,0 +1,211 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.containerview
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.EditorTextField
import com.jetbrains.python.PyBundle
import com.jetbrains.python.debugger.ArrayChunk
import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.PyDebuggerException
import com.jetbrains.python.debugger.PyFrameAccessor
import com.jetbrains.python.debugger.statistics.PyDataViewerCollector
import org.jetbrains.annotations.Nls
import java.awt.BorderLayout
import java.util.concurrent.CopyOnWriteArrayList
import javax.swing.JComponent
import javax.swing.JPanel
abstract class PyDataViewerAbstractPanel(
val dataViewerModel: PyDataViewerModel,
val isPanelFromFactory: Boolean = false,
) : JPanel(BorderLayout()), Disposable {
/**
* Represents a formatting string used for specifying format.
*/
abstract var formatValueFromUI: String
/**
* Represents a slicing string used for slicing data in a viewer panel.
* For example, np_array[0] or df['column_1'].
*/
protected abstract var slicingValueFromUI: String
abstract var isColoredValueFromUI: Boolean
abstract val slicingTextField: EditorTextField
abstract var topToolbar: JPanel?
protected val listeners: CopyOnWriteArrayList<OnNameChangedListener> = CopyOnWriteArrayList<OnNameChangedListener>()
protected abstract fun setError(text: @NlsContexts.Label String, modifier: Boolean)
protected abstract fun setupDataProvider()
protected abstract fun updateUI(chunk: ArrayChunk, originalDebugValue: PyDebugValue, strategy: DataViewStrategy, modifier: Boolean)
abstract fun createTable(originalDebugValue: PyDebugValue? = null, chunk: ArrayChunk? = null): JComponent
abstract fun recreateTable()
protected fun onEnterPressed(commandSource: TextFieldCommandSource) {
apply(commandSource)
}
fun apply(commandSource: TextFieldCommandSource) {
dataViewerModel.format = formatValueFromUI
dataViewerModel.slicing = slicingValueFromUI
dataViewerModel.isColored = isColoredValueFromUI
apply(dataViewerModel.slicing, false, commandSource)
}
fun apply(name: String?, modifier: Boolean, commandSource: TextFieldCommandSource? = null) {
ApplicationManager.getApplication().executeOnPooledThread {
val debugValue = getDebugValue(name, modifier)
ApplicationManager.getApplication().invokeLater { debugValue?.let { apply(it, modifier, commandSource) } }
}
}
fun apply(debugValue: PyDebugValue, modifier: Boolean, commandSource: TextFieldCommandSource? = null) {
if (!modifier) {
when (commandSource) {
TextFieldCommandSource.SLICING -> PyDataViewerCollector.logDataSlicingApplied(isPanelFromFactory)
TextFieldCommandSource.FORMATTING -> PyDataViewerCollector.logDataFormattingApplied(isPanelFromFactory)
else -> Unit
}
val dimensions = getValueDimensions(debugValue)
PyDataViewerCollector.logDataOpened(dataViewerModel.project, debugValue.type,
dimensions?.size,
dimensions?.getOrNull(0) ?: 0,
dimensions?.getOrNull(1) ?: 0,
isNewTable = isPanelFromFactory)
}
val type = debugValue.type
val strategy = DataViewStrategy.getStrategy(type)
if (strategy == null) {
setError(PyBundle.message("debugger.data.view.type.is.not.supported", type), modifier)
return
}
ApplicationManager.getApplication().executeOnPooledThread {
try {
doStrategyInitExecution(debugValue.frameAccessor, strategy)
val arrayChunk = debugValue.frameAccessor.getArrayItems(debugValue, 0, 0, 0, 0, formatValueFromUI)
ApplicationManager.getApplication().invokeLater {
updateUI(arrayChunk, debugValue, strategy, modifier)
dataViewerModel.isModified = modifier
dataViewerModel.debugValue = debugValue
}
}
catch (e: IllegalArgumentException) {
ApplicationManager.getApplication().invokeLater { setError(e.localizedMessage, modifier) } //NON-NLS
}
catch (e: PyDebuggerException) {
thisLogger().error(e)
}
catch (e: Exception) {
if (e.message?.contains("Numpy is not available") == true) {
setError(PyBundle.message("debugger.data.view.numpy.is.not.available", type), modifier)
}
thisLogger().error("PyDataViewer.apply: Numpy is not available", e)
}
}
}
/**
* PyDebugValue shape in case of arrays could be like (10, 14, 23),
* and we can extract these dimensions for analysis and logging.
*/
private fun getValueDimensions(debugValue: PyDebugValue): List<Int>? {
val shape = debugValue.shape?.takeIf { it.startsWith("(") && it.endsWith(")") } ?: return null
return shape
.removeSurrounding("(", ")")
.split(",")
.filter { it.isNotEmpty() }
.mapNotNull { it.trim().toIntOrNull() }
.takeIf { it.size == shape.count { ch -> ch == ',' } + 1 }
}
@Throws(PyDebuggerException::class)
protected fun doStrategyInitExecution(frameAccessor: PyFrameAccessor, strategy: DataViewStrategy) {
val execString = strategy.initExecuteString ?: return
frameAccessor.evaluate(execString, true, false)
}
protected fun updateTabNameSlicingFieldAndFormatField(chunk: ArrayChunk?, originalDebugValue: PyDebugValue, modifier: Boolean) {
// Debugger generates a temporary name for every slice evaluation, so we should select a correct name for it
val debugValue = chunk?.value
val realName = if (debugValue == null || debugValue.name == originalDebugValue.tempName) originalDebugValue.name else chunk.slicePresentation
var shownName = realName
if (modifier && dataViewerModel.originalVarName != shownName) {
shownName = String.format(MODIFIED_VARIABLE_FORMAT, dataViewerModel.originalVarName)
}
else {
dataViewerModel.originalVarName = realName
}
dataViewerModel.originalVarName?.let { slicingValueFromUI = it }
// Modifier flag means that variable changes are temporary
dataViewerModel.modifiedVarName = realName
for (listener in listeners) {
listener.onNameChanged(shownName)
}
if (chunk != null) {
formatValueFromUI = chunk.format
}
}
protected fun getDebugValue(expression: @NlsSafe String?, modifier: Boolean): PyDebugValue? {
return try {
val debugValue = dataViewerModel.frameAccessor.evaluate(expression, false, true)
if (debugValue == null || debugValue.isErrorOnEval) {
ApplicationManager.getApplication().invokeLater {
val debugValueExpression = debugValue.value
val errorText = if (debugValue != null && debugValueExpression != null) {
debugValueExpression
} else {
PyBundle.message("debugger.data.view.failed.to.evaluate.expression", expression)
}
setError(errorText, modifier)
}
null
}
else {
debugValue
}
}
catch (e: PyDebuggerException) {
ApplicationManager.getApplication().invokeLater { setError(e.getTracebackError(), modifier) } //NON-NLS
null
}
}
fun addListener(onNameChangedListener: OnNameChangedListener) {
listeners.add(onNameChangedListener)
}
protected fun composeErrorMessage(text: @NlsSafe String, modifier: Boolean): @Nls String {
return if (modifier) PyBundle.message("debugger.dataViewer.modifier.error", text) else text
}
override fun dispose(): Unit = Unit
fun interface OnNameChangedListener {
fun onNameChanged(name: @NlsContexts.TabTitle String)
}
companion object {
private const val MODIFIED_VARIABLE_FORMAT = "%s*"
}
}

View File

@@ -0,0 +1,273 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.containerview
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.toolbarLayout.ToolbarLayoutStrategy
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.util.NlsContexts
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.ui.components.TwoSideComponent
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.text
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.debugger.ArrayChunk
import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.PyFrameListener
import com.jetbrains.python.debugger.array.AbstractDataViewTable
import com.jetbrains.python.debugger.array.AsyncArrayTableModel
import com.jetbrains.python.debugger.array.JBTableWithRowHeaders
import java.awt.BorderLayout
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import javax.swing.BorderFactory
import javax.swing.JEditorPane
import javax.swing.JPanel
class PyDataViewerCommunityPanel(
dataViewerModel: PyDataViewerModel,
) : PyDataViewerAbstractPanel(dataViewerModel, false) {
private var table: AbstractDataViewTable? = null
private val panelWithTable: JPanel = JPanel(BorderLayout())
private var formatTextField: EditorTextField = createEditorField(TextFieldCommandSource.FORMATTING)
override val slicingTextField: EditorTextField = createEditorField(TextFieldCommandSource.SLICING)
override var topToolbar: JPanel? = null
override var formatValueFromUI: String = ""
get() {
val format = formatTextField.text
return format.ifEmpty { "%s" }
}
set(value) {
field = value
formatTextField.text = value
}
override var slicingValueFromUI: String = ""
get() {
val slicing = slicingTextField.text
return slicing.ifEmpty { "None" }
}
set(value) {
field = value
slicingTextField.text = value
}
override var isColoredValueFromUI: Boolean
get() = dataViewerModel.protectedColored
set(value) {
dataViewerModel.protectedColored = value
val table = table
if (table != null && !table.isEmpty) {
(table.getDefaultRenderer(table.getColumnClass(0)) as ColoredCellRenderer).setColored(value)
table.repaint()
}
}
private val model: AsyncArrayTableModel?
get() {
return table?.model as? AsyncArrayTableModel
}
private lateinit var errorLabel: Cell<JEditorPane>
init {
dataViewerModel.PyDataViewerCompletionProvider().apply(slicingTextField)
formatTextField.text = dataViewerModel.format
add(panelWithTable, BorderLayout.CENTER)
add(panel {
row {
cell(slicingTextField).align(AlignX.FILL).resizableColumn()
label(PyBundle.message("form.data.viewer.format"))
cell(formatTextField)
}
row {
errorLabel = text("").apply { component.setForeground(JBColor.RED) }
}
}, BorderLayout.SOUTH)
setupChangeListener()
topToolbar = createAndSetupTopToolbar()
}
override fun recreateTable() {
panelWithTable.removeAll()
panelWithTable.add(createTable().scrollPane, BorderLayout.CENTER)
}
override fun setupDataProvider() {
val toolbarDataProvider = DataProvider { dataId ->
if (PY_DATA_VIEWER_COMMUNITY_PANEL_KEY.`is`(dataId)) table else null
}
PyDataViewerPanel.addDataProvider(this, toolbarDataProvider)
}
private fun createAndSetupTopToolbar(): JPanel {
val actionManager = ActionManager.getInstance()
val leftActionGroup = DefaultActionGroup(actionManager.getAction("ToggleDataViewColoring"))
val leftActionToolbar = actionManager.createActionToolbar("PyDataView", leftActionGroup, true)
leftActionToolbar.setTargetComponent(panelWithTable)
val rightActionGroup = DefaultActionGroup()
rightActionGroup.add(actionManager.getAction("OpenInEditorAction"))
rightActionGroup.add(actionManager.getAction("ExportTableAction"))
rightActionGroup.add(actionManager.getAction("SwitchBetweenTableModesAction"))
val rightActionToolbar = actionManager.createActionToolbar("PyDataView", rightActionGroup, true).apply {
layoutStrategy = ToolbarLayoutStrategy.NOWRAP_STRATEGY // For removing the empty space on the right of the toolbar.
}
rightActionToolbar.setTargetComponent(panelWithTable)
val twoSideComponent = TwoSideComponent(leftActionToolbar.component, rightActionToolbar.component)
add(twoSideComponent, BorderLayout.BEFORE_FIRST_LINE)
topToolbar = twoSideComponent
return twoSideComponent
}
private fun isVariablePresentInStack(): Boolean {
val values = dataViewerModel.frameAccessor.loadFrame(null) ?: return true
for (i in 0 until values.size()) {
if (values.getValue(i) == dataViewerModel.debugValue) {
return true
}
}
return false
}
private fun setupChangeListener() {
dataViewerModel.frameAccessor.addFrameListener(object : PyFrameListener {
override fun frameChanged() {
dataViewerModel.debugValue ?: return
ApplicationManager.getApplication().executeOnPooledThread {
// Could be that in changed frames our value is missing. (PY-66235)
if (isVariablePresentInStack()) {
updateModel()
}
}
}
})
}
private fun updateModel() {
val model = model ?: return
model.invalidateCache()
if (dataViewerModel.isModified) {
apply(dataViewerModel.modifiedVarName, true)
}
else {
updateDebugValue(model)
ApplicationManager.getApplication().invokeLater {
if (isShowing()) {
model.fireTableDataChanged()
}
}
}
}
private fun updateDebugValue(model: AsyncArrayTableModel) {
val oldValue = model.debugValue
if (oldValue != null && !oldValue.isTemporary || slicingValueFromUI.isEmpty()) {
return
}
val newValue = getDebugValue(slicingValueFromUI, false)
if (newValue != null) {
model.debugValue = newValue
}
}
override fun createTable(originalDebugValue: PyDebugValue?, chunk: ArrayChunk?): AbstractDataViewTable {
val mainTable = JBTableWithRowHeaders(PyDataView.isAutoResizeEnabled(dataViewerModel.project))
mainTable.scrollPane.border = BorderFactory.createEmptyBorder()
panelWithTable.apply {
add(mainTable.scrollPane, BorderLayout.CENTER)
revalidate()
repaint()
}
table = mainTable
return mainTable
}
private fun createEditorField(commandSource: TextFieldCommandSource): EditorTextField {
return object : EditorTextField(EditorFactory.getInstance().createDocument(""), dataViewerModel.project, PythonFileType.INSTANCE, false, true) {
override fun createEditor(): EditorEx {
val editor = super.createEditor()
editor.settings.additionalColumnsCount = 5
editor.getContentComponent().addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER) {
onEnterPressed(commandSource)
}
}
})
return editor
}
}
}
override fun updateUI(
chunk: ArrayChunk,
originalDebugValue: PyDebugValue,
strategy: DataViewStrategy,
modifier: Boolean,
) {
errorLabel.visible(false)
val debugValue = chunk.value
val model = strategy.createTableModel(chunk.rows, chunk.columns, this, debugValue)
model.addToCache(chunk)
UIUtil.invokeLaterIfNeeded {
val table = table ?: createTable()
table.setModel(model, modifier)
updateTabNameSlicingFieldAndFormatField(chunk, originalDebugValue, modifier)
val cellRenderer = strategy.createCellRenderer(Double.MIN_VALUE, Double.MAX_VALUE, chunk)
cellRenderer.setColored(dataViewerModel.protectedColored)
model.fireTableDataChanged()
model.fireTableCellUpdated(0, 0)
if (table.columnCount > 0) {
table.setDefaultRenderer(table.getColumnClass(0), cellRenderer)
}
table.setShowColumns(strategy.showColumnHeader())
}
}
fun resize(autoResize: Boolean) {
table?.setAutoResize(autoResize)
apply(slicingValueFromUI, false)
}
override fun setError(text: @NlsContexts.Label String, modifier: Boolean) {
errorLabel.visible(true)
errorLabel.text(composeErrorMessage(text, modifier))
if (!modifier) {
table?.setEmpty()
for (listener in listeners) {
listener.onNameChanged(PyBundle.message("debugger.data.view.empty.tab"))
}
}
}
companion object {
val PY_DATA_VIEWER_COMMUNITY_PANEL_KEY: DataKey<AbstractDataViewTable> = DataKey.create<AbstractDataViewTable>("PY_DATA_VIEWER_COMMUNITY_PANEL_KEY")
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.containerview
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.util.TextFieldCompletionProvider
import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.PyFrameAccessor
class PyDataViewerModel(
val project: Project,
val frameAccessor: PyFrameAccessor,
) {
/**
* Represents a formatting string used for specifying format.
*
* This field is used as a source of truth during switching between community and powerful tables.
*/
var format: String = ""
/**
* Represents a slicing string used for slicing data in a viewer panel.
* For example, np_array_3d[0] or df['column_1'].
*
* This field is used as a source of truth during switching between community and powerful tables.
*/
var slicing: String = ""
/**
* This field is used as a source of truth during switching between community and powerful tables.
*/
var isColored: Boolean = false
var protectedColored: Boolean = PyDataView.isColoringEnabled(project)
var originalVarName: @NlsSafe String? = null
var modifiedVarName: String? = null
var debugValue: PyDebugValue? = null
var isModified: Boolean = false
inner class PyDataViewerCompletionProvider : TextFieldCompletionProvider() {
override fun addCompletionVariants(text: String, offset: Int, prefix: String, result: CompletionResultSet) {
val values = availableValues.sortedBy { obj: PyDebugValue -> obj.name }
for (i in values.indices) {
val value = values[i]
val element = LookupElementBuilder.create(value.name).withTypeText(value.type, true)
result.addElement(PrioritizedLookupElement.withPriority(element, -i.toDouble()))
}
}
private val availableValues: List<PyDebugValue>
get() {
val values: MutableList<PyDebugValue> = ArrayList()
try {
val list = frameAccessor.loadFrame(null) ?: return values
for (i in 0 until list.size()) {
val value = list.getValue(i) as PyDebugValue
val type = value.type
if (DataViewStrategy.getStrategy(type) != null) {
values.add(value)
}
}
}
catch (e: Exception) {
thisLogger().error(e)
}
return values
}
}
}

View File

@@ -1,411 +1,123 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.containerview
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.ide.DataManager
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.actionSystem.CompositeDataProvider
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.text
import com.intellij.util.TextFieldCompletionProvider
import com.intellij.util.ui.UIUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.debugger.*
import com.jetbrains.python.debugger.array.AbstractDataViewTable
import com.jetbrains.python.debugger.array.AsyncArrayTableModel
import com.jetbrains.python.debugger.array.JBTableWithRowHeaders
import org.jetbrains.annotations.Nls
import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.PyFrameAccessor
import java.awt.BorderLayout
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.util.concurrent.CopyOnWriteArrayList
import javax.swing.BorderFactory
import javax.swing.JEditorPane
import javax.swing.JComponent
import javax.swing.JPanel
open class PyDataViewerPanel(@JvmField protected val project: Project, val frameAccessor: PyFrameAccessor) :
JPanel(BorderLayout()), Disposable {
class PyDataViewerPanel(
val project: Project,
val frameAccessor: PyFrameAccessor,
private var isPanelFromFactory: Boolean = false,
) : JPanel(BorderLayout()), Disposable {
protected val tablePanel: JPanel = JPanel(BorderLayout())
protected var table: AbstractDataViewTable? = null
protected val sliceTextFieldOldTable: EditorTextField = createEditorField(TextFieldCommandSource.SLICING)
protected var formatTextFieldOldTable: EditorTextField = createEditorField(TextFieldCommandSource.FORMATTING)
private var colored: Boolean = PyDataView.isColoringEnabled(project)
private val listeners = CopyOnWriteArrayList<Listener>()
var originalVarName: @NlsSafe String? = null
private set
protected var modifiedVarName: String? = null
protected var debugValue: PyDebugValue? = null
/**
* Represents a formatting string used for specifying format.
*
* This field is needed to synchronize old and new tables
* regarding the actual formatting value.
*/
open var format: String = ""
get() {
val format = formatTextFieldOldTable.getText()
return format.ifEmpty { "%s" }
}
protected set(value) {
field = value
formatTextFieldOldTable.text = value
}
/**
* Represents a slicing string used for slicing data in a viewer panel.
* For example, numpy_array[:, 0] or df['column_1'].
*
* This field is needed to synchronize old and new tables
* regarding the actual slicing value.
*/
open var slicing: String = ""
get() {
val slicing = sliceTextFieldOldTable.getText()
return slicing.ifEmpty { "None" }
}
protected set(value) {
field = value
sliceTextFieldOldTable.text = value
}
var isColored: Boolean
get() = colored
set(state) {
colored = state
val table = table
if (table != null && !table.isEmpty) {
(table.getDefaultRenderer(table.getColumnClass(0)) as ColoredCellRenderer).setColored(state)
table.repaint()
}
}
private val model: AsyncArrayTableModel?
get() {
return table?.model as? AsyncArrayTableModel
}
var isModified: Boolean = false
protected set
private lateinit var errorLabel: Cell<JEditorPane>
protected val isSlicingAndFormattingOldPanelsVisible: AtomicBooleanProperty = AtomicBooleanProperty(true)
var component: PyDataViewerAbstractPanel
init {
PyDataViewCompletionProvider().apply(sliceTextFieldOldTable)
val dataViewerModel = PyDataViewerModel(project, frameAccessor)
add(tablePanel, BorderLayout.CENTER)
add(panel {
row {
cell(sliceTextFieldOldTable).align(AlignX.FILL).resizableColumn()
label(PyBundle.message("form.data.viewer.format"))
cell(formatTextFieldOldTable)
}.visibleIf(isSlicingAndFormattingOldPanelsVisible)
row {
errorLabel = text("").apply { component.setForeground(JBColor.RED) }
}
}, BorderLayout.SOUTH)
setupChangeListener()
}
override fun dispose(): Unit = Unit
private fun isVariablePresentInStack(): Boolean {
val values = frameAccessor.loadFrame(null) ?: return true
for (i in 0 until values.size()) {
if (values.getValue(i) == debugValue) {
return true
}
}
return false
}
private fun setupChangeListener() {
frameAccessor.addFrameListener(object : PyFrameListener {
override fun frameChanged() {
debugValue ?: return
ApplicationManager.getApplication().executeOnPooledThread {
// Could be that in changed frames our value is missing. (PY-66235)
if (isVariablePresentInStack()) {
updateModel()
}
}
}
})
}
private fun updateModel() {
val model = model ?: return
model.invalidateCache()
if (isModified) {
apply(modifiedVarName, true)
var dataViewerPanel = if (isPanelsFromFactoryAvailable()) {
isPanelFromFactory = true
createPanelFromFactory(dataViewerModel)
}
else {
updateDebugValue(model)
ApplicationManager.getApplication().invokeLater {
if (isShowing()) {
model.fireTableDataChanged()
}
}
PyDataViewerCommunityPanel(dataViewerModel)
}
component = dataViewerPanel
add(component)
setupDataProvider()
if (isPanelsFromFactoryAvailable() && !isPanelFromFactory) {
PyDataViewerPanelHelper.createGotItTooltip(tablePanel = component, tableParentPanel = this)
}
}
private fun updateDebugValue(model: AsyncArrayTableModel) {
val oldValue = model.debugValue
if (oldValue != null && !oldValue.isTemporary || slicing.isEmpty()) {
fun getDataViewerModel(): PyDataViewerModel {
return component.dataViewerModel
}
fun addListener(onNameChangedListener: PyDataViewerAbstractPanel.OnNameChangedListener) {
component.addListener(onNameChangedListener)
}
fun switchBetweenCommunityAndFactoriesTables() {
isPanelFromFactory = !isPanelFromFactory
val dataViewerModel = getDataViewerModel()
val debugValue = dataViewerModel.debugValue
if (debugValue != null && !isSupportedByCommunityDataViewer(debugValue)) {
isPanelFromFactory = true
PyDataViewerPanelHelper.showCommunityDataViewerRestrictionsBalloon(tablePanel = component, tableParentPanel = this)
return
}
val newValue = getDebugValue(slicing, false, false)
if (newValue != null) {
model.debugValue = newValue
}
}
protected open fun getOrCreateMainTable(): AbstractDataViewTable {
val mainTable = JBTableWithRowHeaders(PyDataView.isAutoResizeEnabled(project))
mainTable.scrollPane.border = BorderFactory.createEmptyBorder()
tablePanel.apply {
add(mainTable.scrollPane, BorderLayout.CENTER)
revalidate()
repaint()
}
table = mainTable
return mainTable
}
private fun createEditorField(commandSource: TextFieldCommandSource): EditorTextField {
return object : EditorTextField(EditorFactory.getInstance().createDocument(""), project, PythonFileType.INSTANCE, false, true) {
override fun createEditor(): EditorEx {
val editor = super.createEditor()
editor.settings.additionalColumnsCount = 5
editor.getContentComponent().addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER) {
onEnterPressed(commandSource)
}
}
})
return editor
}
}
}
protected fun onEnterPressed(commandSource: TextFieldCommandSource) {
apply(slicing, false, commandSource)
}
fun apply(name: String?, modifier: Boolean, commandSource: TextFieldCommandSource? = null) {
ApplicationManager.getApplication().executeOnPooledThread {
val debugValue = getDebugValue(name, true, modifier)
ApplicationManager.getApplication().invokeLater { debugValue?.let { apply(it, modifier, commandSource) } }
}
}
open fun apply(debugValue: PyDebugValue, modifier: Boolean, commandSource: TextFieldCommandSource? = null) {
errorLabel.visible(false)
val type = debugValue.type
val strategy = DataViewStrategy.getStrategy(type)
if (strategy == null) {
setError(PyBundle.message("debugger.data.view.type.is.not.supported", type), modifier)
return
}
ApplicationManager.getApplication().executeOnPooledThread {
try {
doStrategyInitExecution(debugValue.frameAccessor, strategy)
// Currently does not support pandas dataframes.
val arrayChunk = debugValue.frameAccessor.getArrayItems(debugValue, 0, 0, 0, 0, format)
ApplicationManager.getApplication().invokeLater {
updateUI(arrayChunk, debugValue, strategy, modifier)
isModified = modifier
this.debugValue = debugValue
}
}
catch (e: IllegalArgumentException) {
ApplicationManager.getApplication().invokeLater { setError(e.localizedMessage, modifier) } //NON-NLS
}
catch (e: PyDebuggerException) {
thisLogger().error(e)
}
catch (e: Exception) {
if (e.message?.let { "Numpy is not available" in it } == true) {
setError(PyBundle.message("debugger.data.view.numpy.is.not.available", type), modifier)
}
thisLogger().error(e)
}
}
}
@Throws(PyDebuggerException::class)
protected open fun doStrategyInitExecution(frameAccessor: PyFrameAccessor, strategy: DataViewStrategy): Unit = Unit
// Chunk currently could be null when we are trying to view, for example, pandas dataframe.
protected fun updateTabNameAndSliceField(chunk: ArrayChunk?, originalDebugValue: PyDebugValue, modifier: Boolean) {
// Debugger generates a temporary name for every slice evaluation, so we should select a correct name for it
val debugValue = chunk?.value
val realName = if (debugValue == null || debugValue.name == originalDebugValue.tempName) originalDebugValue.name else chunk.slicePresentation
var shownName = realName
if (modifier && originalVarName != shownName) {
@Suppress("HardCodedStringLiteral") // This is just format like %s and cannot be i18.
shownName = String.format(MODIFIED_VARIABLE_FORMAT, originalVarName)
component = if (isPanelFromFactory && isPanelsFromFactoryAvailable()) {
createPanelFromFactory(dataViewerModel)
}
else {
originalVarName = realName
PyDataViewerCommunityPanel(dataViewerModel)
}
originalVarName?.let { slicing = it }
component.recreateTable()
// Modifier flag means that variable changes are temporary
modifiedVarName = realName
if (sliceTextFieldOldTable.editor != null) {
sliceTextFieldOldTable.getCaretModel().moveToOffset(originalVarName!!.length)
}
for (listener in listeners) {
listener.onNameChanged(shownName)
}
removeAll()
add(component)
if (chunk != null) {
format = chunk.format
if (debugValue != null) {
component.apply(debugValue, false)
}
else {
component.apply(dataViewerModel.slicing, false)
}
}
protected open fun updateUI(
chunk: ArrayChunk, originalDebugValue: PyDebugValue,
strategy: DataViewStrategy, modifier: Boolean,
) {
val debugValue = chunk.value
val model = strategy.createTableModel(chunk.rows, chunk.columns, this, debugValue)
model.addToCache(chunk)
UIUtil.invokeLaterIfNeeded {
val table = table ?: getOrCreateMainTable()
table.setModel(model, modifier)
updateTabNameAndSliceField(chunk, originalDebugValue, modifier)
val cellRenderer = strategy.createCellRenderer(Double.MIN_VALUE, Double.MAX_VALUE, chunk)
cellRenderer.setColored(colored)
model.fireTableDataChanged()
model.fireTableCellUpdated(0, 0)
if (table.columnCount > 0) {
table.setDefaultRenderer(table.getColumnClass(0), cellRenderer)
}
table.setShowColumns(strategy.showColumnHeader())
}
private fun isPanelsFromFactoryAvailable(): Boolean {
return PyDataViewPanelFactory.EP_NAME.extensionList.isNotEmpty()
}
private fun getDebugValue(expression: @NlsSafe String?, pooledThread: Boolean, modifier: Boolean): PyDebugValue? {
return try {
val value = frameAccessor.evaluate(expression, false, true)
if (value == null || value.isErrorOnEval) {
val runnable = Runnable {
setError(if (value != null && value.value != null) value.value!!
else PyBundle.message("debugger.data.view.failed.to.evaluate.expression", expression), modifier)
}
if (pooledThread) {
ApplicationManager.getApplication().invokeLater(runnable)
}
else {
runnable.run()
}
return null
}
value
}
catch (e: PyDebuggerException) {
val runnable = Runnable { setError(e.getTracebackError(), modifier) } //NON-NLS
if (pooledThread) {
ApplicationManager.getApplication().invokeLater(runnable)
}
else {
runnable.run()
}
null
}
private fun createPanelFromFactory(dataViewerModel: PyDataViewerModel): PyDataViewerAbstractPanel {
return PyDataViewPanelFactory.EP_NAME.extensionList.first().createDataViewerPanel(dataViewerModel)
}
fun resize(autoResize: Boolean) {
table?.setAutoResize(autoResize)
apply(slicing, false)
/**
* Old tables cannot work properly with polars dataframes.
*/
private fun isSupportedByCommunityDataViewer(debugValue: PyDebugValue): Boolean {
return debugValue.typeQualifier != "polars.dataframe.frame"
}
open fun setError(text: @NlsContexts.Label String, modifier: Boolean) {
errorLabel.visible(true)
errorLabel.text(composeErrorMessage(text, modifier))
if (!modifier) {
table?.setEmpty()
for (listener in listeners) {
listener.onNameChanged(PyBundle.message("debugger.data.view.empty.tab"))
}
}
}
protected fun composeErrorMessage(text: @NlsContexts.Label String, modifier: Boolean): @Nls String {
return if (modifier) PyBundle.message("debugger.dataviewer.modifier.error", text) else text
}
fun addListener(listener: Listener) {
listeners.add(listener)
}
fun interface Listener {
fun onNameChanged(name: @NlsContexts.TabTitle String)
}
protected inner class PyDataViewCompletionProvider : TextFieldCompletionProvider() {
override fun addCompletionVariants(text: String, offset: Int, prefix: String, result: CompletionResultSet) {
val values = availableValues.sortedBy { obj: PyDebugValue -> obj.name }
for (i in values.indices) {
val value = values[i]
val element = LookupElementBuilder.create(value.name).withTypeText(value.type, true)
result.addElement(PrioritizedLookupElement.withPriority(element, -i.toDouble()))
}
private fun setupDataProvider() {
val toolbarDataProvider = DataProvider { dataId ->
if (PY_DATA_VIEWER_PANEL_KEY.`is`(dataId)) this@PyDataViewerPanel else null
}
private val availableValues: List<PyDebugValue>
get() {
val values: MutableList<PyDebugValue> = ArrayList()
try {
val list = frameAccessor.loadFrame(null) ?: return values
for (i in 0 until list.size()) {
val value = list.getValue(i) as PyDebugValue
val type = value.type
if (DataViewStrategy.getStrategy(type) != null) {
values.add(value)
}
}
}
catch (e: Exception) {
thisLogger().error(e)
}
return values
}
addDataProvider(this, toolbarDataProvider)
}
open fun closeEditorTabs(): Unit = Unit
override fun dispose() {}
companion object {
private const val MODIFIED_VARIABLE_FORMAT = "%s*"
val PY_DATA_VIEWER_PANEL_KEY: DataKey<PyDataViewerPanel> = DataKey.create<PyDataViewerPanel>("PY_DATA_VIEWER_PANEL_KEY")
fun addDataProvider(component: JComponent, provider: DataProvider) {
val currentProvider = DataManager.getDataProvider(component)
if (currentProvider != null) {
DataManager.removeDataProvider(component)
DataManager.registerDataProvider(component, CompositeDataProvider.compose(currentProvider, provider))
}
else {
DataManager.registerDataProvider(component, provider)
}
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.debugger.containerview
import com.intellij.icons.AllIcons
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.ui.GotItTooltip
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.util.preferredHeight
import com.intellij.ui.util.preferredWidth
import com.jetbrains.python.PyBundle
import java.awt.Point
internal object PyDataViewerPanelHelper {
fun showCommunityDataViewerRestrictionsBalloon(tablePanel: PyDataViewerAbstractPanel, tableParentPanel: PyDataViewerPanel) {
createBalloon(tablePanel)
.show(
RelativePoint(
tableParentPanel,
Point(tableParentPanel.width - (tablePanel.topToolbar?.preferredWidth ?: 10) / 2, tablePanel.topToolbar?.preferredHeight ?: 0)
),
Balloon.Position.below
)
}
private fun createBalloon(component: PyDataViewerAbstractPanel): Balloon {
return JBPopupFactory.getInstance()
.createHtmlTextBalloonBuilder(PyBundle.message("debugger.dataViewer.dataframes.unsupported"), MessageType.INFO, null).apply {
setDisposable(component)
setHideOnAction(true)
setHideOnClickOutside(true)
}.createBalloon()
}
/**
* GotIt Tooltip on the button with advertising of new and old table modes for viewing arrays in debug.
*/
fun createGotItTooltip(tablePanel: PyDataViewerAbstractPanel, tableParentPanel: PyDataViewerPanel) {
val tooltip = GotItTooltip("py.data.view.new.table",
PyBundle.message("debugger.dataViewer.switch.between.tables.gotIt.text"),
tableParentPanel)
.withLink(PyBundle.message("debugger.dataViewer.switch.between.tables.gotIt.link")) { tableParentPanel.switchBetweenCommunityAndFactoriesTables() }
.withShowCount(1)
.withIcon(AllIcons.General.BalloonInformation)
tooltip.show(tablePanel.topToolbar!!) { component, _ ->
Point(component.width - component.height / 2, component.height)
}
}
}

View File

@@ -6,7 +6,7 @@ import com.intellij.util.ui.UIUtil;
import com.jetbrains.python.debugger.ArrayChunk;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.array.AsyncArrayTableModel;
import com.jetbrains.python.debugger.containerview.PyDataViewerPanel;
import com.jetbrains.python.debugger.containerview.PyDataViewerCommunityPanel;
import org.jetbrains.annotations.Nullable;
import javax.swing.table.AbstractTableModel;
@@ -20,23 +20,23 @@ public class DataFrameTableModel extends AsyncArrayTableModel {
public DataFrameTableModel(int rows,
int columns,
PyDataViewerPanel dataProvider,
PyDataViewerCommunityPanel dataProvider,
PyDebugValue debugValue,
DataFrameViewStrategy strategy) {
super(rows, columns, dataProvider, debugValue, strategy);
myRowHeaderModel = new RowHeaderModel();
}
/* we use labels for the first column so we need to offset columns by one everywhere */
/* we use labels for the first column, so we need to offset columns by one everywhere */
@Override
public Object getValueAt(int row, int col) {
Object value = super.getValueAt(row, col);
if (value == AsyncArrayTableModel.EMPTY_CELL_VALUE) {
if (value == EMPTY_CELL_VALUE) {
return value;
}
TableValueDescriptor descriptor = createValueWithDescriptor(col, value);
return descriptor != null ? descriptor : AsyncArrayTableModel.EMPTY_CELL_VALUE;
return descriptor != null ? descriptor : EMPTY_CELL_VALUE;
}
private TableValueDescriptor createValueWithDescriptor(int frameCol, Object value) {

View File

@@ -7,7 +7,7 @@ import com.jetbrains.python.debugger.array.AsyncArrayTableModel;
import com.jetbrains.python.debugger.containerview.ColoredCellRenderer;
import com.jetbrains.python.debugger.containerview.ColumnFilter;
import com.jetbrains.python.debugger.containerview.DataViewStrategy;
import com.jetbrains.python.debugger.containerview.PyDataViewerPanel;
import com.jetbrains.python.debugger.containerview.PyDataViewerCommunityPanel;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -35,7 +35,7 @@ public class DataFrameViewStrategy extends DataViewStrategy {
@Override
public AsyncArrayTableModel createTableModel(int rowCount,
int columnCount,
@NotNull PyDataViewerPanel dataProvider,
@NotNull PyDataViewerCommunityPanel dataProvider,
@NotNull PyDebugValue debugValue) {
return new DataFrameTableModel(rowCount, columnCount, dataProvider, debugValue, this);
}

View File

@@ -3,6 +3,7 @@ package com.jetbrains.python.debugger.statistics
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.EventId1
import com.intellij.internal.statistic.eventLog.events.RoundedIntEventField
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.project.Project
@@ -25,8 +26,8 @@ object PyDataViewerCollector : CounterUsagesCollector() {
ROWS_COUNT_FIELD,
COLUMNS_COUNT_FIELD,
IS_NEW_TABLE_FIELD)
val SLICING_APPLIED_EVENT = GROUP.registerEvent("slicing.applied", IS_NEW_TABLE_FIELD)
val FORMATTING_APPLIED_EVENT = GROUP.registerEvent("formatting.applied", IS_NEW_TABLE_FIELD)
val SLICING_APPLIED_EVENT: EventId1<Boolean> = GROUP.registerEvent("slicing.applied", IS_NEW_TABLE_FIELD)
val FORMATTING_APPLIED_EVENT: EventId1<Boolean> = GROUP.registerEvent("formatting.applied", IS_NEW_TABLE_FIELD)
enum class DataType(private val typeName: String?) {
ARRAY("ndarray"),
@@ -68,7 +69,7 @@ object PyDataViewerCollector : CounterUsagesCollector() {
}
}
override fun getGroup() = GROUP
override fun getGroup(): EventLogGroup = GROUP
fun logDataOpened(
project: Project?,