From 66e6d10c4dee085d64ada68e0c8828fa4228d702 Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Fri, 10 May 2024 11:16:07 +0200 Subject: [PATCH] IJPL-149254 Quick Documentation -> Show Table Preview: Columns are incorrectly resized to fit the popup width, condensing the text inside GitOrigin-RevId: afde85e3970fb17b5fc07718dc7f450552d023ab --- .../DocumentationEditorPane.java | 64 ++------ ...DocumentationPanePreferredWidthProvider.kt | 152 ++++++++++++++++++ 2 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationPanePreferredWidthProvider.kt diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java index 33b77f76e8a7..ce977a1e9ca0 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java @@ -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 findSections(@NotNull View view, String sectionClassName) { - var queue = new ArrayList(); - queue.add(view); - - var result = new SmartList(); - 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) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationPanePreferredWidthProvider.kt b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationPanePreferredWidthProvider.kt new file mode 100644 index 000000000000..4a119a4d8d01 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationPanePreferredWidthProvider.kt @@ -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 = MultiMap.createSet() + private val cellPreferredSectionWidth: HashMap = HashMap() + private val viewWidthInsetMap: HashMap = 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,
,  and 
+      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  */)
+    }
+    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  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?): Int {
+    if (cells.isNullOrEmpty()) return -1
+    // Provided cells should have the same  as an ancestor, or no  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 {
+    val queue = ArrayList()
+    queue.add(view)
+
+    val result = SmartList()
+    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
+  }
+
+}
\ No newline at end of file