diff --git a/src/main/kotlin/org/rust/ide/docs/RsDocumentationProvider.kt b/src/main/kotlin/org/rust/ide/docs/RsDocumentationProvider.kt index e2b58e22cfe..e29a7479c1e 100644 --- a/src/main/kotlin/org/rust/ide/docs/RsDocumentationProvider.kt +++ b/src/main/kotlin/org/rust/ide/docs/RsDocumentationProvider.kt @@ -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 @@ -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() } @@ -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().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 { - 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) { @@ -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) diff --git a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt index 832560c59f9..9352a0948f8 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt @@ -22,10 +22,10 @@ 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, @@ -33,7 +33,7 @@ val RS_KEYWORDS = tokenSetOf( LET, LOOP, MATCH, MOD, MOVE, MUT, PUB, - REF, RETURN, + RAW, REF, RETURN, SELF, STATIC, STRUCT, SUPER, TRAIT, TYPE_KW, UNION, UNSAFE, USE, diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/PsiElement.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/PsiElement.kt index 33f33833be4..16b2e57bfdb 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/PsiElement.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/PsiElement.kt @@ -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 @@ -285,3 +285,16 @@ fun PsiWhiteSpace.isMultiLine(): Boolean = getLineCount() > 1 @Suppress("UNCHECKED_CAST") inline val > StubBasedPsiElement.greenStub: T? get() = (this as? StubBasedPsiElementBase)?.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 + } +} diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsQualifiedNamedElement.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsQualifiedNamedElement.kt index 30efa66a619..fff25099226 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsQualifiedNamedElement.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsQualifiedNamedElement.kt @@ -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 @@ -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 @@ -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>? { @@ -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) } } @@ -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` @@ -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 diff --git a/src/main/kotlin/org/rust/lang/doc/RsDocPipeline.kt b/src/main/kotlin/org/rust/lang/doc/RsDocPipeline.kt index ad1c65dcafd..f4cabb0f1e6 100644 --- a/src/main/kotlin/org/rust/lang/doc/RsDocPipeline.kt +++ b/src/main/kotlin/org/rust/lang/doc/RsDocPipeline.kt @@ -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) @@ -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 @@ -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) { diff --git a/src/test/kotlin/org/rust/ide/docs/RsExternalDocUrlStdTest.kt b/src/test/kotlin/org/rust/ide/docs/RsExternalDocUrlStdTest.kt index 25583a93a47..ae465a27e46 100644 --- a/src/test/kotlin/org/rust/ide/docs/RsExternalDocUrlStdTest.kt +++ b/src/test/kotlin/org/rust/ide/docs/RsExternalDocUrlStdTest.kt @@ -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() { @@ -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") } diff --git a/src/test/kotlin/org/rust/ide/docs/RsQuickDocumentationTest.kt b/src/test/kotlin/org/rust/ide/docs/RsQuickDocumentationTest.kt index 8b1beebb47d..c046c3e7a4b 100644 --- a/src/test/kotlin/org/rust/ide/docs/RsQuickDocumentationTest.kt +++ b/src/test/kotlin/org/rust/ide/docs/RsQuickDocumentationTest.kt @@ -7,9 +7,8 @@ package org.rust.ide.docs import com.intellij.psi.PsiElement import org.intellij.lang.annotations.Language -import org.rust.ExpandMacros -import org.rust.ProjectDescriptor -import org.rust.WithStdlibRustProjectDescriptor +import org.rust.* +import org.rust.cargo.project.workspace.CargoWorkspace import org.rust.lang.core.psi.RsBaseType import org.rust.lang.core.psi.RsConstant import org.rust.lang.core.psi.ext.RsElement @@ -1094,6 +1093,87 @@ class RsQuickDocumentationTest : RsDocumentationProviderTest() { originalElement to originalElement.textOffset } + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test keyword doc`() = doTestRegex(""" + enum Foo { V1 } + //^ + """, """ +
std
+        keyword enum

.+

+ """) + + @MinRustcVersion("1.36.0") + @MockEdition(CargoWorkspace.Edition.EDITION_2018) + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test async keyword doc`() = doTestRegex(""" + async fn foo() {} + //^ + """, """ +
std
+        keyword async

.+

+ """) + + @MinRustcVersion("1.36.0") + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test dyn keyword doc`() = doTestRegex(""" + trait Foo {} + fn foo(x: &dyn Foo) {} + //^ + """, """ +
std
+        keyword dyn

.+

+ """) + + @MinRustcVersion("1.36.0") + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test boolean value doc`() = doTestRegex(""" + fn main() { + let a = false; + //^ + } + """, """ +
std
+        keyword false

.+

+ """) + + @MockEdition(CargoWorkspace.Edition.EDITION_2015) + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test await doc 1`() = doTest(""" + fn main() { + foo().await; + //^ + } + """, null) + + @MinRustcVersion("1.36.0") + @MockEdition(CargoWorkspace.Edition.EDITION_2018) + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test await doc 2`() = doTestRegex(""" + fn main() { + foo().await; + //^ + } + """, """ +
std
+        keyword await

.+

+ """) + + @ProjectDescriptor(WithStdlibRustProjectDescriptor::class) + fun `test keyword doc in stdlib`() = doTestRegex(""" + const C: u32 = std::f64::DIGITS; + //^ + """, """ +
std
+        keyword const

.+

+ """) { + val element = findElementWithDataAndOffsetInEditor().first + val const = element.reference?.resolve() as? RsConstant ?: error("Failed to resolve `${element.text}`") + val originalElement = const.const!! + + myFixture.openFileInEditor(const.containingFile.virtualFile) + originalElement to originalElement.textOffset + } + @ExpandMacros fun `test documentation provided via macro definition 1`() = doTest(""" macro_rules! foobar { @@ -1167,12 +1247,12 @@ class RsQuickDocumentationTest : RsDocumentationProviderTest() { """) - private fun doTest(@Language("Rust") code: String, @Language("Html") expected: String) + private fun doTest(@Language("Rust") code: String, @Language("Html") expected: String?) = doTest(code, expected, block = RsDocumentationProvider::generateDoc) private fun doTestRegex( @Language("Rust") code: String, @Language("Html") expected: String, findElement: () -> Pair = { findElementAndOffsetInEditor() } - ) = doTest(code, Regex(expected.trimIndent(), RegexOption.MULTILINE), findElement, RsDocumentationProvider::generateDoc) + ) = doTest(code, Regex(expected.trimIndent(), setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)), findElement, RsDocumentationProvider::generateDoc) } diff --git a/src/test/kotlin/org/rust/ide/docs/RsStdlibResolveLinkTest.kt b/src/test/kotlin/org/rust/ide/docs/RsStdlibResolveLinkTest.kt index 9ff64c918f0..385153ab580 100644 --- a/src/test/kotlin/org/rust/ide/docs/RsStdlibResolveLinkTest.kt +++ b/src/test/kotlin/org/rust/ide/docs/RsStdlibResolveLinkTest.kt @@ -5,6 +5,7 @@ package org.rust.ide.docs +import com.intellij.psi.PsiElement import com.intellij.psi.PsiManager import org.intellij.lang.annotations.Language import org.rust.ProjectDescriptor @@ -37,7 +38,17 @@ class RsStdlibResolveLinkTest : RsTestBase() { fun `test macro fqn link`() = doTest("std/macro.println.html", "...libstd/macros.rs|...std/src/macros.rs") fun `test macro fqn link with reexport`() = doTest("std/macro.assert_eq.html", "...libcore/macros.rs|...libcore/macros/mod.rs|...core/src/macros/mod.rs") - private fun doTest(link: String, expectedPaths: String, @Language("Rust") code: String = DEFAULT_TEXT) { + fun `test fqn link in keyword doc`() = doTest("std/future/trait.Future.html", "...libcore/future/future.rs|...core/src/future/future.rs", """ + async fn foo() {} + //^ + """, PsiElement::class.java) + + private fun doTest( + link: String, + expectedPaths: String, + @Language("Rust") code: String = DEFAULT_TEXT, + psiClass: Class = RsNamedElement::class.java + ) { val paths = expectedPaths.split("|") for (expectedPath in paths) { check(expectedPath.startsWith("...")) { @@ -45,7 +56,7 @@ class RsStdlibResolveLinkTest : RsTestBase() { } } InlineFile(code) - val context = findElementInEditor("^") + val context = findElementInEditor(psiClass, "^") val element = RsDocumentationProvider() .getDocumentationElementForLink(PsiManager.getInstance(project), link, context) ?: error("Failed to resolve link $link")