Skip to content

Commit

Permalink
Merge #6492
Browse files Browse the repository at this point in the history
6492: QDOC: show documentation for keyword like elements r=mchernyavsky a=Undin

Current changes allow opening quick documentation for [keyword](https://doc.rust-lang.org/stable/std/#keywords) like elements.
But for `self`, `super`, `crate` and `Self` documentation won't be shown if they are part of a path. I suppose documentation for the referenced element is more important than documentation about keywords in such cases

Note, currently, if documentation contains a link to a keyword, the plugin can't open the corresponding quick doc. Will be fixed in separate PR.

changelog: show [quick documentation](https://www.jetbrains.com/help/idea/viewing-reference-information.html#inline-quick-documentation) for [keyword](https://doc.rust-lang.org/stable/std/#keywords) like elements like `fn`, `enum`, `async`, etc.

Co-authored-by: Arseniy Pendryak <a.pendryak@yandex.ru>
  • Loading branch information
bors[bot] and Undin committed Dec 21, 2020
2 parents f1bbdd1 + ed4bd4e commit b359266
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 35 deletions.
56 changes: 44 additions & 12 deletions src/main/kotlin/org/rust/ide/docs/RsDocumentationProvider.kt
Expand Up @@ -8,6 +8,7 @@ package org.rust.ide.docs
import com.intellij.codeInsight.documentation.DocumentationManagerUtil
import com.intellij.lang.documentation.AbstractDocumentationProvider
import com.intellij.lang.documentation.DocumentationMarkup
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapiext.Testmark
import com.intellij.openapiext.hitOnFalse
Expand Down Expand Up @@ -38,7 +39,7 @@ class RsDocumentationProvider : AbstractDocumentationProvider() {
is RsDocAndAttributeOwner -> generateDoc(element, buffer)
is RsPatBinding -> definition(buffer) { generateDoc(element, it) }
is RsPath -> generateDoc(element, buffer)
else -> return null
else -> generateCustomDoc(element, buffer)
}
return if (buffer.isEmpty()) null else buffer.toString()
}
Expand Down Expand Up @@ -122,28 +123,47 @@ class RsDocumentationProvider : AbstractDocumentationProvider() {
content(buffer) { it += mod.documentationAsHtml(element) }
}

private fun generateCustomDoc(element: PsiElement, buffer: StringBuilder) {
if (element.isKeywordLike()) {
val keywordDocs = element.project.findFileInStdCrate("keyword_docs.rs") ?: return
val keywordName = element.text
val mod = keywordDocs.childrenOfType<RsModItem>().find {
it.queryAttributes.hasAttributeWithKeyValue("doc", "keyword", keywordName)
} ?: return

definition(buffer) {
it += STD
it += "\n"
it += "keyword "
it.b { it += keywordName }
}
content(buffer) { it += mod.documentationAsHtml(element) }
}
}

override fun getDocumentationElementForLink(psiManager: PsiManager, link: String, context: PsiElement): PsiElement? {
if (context !is RsElement) return null
val element = context as? RsElement ?: context.parent as? RsElement ?: return null
val qualifiedName = RsQualifiedName.from(link)
return if (qualifiedName == null) {
RsCodeFragmentFactory(context.project)
.createPath(link, context)
.createPath(link, element)
?.reference
?.resolve()
} else {
qualifiedName.findPsiElement(psiManager, context)
qualifiedName.findPsiElement(psiManager, element)
}
}

override fun getUrlFor(element: PsiElement, originalElement: PsiElement?): List<String> {
val (qualifiedName, origin) = if (element is RsPath) {
(RsQualifiedName.from(element) ?: return emptyList()) to STDLIB
} else {
if (element !is RsDocAndAttributeOwner ||
element !is RsQualifiedNamedElement ||
!element.hasExternalDocumentation) return emptyList()
val origin = element.containingCrate?.origin
RsQualifiedName.from(element) to origin
val (qualifiedName, origin) = when {
element is RsDocAndAttributeOwner && element is RsQualifiedNamedElement && element.hasExternalDocumentation -> {
val origin = element.containingCrate?.origin
RsQualifiedName.from(element) to origin
}
else -> {
val qualifiedName = RsQualifiedName.from(element) ?: return emptyList()
qualifiedName to STDLIB
}
}

val pagePrefix = when (origin) {
Expand All @@ -167,6 +187,18 @@ class RsDocumentationProvider : AbstractDocumentationProvider() {
return listOf("$pagePrefix/$pagePath")
}

override fun getCustomDocumentationElement(
editor: Editor,
file: PsiFile,
contextElement: PsiElement?,
targetOffset: Int
): PsiElement? {
// Don't show documentation for keywords like `self`, `super`, etc. when they are part of path.
// We want to show documentation for the corresponding item that path references to
if (contextElement?.isKeywordLike() == true && contextElement.parent !is RsPath) return contextElement
return null
}

@Suppress("UnstableApiUsage")
override fun generateRenderedDoc(comment: PsiDocCommentBase): String? {
return (comment as? RsDocCommentImpl)?.documentationAsHtml(renderMode = RsDocRenderMode.INLINE_DOC_COMMENT)
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt
Expand Up @@ -22,18 +22,18 @@ open class RsTokenType(debugName: String) : IElementType(debugName, RsLanguage)
fun tokenSetOf(vararg tokens: IElementType) = TokenSet.create(*tokens)

val RS_KEYWORDS = tokenSetOf(
AS,
AS, ASYNC, AUTO,
BOX, BREAK,
CONST, CONTINUE, CRATE, CSELF,
DEFAULT,
DEFAULT, DYN,
ELSE, ENUM, EXTERN,
FN, FOR,
IF, IMPL, IN,
MACRO_KW,
LET, LOOP,
MATCH, MOD, MOVE, MUT,
PUB,
REF, RETURN,
RAW, REF, RETURN,
SELF, STATIC, STRUCT, SUPER,
TRAIT, TYPE_KW,
UNION, UNSAFE, USE,
Expand Down
17 changes: 15 additions & 2 deletions src/main/kotlin/org/rust/lang/core/psi/ext/PsiElement.kt
Expand Up @@ -16,8 +16,8 @@ import com.intellij.psi.tree.IElementType
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtilCore
import com.intellij.util.SmartList
import org.rust.lang.core.psi.RsFile
import org.rust.lang.core.psi.RsReplCodeFragment
import org.rust.cargo.project.workspace.CargoWorkspace
import org.rust.lang.core.psi.*
import org.rust.lang.core.stubs.RsFileStub
import org.rust.openapiext.document
import org.rust.openapiext.findDescendantsWithMacrosOfAnyType
Expand Down Expand Up @@ -285,3 +285,16 @@ fun PsiWhiteSpace.isMultiLine(): Boolean = getLineCount() > 1
@Suppress("UNCHECKED_CAST")
inline val <T : StubElement<*>> StubBasedPsiElement<T>.greenStub: T?
get() = (this as? StubBasedPsiElementBase<T>)?.greenStub

fun PsiElement.isKeywordLike(): Boolean {
return when (elementType) {
in RS_KEYWORDS,
RsElementTypes.BOOL_LITERAL -> true
RsElementTypes.IDENTIFIER -> {
val parent = parent as? RsFieldLookup ?: return false
if (parent.edition == CargoWorkspace.Edition.EDITION_2015) return false
text == "await"
}
else -> false
}
}
Expand Up @@ -7,6 +7,7 @@ package org.rust.lang.core.psi.ext

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.stubs.StubIndex
Expand Down Expand Up @@ -236,13 +237,14 @@ data class RsQualifiedName private constructor(
val parentItem = element.toParentItem() ?: return null
parentItem to emptyList()
}
val crateName = if (parentItem.type == PRIMITIVE) {
val parentType = parentItem.type
val crateName = if (parentType == PRIMITIVE || parentType == KEYWORD) {
STD
} else {
element.containingCrate?.normName ?: return null
}

val modSegments = if (parentItem.type == PRIMITIVE || parentItem.type == MACRO) {
val modSegments = if (parentType == PRIMITIVE || parentType == KEYWORD || parentType == MACRO) {
listOf()
} else {
val parentElement = parentItem.element ?: return null
Expand Down Expand Up @@ -277,9 +279,17 @@ data class RsQualifiedName private constructor(
}

@JvmStatic
fun from(path: RsPath): RsQualifiedName? {
val primitiveType = TyPrimitive.fromPath(path) ?: return null
return RsQualifiedName(STD, emptyList(), Item.primitive(primitiveType.name), emptyList())
fun from(element: PsiElement): RsQualifiedName? {
return when {
element is RsPath -> {
val primitiveType = TyPrimitive.fromPath(element) ?: return null
RsQualifiedName(STD, emptyList(), Item.primitive(primitiveType.name), emptyList())
}
element.isKeywordLike() -> {
return RsQualifiedName(STD, emptyList(), Item.keyword(element.text), emptyList())
}
else -> null
}
}

private fun RsQualifiedNamedElement.toItems(): Pair<Item, List<Item>>? {
Expand Down Expand Up @@ -414,6 +424,7 @@ data class RsQualifiedName private constructor(

companion object {
fun primitive(name: String): Item = Item(name, PRIMITIVE)
fun keyword(name: String): Item = Item(name, KEYWORD)
}
}

Expand All @@ -428,6 +439,7 @@ data class RsQualifiedName private constructor(
CONSTANT,
MACRO,
PRIMITIVE,
KEYWORD,
// Synthetic types - rustdoc uses different links for mods and crates items
// It generates `crateName/index.html` and `path/modName/index.html` links for crates and modules respectively
// instead of `path/type.Name.html`
Expand All @@ -448,6 +460,7 @@ data class RsQualifiedName private constructor(
"constant" -> CONSTANT
"macro" -> MACRO
"primitive" -> PRIMITIVE
"keyword" -> KEYWORD
else -> {
LOG.warn("Unexpected parent item type: `$name`")
null
Expand Down
14 changes: 8 additions & 6 deletions src/main/kotlin/org/rust/lang/doc/RsDocPipeline.kt
Expand Up @@ -47,7 +47,7 @@ fun RsDocAndAttributeOwner.documentation(): String =
.joinToString("\n")

fun RsDocAndAttributeOwner.documentationAsHtml(
originalElement: RsElement = this,
originalElement: PsiElement = this,
renderMode: RsDocRenderMode = RsDocRenderMode.QUICK_DOC_POPUP
): String? {
return documentationAsHtml(documentation(), originalElement, renderMode)
Expand All @@ -64,7 +64,7 @@ fun RsDocCommentImpl.documentationAsHtml(renderMode: RsDocRenderMode = RsDocRend

private fun documentationAsHtml(
rawDocumentationText: String,
context: RsElement,
context: PsiElement,
renderMode: RsDocRenderMode
): String? {
// We need some host with unique scheme to
Expand All @@ -75,10 +75,12 @@ private fun documentationAsHtml(
// We can't use `DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL` scheme here
// because it contains `_` and it is invalid symbol for URI scheme
val tmpUriPrefix = "psi://element/"
val path = when (context) {
is RsQualifiedNamedElement -> RsQualifiedName.from(context)?.toUrlPath()
// generating documentation for primitive types via the corresponding module
is RsPath -> if (TyPrimitive.fromPath(context) != null) "$STD/" else return null
val path = when {
context is RsQualifiedNamedElement -> RsQualifiedName.from(context)?.toUrlPath()
// documentation generation for primitive types via the corresponding module
context is RsPath -> if (TyPrimitive.fromPath(context) != null) "$STD/" else return null
// documentation generation for keywords
context.isKeywordLike() -> "$STD/"
else -> return null
}
val baseURI = if (path != null) {
Expand Down
22 changes: 22 additions & 0 deletions src/test/kotlin/org/rust/ide/docs/RsExternalDocUrlStdTest.kt
Expand Up @@ -6,8 +6,10 @@
package org.rust.ide.docs

import junit.framework.AssertionFailedError
import org.rust.MockEdition
import org.rust.ProjectDescriptor
import org.rust.WithStdlibRustProjectDescriptor
import org.rust.cargo.project.workspace.CargoWorkspace

@ProjectDescriptor(WithStdlibRustProjectDescriptor::class)
class RsExternalDocUrlStdTest : RsDocumentationProviderTest() {
Expand Down Expand Up @@ -105,4 +107,24 @@ class RsExternalDocUrlStdTest : RsDocumentationProviderTest() {
fn foo() -> bool {}
//^
""", "https://doc.rust-lang.org/std/primitive.bool.html")

fun `test keyword`() = doUrlTestByText("""
struct Foo;
//^
""", "https://doc.rust-lang.org/std/keyword.struct.html")

fun `test boolean value`() = doUrlTestByText("""
fn main() {
let a = true;
//^
}
""", "https://doc.rust-lang.org/std/keyword.true.html")

@MockEdition(CargoWorkspace.Edition.EDITION_2018)
fun `test await`() = doUrlTestByText("""
fn main() {
foo().await;
//^
}
""", "https://doc.rust-lang.org/std/keyword.await.html")
}

0 comments on commit b359266

Please sign in to comment.