IJPL-149254 Quick Documentation -> Show Table Preview: Columns are incorrectly resized to fit the popup width, condensing the text inside

GitOrigin-RevId: afde85e3970fb17b5fc07718dc7f450552d023ab
This commit is contained in:
Piotr Tomiak
2024-05-10 11:16:07 +02:00
committed by intellij-monorepo-bot
parent 392e797352
commit 66e6d10c4d
2 changed files with 161 additions and 55 deletions

View File

@@ -11,33 +11,28 @@ import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBHtmlPane;
import com.intellij.ui.components.JBHtmlPaneConfiguration;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.util.SmartList;
import com.intellij.util.ui.ExtendableHTMLViewFactory;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import com.intellij.util.ui.html.UtilsKt;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Highlighter;
import javax.swing.text.StyledDocument;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static com.intellij.codeInsight.documentation.DocumentationHtmlUtil.*;
import static com.intellij.lang.documentation.DocumentationMarkup.CLASS_BOTTOM;
import static com.intellij.lang.documentation.DocumentationMarkup.CLASS_DEFINITION;
import static com.intellij.lang.documentation.QuickDocHighlightingHelper.getDefaultDocStyleOptions;
import static com.intellij.ui.components.impl.JBHtmlPaneStyleSheetRulesProviderKt.CODE_BLOCK_CLASS;
@Internal
public abstract class DocumentationEditorPane extends JBHtmlPane implements Disposable {
@@ -91,7 +86,7 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp
@NotNull
Dimension getPackedSize(int minWidth, int maxWidth) {
int width = Math.min(
Math.max(Math.max(definitionPreferredWidth(), getMinimumSize().width), minWidth),
Math.max(Math.max(contentsPreferredWidth(), getMinimumSize().width), minWidth),
maxWidth
);
int height = getPreferredHeightByWidth(width);
@@ -109,56 +104,15 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp
}
int getPreferredWidth() {
int definitionPreferredWidth = definitionPreferredWidth();
int definitionPreferredWidth = contentsPreferredWidth();
return definitionPreferredWidth < 0 ? getPreferredSize().width
: Math.max(definitionPreferredWidth, getMinimumSize().width);
}
private int definitionPreferredWidth() {
int preferredDefinitionWidth = getPreferredSectionsWidth(CLASS_DEFINITION);
if (preferredDefinitionWidth < 0) {
return -1;
}
int preferredLocationWidth = getPreferredSectionsWidth(CLASS_BOTTOM);
int preferredCodeBlockWidth = getPreferredSectionsWidth(CODE_BLOCK_CLASS);
private int contentsPreferredWidth() {
int elementsPreferredWidth = new DocumentationPanePreferredWidthProvider(getUI().getRootView(this)).get();
int preferredContentWidth = getPreferredContentWidth(getDocument().getLength());
return Math.max(Math.max(preferredCodeBlockWidth, preferredContentWidth), Math.max(preferredDefinitionWidth, preferredLocationWidth));
}
private int getPreferredSectionsWidth(String sectionClassName) {
var definitions = findSections(getUI().getRootView(this), sectionClassName);
return StreamEx.of(definitions).mapToInt(it -> getPreferredWidth(it)).max().orElse(-1);
}
private static int getPreferredWidth(View view) {
var result = (int)view.getPreferredSpan(View.X_AXIS);
if (result > 0) {
result += UtilsKt.getWidth(UtilsKt.getCssMargin(view));
var parent = view.getParent();
while (parent != null) {
result += UtilsKt.getWidth(UtilsKt.getCssMargin(parent)) + UtilsKt.getWidth(UtilsKt.getCssPadding(parent));
parent = parent.getParent();
}
}
return result;
}
private static @NotNull List<View> findSections(@NotNull View view, String sectionClassName) {
var queue = new ArrayList<View>();
queue.add(view);
var result = new SmartList<View>();
while (!queue.isEmpty()) {
var cur = queue.remove(queue.size() - 1);
if (cur == null) continue;
if (sectionClassName.equals(cur.getElement().getAttributes().getAttribute(HTML.Attribute.CLASS))) {
result.add(cur);
}
for (int i = 0; i < cur.getViewCount(); i++) {
queue.add(cur.getView(i));
}
}
return result;
return Math.max(elementsPreferredWidth, preferredContentWidth);
}
private static int getPreferredContentWidth(int textLength) {

View File

@@ -0,0 +1,152 @@
// 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.codeInsight.documentation
import com.intellij.lang.documentation.DocumentationMarkup
import com.intellij.util.SmartList
import com.intellij.util.containers.MultiMap
import com.intellij.util.ui.html.cssBorderWidths
import com.intellij.util.ui.html.cssMargin
import com.intellij.util.ui.html.cssPadding
import com.intellij.util.ui.html.width
import javax.swing.text.StyleConstants
import javax.swing.text.View
import javax.swing.text.html.HTML
import javax.swing.text.html.InlineView
import kotlin.math.max
import kotlin.math.min
internal class DocumentationPanePreferredWidthProvider(
private val rootView: View
) {
private val cell2SubCells: MultiMap<View, View> = MultiMap.createSet()
private val cellPreferredSectionWidth: HashMap<View, Int> = HashMap()
private val viewWidthInsetMap: HashMap<View, Int> = HashMap()
companion object {
private const val MIN_CELL_WIDTH = 75
}
fun get(): Int {
val views = findViews(rootView) { view ->
val className = view.element.attributes.getAttribute(HTML.Attribute.CLASS) as? String
val tagName = view.element.attributes.getAttribute(StyleConstants.NameAttribute) as? HTML.Tag
// Let's select all relevant views, i.e. definitions, bottom sections, <pre>, <td> and <code>
DocumentationMarkup.CLASS_DEFINITION == className || DocumentationMarkup.CLASS_BOTTOM == className
|| tagName === HTML.Tag.PRE || tagName === HTML.Tag.TD
|| (view is InlineView && view.cssPadding.width > 0 /* most likely <code> */)
}
if (views.isEmpty()) return -1
val preferredWidthNoTables = views.maxOf { view ->
var width = view.getPreferredSpan(View.X_AXIS).toInt()
if (width < 0) return@maxOf -1
if (isTd(view)) {
// This is the main difference wrt to regular table layout.
// We want table cells to have some minimum size, but not too large;
// otherwise, tables would try to occupy as much space as possible to
// render each cell in a single line. We want this behavior only with
// code blocks or fragments.
width = min(width, MIN_CELL_WIDTH)
}
width -= view.cssPadding.width
var cur: View? = view
// Let's accumulate ancestors insets width stopping at the root or <td> element
while (cur != null) {
width += getWidthInsets(cur)
// We need to take special care of table cells to calculate table size correctly in case of code fragments and blocks
if (isTd(cur)) {
cellPreferredSectionWidth.merge(cur, width) { a, b -> Integer.max(a, b) }
return@maxOf -1
}
cur = cur.parent
}
width
}
if (cellPreferredSectionWidth.isEmpty()) {
// It looks like we don't have any tables
return preferredWidthNoTables
}
// Let's build a cell dependency tree to be able to iterate over cells in a reasonable manner
cellPreferredSectionWidth.keys.forEach {
var cell = it
var parent = cell.parent
while (parent != null) {
if (isTd(parent)) {
if (cell2SubCells.getModifiable(parent).add(cell)) {
cell = parent
}
else {
return@forEach
}
}
parent = parent.parent
}
cell2SubCells.putValue(null, cell)
}
// Let's start with cells, which do not have any cell ancestors
return max(preferredWidthNoTables, getPreferredWidthForRows(null, cell2SubCells[null]))
}
private fun getPreferredWidthForRows(calculationsRoot: View?, cells: Collection<View>?): Int {
if (cells.isNullOrEmpty()) return -1
// Provided cells should have the same <td> as an ancestor, or no <td> at all
// Now let's find out all the table rows we need to analyze
val rows = cells.asSequence().mapNotNull { it.parent }.toSet()
// Let's find out the maximum preferred size for any of the rows
return rows.maxOf { row ->
// Accumulate ancestors insets width till we reach calculations root view
var width = 0
var cur: View? = row
while (cur != null && cur !== calculationsRoot) {
width += getWidthInsets(cur)
cur = cur.parent
}
// Return accumulated ancestors width plus sum of cells preferred widths
width + IntRange(0, row.viewCount - 1)
.map { row.getView(it) }
.sumOf { getPreferredWidthForCell(it) }
}
}
private fun getPreferredWidthForCell(cell: View): Int {
// For a cell, the base preferred width is the maximum width calculated from child code fragments
// or an actual preferred span no larger than MIN_CELL_WIDTH
val preferredSectionWidth =
cellPreferredSectionWidth[cell]
?: min(MIN_CELL_WIDTH, cell.getPreferredSpan(View.X_AXIS).toInt().takeIf { it > 0 }?.let { it + cell.cssMargin.width } ?: MIN_CELL_WIDTH)
// A cell can contain a table, so let's find out the maximum preferred size from any child table
return max(preferredSectionWidth, getPreferredWidthForRows(cell.parent, cell2SubCells[cell]))
}
private fun getWidthInsets(view: View) =
viewWidthInsetMap.computeIfAbsent(view) { it.cssMargin.width + it.cssPadding.width + it.cssBorderWidths.width }
private fun isTd(view: View): Boolean {
return view.element.attributes.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.TD
}
private fun findViews(view: View, viewSelector: (View) -> Boolean): List<View> {
val queue = ArrayList<View>()
queue.add(view)
val result = SmartList<View>()
while (!queue.isEmpty()) {
val cur = queue.removeAt(queue.size - 1)
if (viewSelector(cur)) {
result.add(cur)
}
for (i in 0 until cur.viewCount) {
queue.add(cur.getView(i))
}
}
return result
}
}