Skip to content

Commit

Permalink
add support for Markdown in comments
Browse files Browse the repository at this point in the history
  • Loading branch information
i582 committed Oct 17, 2024
1 parent 35446a8 commit 896757f
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 5 deletions.
7 changes: 2 additions & 5 deletions src/main/kotlin/org/move/ide/docs/MvDocumentationProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,8 @@ class MvDocumentationProvider : AbstractDocumentationProvider() {
}

fun MvDocAndAttributeOwner.documentationAsHtml(): String {
return docComments()
.flatMap { it.text.split("\n") }
.map { it.trimStart('/', ' ') }
.map { "<p>$it</p>" }
.joinToString("\n")
val commentText = docComments().map { it.text }.joinToString("\n")
return documentationAsHtml(commentText, this)
}

fun generateFunction(function: MvFunction, buffer: StringBuilder) {
Expand Down
162 changes: 162 additions & 0 deletions src/main/kotlin/org/move/ide/docs/MvRenderedDocumentationPipeline.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.move.ide.docs

import com.intellij.codeEditor.printing.HTMLTextPainter
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.psi.PsiElement
import com.intellij.ui.ColorHexUtil
import com.intellij.ui.ColorUtil
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.ast.getTextInNode
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.html.*
import org.intellij.markdown.parser.LinkMap
import org.intellij.markdown.parser.MarkdownParser
import java.net.URI

enum class MvDocRenderMode {
QUICK_DOC_POPUP,
INLINE_DOC_COMMENT,
}

fun documentationAsHtml(text: String, context: PsiElement): String {
val documentationText = processDocumentationText(text)
val flavour = MvDocMarkdownFlavourDescriptor(context, null, MvDocRenderMode.QUICK_DOC_POPUP)
val root = MarkdownParser(flavour).buildMarkdownTreeFromString(documentationText)
return HtmlGenerator(documentationText, root, flavour).generateHtml()
}

fun processDocumentationText(text: String): String {
// Comments spanning multiple lines are merged using spaces, unless
//
// - the line is empty
// - the line ends with a . (end of sentence)
// - the line is purely of at least 3 of -, =, _, *, ~ (horizontal rule)
// - the line starts with at least one # followed by a space (header)
// - the line starts and ends with a | (table)
// - the line starts with - (list)

var insideCodeBlock = false
val lines = text.lines()
val newLines = lines.map { it.trimStart('/', ' ') }.map { line ->
if (line.startsWith("```")) {
insideCodeBlock = !insideCodeBlock
} else if (insideCodeBlock) {
// don't add any extra new lines to code blocks
return@map line
}

if (ORDERED_LIST_REGEX.matches(line)) {
line // don't add any extra new lines to lists
} else if (line.endsWith(".") || line.endsWith("!") || line.endsWith("?") ||
line.matches(Regex("^[-=_*~]{3,}\$")) ||
line.endsWith("|") ||
line.startsWith("|") ||
line.startsWith("-")
) {
line + "\n\n"
} else {
line
}
}

return newLines.joinToString("\n")
}

val ORDERED_LIST_REGEX = """^(\d+\.|-|\*)\s.*$""".toRegex()

private class MvDocMarkdownFlavourDescriptor(
private val context: PsiElement,
private val uri: URI? = null,
private val renderMode: MvDocRenderMode,
private val gfm: MarkdownFlavourDescriptor = GFMFlavourDescriptor(useSafeLinks = false, absolutizeAnchorLinks = true),
) : MarkdownFlavourDescriptor by gfm {

override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map<IElementType, GeneratingProvider> {
val generatingProviders = HashMap(gfm.createHtmlGeneratingProviders(linkMap, uri ?: baseURI))
// Filter out MARKDOWN_FILE to avoid producing unnecessary <body> tags
generatingProviders.remove(MarkdownElementTypes.MARKDOWN_FILE)
// h1 and h2 are too large
generatingProviders[MarkdownElementTypes.ATX_1] = SimpleTagProvider("h2")
generatingProviders[MarkdownElementTypes.ATX_2] = SimpleTagProvider("h3")
generatingProviders[MarkdownElementTypes.CODE_FENCE] = MvCodeFenceProvider(context, renderMode)

return generatingProviders
}
}

private class MvCodeFenceProvider(
private val context: PsiElement,
private val renderMode: MvDocRenderMode,
) : GeneratingProvider {

override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) {
val indentBefore = node.getTextInNode(text).commonPrefixWith(" ".repeat(10)).length

val codeText = StringBuilder()

var childrenToConsider = node.children
if (childrenToConsider.last().type == MarkdownTokenTypes.CODE_FENCE_END) {
childrenToConsider = childrenToConsider.subList(0, childrenToConsider.size - 1)
}

var isContentStarted = false

loop@ for (child in childrenToConsider) {
if (isContentStarted && child.type in listOf(MarkdownTokenTypes.CODE_FENCE_CONTENT, MarkdownTokenTypes.EOL)) {
val rawLine = HtmlGenerator.trimIndents(child.getTextInNode(text), indentBefore)
codeText.append(rawLine)
}

if (!isContentStarted && child.type == MarkdownTokenTypes.EOL) {
isContentStarted = true
}
}

visitor.consumeHtml(convertToHtmlWithHighlighting(codeText.toString()))
}

private fun convertToHtmlWithHighlighting(codeText: String): String {
var htmlCodeText = HTMLTextPainter.convertCodeFragmentToHTMLFragmentWithInlineStyles(context, codeText)

val scheme = EditorColorsManager.getInstance().globalScheme
htmlCodeText = htmlCodeText.replaceFirst(
"<pre>",
"<pre style=\"text-indent: ${CODE_SNIPPET_INDENT}px; margin-bottom: -20px;\">"
)

return when (renderMode) {
MvDocRenderMode.INLINE_DOC_COMMENT -> htmlCodeText.dimColors(scheme)
else -> htmlCodeText
}
}

private fun String.dimColors(scheme: EditorColorsScheme): String {
val alpha = if (isColorSchemeDark(scheme)) DARK_THEME_ALPHA else LIGHT_THEME_ALPHA

return replace(COLOR_PATTERN) { result ->
val colorHexValue = result.groupValues[1]
val fgColor = ColorHexUtil.fromHexOrNull(colorHexValue) ?: return@replace result.value
val bgColor = scheme.defaultBackground
val finalColor = ColorUtil.mix(bgColor, fgColor, alpha)

"color: #${ColorUtil.toHex(finalColor)}"
}
}

private fun isColorSchemeDark(scheme: EditorColorsScheme): Boolean {
return ColorUtil.isDark(scheme.defaultBackground)
}

companion object {
private val COLOR_PATTERN = """color:\s*#(\p{XDigit}{3,})""".toRegex()

private const val CODE_SNIPPET_INDENT = 10
private const val LIGHT_THEME_ALPHA = 0.6
private const val DARK_THEME_ALPHA = 0.78
}
}

0 comments on commit 896757f

Please sign in to comment.