[command-completion] IDEA-374206 Use action preview instead of javadoc for command completion or customized javadoc

GitOrigin-RevId: a0175f4bb548de35bea02ceb320fe552fbf43fd8
This commit is contained in:
Mikhail Pyltsin
2025-07-11 17:48:35 +02:00
committed by intellij-monorepo-bot
parent cfb2e2fe61
commit 92f157bdfd
10 changed files with 162 additions and 31 deletions

View File

@@ -776,9 +776,9 @@ class JavaCommandsCompletionTest : LightFixtureCompletionTestCase() {
assertNotNull(documentation)
val resultDocumentation = documentation?.supplier?.invoke() as? DocumentationData
assertNotNull(resultDocumentation)
val expected = "<div style=\"min-width: 150px; max-width: 250px; padding: 0; margin: 0;\"> \n" +
"<div style=\"width: 95%; background-color:#ffffff; line-height: 1.3200000524520874\"><div style=\"background-color:#ffffff;color:#000000\"><pre style=\"font-family:'JetBrains Mono',monospace;\"><span style=\"font-size: 90%; color:#999999;\"> 2 </span><span style=\"color:#000080;font-weight:bold;\">int&#32;</span>y&#32;=&#32;<span style=\"color:#0000ff;background-color:#cad9fa;\">10</span>;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;&#32;</pre></div><br/>\n" +
"</div></div>"
val expected = """<ideaFloatingCodePreview background-color="#ffffff" font-size="13">
<div style="#ffffff; line-height:1.3200000524520874;"><div style="background-color:#ffffff;color:#000000"><pre style="font-family:'JetBrains Mono',monospace;"><span style="font-size: 90%; color:#999999;"> 3 </span><span style="color:#000080;font-weight:bold;">int&#32;</span>y&#32;=&#32;<span style="color:#0000ff;background-color:#cad9fa;">10</span>;</pre></div>
</div></ideaFloatingCodePreview>"""
assertEquals(expected, resultDocumentation?.html ?: "")
selectItem(item)
myFixture.checkResult("""

View File

@@ -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<IntentionPreviewDiffResult.DiffInfo>): 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 """
<div style="min-width: 150px; max-width: 250px; padding: 0; margin: 0;">
<div style="width: 95%; background-color:$backgroundColor; line-height: ${lineSpacing * 1.1}">$builder<br/>
</div></div>""".trimIndent()
val editorFontSize = scheme.editorFontSize
return """<${DocumentationHtmlUtil.codePreviewFloatingKey} background-color="$backgroundColor" font-size="$editorFontSize">""" + """
<div style="$backgroundColor; line-height:${lineSpacing * 1.1};">$builder
</div>""" + "</${DocumentationHtmlUtil.codePreviewFloatingKey}>".trimIndent()
}
private fun createLineNumberTextHandler(

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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<in String?, out Icon?>): (String) -> Icon? = { key: String ->
baseIconResolver.apply(key)

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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 {

View File

@@ -170,6 +170,7 @@ internal class DocumentationPopupUI(
EDT.assertIsEdt()
browser.clearCloseTrigger()
val ui = ui
ui.reloadAsDetached()
_ui = null
return ui
}

View File

@@ -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("<div style=\"min-width: 150px; max-width: 300px; padding: 0; margin: 0;\"> " +
element.children()[0].html() + "</div></div>", 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 -> {