From 92f157bdfd3bd2932a4b44040ed890ccf7dae412 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Fri, 11 Jul 2025 17:48:35 +0200 Subject: [PATCH] [command-completion] IDEA-374206 Use action preview instead of javadoc for command completion or customized javadoc GitOrigin-RevId: a0175f4bb548de35bea02ceb320fe552fbf43fd8 --- .../commands/JavaCommandsCompletionTest.kt | 6 +- .../CommandCompletionDocumentationProvider.kt | 19 ++-- .../DocumentationEditorPane.java | 33 +++++-- .../DocumentationHintEditorPane.java | 26 ++++++ .../documentation/DocumentationHtmlUtil.kt | 8 ++ .../ide/actions/AdjustFontSizeAction.kt | 5 +- .../ide/impl/DocumentationTargetHoverInfo.kt | 2 +- .../lang/documentation/ide/impl/popup.kt | 2 +- .../ide/ui/DocumentationPopupUI.kt | 1 + .../documentation/ide/ui/DocumentationUI.kt | 91 +++++++++++++++++-- 10 files changed, 162 insertions(+), 31 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionTest.kt index 304d15e4ca56..b6f15f5d91c0 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionTest.kt @@ -776,9 +776,9 @@ class JavaCommandsCompletionTest : LightFixtureCompletionTestCase() { assertNotNull(documentation) val resultDocumentation = documentation?.supplier?.invoke() as? DocumentationData assertNotNull(resultDocumentation) - val expected = "
\n" + - "
  2  int y = 10;                        

\n" + - "
" + val expected = """ +
  3  int y = 10;
+
""" assertEquals(expected, resultDocumentation?.html ?: "") selectItem(item) myFixture.checkResult(""" diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt index 7d86ac70ad9e..82d8a4bc0aa5 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/CommandCompletionDocumentationProvider.kt @@ -3,6 +3,7 @@ package com.intellij.codeInsight.completion.command import com.intellij.codeInsight.CodeInsightSettings import com.intellij.codeInsight.completion.command.configuration.ApplicationCommandCompletionService +import com.intellij.codeInsight.documentation.DocumentationHtmlUtil import com.intellij.codeInsight.documentation.actions.ShowQuickDocInfoAction import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewDiffResult.HighlightingType @@ -163,18 +164,14 @@ private class CommandCompletionDocumentationTarget( @NlsSafe private fun renderHtml(diffs: List): String { val builder = StringBuilder() - val maxLine = (diffs.maxOfOrNull { it.startLine + it.length } ?: 1000).toString().length + val maxLine = (diffs.maxOfOrNull { it.startLine + it.length + 1 } ?: 1000).toString().length for (i in 0..diffs.size - 1) { ProgressManager.checkCanceled() - var codeSnippet = diffs[i].fileText - val length = codeSnippet.split("\n").lastOrNull()?.length ?: -1 - if (length > 0 && length < 35) { - codeSnippet += " ".repeat(35 - length) - } + val codeSnippet = diffs[i].fileText val lineNumberIndexes = codeSnippet.indexesOf("\n").map { it + 1 }.toMutableList() lineNumberIndexes.add(0, 0) val additionalHighlighting = additionalHighlighting(diffs[i].fragments, lineNumberIndexes) - val textHandler = if (diffs[i].startLine != -1) createLineNumberTextHandler(lineNumberIndexes, diffs[i].startLine, maxLine) else null + val textHandler = if (diffs[i].startLine != -1) createLineNumberTextHandler(lineNumberIndexes, diffs[i].startLine + 1, maxLine) else null var properties = HtmlSyntaxInfoUtil.HtmlGeneratorProperties.createDefault() .generateWrappedTags() .generateBackground() @@ -196,10 +193,10 @@ private class CommandCompletionDocumentationTarget( val defaultBackground = scheme.defaultBackground val lineSpacing = scheme.lineSpacing val backgroundColor = ColorUtil.toHtmlColor(defaultBackground) - return """ -
-
$builder
-
""".trimIndent() + val editorFontSize = scheme.editorFontSize + return """<${DocumentationHtmlUtil.codePreviewFloatingKey} background-color="$backgroundColor" font-size="$editorFontSize">""" + """ +
$builder +
""" + "".trimIndent() } private fun createLineNumberTextHandler( 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 8206ea11b898..3a7d4772579b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationEditorPane.java @@ -15,6 +15,7 @@ import com.intellij.ui.scale.JBUIScale; import com.intellij.util.ui.ExtendableHTMLViewFactory; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.accessibility.ScreenReader; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -65,7 +66,14 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp .keyboardActions(keyboardActions) .imageResolverFactory(component -> new JBHtmlPaneImageResolver(component, it -> imageResolver.resolveImage(it))) .iconResolver(name -> iconResolver.apply(name)) - .customStyleSheetProvider(pane -> getDocumentationPaneAdditionalCssRules(num -> (int)(pane.getContentsScaleFactor() * num))) + .customStyleSheetProvider(pane -> { + return getDocumentationPaneAdditionalCssRules(num -> { + if (pane instanceof DocumentationHintEditorPane hintEditorPane && hintEditorPane.isCustomSettingsEnabled()) { + return 0; + } + return (int)(pane.getContentsScaleFactor() * num); + }); + }) .extensions(ExtendableHTMLViewFactory.Extensions.FIT_TO_WIDTH_IMAGES) .build() ); @@ -106,10 +114,18 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp } setSize(width, Short.MAX_VALUE); Dimension result = getPreferredSize(); - myCachedPreferredSize = new Dimension(width, result.height); + // Add extra height to prevent bottom clipping + int extraHeight = getExtraHeight(result.height, contentsPreferredWidth(), width); + myCachedPreferredSize = new Dimension(width, result.height + extraHeight); return myCachedPreferredSize.height; } + @Internal + @ApiStatus.Experimental + protected int getExtraHeight(int height, int contentPreferredWidth, int expectedWidth) { + return 0; + } + int getPreferredWidth() { int definitionPreferredWidth = contentsPreferredWidth(); return definitionPreferredWidth < 0 ? getPreferredSize().width @@ -149,14 +165,19 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp if (!(document instanceof StyledDocument)) { return; } - String fontName = Registry.is("documentation.component.editor.font") - ? EditorColorsManager.getInstance().getGlobalScheme().getEditorFontName() - : getFont().getFontName(); myFontSize = size; // changing font will change the doc's CSS as myEditorPane has JEditorPane.HONOR_DISPLAY_PROPERTIES via UIUtil.getHTMLEditorKit - setFont(UIUtil.getFontWithFallback(fontName, Font.PLAIN, JBUIScale.scale(size.getSize()))); + setFont(UIUtil.getFontWithFallback(getFontName(), Font.PLAIN, JBUIScale.scale(size.getSize()))); + } + + @Internal + public String getFontName() { + String fontName = Registry.is("documentation.component.editor.font") + ? EditorColorsManager.getInstance().getGlobalScheme().getEditorFontName() + : getFont().getFontName(); + return fontName; } @Override diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHintEditorPane.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHintEditorPane.java index 8c1c802d5f6e..c5aa6700c4f9 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHintEditorPane.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHintEditorPane.java @@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.ex.WindowManagerEx; +import com.intellij.ui.scale.JBUIScale; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; @@ -17,6 +18,8 @@ import java.awt.*; import java.awt.event.*; import java.util.Map; +import static com.intellij.codeInsight.documentation.DocumentationHtmlUtil.getDocPopupPreferredMinWidth; + @Internal public final class DocumentationHintEditorPane extends DocumentationEditorPane { @@ -33,6 +36,18 @@ public final class DocumentationHintEditorPane extends DocumentationEditorPane { myProject = project; } + private boolean customSettingsEnabled = false; + + @Internal + public boolean isCustomSettingsEnabled() { + return customSettingsEnabled; + } + + @Internal + public void setCustomSettingsEnabled(boolean customSettingsEnabled) { + this.customSettingsEnabled = customSettingsEnabled; + } + public void setHint(@NotNull JBPopup hint) { myHint = hint; FocusListener focusAdapter = new FocusAdapter() { @@ -93,4 +108,15 @@ public final class DocumentationHintEditorPane extends DocumentationEditorPane { } super.processMouseMotionEvent(e); } + + @Override + protected int getExtraHeight(int height, int contentPreferredWidth, int expectedWidth) { + if (!customSettingsEnabled) return 0; + if (contentPreferredWidth <= getDocPopupPreferredMinWidth()) return 0; + int lines = (int)Math.ceil(contentPreferredWidth * 1.0 / expectedWidth); + if (lines <= 1) return 0; + FontMetrics fontMetrics = this.getFontMetrics(getFont()); + int lineHeight = fontMetrics.getHeight(); + return JBUIScale.scale((lines - 1) * lineHeight); + } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHtmlUtil.kt b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHtmlUtil.kt index d49810f28697..a68f507da284 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHtmlUtil.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationHtmlUtil.kt @@ -60,6 +60,14 @@ object DocumentationHtmlUtil { @JvmStatic val lookupDocPopupMinHeight: Int get() = 300 + /** + * Represents a key used internally to identify the configuration or behavior of the code preview for floating documentation popup. + * It enables customization of this popup and some documentation panes + */ + @JvmStatic + @get:ApiStatus.Internal + val codePreviewFloatingKey: String get() = "ideaFloatingCodePreview" + @JvmStatic fun getModuleIconResolver(baseIconResolver: Function): (String) -> Icon? = { key: String -> baseIconResolver.apply(key) diff --git a/platform/lang-impl/src/com/intellij/lang/documentation/ide/actions/AdjustFontSizeAction.kt b/platform/lang-impl/src/com/intellij/lang/documentation/ide/actions/AdjustFontSizeAction.kt index 3e1eb41d1a15..8dcc0a96a667 100644 --- a/platform/lang-impl/src/com/intellij/lang/documentation/ide/actions/AdjustFontSizeAction.kt +++ b/platform/lang-impl/src/com/intellij/lang/documentation/ide/actions/AdjustFontSizeAction.kt @@ -14,7 +14,10 @@ class AdjustFontSizeAction : AnAction(CodeInsightBundle.message("javadoc.adjust. override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT override fun update(e: AnActionEvent) { - e.presentation.isEnabledAndVisible = documentationBrowser(e.dataContext) != null + val documentationBrowser = documentationBrowser(e.dataContext) + val ui = documentationBrowser?.ui + e.presentation.isEnabledAndVisible = documentationBrowser != null && + ui?.editorPane?.isCustomSettingsEnabled == false } override fun actionPerformed(e: AnActionEvent) { diff --git a/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/DocumentationTargetHoverInfo.kt b/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/DocumentationTargetHoverInfo.kt index d703c67b1f5e..7bb2051e1429 100644 --- a/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/DocumentationTargetHoverInfo.kt +++ b/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/DocumentationTargetHoverInfo.kt @@ -102,7 +102,7 @@ private class DocumentationTargetHoverInfo( override fun createQuickDocComponent(editor: Editor, jointPopup: Boolean, bridge: PopupBridge): JComponent { val project = editor.project!! - val documentationUI = DocumentationUI(project, browser) + val documentationUI = DocumentationUI(project, browser, isPopup = true) val popupUI = DocumentationPopupUI(project, documentationUI) if (jointPopup) { popupUI.jointHover() diff --git a/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/popup.kt b/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/popup.kt index 5b65811965e1..06ba978fa09e 100644 --- a/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/popup.kt +++ b/platform/lang-impl/src/com/intellij/lang/documentation/ide/impl/popup.kt @@ -40,7 +40,7 @@ internal suspend fun showDocumentationPopup( Disposer.dispose(browser) throw ce } - val popupUI = DocumentationPopupUI(project, DocumentationUI(project, browser)) + val popupUI = DocumentationPopupUI(project, DocumentationUI(project, browser, isPopup = true)) val popup = createDocumentationPopup(project, popupUI, popupContext) try { writeIntentReadAction { diff --git a/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationPopupUI.kt b/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationPopupUI.kt index 6ed870d41ac7..0528b922bc3b 100644 --- a/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationPopupUI.kt +++ b/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationPopupUI.kt @@ -170,6 +170,7 @@ internal class DocumentationPopupUI( EDT.assertIsEdt() browser.clearCloseTrigger() val ui = ui + ui.reloadAsDetached() _ui = null return ui } diff --git a/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationUI.kt b/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationUI.kt index 3bf1f8b5b472..ffadb3f538bf 100644 --- a/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationUI.kt +++ b/platform/lang-impl/src/com/intellij/lang/documentation/ide/ui/DocumentationUI.kt @@ -24,14 +24,17 @@ import com.intellij.lang.documentation.ide.ui.PopupUpdateEvent.ContentUpdateKind import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.application.EDT +import com.intellij.openapi.options.FontSize import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.text.HtmlChunk import com.intellij.platform.backend.documentation.impl.DocumentationRequest import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.platform.ide.documentation.DOCUMENTATION_BROWSER import com.intellij.platform.util.coroutines.flow.collectLatestUndispatched import com.intellij.ui.PopupHandler +import com.intellij.ui.scale.JBUIScale import com.intellij.util.ui.EDT import com.intellij.util.ui.JBUI import com.intellij.util.ui.SwingTextTrimmer @@ -44,7 +47,10 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest import org.jetbrains.annotations.Nls +import org.jsoup.Jsoup +import org.jsoup.nodes.Document import java.awt.Color +import java.awt.Font import java.awt.Rectangle import javax.swing.JComponent import javax.swing.JLabel @@ -53,6 +59,7 @@ import javax.swing.JScrollPane internal class DocumentationUI( val project: Project, val browser: DocumentationBrowser, + private var isPopup: Boolean = false, ) : DataProvider, Disposable { val scrollPane: JScrollPane @@ -183,6 +190,16 @@ internal class DocumentationUI( imageResolver = null } + fun reloadAsDetached() { + val localInitialDecoratedData = initialDecoratedData + if (localInitialDecoratedData != null && editorPane.isCustomSettingsEnabled) { + editorPane.isCustomSettingsEnabled = false + editorPane.background = localInitialDecoratedData.backgroundColor + editorPane.applyFontProps(localInitialDecoratedData.fontSize) + } + isPopup = false + } + private suspend fun handlePage(page: DocumentationPage) { val presentation = page.request.presentation val i = switcher.elements.indexOf(page.request) @@ -193,16 +210,20 @@ internal class DocumentationUI( else { switcher.index = i } - updateSwitcherVisibility() + if (!editorPane.isCustomSettingsEnabled) { + updateSwitcherVisibility() + } page.contentFlow.collectLatest { handleContent(presentation, it) } } - fun updateSwitcherVisibility() { + fun updateSwitcherVisibility(forceTopMarginDisabled: Boolean = false) { val visible = switcher.elements.count() > 1 switcherToolbarComponent.isVisible = visible - editorPane.border = JBUI.Borders.emptyTop( + editorPane.border = + if (forceTopMarginDisabled) JBUI.Borders.empty(0, 0, 2, JBUI.scale(20)) + else JBUI.Borders.emptyTop( if (visible) 0 else DocumentationHtmlUtil.contentOuterPadding - DocumentationHtmlUtil.spaceBeforeParagraph ) } @@ -232,8 +253,9 @@ internal class DocumentationUI( val content = pageContent.content imageResolver = content.imageResolver val linkChunk = linkChunk(presentation.presentableText, pageContent.links) - val decorated = decorate(content.html, null, linkChunk, pageContent.downloadSourcesLink) - if (!updateContent(decorated, presentation, ContentKind.DocumentationPage)) { + val decoratedData = extractAdditionalData(content.html) + val decorated = decorate(decoratedData?.html ?: content.html, null, linkChunk, pageContent.downloadSourcesLink) + if (!updateContent(decorated, presentation, ContentKind.DocumentationPage, decoratedData?.decoratedStyle)) { return } val uiState = pageContent.uiState @@ -243,6 +265,25 @@ internal class DocumentationUI( } } + private data class DecoratedData(@NlsSafe val html: String, val decoratedStyle: DecoratedStyle?) + private data class DecoratedStyle(val fontSize: Float, val backgroundColor: Color) + private data class PreviousDecoratedStyle(val fontSize: FontSize, val backgroundColor: Color) + private fun extractAdditionalData(@NlsSafe html: String): DecoratedData? { + if (!html.startsWith("<" + DocumentationHtmlUtil.codePreviewFloatingKey)) return null + val document: Document = Jsoup.parse(html) + val children = document.getElementsByTag(DocumentationHtmlUtil.codePreviewFloatingKey) + if (children.size != 1) return null + val element = children[0] + if (!isPopup) { + return DecoratedData("
" + + element.children()[0].html() + "
", null) + } + val backgroundColor = element.attribute("background-color")?.value ?: return null + val fontSize = element.attribute("font-size")?.value?.toFloat() ?: return null + if(element.children().size!=1) return null + return DecoratedData(element.children()[0].html(), DecoratedStyle(fontSize, Color.decode(backgroundColor))) + } + private fun fetchingMessage() { updateContent(message(CodeInsightBundle.message("javadoc.fetching.progress")), null, ContentKind.InfoMessage) } @@ -260,9 +301,13 @@ internal class DocumentationUI( .toString() } - private fun updateContent(text: @Nls String, - presentation: TargetPresentation?, - newContentKind: ContentKind): Boolean { + private var initialDecoratedData: PreviousDecoratedStyle? = null + private fun updateContent( + text: @Nls String, + presentation: TargetPresentation?, + newContentKind: ContentKind, + decoratedStyle: DecoratedStyle? = null, + ): Boolean { EDT.assertIsEdt() if (editorPane.text == text && locationLabel.text == presentation?.locationText && @@ -273,6 +318,7 @@ internal class DocumentationUI( val oldContentKind = contentKind contentKind = newContentKind editorPane.text = text + customizePane(decoratedStyle) if (presentation?.locationText != null) { locationLabel.text = presentation.locationText locationLabel.toolTipText = presentation.locationText @@ -294,6 +340,35 @@ internal class DocumentationUI( return true } + private fun customizePane(decoratedStyle: DecoratedStyle?) { + if (decoratedStyle != null) { + updateSwitcherVisibility(true) + } + else { + updateSwitcherVisibility() + } + if (isPopup && (decoratedStyle != null && decoratedStyle.backgroundColor != editorPane.background || + decoratedStyle == null && initialDecoratedData != null && + initialDecoratedData?.backgroundColor != editorPane.background)) { + if (initialDecoratedData == null) { + initialDecoratedData = PreviousDecoratedStyle(fontSize.value, editorPane.background) + } + if (decoratedStyle != null) { + editorPane.isCustomSettingsEnabled = true + editorPane.setFont(UIUtil.getFontWithFallback(editorPane.getFontName(), Font.PLAIN, JBUIScale.scale(decoratedStyle.fontSize).toInt())) + editorPane.background = decoratedStyle.backgroundColor + } + else { + val localInitialDecoratedData = initialDecoratedData + if (localInitialDecoratedData != null) { + editorPane.isCustomSettingsEnabled = false + editorPane.background = localInitialDecoratedData.backgroundColor + editorPane.applyFontProps(localInitialDecoratedData.fontSize) + } + } + } + } + private fun applyUIState(uiState: UIState) { when (uiState) { UIState.Reset -> {