Override/Implement: Implement JavaDoc/KDoc copying
#KT-8682 Fixed #KT-12735 Fixed (cherry picked from commit f4486be)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<orderEntry type="module" module-name="light-classes" />
|
||||
<orderEntry type="module" module-name="frontend.java" />
|
||||
<orderEntry type="module" module-name="util" />
|
||||
<orderEntry type="module" module-name="j2k" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="gradle-and-groovy-plugin" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+15
-6
@@ -72,18 +72,22 @@ abstract class OverrideImplementMembersHandler : LanguageCodeInsightActionHandle
|
||||
return
|
||||
}
|
||||
|
||||
val selectedElements = if (implementAll) {
|
||||
members
|
||||
val copyDoc: Boolean
|
||||
val selectedElements: Collection<OverrideMemberChooserObject>
|
||||
if (implementAll) {
|
||||
selectedElements = members
|
||||
copyDoc = false
|
||||
}
|
||||
else {
|
||||
val chooser = showOverrideImplementChooser(project, members.toTypedArray()) ?: return
|
||||
chooser.selectedElements ?: return
|
||||
selectedElements = chooser.selectedElements ?: return
|
||||
copyDoc = chooser.isCopyJavadoc
|
||||
}
|
||||
if (selectedElements.isEmpty()) return
|
||||
|
||||
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
||||
|
||||
generateMembers(editor, classOrObject, selectedElements)
|
||||
generateMembers(editor, classOrObject, selectedElements, copyDoc)
|
||||
}
|
||||
|
||||
override fun invoke(project: Project, editor: Editor, file: PsiFile) {
|
||||
@@ -93,9 +97,14 @@ abstract class OverrideImplementMembersHandler : LanguageCodeInsightActionHandle
|
||||
override fun startInWriteAction(): Boolean = false
|
||||
|
||||
companion object {
|
||||
fun generateMembers(editor: Editor?, classOrObject: KtClassOrObject, selectedElements: Collection<OverrideMemberChooserObject>) {
|
||||
fun generateMembers(
|
||||
editor: Editor?,
|
||||
classOrObject: KtClassOrObject,
|
||||
selectedElements: Collection<OverrideMemberChooserObject>,
|
||||
copyDoc: Boolean
|
||||
) {
|
||||
val project = classOrObject.project
|
||||
insertMembersAfter(editor, classOrObject, selectedElements.map { it.generateMember(project) })
|
||||
insertMembersAfter(editor, classOrObject, selectedElements.map { it.generateMember(project, copyDoc) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -20,6 +20,7 @@ import com.intellij.codeInsight.generation.ClassMember
|
||||
import com.intellij.codeInsight.generation.MemberChooserObject
|
||||
import com.intellij.codeInsight.generation.MemberChooserObjectBase
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiDocCommentOwner
|
||||
import com.intellij.psi.PsiElement
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
@@ -27,8 +28,11 @@ import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
|
||||
import org.jetbrains.kotlin.idea.core.TemplateKind
|
||||
import org.jetbrains.kotlin.idea.core.getFunctionBodyTextFromTemplate
|
||||
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject
|
||||
import org.jetbrains.kotlin.idea.j2k.IdeaDocCommentConverter
|
||||
import org.jetbrains.kotlin.idea.kdoc.KDocElementFactory
|
||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.findDocComment.findDocComment
|
||||
import org.jetbrains.kotlin.renderer.*
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.setSingleOverridden
|
||||
|
||||
@@ -88,15 +92,33 @@ interface OverrideMemberChooserObject : ClassMember {
|
||||
}
|
||||
}
|
||||
|
||||
fun OverrideMemberChooserObject.generateMember(project: Project): KtCallableDeclaration {
|
||||
fun OverrideMemberChooserObject.generateMember(project: Project, copyDoc: Boolean): KtCallableDeclaration {
|
||||
val descriptor = immediateSuper
|
||||
if (preferConstructorParameter && descriptor is PropertyDescriptor) return generateConstructorParameter(project, descriptor)
|
||||
|
||||
return when (descriptor) {
|
||||
val newMember: KtCallableDeclaration = when (descriptor) {
|
||||
is SimpleFunctionDescriptor -> generateFunction(project, descriptor, bodyType)
|
||||
is PropertyDescriptor -> generateProperty(project, descriptor, bodyType)
|
||||
else -> error("Unknown member to override: $descriptor")
|
||||
}
|
||||
|
||||
if (copyDoc) {
|
||||
val superDeclaration = DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor)
|
||||
val kDoc = when (superDeclaration) {
|
||||
is KtDeclaration ->
|
||||
findDocComment(superDeclaration)
|
||||
is PsiDocCommentOwner -> {
|
||||
val kDocText = superDeclaration.docComment?.let { IdeaDocCommentConverter.convertDocComment(it) }
|
||||
if (kDocText.isNullOrEmpty()) null else KDocElementFactory(project).createKDocFromText(kDocText!!)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (kDoc != null) {
|
||||
newMember.addAfter(kDoc, null)
|
||||
}
|
||||
}
|
||||
|
||||
return newMember
|
||||
}
|
||||
|
||||
private val OVERRIDE_RENDERER = DescriptorRenderer.withOptions {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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.j2k
|
||||
|
||||
import com.intellij.ide.highlighter.HtmlFileType
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFileFactory
|
||||
import com.intellij.psi.PsiWhiteSpace
|
||||
import com.intellij.psi.XmlRecursiveElementVisitor
|
||||
import com.intellij.psi.javadoc.PsiDocComment
|
||||
import com.intellij.psi.javadoc.PsiDocTag
|
||||
import com.intellij.psi.javadoc.PsiDocToken
|
||||
import com.intellij.psi.javadoc.PsiInlineDocTag
|
||||
import com.intellij.psi.xml.XmlFile
|
||||
import com.intellij.psi.xml.XmlTag
|
||||
import com.intellij.psi.xml.XmlText
|
||||
import com.intellij.psi.xml.XmlTokenType
|
||||
import org.jetbrains.kotlin.j2k.DocCommentConverter
|
||||
import org.jetbrains.kotlin.j2k.content
|
||||
|
||||
object IdeaDocCommentConverter : DocCommentConverter {
|
||||
override fun convertDocComment(docComment: PsiDocComment): String {
|
||||
val html = buildString {
|
||||
appendJavadocElements(docComment.descriptionElements)
|
||||
|
||||
tagsLoop@
|
||||
for (tag in docComment.tags) {
|
||||
when (tag.name) {
|
||||
"deprecated" -> continue@tagsLoop
|
||||
"see" -> append("@see ${convertJavadocLink(tag.content())}\n")
|
||||
else -> appendJavadocElements(tag.children).append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (html.trim().isEmpty() && docComment.findTagByName("deprecated") != null) {
|
||||
// @deprecated was the only content of the doc comment; we can drop the comment
|
||||
return ""
|
||||
}
|
||||
|
||||
val htmlFile = PsiFileFactory.getInstance(docComment.project).createFileFromText(
|
||||
"javadoc.html", HtmlFileType.INSTANCE, html)
|
||||
val htmlToMarkdownConverter = HtmlToMarkdownConverter()
|
||||
htmlFile.accept(htmlToMarkdownConverter)
|
||||
return htmlToMarkdownConverter.result
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendJavadocElements(elements: Array<PsiElement>): StringBuilder {
|
||||
elements.forEach {
|
||||
if (it is PsiInlineDocTag) {
|
||||
append(convertInlineDocTag(it))
|
||||
}
|
||||
else {
|
||||
append(it.text)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun convertInlineDocTag(tag: PsiInlineDocTag) = when(tag.name) {
|
||||
"code", "literal" -> {
|
||||
val text = tag.dataElements.joinToString("") { it.text }
|
||||
val escaped = StringUtil.escapeXml(text.trimStart())
|
||||
if (tag.name == "code") "<code>$escaped</code>" else escaped
|
||||
}
|
||||
|
||||
"link", "linkplain" -> {
|
||||
val valueElement = tag.linkElement()
|
||||
val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: ""
|
||||
val kdocLink = convertJavadocLink(valueElement?.text)
|
||||
val linkText = if (labelText.isEmpty()) kdocLink else StringUtil.escapeXml(labelText)
|
||||
"<a docref=\"$kdocLink\">$linkText</a>"
|
||||
}
|
||||
|
||||
else -> tag.text
|
||||
}
|
||||
|
||||
private fun convertJavadocLink(link: String?): String =
|
||||
if (link != null) link.substringBefore('(').replace('#', '.') else ""
|
||||
|
||||
private fun PsiDocTag.linkElement(): PsiElement? =
|
||||
valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
|
||||
|
||||
private class HtmlToMarkdownConverter() : XmlRecursiveElementVisitor() {
|
||||
private enum class ListType { Ordered, Unordered; }
|
||||
data class MarkdownSpan(val prefix: String, val suffix: String) {
|
||||
companion object {
|
||||
val Empty = MarkdownSpan("", "")
|
||||
|
||||
fun wrap(text: String) = MarkdownSpan(text, text)
|
||||
fun prefix(text: String) = MarkdownSpan(text, "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val result: String
|
||||
get() = markdownBuilder.toString()
|
||||
|
||||
private val markdownBuilder = StringBuilder("/**")
|
||||
private var afterLineBreak = false
|
||||
private var whitespaceIsPartOfText = true
|
||||
private var currentListType = ListType.Unordered
|
||||
|
||||
override fun visitWhiteSpace(space: PsiWhiteSpace) {
|
||||
super.visitWhiteSpace(space)
|
||||
|
||||
if (whitespaceIsPartOfText) {
|
||||
appendPendingText()
|
||||
markdownBuilder.append(space.text)
|
||||
if (space.textContains('\n')) {
|
||||
afterLineBreak = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitElement(element: PsiElement) {
|
||||
super.visitElement(element)
|
||||
|
||||
val tokenType = element.node.elementType
|
||||
if (tokenType == XmlTokenType.XML_DATA_CHARACTERS || tokenType == XmlTokenType.XML_CHAR_ENTITY_REF) {
|
||||
appendPendingText()
|
||||
markdownBuilder.append(element.text)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitXmlTag(tag: XmlTag) {
|
||||
withWhitespaceAsPartOfText(false) {
|
||||
val oldListType = currentListType
|
||||
val atLineStart = afterLineBreak
|
||||
appendPendingText()
|
||||
val (openingMarkdown, closingMarkdown) = getMarkdownForTag(tag, atLineStart)
|
||||
markdownBuilder.append(openingMarkdown)
|
||||
|
||||
super.visitXmlTag(tag)
|
||||
|
||||
markdownBuilder.append(closingMarkdown)
|
||||
currentListType = oldListType
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitXmlText(text: XmlText) {
|
||||
withWhitespaceAsPartOfText(true) {
|
||||
super.visitXmlText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun withWhitespaceAsPartOfText(newValue: Boolean, block: () -> Unit) {
|
||||
val oldValue = whitespaceIsPartOfText
|
||||
whitespaceIsPartOfText = newValue
|
||||
try {
|
||||
block()
|
||||
}
|
||||
finally {
|
||||
whitespaceIsPartOfText = oldValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarkdownForTag(tag: XmlTag, atLineStart: Boolean): MarkdownSpan = when(tag.name) {
|
||||
"b", "strong" -> MarkdownSpan.wrap("**")
|
||||
|
||||
"p" -> if (atLineStart) MarkdownSpan.prefix("\n * ") else MarkdownSpan.prefix("\n *\n *")
|
||||
|
||||
"i", "em" -> MarkdownSpan.wrap("*")
|
||||
|
||||
"s", "del" -> MarkdownSpan.wrap("~~")
|
||||
|
||||
"code" -> MarkdownSpan.wrap("`")
|
||||
|
||||
"a" -> {
|
||||
if (tag.getAttributeValue("docref") != null) {
|
||||
val docRef = tag.getAttributeValue("docref")
|
||||
val innerText = tag.value.text
|
||||
if (docRef == innerText) MarkdownSpan("[", "]") else MarkdownSpan("[", "][$docRef]")
|
||||
}
|
||||
else {
|
||||
MarkdownSpan("[", "](${tag.getAttributeValue("href")})")
|
||||
}
|
||||
}
|
||||
|
||||
"ul" -> { currentListType = ListType.Unordered; MarkdownSpan.Empty }
|
||||
|
||||
"ol" -> { currentListType = ListType.Ordered; MarkdownSpan.Empty }
|
||||
|
||||
"li" -> if (currentListType == ListType.Unordered) MarkdownSpan.prefix(" * ") else MarkdownSpan.prefix(" 1. ")
|
||||
|
||||
else -> MarkdownSpan.Empty
|
||||
}
|
||||
|
||||
private fun appendPendingText() {
|
||||
if (afterLineBreak ) {
|
||||
markdownBuilder.append(" * ")
|
||||
afterLineBreak = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitXmlFile(file: XmlFile) {
|
||||
super.visitXmlFile(file)
|
||||
|
||||
markdownBuilder.append(" */")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user