Question about extending PsiDocumentationTargetProvider

SHORT VERSION: Is there a way I can extend PsiDocumentationTargetProvider to add an additional page of docs on top of the KDoc page that is already shown?


I am working on a Kotlin framework that wraps, among other things, web CSS properties. I am also writing an IDEA plugin for working with this framework.

What I would like to do is extend the existing Kotlin “quick documentation” popup with an additional page that shows the CSS documentation from the MDN.

For example, the quick docs for Modifier.backgroundColor would show the KDoc I wrote for that function on page 1 / 2, and the MDN docs for the background-color property on page 2 / 2.

However, the way that PsiDocumentationTargetProvider works (it seems) is it finds the first docs provider that matches and stops there. Which means I can show either MDN docs or KDocs but not both.

The way I “fixed” this was by basically instantiating an instance of KotlinPsiDocumentationTargetProvider myself and then calling kotlinDocProvider.documentationTargets(element, originalElement) to get the KDocs, and to that result I add my own additional documentation target as an additional entry. In plugin.xml I also declared that my documentation provider should be triggered before the kotlin documentation provider. And that worked!

However, it did trigger a verification violation:

Override-only method
PsiDocumentationTargetProvider.documentationTargets(PsiElement, PsiElement) 

is invoked in 
CssModifierDocumentationTargetProvider.documentationTargets(PsiElement, PsiElement) : List.
This method is marked with @ApiStatus.OverrideOnly annotation, which indicates that
the method must be only overridden but not invoked by client code. See documentation
of the @ApiStatus.OverrideOnly for more info.

but the verifyPlugin task still passed at the time. This was with intellij-platform version 2.10.5.

I just upgraded to 2.18.1 and it seems like the violation has been upgraded into an error.

Is there another way I’m supposed to be doing this? Is this going to get my plugin rejected in the future, meaning I need to back the feature out?


The following is all relevant code:

Plugin.xml

<platform.backend.documentation.psiTargetProvider
  order="before KotlinDocumentationTargetProvider"
  implementation="com.varabyte.kobweb.intellij.docs.CssModifierDocumentationTargetProvider"/>

CssModifierDocumentationTargetProvider.kt

private val MODIFIER_ID = ClassId.fromString("com/varabyte/kobweb/compose/ui/Modifier")

class CssModifierDocumentationTarget(val cssPropertyName: String, val element: PsiElement) : DocumentationTarget {
    override fun computePresentation(): TargetPresentation =
        TargetPresentation.builder(cssPropertyName)
            .icon(com.intellij.icons.AllIcons.FileTypes.Css)
            .presentation()

    override fun computeDocumentation(): DocumentationResult? {
        val mdnDoc = getCssMdnDocumentation(cssPropertyName, MdnCssSymbolKind.Property)
            ?: return null
        val html = mdnDoc.getDocumentation(withDefinition = true)
        return DocumentationResult.documentation(html).externalUrl(mdnDoc.url)
    }

    override fun createPointer(): Pointer<out DocumentationTarget> {
        val elementPointer = element.createSmartPointer()

        return Pointer {
            elementPointer.element?.let {
                CssModifierDocumentationTarget(cssPropertyName, it)
            }
        }
    }
}

/**
 * Provides documentation for CSS modifier functions tied to CSS properties.
 *
 * For example, `Modifier.backgroundColor(...)` will show the MDN documentation for the `background-color` CSS property
 * on the second page.
 */
class CssModifierDocumentationTargetProvider : PsiDocumentationTargetProvider {
    val kotlinDocProvider = KotlinPsiDocumentationTargetProvider()

    override fun documentationTargets(
        element: PsiElement,
        originalElement: PsiElement?,
    ): List<DocumentationTarget> {
        return kotlinDocProvider.documentationTargets(element, originalElement) +
                listOfNotNull(documentationTarget(element))
    }

    private fun documentationTarget(element: PsiElement): DocumentationTarget? {
        val function = element.asKtNamedFunction() ?: return null

        analyze(function) {
            if (function.receiverTypeReference?.type?.isClassType(MODIFIER_ID) != true) {
                return null
            }
            if (!function.returnType.isClassType(MODIFIER_ID)) {
                return null
            }
        }

        val propertyName = function.name?.camelCaseToKebabCase() ?: return null

        return CssModifierDocumentationTarget(propertyName, element)
    }
}

And the line that is blowing up the verifier is:

return kotlinDocProvider.documentationTargets(element, originalElement) +

In case this ever gets to the relevant team, here are some possibilities that I could think of on how to move forward:

  1. Update the behavior of the documentation provider to be additive instead of “first success”

  2. Make it so that I can create a KDoc DocumentationTarget myself (currently, all of the classes around that are declared internal, meaning I can only create them indirectly through a KotlinDocumentationTargetProvider, which the verifier step is preventing here)

  3. Remove the OverrideOnly annotation from PsiDocumentationTargetProvider

  4. Make KotlinDocumentationTargetProvider an open class that can be inherited (I don’t like having open classes that aren’t abstract, but with OverrideOnly, delegation has been taken off the table)