From 659c54d7e67dc7b998ad59e6c927998c830fc85f Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 21 Jan 2015 16:20:20 +0100 Subject: [PATCH] structured rendering of doc comments in quick documentation dialog --- .../kotlin/kdoc/parser/KDocKnownTag.java | 11 ++- .../psi/impl/{KDocLink.java => KDocLink.kt} | 15 +-- .../impl/{KDocSection.java => KDocSection.kt} | 19 ++-- .../kotlin/kdoc/psi/impl/KDocTag.java | 84 ---------------- .../jetbrains/kotlin/kdoc/psi/impl/KDocTag.kt | 98 +++++++++++++++++++ .../idea/JetQuickDocumentationProvider.java | 13 +-- .../kotlin/idea/kdoc/KDocRenderer.kt | 66 +++++++++++++ .../OnFunctionDeclarationWithPackage.kt | 6 +- .../testData/editor/quickDoc/OnMethodUsage.kt | 6 +- .../OnMethodUsageWithReturnAndThrows.kt | 17 ++++ .../JetQuickDocProviderTestGenerated.java | 6 ++ 11 files changed, 222 insertions(+), 119 deletions(-) rename compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/{KDocLink.java => KDocLink.kt} (64%) rename compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/{KDocSection.java => KDocSection.kt} (63%) delete mode 100644 compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.java create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt create mode 100644 idea/testData/editor/quickDoc/OnMethodUsageWithReturnAndThrows.kt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/parser/KDocKnownTag.java b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/parser/KDocKnownTag.java index f7bb5a6b4d0..be5b5e9cd59 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/parser/KDocKnownTag.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/parser/KDocKnownTag.java @@ -45,11 +45,12 @@ public enum KDocKnownTag { public static KDocKnownTag findByTagName(String tagName) { if (tagName.startsWith("@")) { - try { - return valueOf(tagName.substring(1).toUpperCase()); - } - catch (IllegalArgumentException ignored) { - } + tagName = tagName.substring(1); + } + try { + return valueOf(tagName.toUpperCase()); + } + catch (IllegalArgumentException ignored) { } return null; } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.java b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.kt similarity index 64% rename from compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.java rename to compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.kt index b733e0a4b17..85ef149d855 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocLink.kt @@ -14,13 +14,16 @@ * limitations under the License. */ -package org.jetbrains.kotlin.kdoc.psi.impl; +package org.jetbrains.kotlin.kdoc.psi.impl -import com.intellij.lang.ASTNode; -import org.jetbrains.annotations.NotNull; +import com.intellij.lang.ASTNode -public class KDocLink extends KDocElementImpl { - public KDocLink(@NotNull ASTNode node) { - super(node); +public class KDocLink(node: ASTNode) : KDocElementImpl(node) { + fun getLinkText(): String { + val text = getText() + if (text.startsWith('[') && text.endsWith(']')) { + return text.substring(1, text.length()-1) + } + return text } } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.java b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.kt similarity index 63% rename from compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.java rename to compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.kt index 292d9524577..52b8fc77af9 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocSection.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.jetbrains.kotlin.kdoc.psi.impl; +package org.jetbrains.kotlin.kdoc.psi.impl -import com.intellij.lang.ASTNode; -import org.jetbrains.annotations.NotNull; +import com.intellij.lang.ASTNode +import com.intellij.psi.util.PsiTreeUtil /** * The part of a doc comment which describes a single class, method or property @@ -25,8 +25,15 @@ import org.jetbrains.annotations.NotNull; * can have sections for the class itself, its primary constructor and each of the * properties defined in the primary constructor. */ -public class KDocSection extends KDocTag { - public KDocSection(@NotNull ASTNode node) { - super(node); +public class KDocSection(node: ASTNode) : KDocTag(node) { + public fun findTagsByName(name: String): List { + val tags = PsiTreeUtil.getChildrenOfType(this, javaClass()) + if (tags == null) { + return listOf() + } + return tags.filter { it.getName() == name } } + + public fun findTagByName(name: String): KDocTag? + = findTagsByName(name).firstOrNull() } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.java b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.java deleted file mode 100644 index 737a60da377..00000000000 --- a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2010-2015 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.kdoc.psi.impl; - -import com.intellij.lang.ASTNode; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.psi.TokenType; -import com.intellij.psi.tree.IElementType; -import com.intellij.psi.util.PsiTreeUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.kotlin.kdoc.lexer.KDocTokens; -import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes; - -public class KDocTag extends KDocElementImpl { - public KDocTag(@NotNull ASTNode node) { - super(node); - } - - public String getContent() { - StringBuilder builder = new StringBuilder(); - - boolean contentStarted = false; - boolean afterAsterisk = false; - boolean startsWithTagName = false; - - ASTNode[] children = getNode().getChildren(null); - for (int i = 0; i < children.length; i++) { - ASTNode node = children[i]; - IElementType type = node.getElementType(); - if (i == 0 && type == KDocTokens.TAG_NAME) { - startsWithTagName = true; - } - if (KDocTokens.CONTENT_TOKENS.contains(type) || type == KDocElementTypes.KDOC_LINK) { - contentStarted = true; - builder.append(afterAsterisk ? StringUtil.trimLeading(node.getText()) : node.getText()); - afterAsterisk = false; - } - - if (type == KDocTokens.LEADING_ASTERISK) { - afterAsterisk = true; - } - - if (type == TokenType.WHITE_SPACE) { - if (i == 1 && startsWithTagName) { - builder.append(node.getText()); - } - else if (contentStarted) { - builder.append(StringUtil.repeat("\n", StringUtil.countNewLines(node.getText()))); - } - } - - if (type == KDocElementTypes.KDOC_TAG) { - break; - } - } - - return builder.toString(); - } - - public String getContentWithTags() { - StringBuilder content = new StringBuilder(getContent()); - KDocTag[] subTags = PsiTreeUtil.getChildrenOfType(this, KDocTag.class); - if (subTags != null) { - for (KDocTag tag : subTags) { - content.append(tag.getContentWithTags()).append("\n"); - } - } - return content.toString(); - } -} diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.kt b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.kt new file mode 100644 index 00000000000..e95f9e104a2 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocTag.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2015 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.kdoc.psi.impl + +import com.intellij.lang.ASTNode +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.TokenType +import org.jetbrains.kotlin.kdoc.lexer.KDocTokens +import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag + +public open class KDocTag(node: ASTNode) : KDocElementImpl(node) { + + /** + * Returns the name of this tag, not including the leading @ character. + * + * @return tag name or null if this tag represents the default section of a doc comment + * or the code has a syntax error. + */ + override fun getName(): String? { + val tagName = findChildByType(KDocTokens.TAG_NAME) + if (tagName != null) { + return tagName.getText().substring(1) + } + return null + } + + /** + * Returns the name of the entity documented by this tag (for example, the name of the parameter + * for the @param tag), or null if this tag does not document any specific entity. + */ + public fun getSubjectName(): String? { + val children = childrenAfterTagName() + if (hasSubject(children)) { + return (children.firstOrNull()?.getPsi() as? KDocLink)?.getLinkText() + } + return null + } + + private fun hasSubject(contentChildren: List): Boolean { + val name = getName() + val knownTag = if (name != null) KDocKnownTag.findByTagName(name) else null + if (knownTag?.isReferenceRequired() ?: false) { + return contentChildren.firstOrNull()?.getElementType() == KDocElementTypes.KDOC_LINK; + } + return false + } + + private fun childrenAfterTagName(): List = + getNode().getChildren(null) + .dropWhile { it.getElementType() == KDocTokens.TAG_NAME } + .dropWhile { it.getElementType() == TokenType.WHITE_SPACE } + + public fun getContent(): String { + val builder = StringBuilder() + + var contentStarted = false + var afterAsterisk = false + + var children = childrenAfterTagName() + if (hasSubject(children)) { + children = children.drop(1) + } + for (node in children) { + val type = node.getElementType() + if (KDocTokens.CONTENT_TOKENS.contains(type) || type == KDocElementTypes.KDOC_LINK) { + contentStarted = true + builder.append(if (afterAsterisk) StringUtil.trimLeading(node.getText()) else node.getText()) + afterAsterisk = false + } + if (type == KDocTokens.LEADING_ASTERISK) { + afterAsterisk = true + } + if (type == TokenType.WHITE_SPACE && contentStarted) { + builder.append(StringUtil.repeat("\n", StringUtil.countNewLines(node.getText()))) + } + if (type == KDocElementTypes.KDOC_TAG) { + break + } + } + + return builder.toString() + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/JetQuickDocumentationProvider.java b/idea/src/org/jetbrains/kotlin/idea/JetQuickDocumentationProvider.java index b94e1c137c3..522ad38407e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/JetQuickDocumentationProvider.java +++ b/idea/src/org/jetbrains/kotlin/idea/JetQuickDocumentationProvider.java @@ -19,7 +19,6 @@ package org.jetbrains.kotlin.idea; import com.google.common.base.Predicate; import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.lang.java.JavaDocumentationProvider; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; @@ -93,7 +92,7 @@ public class JetQuickDocumentationProvider extends AbstractDocumentationProvider KDocTag comment = KdocPackage.findKDoc(declarationDescriptor); if (comment != null) { - renderedDecl = renderedDecl + "
" + kDocToHtml(comment); + renderedDecl = renderedDecl + "
" + org.jetbrains.kotlin.idea.kdoc.KdocPackage.renderKDoc(comment); } return renderedDecl; @@ -111,14 +110,4 @@ public class JetQuickDocumentationProvider extends AbstractDocumentationProvider return null; } - - private static String kDocToHtml(@NotNull KDocTag comment) { - // TODO: Parse and show markdown comments as html - String content = comment.getContentWithTags(); - String htmlContent = StringUtil.replace(content, "\n", "
") - .replaceAll("(@param)\\s+(\\w+)", "@param - $2") - .replaceAll("(@\\w+)", "$1"); - - return "

" + htmlContent + "

"; - } } diff --git a/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt new file mode 100644 index 00000000000..271aecacac0 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocRenderer.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2015 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.kdoc + +import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection + +fun renderKDoc(docComment: KDocTag): String { + val content = docComment.getContent() + val result = StringBuilder("

") + result.append(markdownToHtml(content)) + if (docComment is KDocSection) { + val paramTags = docComment.findTagsByName("param").filter { it.getSubjectName() != null } + renderTagList(paramTags, "Parameters", result) + + renderTag(docComment.findTagByName("return"), "Returns", result) + + val throwsTags = (docComment.findTagsByName("exception").union(docComment.findTagsByName("throws"))) + .filter { it.getSubjectName() != null } + renderTagList(throwsTags, "Throws", result) + + renderTag(docComment.findTagByName("author"), "Author", result) + renderTag(docComment.findTagByName("since"), "Since", result) + } + result.append("

") + return result.toString() +} + +private fun renderTagList(tags: List, title: String, to: StringBuilder) { + if (tags.size() == 0) { + return + } + to.append("
${title}:
") + tags.forEach { + to.append("
${it.getSubjectName()} - ${markdownToHtml(it.getContent().trimLeading())}
") + } + to.append("
") +} + +private fun renderTag(tag: KDocTag?, title: String, to: StringBuilder) { + if (tag != null) { + to.append("
${title}:
") + to.append("
${markdownToHtml(tag.getContent())}
") + to.append("
") + } +} + +fun markdownToHtml(markdown: String): String { + // TODO Integrate a real Markdown parser + return StringUtil.replace(markdown, "\n", "
"); +} diff --git a/idea/testData/editor/quickDoc/OnFunctionDeclarationWithPackage.kt b/idea/testData/editor/quickDoc/OnFunctionDeclarationWithPackage.kt index 86883cd7356..7f1a2cd4f31 100644 --- a/idea/testData/editor/quickDoc/OnFunctionDeclarationWithPackage.kt +++ b/idea/testData/editor/quickDoc/OnFunctionDeclarationWithPackage.kt @@ -7,9 +7,9 @@ package test * Test function * - * @param first - Some - * @param second - Other + * @param first Some + * @param second Other */ fun testFun(first: String, second: Int) = 12 -// INFO: internal fun testFun(first: String, second: Int): Int

Test function


@param - first - Some
@param - second - Other

\ No newline at end of file +// INFO: internal fun testFun(first: String, second: Int): Int

Test function


Parameters:
first - Some
second - Other

\ No newline at end of file diff --git a/idea/testData/editor/quickDoc/OnMethodUsage.kt b/idea/testData/editor/quickDoc/OnMethodUsage.kt index 8aef1353133..9f1ab9a90f4 100644 --- a/idea/testData/editor/quickDoc/OnMethodUsage.kt +++ b/idea/testData/editor/quickDoc/OnMethodUsage.kt @@ -1,8 +1,8 @@ /** Some documentation - @param a - Some int - * @param b: String + * @param a Some int + * @param b String */ fun testMethod(a: Int, b: String) { @@ -12,4 +12,4 @@ fun test() { testMethod(1, "value") } -// INFO: internal fun testMethod(a: Int, b: String): Unit

Some documentation

@param - a - Some int
@param - b: String

\ No newline at end of file +// INFO: internal fun testMethod(a: Int, b: String): Unit

Some documentation

Parameters:
a - Some int
b - String

\ No newline at end of file diff --git a/idea/testData/editor/quickDoc/OnMethodUsageWithReturnAndThrows.kt b/idea/testData/editor/quickDoc/OnMethodUsageWithReturnAndThrows.kt new file mode 100644 index 00000000000..6d7cd5f1b77 --- /dev/null +++ b/idea/testData/editor/quickDoc/OnMethodUsageWithReturnAndThrows.kt @@ -0,0 +1,17 @@ +/** +Some documentation + + * @param a Some int + * @param b String + * @return Return value + * @throws IllegalArgumentException if the weather is bad + */ +fun testMethod(a: Int, b: String) { + +} + +fun test() { + testMethod(1, "value") +} + +// INFO: internal fun testMethod(a: Int, b: String): Unit

Some documentation

Parameters:
a - Some int
b - String
Returns:
value
Throws:
IllegalArgumentException - if the weather is bad

\ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/JetQuickDocProviderTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/JetQuickDocProviderTestGenerated.java index 1f8bbbebd13..9a8f1a7e5ef 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/JetQuickDocProviderTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/JetQuickDocProviderTestGenerated.java @@ -107,6 +107,12 @@ public class JetQuickDocProviderTestGenerated extends AbstractJetQuickDocProvide doTest(fileName); } + @TestMetadata("OnMethodUsageWithReturnAndThrows.kt") + public void testOnMethodUsageWithReturnAndThrows() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/quickDoc/OnMethodUsageWithReturnAndThrows.kt"); + doTest(fileName); + } + @TestMetadata("TopLevelMethodFromJava.java") public void testTopLevelMethodFromJava() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/editor/quickDoc/TopLevelMethodFromJava.java");