Support wildcard element in descriptor documentation providers

They are useful to provide documentation for attributes or children of elements with unknown names (for example, id of an extension registration in plugin.xml)

GitOrigin-RevId: 439ab24c45b6429d00c467b5ae01b7309a84ed8b
This commit is contained in:
Karol Lewandowski
2025-01-08 12:55:44 +01:00
committed by intellij-monorepo-bot
parent 7723435cc8
commit 3836219950
6 changed files with 78 additions and 10 deletions

View File

@@ -31,7 +31,7 @@
"properties": {
"name": {
"type": "string",
"description": "Name of the element."
"description": "Name of the element. For elements with unknown names, it supports wildcard: *. The purpose of wildcard elements is to provide documentation for known sub-children or attributes."
},
"sdkDocsFixedPath": {
"type": "array",

View File

@@ -43,7 +43,7 @@ internal abstract class AbstractXmlDescriptorDocumentationTargetProvider : PsiDo
val content = DocumentationContentProvider.getInstance().getContent(docYamlCoordinates) ?: return null
return when (xmlElement) {
is XmlTag -> {
val docElement = content.findElement(elementPath) ?: return null
val docElement = content.findElement(elementPath)?.takeIf { !it.isWildcard() } ?: return null
XmlDescriptorElementDocumentationTarget(element.project, content, docElement)
}
is XmlAttribute -> {

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.documentation
internal data class DocumentationContent(
@@ -14,7 +14,7 @@ internal data class DocumentationContent(
if (path.isEmpty()) return null
val name = path.first()
val remainingPath = path.drop(1)
val currentElement = currentElements.find { it.element?.name == name }?.element ?: return null
val currentElement = currentElements.find { it.element?.name == name || it.element?.isWildcard() == true }?.element ?: return null
return if (remainingPath.isEmpty()) currentElement else findElementRecursively(currentElement.children, remainingPath)
}
@@ -51,6 +51,11 @@ internal data class Element(
var examples: List<String> = emptyList(),
var path: List<String> = emptyList(),
) {
fun isWildcard(): Boolean {
return name == "*"
}
fun copy(): Element {
return this.copy(attributes = this.attributes.map { it.copy() })
}

View File

@@ -168,23 +168,28 @@ internal class DocumentationRenderer(private val project: Project) {
if (i > 0) {
append(" / ")
}
append(elementLink(linkElements[i], linkElements.take(i + 1)))
append(elementLinkOrWildcard(linkElements[i], linkElements.take(i + 1)))
}
if (linkElements.isNotEmpty()) {
append(" / ")
}
val lastElementName = elementPath.last()
if (linkForLast) {
append(elementLink(lastElementName, elementPath))
append(elementLinkOrWildcard(lastElementName, elementPath))
}
else {
append("**`<$lastElementName>`**")
}
}
private fun elementLink(text: String, path: List<String>): String {
val linkPath = path.toPathString()
return "[`<$text>`]($ELEMENT_DOC_LINK_PREFIX$linkPath)"
private fun elementLinkOrWildcard(text: String, path: List<String>): String {
if (text != "*") {
val linkPath = path.toPathString()
return "[`<$text>`]($ELEMENT_DOC_LINK_PREFIX$linkPath)"
}
else {
return "`*`"
}
}
private fun StringBuilder.appendDeprecation(deprecatedSince: String?, deprecationNote: String?) {
@@ -285,7 +290,7 @@ internal class DocumentationRenderer(private val project: Project) {
appendParagraphSeparator()
} else {
for (child in element.children) {
val childElement = child.element ?: continue
val childElement = child.element?.takeIf { !it.isWildcard() } ?: continue
val linkText = childElement.name
val linkPath = childElement.path.toPathString()
appendLine("- [`<$linkText>`]($ELEMENT_DOC_LINK_PREFIX$linkPath)${getRequirementSimpleText(child.element?.requirement)}")

View File

@@ -201,3 +201,17 @@ elements:
name: child1
- element:
name: child2
- element:
name: "*"
attributes:
- attribute:
name: attributeUnderWildcard
description: Description of `attributeUnderWildcard`.
children:
- element:
name: childUnderWildcard
description: Description of `childUnderWildcard`.
attributes:
- attribute:
name: attributeOfElementUnderWildcard
description: Description of `attributeOfElementUnderWildcard`.

View File

@@ -270,6 +270,50 @@ class XmlDescriptorDocumentationProviderTest : CodeInsightFixtureTestCase<Module
)
}
fun `test attribute of a wildcard element`() {
doTestDocContains(
"""
<root>
<anyNameElement attribute<caret>UnderWildcard="any"/>
</root>
""".trimIndent(),
"<p><a href=\"psi_element://#element:root\"><code>&lt;root&gt;</code></a> / <code>*</code> / <b><code>@attributeUnderWildcard</code></b><hr/>\n" +
"Description of <code>attributeUnderWildcard</code>."
)
}
fun `test element under wildcard element`() {
doTestDocContains(
"""
<root>
<anyNameElement>
<childUnder<caret>Wildcard/>
</anyNameElement>
</root>
""".trimIndent(),
"<p><a href=\"psi_element://#element:root\"><code>&lt;root&gt;</code></a> / <code>*</code> / <b><code>&lt;childUnderWildcard&gt;</code></b><hr/>\n" +
"Description of <code>childUnderWildcard</code>." +
"<h5>Attributes</h5>" +
"<ul>" +
"<li><a href=\"psi_element://#attribute:root__*__childUnderWildcard__attributeOfElementUnderWildcard\"><code>attributeOfElementUnderWildcard</code></a></li>" +
"</ul>"
)
}
fun `test attribute of element under wildcard element`() {
doTestDocContains(
"""
<root>
<anyNameElement>
<childUnderWildcard attribute<caret>OfElementUnderWildcard="any"/>
</anyNameElement>
</root>
""".trimIndent(),
"<p><a href=\"psi_element://#element:root\"><code>&lt;root&gt;</code></a> / <code>*</code> / <a href=\"psi_element://#element:root__*__childUnderWildcard\"><code>&lt;childUnderWildcard&gt;</code></a> / <b><code>@attributeOfElementUnderWildcard</code></b><hr/>\n" +
"Description of <code>attributeOfElementUnderWildcard</code>."
)
}
private fun doTestDocContains(@Language("XML") fileText: String, @Language("HTML") expectedDoc: String) {
myFixture.configureByText(TEST_XML_FILE_NAME, fileText)
val targets = IdeDocumentationTargetProvider.getInstance(project)