From 2824c8d4cd433c4c3f9a899677509f6e04df6d08 Mon Sep 17 00:00:00 2001 From: Simon Ogorodnik Date: Tue, 15 Nov 2016 12:57:47 +0300 Subject: [PATCH] Fix for KT-11032 Samples not rendered in Quick Doc Now samples included as code blocks into quick doc --- .../kotlin/idea/kdoc/KDocRenderer.kt | 68 ++++++++++++++++++- .../idea/util/xmlMarkupStringBuilderUtil.kt | 34 ++++++++++ idea/testData/editor/quickDoc/Samples.kt | 28 ++++++++ .../QuickDocProviderTestGenerated.java | 6 ++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/util/xmlMarkupStringBuilderUtil.kt create mode 100644 idea/testData/editor/quickDoc/Samples.kt diff --git a/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt index 06c9aaeffa5..6f25f39719f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt +++ b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt @@ -17,14 +17,22 @@ package org.jetbrains.kotlin.idea.kdoc import com.intellij.codeInsight.documentation.DocumentationManagerUtil +import com.intellij.psi.PsiElement import org.intellij.markdown.IElementType import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.parser.MarkdownParser +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.util.wrapTag +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.kdoc.psi.impl.KDocName import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType object KDocRenderer { fun renderKDoc(docComment: KDocTag): String { @@ -47,10 +55,67 @@ object KDocRenderer { renderTag(docComment.findTagByName("since"), "Since", result) renderSeeAlso(docComment, result) + + val sampleTags = docComment.findTagsByName("sample").filter { it.getSubjectLink() != null } + renderSamplesList(sampleTags, result) } return result.toString() } + private fun KDocLink.createHyperlink(to: StringBuilder) { + DocumentationManagerUtil.createHyperlink(to, getLinkText(), getLinkText(), false) + } + + private fun KDocLink.getTargetElement(): PsiElement? { + return this.getChildrenOfType().last().mainReference.resolve() + } + + private fun PsiElement.extractExampleText() = when (this) { + is KtDeclarationWithBody -> { + val bodyExpression = bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> text + } + + private fun trimCommonIndent(text: String): String { + fun String.leadingIndent() = indexOfFirst { !it.isWhitespace() } + + val lines = text.split('\n') + val minIndent = lines.filter { it.trim().isNotEmpty() }.map(String::leadingIndent).min() ?: 0 + return lines.map { it.drop(minIndent) }.joinToString("\n") + } + + + private fun renderSamplesList(sampleTags: List, to: StringBuilder) { + if (sampleTags.isEmpty()) return + to.apply { + wrapTag("dl") { + append("
Samples:
") + sampleTags.forEach { + wrapTag("dd") { + it.getSubjectLink()?.let { subjectLink -> + subjectLink.createHyperlink(to) + val target = subjectLink.getTargetElement() + wrapTag("pre") { + wrapTag("code") { + if (target == null) + to.append("// Unresolved") + else { + to.append(trimCommonIndent(target.extractExampleText())) + } + } + } + } + } + } + } + } + } + private fun renderSeeAlso(docComment: KDocSection, to: StringBuilder) { val seeTags = docComment.findTagsByName("see") if (seeTags.isEmpty()) return @@ -99,7 +164,8 @@ object KDocRenderer { val maybeSingleParagraph = markdownNode.children.filter { it.type != MarkdownTokenTypes.EOL }.singleOrNull() if (maybeSingleParagraph != null && !allowSingleParagraph) { return maybeSingleParagraph.children.joinToString("") { it.toHtml() } - } else { + } + else { return markdownNode.toHtml() } } diff --git a/idea/src/org/jetbrains/kotlin/idea/util/xmlMarkupStringBuilderUtil.kt b/idea/src/org/jetbrains/kotlin/idea/util/xmlMarkupStringBuilderUtil.kt new file mode 100644 index 00000000000..108d8061bf9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/util/xmlMarkupStringBuilderUtil.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.util + + +inline fun StringBuilder.wrap(prefix: String, postfix: String, crossinline body: () -> Unit) { + append(prefix) + body() + append(postfix) +} + +inline fun StringBuilder.wrapTag(tag: String, crossinline body: () -> Unit) { + wrap("<$tag>", "", body) +} + +inline fun StringBuilder.wrapTag(tag: String, params: String, crossinline body: () -> Unit) { + wrap("<$tag $params>", "", body) +} + + diff --git a/idea/testData/editor/quickDoc/Samples.kt b/idea/testData/editor/quickDoc/Samples.kt new file mode 100644 index 00000000000..d1d391304e5 --- /dev/null +++ b/idea/testData/editor/quickDoc/Samples.kt @@ -0,0 +1,28 @@ +package magic + +object Samples { + fun sampleMagic() { + castTextSpell("[asd] [dse] [asz]") + } +} + +fun sampleScroll() { + val reader = Scroll("[asd] [dse] [asz]").reader() + castTextSpell(reader.readAll()) +} + +/** + * @sample Samples.sampleMagic + * @sample sampleScroll + */ +fun castTextSpell(spell: String) { + throw SecurityException("Magic prohibited outside Hogwarts") +} + +//INFO: public fun castTextSpell(spell: String): Unit defined in magic
+//INFO:
Samples:
Samples.sampleMagic

+//INFO: castTextSpell("[asd] [dse] [asz]")
+//INFO: 
sampleScroll

+//INFO: val reader = Scroll("[asd] [dse] [asz]").reader()
+//INFO: castTextSpell(reader.readAll())
+//INFO: 
diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/QuickDocProviderTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/QuickDocProviderTestGenerated.java index b08c14c2211..f2366cc08b1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/QuickDocProviderTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/QuickDocProviderTestGenerated.java @@ -204,6 +204,12 @@ public class QuickDocProviderTestGenerated extends AbstractQuickDocProviderTest doTest(fileName); } + @TestMetadata("Samples.kt") + public void testSamples() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/editor/quickDoc/Samples.kt"); + doTest(fileName); + } + @TestMetadata("TopLevelMethodFromJava.java") public void testTopLevelMethodFromJava() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/editor/quickDoc/TopLevelMethodFromJava.java");