From e76003bcfbb244dec9979fbc21cb9085d659d10f Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Sat, 23 Nov 2024 18:46:05 +0100 Subject: [PATCH] Support hover documentation for the enums / enum variants / spec items. Add highlighting to the documentation. (#250) * separate markdown docs tests * add docs for missing module items, coloring for the documentation * highlight const initializer * bug with abilities highlighting * parameter / value parameter / type parameter docs is monospaced * docs for schema --- src/main/grammars/MoveParser.bnf | 13 +- .../kotlin/org/move/ide/docs/MvColorUtils.kt | 57 +++++ .../docs/MvPsiDocumentationTargetProvider.kt | 196 ++++++------------ .../org/move/ide/docs/RenderSignature.kt | 195 +++++++++++++++++ .../kotlin/org/move/ide/presentation/Utils.kt | 10 + .../kotlin/org/move/ide/presentation/ty.kt | 192 +++++++++++------ .../move/lang/core/psi/ext/MvEnumVariant.kt | 2 - .../org/move/lang/core/psi/ext/MvFunction.kt | 16 ++ .../org/move/lang/core/psi/ext/MvSchema.kt | 11 +- .../move/lang/core/psi/ext/MvSpecFunction.kt | 31 ++- .../kotlin/org/move/stdext/Collections.kt | 25 ++- .../kotlin/org/move/utils/SignatureUtils.kt | 2 +- .../ide/docs/MoveDocumentationProviderTest.kt | 193 ++++++++++------- .../docs/MoveNamedAddressDocumentationTest.kt | 2 +- .../ide/docs/MvMarkdownDocsRendererTest.kt | 96 +++++++++ .../MoveDocumentationProviderTestCase.kt | 15 +- 16 files changed, 768 insertions(+), 288 deletions(-) create mode 100644 src/main/kotlin/org/move/ide/docs/MvColorUtils.kt create mode 100644 src/main/kotlin/org/move/ide/docs/RenderSignature.kt create mode 100644 src/test/kotlin/org/move/ide/docs/MvMarkdownDocsRendererTest.kt diff --git a/src/main/grammars/MoveParser.bnf b/src/main/grammars/MoveParser.bnf index ee482d8e6..108ff6a72 100644 --- a/src/main/grammars/MoveParser.bnf +++ b/src/main/grammars/MoveParser.bnf @@ -325,12 +325,14 @@ SpecFunctionInner ::= Attr* spec fun IDENTIFIER TypeParameterList? { pin = 3 elementType = SpecFunction + hooks = [ leftBinder = "ADJACENT_LINE_COMMENTS" ] } NativeSpecFunctionInner ::= Attr* spec (native fun) IDENTIFIER TypeParameterList? FunctionParameterList ReturnType? ';' { pin = 3 elementType = SpecFunction + hooks = [ leftBinder = "ADJACENT_LINE_COMMENTS" ] } fake SpecInlineFunction ::= Attr* native? fun IDENTIFIER? TypeParameterList? @@ -338,6 +340,7 @@ fake SpecInlineFunction ::= Attr* native? fun IDENTIFIER? TypeParameterList? (SpecCodeBlock | ';')? { implements = [ + "org.move.lang.core.psi.MvQualNamedElement" "org.move.lang.core.psi.MvFunctionLike" "org.move.lang.core.types.infer.MvInferenceContextOwner" "org.move.lang.core.psi.ext.MvItemElement" @@ -350,17 +353,19 @@ fake SpecInlineFunction ::= Attr* native? fun IDENTIFIER? TypeParameterList? // | NativeSpecInlineFunctionInner // | UninterpretedSpecInlineFunctionInner -SpecInlineFunctionInner ::= fun IDENTIFIER TypeParameterList? +SpecInlineFunctionInner ::= Attr* fun IDENTIFIER TypeParameterList? FunctionParameterList ReturnType? (<> | ';') { - pin = 1 + pin = 2 elementType = SpecInlineFunction + hooks = [ leftBinder = "ADJACENT_LINE_COMMENTS" ] } -NativeSpecInlineFunctionInner ::= (native fun) IDENTIFIER TypeParameterList? +NativeSpecInlineFunctionInner ::= Attr* (native fun) IDENTIFIER TypeParameterList? FunctionParameterList ReturnType? ';' { + pin = 2 elementType = SpecInlineFunction - pin = 1 + hooks = [ leftBinder = "ADJACENT_LINE_COMMENTS" ] } SpecInlineFunctionStmt ::= SpecInlineFunctionInner diff --git a/src/main/kotlin/org/move/ide/docs/MvColorUtils.kt b/src/main/kotlin/org/move/ide/docs/MvColorUtils.kt new file mode 100644 index 000000000..971c28204 --- /dev/null +++ b/src/main/kotlin/org/move/ide/docs/MvColorUtils.kt @@ -0,0 +1,57 @@ +package org.move.ide.docs + +import com.intellij.lang.documentation.DocumentationSettings +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil +import io.ktor.util.escapeHTML +import org.move.ide.colors.MvColor + +@Suppress("UnstableApiUsage") +object MvColorUtils { + + val asKeyword get() = loadKey(MvColor.KEYWORD) + val asFunction get() = loadKey(MvColor.FUNCTION) + val asStruct get() = loadKey(MvColor.STRUCT) + val asField get() = loadKey(MvColor.FIELD) + val asEnum get() = loadKey(MvColor.ENUM) + val asEnumVariant get() = loadKey(MvColor.ENUM_VARIANT) + val asConst get() = loadKey(MvColor.CONSTANT) + val asTypeParam get() = loadKey(MvColor.TYPE_PARAMETER) + val asAbility get() = loadKey(MvColor.ABILITY) + + val asPrimitiveType get() = loadKey(MvColor.PRIMITIVE_TYPE) + val asBuiltinType get() = loadKey(MvColor.BUILTIN_TYPE) + val asStructType get() = loadKey(MvColor.STRUCT) + val asEnumType get() = loadKey(MvColor.ENUM) + val asEnumVariantType get() = loadKey(MvColor.ENUM_VARIANT) + + val asBraces get() = loadKey(MvColor.BRACES) + val asBrackets get() = loadKey(MvColor.BRACKETS) + val asParens get() = loadKey(MvColor.PARENTHESES) + val asComma get() = loadKey(MvColor.COMMA) + val asSemicolon get() = loadKey(MvColor.SEMICOLON) + val asOperator get() = loadKey(MvColor.OPERATORS) + + fun StringBuilder.keyword(text: String) = colored(text, asKeyword) + fun StringBuilder.op(text: String) = colored(text, asOperator) + fun StringBuilder.comma() = colored(",", asComma) + fun StringBuilder.semicolon() = colored(";", asSemicolon) + + fun StringBuilder.colored(text: String?, color: TextAttributes, noHtml: Boolean = false) { + if (noHtml) { + append(text) + return + } + HtmlSyntaxInfoUtil.appendStyledSpan( + this, color, text?.escapeHTML() ?: "", + DocumentationSettings.getHighlightingSaturation(false) + ) + } + + private fun loadKey(color: MvColor): TextAttributes = loadKey(color.textAttributesKey) + + private fun loadKey(key: TextAttributesKey): TextAttributes = + EditorColorsManager.getInstance().globalScheme.getAttributes(key)!! +} \ No newline at end of file diff --git a/src/main/kotlin/org/move/ide/docs/MvPsiDocumentationTargetProvider.kt b/src/main/kotlin/org/move/ide/docs/MvPsiDocumentationTargetProvider.kt index cc470128c..57876fb83 100644 --- a/src/main/kotlin/org/move/ide/docs/MvPsiDocumentationTargetProvider.kt +++ b/src/main/kotlin/org/move/ide/docs/MvPsiDocumentationTargetProvider.kt @@ -9,36 +9,35 @@ import com.intellij.platform.backend.documentation.PsiDocumentationTargetProvide import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.psi.PsiElement import com.intellij.psi.createSmartPointer +import io.ktor.http.quote +import org.move.ide.docs.MvColorUtils.asAbility +import org.move.ide.docs.MvColorUtils.asTypeParam +import org.move.ide.docs.MvColorUtils.colored +import org.move.ide.docs.MvColorUtils.keyword +import org.move.ide.presentation.declaringModule import org.move.ide.presentation.presentationInfo import org.move.ide.presentation.text -import org.move.ide.presentation.typeLabel import org.move.lang.core.psi.MvAbility import org.move.lang.core.psi.MvConst import org.move.lang.core.psi.MvElement -import org.move.lang.core.psi.MvFunction import org.move.lang.core.psi.MvFunctionParameter import org.move.lang.core.psi.MvFunctionParameterList -import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.MvNamedAddress -import org.move.lang.core.psi.MvNamedFieldDecl import org.move.lang.core.psi.MvPatBinding import org.move.lang.core.psi.MvReturnType -import org.move.lang.core.psi.MvStruct import org.move.lang.core.psi.MvType import org.move.lang.core.psi.MvTypeParameter import org.move.lang.core.psi.MvTypeParameterList +import org.move.lang.core.psi.containingModule import org.move.lang.core.psi.ext.* -import org.move.lang.core.psi.isNative -import org.move.lang.core.psi.module import org.move.lang.core.types.infer.inference import org.move.lang.core.types.infer.loweredType -import org.move.lang.core.types.ty.Ty -import org.move.lang.core.types.ty.TyUnknown import org.move.lang.moveProject import org.move.stdext.joinToWithBuffer import org.toml.lang.psi.TomlKeySegment +import kotlin.collections.isNotEmpty -class MvPsiDocumentationTargetProvider : PsiDocumentationTargetProvider { +class MvPsiDocumentationTargetProvider: PsiDocumentationTargetProvider { override fun documentationTarget(element: PsiElement, originalElement: PsiElement?): DocumentationTarget? { if (element is MvElement) { return MvDocumentationTarget(element, originalElement) @@ -53,7 +52,10 @@ class MvPsiDocumentationTargetProvider : PsiDocumentationTargetProvider { } @Suppress("UnstableApiUsage") -class MvDocumentationTarget(val element: PsiElement, private val originalElement: PsiElement?) : DocumentationTarget { +class MvDocumentationTarget( + val element: PsiElement, + private val originalElement: PsiElement? +): DocumentationTarget { override fun computePresentation(): TargetPresentation { val project = element.project val file = element.containingFile?.virtualFile @@ -86,47 +88,57 @@ class MvDocumentationTarget(val element: PsiElement, private val originalElement } when (docElement) { + is MvDocAndAttributeOwner -> generateDocumentationOwnerDoc(docElement, buffer) is MvNamedAddress -> { - // TODO: add docs for both [addresses] and [dev-addresses] val moveProject = docElement.moveProject ?: return null - val refName = docElement.referenceName - val named = moveProject.getNamedAddressTestAware(refName) ?: return null + val addressName = docElement.referenceName + val named = moveProject.getNamedAddressTestAware(addressName) ?: return null val address = - named.addressLit()?.original ?: angleWrapped("unassigned") - return "$refName = \"$address\"" + named.addressLit()?.original ?: "".escapeForHtml() + definition(buffer) { + it += "$addressName = ${address.quote()}" + } +// return "$refName = \"$address\"" } - is MvDocAndAttributeOwner -> generateOwnerDoc(docElement, buffer) is MvPatBinding -> { val presentationInfo = docElement.presentationInfo ?: return null - val msl = docElement.isMslOnlyItem - val inference = docElement.inference(msl) ?: return null - val type = inference.getBindingType(docElement).renderForDocs(true) - buffer += presentationInfo.type - buffer += " " - buffer.b { it += presentationInfo.name } - buffer += ": " - buffer += type + definition(buffer) { + it.keyword(presentationInfo.type) + it += " " + it += presentationInfo.name + val msl = docElement.isMslOnlyItem + val inference = docElement.inference(msl) ?: return null + val tyText = inference + .getBindingType(docElement) + .text(fq = true, colors = true) + it += ": " + it += tyText + } } is MvTypeParameter -> { val presentationInfo = docElement.presentationInfo ?: return null - buffer += presentationInfo.type - buffer += " " - buffer.b { it += presentationInfo.name } - val abilities = docElement.abilityBounds - if (abilities.isNotEmpty()) { - abilities.joinToWithBuffer(buffer, " + ", ": ") { generateDocumentation(it) } + definition(buffer) { + it.keyword(presentationInfo.type) + it += " " + it += presentationInfo.name + val abilities = docElement.abilityBounds + if (abilities.isNotEmpty()) { + abilities.joinToWithBuffer(it, " + ", ": ") { generateDoc(it) } + } } } } return if (buffer.isEmpty()) null else buffer.toString() } - private fun generateOwnerDoc(element: MvDocAndAttributeOwner, buffer: StringBuilder) { + private fun generateDocumentationOwnerDoc(element: MvDocAndAttributeOwner, buffer: StringBuilder) { definition(buffer) { + element.header(it) element.signature(it) } val text = element.documentationAsHtml() + if (text.isEmpty()) return buffer += "\n" // Just for more pretty html text representation content(buffer) { it += text } } @@ -137,118 +149,50 @@ fun MvDocAndAttributeOwner.documentationAsHtml(): String { return documentationAsHtml(commentText, this) } -fun generateFunction(function: MvFunction, buffer: StringBuilder) { - val module = function.module - if (module != null) { - buffer += module.qualName?.editorText() ?: "unknown" - buffer += "\n" - } - if (function.isNative) buffer += "native " - buffer += "fun " - buffer.b { it += function.name } - function.typeParameterList?.generateDocumentation(buffer) - function.functionParameterList?.generateDocumentation(buffer) - function.returnType?.generateDocumentation(buffer) -} - -fun MvElement.signature(builder: StringBuilder) { - val buffer = StringBuilder() - // no need for msl type conversion in docs - val msl = false -// val msl = this.isMslLegacy() - when (this) { - is MvFunction -> generateFunction(this, buffer) - is MvModule -> { - buffer += "module " - buffer += this.qualName?.editorText() ?: "unknown" - } - - is MvStruct -> { - buffer += this.module.qualName?.editorText() ?: "unknown" - buffer += "\n" - - buffer += "struct " - buffer.b { it += this.name } - this.typeParameterList?.generateDocumentation(buffer) - this.abilitiesList?.abilityList - ?.joinToWithBuffer(buffer, ", ", " has ") { generateDocumentation(it) } - } - - is MvNamedFieldDecl -> { - val module = this.fieldOwner.itemElement.module -// val itemContext = this.structItem.outerItemContext(msl) - buffer += module.qualName?.editorText() ?: "unknown" - buffer += "::" - buffer += this.fieldOwner.name ?: angleWrapped("anonymous") - buffer += "\n" - buffer.b { it += this.name } - buffer += ": ${(this.type?.loweredType(msl) ?: TyUnknown).renderForDocs(true)}" -// buffer += ": ${itemContext.getStructFieldItemTy(this).renderForDocs(true)}" - } - - is MvConst -> { -// val itemContext = this.outerItemContext(msl) - buffer += this.module?.qualName?.editorText() ?: angleWrapped("unknown") - buffer += "\n" - buffer += "const " - buffer.b { it += this.name ?: angleWrapped("unknown") } - buffer += ": ${(this.type?.loweredType(msl) ?: TyUnknown).renderForDocs(false)}" -// buffer += ": ${itemContext.getConstTy(this).renderForDocs(false)}" - this.initializer?.let { buffer += " ${it.text}" } - } - - else -> return - } ?: return - listOf(buffer.toString()).joinTo(builder, "
") -} - -private fun PsiElement.generateDocumentation( - buffer: StringBuilder, - prefix: String = "", - suffix: String = "" -) { - buffer += prefix +fun PsiElement.generateDoc(buf: StringBuilder) { when (this) { is MvType -> { val msl = this.isMsl() - buffer += this.loweredType(msl) - .typeLabel(this) - .replace("<", "<") - .replace(">", ">") + val ty = this.loweredType(msl) + + val tyItemModule = ty.declaringModule() + val fq = tyItemModule != null && tyItemModule != this.containingModule + buf += ty.text(fq, colors = true) } is MvFunctionParameterList -> this.functionParameterList - .joinToWithBuffer(buffer, ", ", "(", ")") { generateDocumentation(it) } + .joinToWithBuffer(buf, ", ", "(", ")") { generateDoc(it) } is MvFunctionParameter -> { - buffer += this.patBinding.identifier.text - this.type?.generateDocumentation(buffer, ": ") + buf += this.patBinding.identifier.text + this.type?.let { + buf += ": " + it.generateDoc(buf) + } } is MvTypeParameterList -> this.typeParameterList - .joinToWithBuffer(buffer, ", ", "<", ">") { generateDocumentation(it) } + .joinToWithBuffer(buf, ", ", "<", ">") { generateDoc(it) } is MvTypeParameter -> { if (this.isPhantom) { - buffer += "phantom" - buffer += " " + buf.keyword("phantom") + buf += " " } - buffer += this.identifier?.text + buf.colored(this.name, asTypeParam) val bound = this.typeParamBound if (bound != null) { - abilityBounds.joinToWithBuffer(buffer, " + ", ": ") { generateDocumentation(it) } + abilityBounds.joinToWithBuffer(buf, " + ", ": ") { generateDoc(it) } } } - - is MvAbility -> { - buffer += this.text + is MvAbility -> buf.colored(this.text, asAbility) + is MvReturnType -> this.type?.let { + buf += ": " + it.generateDoc(buf) } - - is MvReturnType -> this.type?.generateDocumentation(buffer, ": ") } - buffer += suffix } private inline fun definition(buffer: StringBuilder, block: (StringBuilder) -> Unit) { @@ -263,16 +207,13 @@ private inline fun content(buffer: StringBuilder, block: (StringBuilder) -> Unit buffer += DocumentationMarkup.CONTENT_END } -private fun angleWrapped(text: String): String = "<$text>" - -private fun Ty.renderForDocs(fq: Boolean): String { - val original = this.text(fq) - return original +fun String.escapeForHtml(): String { + return this .replace("<", "<") .replace(">", ">") } -private operator fun StringBuilder.plusAssign(value: String?) { +operator fun StringBuilder.plusAssign(value: String?) { if (value != null) { append(value) } @@ -283,3 +224,4 @@ private inline fun StringBuilder.b(action: (StringBuilder) -> Unit) { action(this) append("") } + diff --git a/src/main/kotlin/org/move/ide/docs/RenderSignature.kt b/src/main/kotlin/org/move/ide/docs/RenderSignature.kt new file mode 100644 index 000000000..baffc6eeb --- /dev/null +++ b/src/main/kotlin/org/move/ide/docs/RenderSignature.kt @@ -0,0 +1,195 @@ +package org.move.ide.docs + +import com.intellij.codeEditor.printing.HTMLTextPainter +import com.intellij.psi.PsiElement +import org.move.ide.docs.MvColorUtils.asConst +import org.move.ide.docs.MvColorUtils.asEnum +import org.move.ide.docs.MvColorUtils.asEnumVariant +import org.move.ide.docs.MvColorUtils.asField +import org.move.ide.docs.MvColorUtils.asFunction +import org.move.ide.docs.MvColorUtils.asStruct +import org.move.ide.docs.MvColorUtils.colored +import org.move.ide.docs.MvColorUtils.keyword +import org.move.ide.presentation.presentableQualifiedName +import org.move.ide.presentation.text +import org.move.lang.core.psi.MvConst +import org.move.lang.core.psi.MvEnum +import org.move.lang.core.psi.MvEnumVariant +import org.move.lang.core.psi.MvFunction +import org.move.lang.core.psi.MvFunctionLike +import org.move.lang.core.psi.MvModule +import org.move.lang.core.psi.MvNamedFieldDecl +import org.move.lang.core.psi.MvSchema +import org.move.lang.core.psi.MvSpecFunction +import org.move.lang.core.psi.MvSpecInlineFunction +import org.move.lang.core.psi.MvStruct +import org.move.lang.core.psi.ext.MvDocAndAttributeOwner +import org.move.lang.core.psi.ext.MvStructOrEnumItemElement +import org.move.lang.core.psi.ext.enumItem +import org.move.lang.core.psi.ext.fieldOwner +import org.move.lang.core.psi.ext.modifiers +import org.move.lang.core.types.ty.TyUnknown +import org.move.stdext.joinToWithBuffer + +fun MvDocAndAttributeOwner.header(buffer: StringBuilder) { + val rawLines = when (this) { + is MvNamedFieldDecl -> listOfNotNull((fieldOwner as? MvDocAndAttributeOwner)?.presentableQualifiedName) + is MvStructOrEnumItemElement, + is MvFunctionLike, + is MvConst, + is MvSchema -> listOfNotNull(presentableQualifiedModName) + else -> emptyList() + } + rawLines.joinTo(buffer, "
") + if (rawLines.isNotEmpty()) { + buffer += "\n" + } +} + +fun MvDocAndAttributeOwner.signature(builder: StringBuilder) { + // no need for msl type conversion in docs +// val msl = false + val buffer = StringBuilder() + when (this) { + is MvFunction -> buffer.generateFunction(this) + is MvSpecFunction -> buffer.generateSpecFunction(this) + is MvSpecInlineFunction -> buffer.generateSpecInlineFunction(this) + is MvModule -> buffer.generateModule(this) + is MvStructOrEnumItemElement -> buffer.generateStructOrEnum(this) + is MvSchema -> buffer.generateSchema(this) + is MvNamedFieldDecl -> buffer.generateNamedField(this) + is MvConst -> buffer.generateConst(this) + is MvEnumVariant -> buffer.generateEnumVariant(this) + else -> return + } + listOf(buffer.toString()).joinTo(builder, "
") +} + +private fun StringBuilder.generateFunction(fn: MvFunction) { + for (modifier in fn.modifiers) { + keyword(modifier) + this += " " + } + keyword("fun") + this += " " + colored(fn.name, asFunction) + + fn.typeParameterList?.generateDoc(this) + fn.functionParameterList?.generateDoc(this) + fn.returnType?.generateDoc(this) +} + +private fun StringBuilder.generateSpecFunction(specFn: MvSpecFunction) { + for (modifier in specFn.modifiers) { + keyword(modifier) + this += " " + } + keyword("spec") + this += " " + keyword("fun") + this += " " + colored(specFn.name, asFunction) + + specFn.typeParameterList?.generateDoc(this) + specFn.functionParameterList?.generateDoc(this) + specFn.returnType?.generateDoc(this) +} + +private fun StringBuilder.generateSpecInlineFunction(specInlineFn: MvSpecInlineFunction) { + for (modifier in specInlineFn.modifiers) { + keyword(modifier) + this += " " + } + keyword("fun") + this += " " + colored(specInlineFn.name, asFunction) + + specInlineFn.typeParameterList?.generateDoc(this) + specInlineFn.functionParameterList?.generateDoc(this) + specInlineFn.returnType?.generateDoc(this) +} + +private fun StringBuilder.generateModule(mod: MvModule) { + keyword("module") + this += " " + this += mod.qualName?.editorText() ?: "unknown" +} + +private fun StringBuilder.generateStructOrEnum(structOrEnum: MvStructOrEnumItemElement) { + when (structOrEnum) { + is MvStruct -> { + keyword("struct") + this += " " + colored(structOrEnum.name, asStruct) + } + is MvEnum -> { + keyword("enum") + this += " " + colored(structOrEnum.name, asEnum) + } + } + structOrEnum.typeParameterList?.generateDoc(this) + + val abilities = structOrEnum.abilitiesList?.abilityList + if (abilities != null && abilities.isNotEmpty()) { + this += " " + this.keyword("has") + this += " " + abilities.joinToWithBuffer(this, ", ") { generateDoc(it) } + } +} + +private fun StringBuilder.generateSchema(schema: MvSchema) { + this.keyword("spec") + this += " " + this.keyword("schema") + this += " " + this += schema.name + schema.typeParameterList?.generateDoc(this) +} + +private fun StringBuilder.generateEnumVariant(variant: MvEnumVariant) { + this += variant.enumItem.presentableQualifiedName + this += "::" + this.colored(variant.name, asEnumVariant) +} + +private fun StringBuilder.generateNamedField(field: MvNamedFieldDecl) { + colored(field.name, asField) + this += ": " + val fieldType = field.type + if (fieldType == null) { + this += TyUnknown.text(colors = true) + return + } + fieldType.generateDoc(this) +} + +private fun StringBuilder.generateConst(const: MvConst) { + keyword("const") + this += " " + colored(const.name, asConst) + + this += ": " + val constType = const.type + if (constType == null) { + this += TyUnknown.text(colors = true) + return + } + constType.generateDoc(this) + + const.initializer?.expr?.let { expr -> + this += " = " + this += highlightWithLexer(expr, expr.text) + } +} + +private fun highlightWithLexer(context: PsiElement, text: String): String { + val highlighed = + HTMLTextPainter.convertCodeFragmentToHTMLFragmentWithInlineStyles(context, text) + return highlighed.trimEnd() + .removeSurrounding("
", "
") +} + +private val MvDocAndAttributeOwner.presentableQualifiedModName: String? + get() = presentableQualifiedName?.removeSuffix("::$name") diff --git a/src/main/kotlin/org/move/ide/presentation/Utils.kt b/src/main/kotlin/org/move/ide/presentation/Utils.kt index ef478beac..f32799c8e 100644 --- a/src/main/kotlin/org/move/ide/presentation/Utils.kt +++ b/src/main/kotlin/org/move/ide/presentation/Utils.kt @@ -12,6 +12,9 @@ import org.move.lang.core.psi.MvFunctionLike import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.MvNamedElement import org.move.lang.core.psi.MvNamedFieldDecl +import org.move.lang.core.psi.MvQualNamedElement +import org.move.lang.core.psi.ext.MvDocAndAttributeOwner +import org.move.lang.core.psi.ext.enumItem import org.move.lang.core.psi.signatureText import org.move.lang.core.types.address import org.move.lang.moveProject @@ -69,6 +72,13 @@ fun PsiElement.locationString(tryRelative: Boolean): String? = when (this) { else -> containingFilePath(tryRelative)?.toString() } +val MvDocAndAttributeOwner.presentableQualifiedName: String? + get() { + val qName = (this as? MvQualNamedElement)?.qualName?.editorText() + if (qName != null) return qName + return name + } + private fun PsiElement.containingFilePath(tryRelative: Boolean): Path? { val containingFilePath = this.containingFile.toNioPathOrNull() ?: return null if (tryRelative) { diff --git a/src/main/kotlin/org/move/ide/presentation/ty.kt b/src/main/kotlin/org/move/ide/presentation/ty.kt index d201859c6..05eff36f3 100644 --- a/src/main/kotlin/org/move/ide/presentation/ty.kt +++ b/src/main/kotlin/org/move/ide/presentation/ty.kt @@ -1,9 +1,16 @@ package org.move.ide.presentation -import org.move.lang.core.psi.MvElement +import com.intellij.openapi.editor.markup.TextAttributes +import org.move.ide.docs.MvColorUtils.asBuiltinType +import org.move.ide.docs.MvColorUtils.asKeyword +import org.move.ide.docs.MvColorUtils.asPrimitiveType +import org.move.ide.docs.MvColorUtils.asTypeParam +import org.move.ide.docs.MvColorUtils.colored +import org.move.ide.docs.escapeForHtml import org.move.lang.core.psi.MvModule import org.move.lang.core.psi.containingModule import org.move.lang.core.types.ty.* +import org.move.stdext.chainIf // null -> builtin module fun Ty.declaringModule(): MvModule? = when (this) { @@ -16,35 +23,27 @@ fun Ty.nameNoArgs(): String { return this.name().replace(Regex("<.*>"), "") } -fun Ty.name(): String { - return text(fq = false) +fun Ty.name(colors: Boolean = false): String { + return text(fq = false, colors = colors) } fun Ty.fullnameNoArgs(): String { return this.fullname().replace(Regex("<.*>"), "") } -fun Ty.fullname(): String { - return text(fq = true) -} - -fun Ty.typeLabel(relativeTo: MvElement): String { - val typeModule = this.declaringModule() - if (typeModule != null && typeModule != relativeTo.containingModule) { - return this.fullname() - } else { - return this.name() - } +fun Ty.fullname(colors: Boolean = false): String { + return text(fq = true, colors = colors) } fun Ty.hintText(): String = render(this, level = 3, unknown = "?", tyVar = { "?" }) -fun Ty.text(fq: Boolean = false): String = +fun Ty.text(fq: Boolean = false, colors: Boolean = false): String = render( this, level = 3, - fq = fq + fq = fq, + toHtml = colors, ) fun Ty.expectedTyText(): String { @@ -71,14 +70,14 @@ fun Ty.expectedTyText(): String { ) } -val Ty.insertionSafeText: String - get() = render( - this, - level = Int.MAX_VALUE, - unknown = "_", - anonymous = "_", - integer = "_" - ) +//val Ty.insertionSafeText: String +// get() = render( +// this, +// level = Int.MAX_VALUE, +// unknown = "_", +// anonymous = "_", +// integer = "_" +// ) fun tyToString(ty: Ty) = render(ty, Int.MAX_VALUE) @@ -88,35 +87,28 @@ private fun render( unknown: String = "", anonymous: String = "", integer: String = "integer", - typeParam: (TyTypeParameter) -> String = { it.name ?: anonymous }, - tyVar: (TyInfer.TyVar) -> String = { "?${it.origin?.name ?: "_"}" }, - fq: Boolean = false + fq: Boolean = false, + toHtml: Boolean = false, + typeParam: (TyTypeParameter) -> String = { + it.name?.chainIf(toHtml) { colored(this, asTypeParam) } + ?: anonymous +// colored(it.name, asTypeParam, toHtml) ?: anonymous + }, + tyVar: (TyInfer.TyVar) -> String = { + val varName = + it.origin?.name?.chainIf(toHtml) { colored(this, asTypeParam) } + ?: "_" + "?$varName" +// colored(it.origin?.name, asTypeParam, toHtml) ?: "_" + }, ): String { check(level >= 0) - if (ty is TyUnknown) return unknown - if (ty is TyPrimitive) { - return when (ty) { - is TyBool -> "bool" - is TyAddress -> "address" - is TySigner -> "signer" - is TyUnit -> "()" - is TyNum -> "num" - is TySpecBv -> "bv" - is TyInteger -> { - if (ty.kind == TyInteger.DEFAULT_KIND) { - integer - } else { - ty.kind.toString() - } - } - is TyNever -> "" - else -> error("unreachable") - } - } + if (ty is TyUnknown) return unknown.chainIf(toHtml) { escapeForHtml() } + if (ty is TyPrimitive) return renderPrimitive(ty, integer, toHtml = toHtml) if (level == 0) return "_" - val r = { subTy: Ty -> render(subTy, level - 1, unknown, anonymous, integer, typeParam, tyVar, fq) } + val r = { subTy: Ty -> render(subTy, level - 1, unknown, anonymous, integer, fq, toHtml, typeParam, tyVar) } return when (ty) { is TyFunction -> { @@ -128,26 +120,42 @@ private fun render( s } is TyTuple -> ty.types.joinToString(", ", "(", ")", transform = r) - is TyVector -> "vector<${r(ty.item)}>" - is TyRange -> "range<${r(ty.item)}>" + is TyVector -> { + val buf = StringBuilder() + buf.colored("vector", asBuiltinType, noHtml = !toHtml) + buf += "<".escapeIf(toHtml) + r(ty.item) + ">".escapeIf(toHtml) + buf.toString() + } + is TyRange -> { + val buf = StringBuilder() + buf.colored("range", asBuiltinType, noHtml = !toHtml) + buf += "<".escapeIf(toHtml) + r(ty.item) + ">".escapeIf(toHtml) + buf.toString() + } is TyReference -> { - val prefix = if (ty.mutability.isMut) "&mut " else "&" - "$prefix${r(ty.referenced)}" + val buf = StringBuilder() + // todo: escape? + buf += "&" + if (ty.mutability.isMut) { + buf += colored("mut", asKeyword, toHtml) + buf += " " + } + buf += r(ty.referenced) + buf.toString() } is TyTypeParameter -> typeParam(ty) -// is TyStruct -> { -// val name = if (fq) ty.item.qualName?.editorText() ?: anonymous else (ty.item.name ?: anonymous) -// val args = -// if (ty.typeArguments.isEmpty()) "" -// else ty.typeArguments.joinToString(", ", "<", ">", transform = r) -// name + args -// } is TyAdt -> { - val name = if (fq) ty.item.qualName?.editorText() ?: anonymous else (ty.item.name ?: anonymous) - val args = + val itemName = if (fq) ty.item.qualName?.editorText() ?: anonymous else (ty.item.name ?: anonymous) + val typeArgs = if (ty.typeArguments.isEmpty()) "" - else ty.typeArguments.joinToString(", ", "<", ">", transform = r) - name + args + else ty.typeArguments.joinToString( + ", ", + "<".escapeIf(toHtml), + ">".escapeIf(toHtml), + transform = r + ) + itemName + typeArgs +// itemName + typeArgs.chainIf(toHtml) { escapeForHtml() } } is TyInfer -> when (ty) { is TyInfer.TyVar -> tyVar(ty) @@ -163,11 +171,61 @@ private fun render( } is TySchema -> { val name = if (fq) ty.item.qualName?.editorText() ?: anonymous else (ty.item.name ?: anonymous) - val args = - if (ty.typeArguments.isEmpty()) "" - else ty.typeArguments.joinToString(", ", "<", ">", transform = r) - name + args + val typeArgs = + if (ty.typeArguments.isEmpty()) { + "" + } else { + ty.typeArguments.joinToString( + ", ", + "<".escapeIf(toHtml), + ">".escapeIf(toHtml), + transform = r + ) + } + name + typeArgs } else -> error("unimplemented for type ${ty.javaClass.name}") } } + +private fun renderPrimitive(ty: TyPrimitive, integer: String = "integer", toHtml: Boolean = false): String { + val tyText = when (ty) { + is TyBool -> "bool" + is TyAddress -> "address" + is TySigner -> "signer" + is TyUnit -> "()" + is TyNum -> "num" + is TySpecBv -> "bv" + is TyInteger -> { + if (ty.kind == TyInteger.DEFAULT_KIND) { + integer + } else { + ty.kind.toString() + } + } + is TyNever -> "" + else -> error("unreachable") + } + .chainIf(toHtml) { escapeForHtml() } + + return colored(tyText, asPrimitiveType, toHtml)!! +} + +private fun String.escapeIf(toHtml: Boolean) = this.chainIf(toHtml) { escapeForHtml() } + +private fun colored(text: String, color: TextAttributes): String { + return colored(text, color, html = true)!! +} + +private fun colored(text: String?, color: TextAttributes, html: Boolean = true): String? { + if (text == null) return null + val buf = StringBuilder() + buf.colored(text, color, !html) + return buf.toString() +} + +operator fun StringBuilder.plusAssign(value: String?) { + if (value != null) { + append(value) + } +} diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvEnumVariant.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvEnumVariant.kt index e00e6f841..cba128e2f 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvEnumVariant.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvEnumVariant.kt @@ -1,8 +1,6 @@ package org.move.lang.core.psi.ext -import com.intellij.ide.projectView.PresentationData import com.intellij.lang.ASTNode -import com.intellij.navigation.ItemPresentation import com.intellij.psi.stubs.IStubElementType import org.move.ide.MoveIcons import org.move.lang.core.psi.MvEnum diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvFunction.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvFunction.kt index e886d32c4..df2f10da8 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvFunction.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvFunction.kt @@ -35,6 +35,22 @@ val MvFunction.isView: Boolean return stub?.isView ?: queryAttributes.isView } +val MvFunctionLike.modifiers: List get() { + // todo: order of appearance + val item = this + return buildList { + if (item is MvFunction) { + val vis = item.visibilityModifier + if (vis != null) { + add(vis.stubVisKind.keyword) + } + } + if (item is MvFunction && item.isEntry) add("entry") + if (item.isNative) add("native") + if (item is MvFunction && item.isInline) add("inline") + } +} + fun MvFunction.functionId(): String? = qualName?.cmdText() val MvFunction.testAttrItem: MvAttrItem? get() = queryAttributes.getAttrItem("test") diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvSchema.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvSchema.kt index ed28342e6..e14d44ae2 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvSchema.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvSchema.kt @@ -15,7 +15,14 @@ import org.move.lang.core.types.ty.TyUnknown val MvSchema.specBlock: MvSpecCodeBlock? get() = this.childOfType() -val MvSchema.module: MvModule? get() = this.parent as? MvModule +val MvSchema.parentModule: MvModule? get() { + val parent = this.parent + if (parent is MvModule) return parent + if (parent is MvModuleSpecBlock) { + return parent.moduleSpec.moduleItem + } + return null +} val MvSchema.requiredTypeParams: List get() { @@ -46,7 +53,7 @@ abstract class MvSchemaMixin: MvStubbedNamedElementImpl, override val qualName: ItemQualName? get() { val itemName = this.name ?: return null - val moduleFQName = this.module?.qualName ?: return null + val moduleFQName = this.parentModule?.qualName ?: return null return ItemQualName(this, moduleFQName.address, moduleFQName.itemName, itemName) } diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvSpecFunction.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvSpecFunction.kt index ee6e6a63f..ffdb5296a 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvSpecFunction.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvSpecFunction.kt @@ -4,15 +4,34 @@ import com.intellij.lang.ASTNode import com.intellij.psi.stubs.IStubElementType import org.move.ide.MoveIcons import org.move.lang.core.psi.MvModule +import org.move.lang.core.psi.MvModuleItemSpec +import org.move.lang.core.psi.MvModuleSpec +import org.move.lang.core.psi.MvModuleSpecBlock +import org.move.lang.core.psi.MvSpecCodeBlock import org.move.lang.core.psi.MvSpecFunction import org.move.lang.core.psi.MvSpecInlineFunction +import org.move.lang.core.psi.containingModule import org.move.lang.core.psi.impl.MvNameIdentifierOwnerImpl +import org.move.lang.core.psi.namespaceModule import org.move.lang.core.stubs.MvSpecFunctionStub import org.move.lang.core.stubs.MvStubbedNamedElementImpl import org.move.lang.core.types.ItemQualName import javax.swing.Icon -val MvSpecFunction.module: MvModule? get() = this.parent as? MvModule +val MvSpecFunction.parentModule: MvModule? get() { + val parent = this.parent + if (parent is MvModule) return parent + if (parent is MvModuleSpecBlock) { + return parent.moduleSpec.moduleItem + } + return null +} + +val MvSpecInlineFunction.parentModule: MvModule? get() { + val specCodeBlock = this.parent.parent as MvSpecCodeBlock + val moduleSpec = specCodeBlock.parent as? MvModuleItemSpec ?: return null + return moduleSpec.namespaceModule +} abstract class MvSpecFunctionMixin: MvStubbedNamedElementImpl, MvSpecFunction { @@ -24,7 +43,7 @@ abstract class MvSpecFunctionMixin: MvStubbedNamedElementImpl Set.containsAny(vararg items: T): Boolean = items.any { this.contains inline fun Iterable.joinToWithBuffer( buffer: StringBuilder, - separator: CharSequence = ", ", + sep: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", action: T.(StringBuilder) -> Unit, @@ -144,7 +144,7 @@ inline fun Iterable.joinToWithBuffer( var needInsertSeparator = false for (element in this) { if (needInsertSeparator) { - buffer.append(separator) + buffer.append(sep) } element.action(buffer) needInsertSeparator = true @@ -223,3 +223,24 @@ inline fun T.asMap() : Map { return props.keys.associateWith { props[it]?.get(this) } } +inline fun T.applyIf(condition: Boolean, body: T.() -> Unit) { + if (!condition) { + this.body() + } +} + +inline fun T.chainIf(condition: Boolean, body: T.() -> T): T { + return if (!condition) { + this + } else { + this.body() + } +} + +//inline fun T.mapIf(condition: Boolean, fn: (T) -> T): T { +// if (!condition) { +// return this +// } else { +// return this.body() +// } +//} diff --git a/src/main/kotlin/org/move/utils/SignatureUtils.kt b/src/main/kotlin/org/move/utils/SignatureUtils.kt index bc09a54c6..269310c32 100644 --- a/src/main/kotlin/org/move/utils/SignatureUtils.kt +++ b/src/main/kotlin/org/move/utils/SignatureUtils.kt @@ -6,7 +6,7 @@ object SignatureUtils { fun joinParameters(params: List>): String = buildString { append("(") - params.joinToWithBuffer(this, separator = ", ") { sb -> + params.joinToWithBuffer(this, sep = ", ") { sb -> val (name, type) = this sb.append(name) if (type != null) { diff --git a/src/test/kotlin/org/move/ide/docs/MoveDocumentationProviderTest.kt b/src/test/kotlin/org/move/ide/docs/MoveDocumentationProviderTest.kt index 535c9b047..56440cd63 100644 --- a/src/test/kotlin/org/move/ide/docs/MoveDocumentationProviderTest.kt +++ b/src/test/kotlin/org/move/ide/docs/MoveDocumentationProviderTest.kt @@ -1,5 +1,6 @@ package org.move.ide.docs +import org.move.utils.tests.MoveV2 import org.move.utils.tests.MvDocumentationProviderTestCase class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { @@ -11,10 +12,10 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ -
0x0::builtins
-        native fun move_from<T: key>(addr: address): T
-

Removes T from address and returns it. - Aborts if address does not hold a T.

+
0x0::builtins
+native fun move_from<T: key>(addr: address): T
+

Removes T from address and returns it. +Aborts if address does not hold a T.

""") fun `test show doc comment for module`() = doTest(""" @@ -22,21 +23,21 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { module 0x1::M {} //^ """, expected = """ -
module 0x1::M
-

module docstring

+
module 0x1::M
+

module docstring

""" ) fun `test show doc comment for const`() = doTest(""" module 0x1::M { /// const docstring - const ERR_COLLECTION_IS_ALREADY_EXISTS: u64 = 1; + const ERR_COLLECTION_IS_ALREADY_EXISTS: u64 = 0x1; //^ } """, expected = """ -
0x1::M
-        const ERR_COLLECTION_IS_ALREADY_EXISTS: u64 = 1
-

const docstring

+
0x1::M
+const ERR_COLLECTION_IS_ALREADY_EXISTS: u64 = 0x1
+

const docstring

""") fun `test show doc comments and signature for function`() = doTest(""" @@ -49,9 +50,9 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ -
0x1::M
-        fun add(a: u8, b: u8): u8
-

Adds two numbers.

Returns their sum.

+
0x1::M
+fun add(a: u8, b: u8): u8
+

Adds two numbers.

Returns their sum.

""") fun `test show signature for function parameter`() = doTest(""" @@ -62,7 +63,7 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ - value parameter a: u8 +
value parameter a: u8
""") fun `test show signature for type parameter`() = doTest(""" @@ -72,7 +73,7 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ - type parameter R: store + drop +
type parameter R: store + drop
""") fun `test show signature for simple let variable`() = doTest(""" @@ -84,7 +85,7 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ - variable a: vector<u8> +
variable a: vector<u8>
""") fun `test struct docstring`() = doTest(""" @@ -97,9 +98,9 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ -
0x1::M
-        struct S<R: store, phantom PH> has copy, drop, store
-

docstring

+
0x1::M
+struct S<R: store, phantom PH> has copy, drop, store
+

docstring

""") fun `test struct field as vector`() = doTest( @@ -117,9 +118,10 @@ class MvDocumentationProviderTest : MvDocumentationProviderTestCase() { } } """, expected = """ -
0x1::M::Collection
-        nfts: vector<0x1::M::NFT>
-

docstring

""" +
0x1::M::Collection
+nfts: vector<NFT>
+

docstring

+ """ ) fun `test function signature with return generic`() = doTest(""" @@ -134,8 +136,7 @@ module 0x1::main { } """, expected = """
0x1::main
-fun box3<T>(x: T): Box3<T>
-
+fun box3<T>(x: T): Box3<T> """) fun `test result type documentation`() = doTest(""" @@ -147,7 +148,7 @@ module 0x1::m { } } """, """ -value parameter result: num +
value parameter result: num
""") fun `test generic result type documentation`() = doTest(""" @@ -159,76 +160,112 @@ module 0x1::m { } } """, """ -value parameter result: &mut T +
value parameter result: &mut T
""") - fun `test markdown text styles`() = doTest(""" -/// This string contains some *bold* and **italic** words. -module 0x1::M {} - //^ + @MoveV2 + fun `test enum`() = doTest(""" + module 0x1::m { + /// enum S documentation + enum S { Inner(T), Outer(T) } + //^ + } """, """ -
module 0x1::M
-

This string contains some bold and italic words.

+
0x1::m
+enum S<T>
+

enum S documentation

""") - fun `test markdown inline code`() = doTest(""" -/// Maybe some `inline` keyword -module 0x1::M {} - //^ + @MoveV2 + fun `test enum variant`() = doTest(""" + module 0x1::m { + enum S { + /// i am a well documented enum variant + Inner(T), + Outer(T) + } + fun main() { + let _ = S::Inner(1); + //^ + } + } """, """ -
module 0x1::M
-

Maybe some inline keyword

+
0x1::m::S::Inner
+

i am a well documented enum variant

""") - fun `test markdown multiline code`() = doTest(""" -/// Move code: -/// ``` -/// module 0x1::M {} -/// ``` -module 0x1::M {} - //^ + fun `test function docs through spec reference`() = doTest(""" + module 0x1::m { + /// main function + fun main() {} + } + spec 0x1::m { + spec main {} + //^ + } """, """ -
module 0x1::M
-

Move code:

module 0x1::M {}
-
-
+
0x1::m
+fun main()
+

main function

""") - fun `test markdown multiline code with extra spaces`() = doTest(""" -/// Move code: -/// ``` -/// module 0x1::M { -/// // comment -/// } -/// ``` -module 0x1::M {} - //^ + fun `test struct docs through spec reference`() = doTest(""" + module 0x1::m { + /// main struct + struct S { val: u8 } + } + spec 0x1::m { + spec S {} + //^ + } """, """ -
module 0x1::M
-

Move code:

module 0x1::M {
-   // comment
-}
-
-
+
0x1::m
+        struct S
+

main struct

""") - fun `test markdown list`() = doTest(""" -/// - The number of "items" in global storage. -/// - The number of bytes in global storage. -module 0x1::M {} - //^ + fun `test spec fun docs`() = doTest(""" + module 0x1::m { + } + spec 0x1::m { + /// my specification function + spec fun ident(x: u8): u8 { x } + //^ + } """, """ -
module 0x1::M
-
  • The number of "items" in global storage.
  • The number of bytes in global storage.
+
0x1::m
+spec fun ident(x: num): num
+

my specification function

+""") + + // todo: add context support + fun `test inline spec fun docs`() = doTest(""" + module 0x1::m { + spec module { + /// my inline spec fun + fun inline_spec_fun(); + //^ + } + } + """, """ +
0x1::m
+fun inline_spec_fun()
""") - fun `test markdown numbered list`() = doTest(""" -/// 1. The number of "items" in global storage. -/// 2. The number of bytes in global storage. -module 0x1::M {} - //^ + fun `test schema docs`() = doTest(""" + module 0x1::m { + } + spec 0x1::m { + /// my schema + spec schema CreateAccountAbortsIf { + //^ + addr: address; + val: T; + } + } """, """ -
module 0x1::M
-
  1. The number of "items" in global storage.
  2. The number of bytes in global storage.
+
0x1::m
+spec schema CreateAccountAbortsIf<T>
+

my schema

""") } diff --git a/src/test/kotlin/org/move/ide/docs/MoveNamedAddressDocumentationTest.kt b/src/test/kotlin/org/move/ide/docs/MoveNamedAddressDocumentationTest.kt index eba5e7ec5..891f523af 100644 --- a/src/test/kotlin/org/move/ide/docs/MoveNamedAddressDocumentationTest.kt +++ b/src/test/kotlin/org/move/ide/docs/MoveNamedAddressDocumentationTest.kt @@ -24,6 +24,6 @@ class MvNamedModulePathDocumentationTest : MvDocumentationProviderProjectTestCas ) } }, - "Std = \"0x42\"" + "
Std = \"0x42\"
" ) } diff --git a/src/test/kotlin/org/move/ide/docs/MvMarkdownDocsRendererTest.kt b/src/test/kotlin/org/move/ide/docs/MvMarkdownDocsRendererTest.kt new file mode 100644 index 000000000..24c51d8ed --- /dev/null +++ b/src/test/kotlin/org/move/ide/docs/MvMarkdownDocsRendererTest.kt @@ -0,0 +1,96 @@ +package org.move.ide.docs + +import org.intellij.lang.annotations.Language +import org.move.utils.tests.MvDocumentationProviderTestCase + +class MvMarkdownDocsRendererTest: MvDocumentationProviderTestCase() { + + fun `test markdown text styles`() = doTest( + """ +/// This string contains some *bold* and **italic** words. +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+

This string contains some bold and italic words.

+ """ + ) + + fun `test markdown inline code`() = doTest( + """ +/// Maybe some `inline` keyword +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+

Maybe some inline keyword

+ """ + ) + + fun `test markdown multiline code`() = doTest( + """ +/// Move code: +/// ``` +/// module 0x1::M {} +/// ``` +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+

Move code:

module 0x1::M {}
+
+
+ """ + ) + + fun `test markdown multiline code with extra spaces`() = doTest( + """ +/// Move code: +/// ``` +/// module 0x1::M { +/// // comment +/// } +/// ``` +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+

Move code:

module 0x1::M {
+   // comment
+}
+
+
+ """ + ) + + fun `test markdown list`() = doTest( + """ +/// - The number of "items" in global storage. +/// - The number of bytes in global storage. +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+
  • The number of "items" in global storage.
  • The number of bytes in global storage.
+ """ + ) + + fun `test markdown numbered list`() = doTest( + """ +/// 1. The number of "items" in global storage. +/// 2. The number of bytes in global storage. +module 0x1::M {} + //^ + """, """ +
module 0x1::M
+
  1. The number of "items" in global storage.
  2. The number of bytes in global storage.
+ """ + ) + + private fun doTest( + @Language("Move") code: String, + @Language("Html") expected: String?, + ) { + doTest(code, expected, hideStyles = false) + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/utils/tests/MoveDocumentationProviderTestCase.kt b/src/test/kotlin/org/move/utils/tests/MoveDocumentationProviderTestCase.kt index 821c76f71..0d5c7f6ac 100644 --- a/src/test/kotlin/org/move/utils/tests/MoveDocumentationProviderTestCase.kt +++ b/src/test/kotlin/org/move/utils/tests/MoveDocumentationProviderTestCase.kt @@ -5,8 +5,10 @@ import com.intellij.platform.backend.documentation.DocumentationData import com.intellij.platform.backend.documentation.DocumentationResult import com.intellij.psi.PsiElement import org.intellij.lang.annotations.Language +import org.move.ide.docs.MvDocumentationTarget import org.move.ide.docs.MvPsiDocumentationTargetProvider import org.move.lang.core.psi.MvElement +import org.move.stdext.chainIf import org.move.utils.tests.base.findElementAndOffsetInEditor abstract class MvDocumentationProviderProjectTestCase : MvProjectTestBase() { @@ -37,10 +39,11 @@ abstract class MvDocumentationProviderTestCase : MvTestBase() { protected fun doTest( @Language("Move") code: String, @Language("Html") expected: String?, + hideStyles: Boolean = true, findElement: () -> Pair = { myFixture.findElementAndOffsetInEditor() }, ) { @Suppress("NAME_SHADOWING") - doTest(code, expected, findElement) { actual, expected -> + doTest(code, expected, findElement, hideStyles) { actual, expected -> assertSameLines(expected.trimIndent(), actual) } } @@ -57,10 +60,12 @@ abstract class MvDocumentationProviderTestCase : MvTestBase() { // } // } + @Suppress("OverrideOnly") protected fun doTest( @Language("Move") code: String, expected: T?, findElement: () -> Pair = { myFixture.findElementAndOffsetInEditor() }, + hideStyles: Boolean = true, check: (String, T) -> Unit ) { InlineFile(myFixture, code, "main.move") @@ -72,7 +77,7 @@ abstract class MvDocumentationProviderTestCase : MvTestBase() { val target = provider.documentationTarget(element, originalElement)!! val doc = target.computeDocumentation() - val actual = doc?.content() + val actual = doc?.content()?.chainIf(hideStyles) { hideSpecificStyles() } if (expected == null) { check(actual == null) { "Expected null, got `$actual`" } } else { @@ -80,6 +85,12 @@ abstract class MvDocumentationProviderTestCase : MvTestBase() { check(actual, expected) } } + + protected fun String.hideSpecificStyles(): String = replace(STYLE_REGEX, """style="..."""") + + companion object { + private val STYLE_REGEX = Regex("""style=".*?"""") + } } @Suppress("UnstableApiUsage")