Skip to content

Commit

Permalink
port inline inlay type hints to the new API
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurnikov committed Oct 24, 2024
1 parent 7ab5799 commit e1db952
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 93 deletions.
172 changes: 172 additions & 0 deletions src/main/kotlin/org/move/ide/hints/type/MvTypeHintsFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package org.move.ide.hints.type

import com.intellij.codeInsight.hints.declarative.CollapseState.Collapsed
import com.intellij.codeInsight.hints.declarative.CollapseState.Expanded
import com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder
import org.move.ide.presentation.hintText
import org.move.lang.core.types.ty.Ty
import org.move.lang.core.types.ty.TyAdt
import org.move.lang.core.types.ty.TyTuple
import org.move.lang.core.types.ty.TyVector

object MvTypeHintsFactory {
private const val COLLAPSE_FROM_LEVEL: Int = 2
//
// private const val CAPTURE_OF = "capture of "
// private const val UNNAMED_MARK = "<unnamed>"
private const val ANONYMOUS_MARK = "<anonymous>"

fun typeHint(type: Ty, treeBuilder: PresentationTreeBuilder) {
treeBuilder.typeHint(0, type)
}

private fun PresentationTreeBuilder.typeHint(level: Int, type: Ty) {
when (type) {
is TyTuple -> tupleTypeHint(level, type)
is TyAdt -> adtTypeHint(level, type)
is TyVector -> vectorTypeHint(level, type)
// is PsiClassType -> classTypeHint(level, type)
// is PsiDisjunctionType -> {
// join(
// type.disjunctions,
// op = {
// typeHint(level, it)
// },
// separator = {
// text(" | ")
// }
// )
// }
// is PsiIntersectionType -> {
// join(
// type.conjuncts,
// op = {
// typeHint(level, it)
// },
// separator = {
// text(" & ")
// }
// )
// }
else -> {
text(type.hintText())
}
}
}

private fun PresentationTreeBuilder.adtTypeHint(level: Int, tyAdt: TyAdt) {
val itemName = tyAdt.item.name ?: ANONYMOUS_MARK
text(itemName)
if (tyAdt.typeArguments.isEmpty()) return
collapsibleList(
state = if (level < COLLAPSE_FROM_LEVEL) Expanded else Collapsed,
expandedState = {
toggleButton { text("<") }
join(
tyAdt.typeArguments,
op = {
typeHint(level + 1, it)
},
separator = { text(", ") }
)
toggleButton { text(">") }
},
collapsedState = {
toggleButton { text("<...>") }
})
}

private fun PresentationTreeBuilder.tupleTypeHint(level: Int, tyTuple: TyTuple) {
collapsibleList(
state = if (level < COLLAPSE_FROM_LEVEL) Expanded else Collapsed,
expandedState = {
toggleButton { text("(") }
join(
tyTuple.types,
op = {
typeHint(level + 1, it)
},
separator = { text(", ") }
)
toggleButton { text(")") }
},
collapsedState = {
toggleButton { text("(...)") }
})
}

private fun PresentationTreeBuilder.vectorTypeHint(level: Int, tyVector: TyVector) {
text("vector")
collapsibleList(
state = if (level < COLLAPSE_FROM_LEVEL) Expanded else Collapsed,
expandedState = {
toggleButton { text("[") }
typeHint(level + 1, tyVector.item)
toggleButton { text("]") }
},
collapsedState = {
toggleButton { text("[...]") }
})
}

// private fun PresentationTreeBuilder.classTypeHint(level: Int, classType: PsiClassType) {
// val aClass = classType.resolve()
//
// val className = classType.className ?: ANONYMOUS_MARK // TODO here it may be not exactly true, the class might be unresolved
// text(className, aClass?.qualifiedName?.let { InlayActionData(StringInlayActionPayload(it), JavaFqnDeclarativeInlayActionHandler.HANDLER_NAME) })
// if (classType.parameterCount == 0) return
// collapsibleList(expandedState = {
// toggleButton {
// text("<")
// }
// join(
// classType.parameters,
// op = {
// typeHint(level + 1, it)
// },
// separator = {
// text(", ")
// }
// )
// toggleButton {
// text(">")
// }
// }, collapsedState = {
// toggleButton {
// text("<...>")
// }
// })
// }

// private fun <T> PresentationTreeBuilder.join(
// elements: Array<T>,
// op: PresentationTreeBuilder.(T) -> Unit,
// separator: PresentationTreeBuilder.() -> Unit
// ) {
// var isFirst = true
// for (element in elements) {
// if (isFirst) {
// isFirst = false
// } else {
// separator()
// }
// op(this, element)
// }
// }

private fun <T> PresentationTreeBuilder.join(
elements: List<T>,
op: PresentationTreeBuilder.(T) -> Unit,
separator: PresentationTreeBuilder.() -> Unit
) {
var isFirst = true
for (element in elements) {
if (isFirst) {
isFirst = false
} else {
separator()
}
op(this, element)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.move.ide.hints.type

import com.intellij.codeInsight.hints.declarative.*
import com.intellij.codeInsight.hints.declarative.HintFontSize.ABitSmallerThanInEditor
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import org.move.ide.presentation.hintText
import org.move.lang.core.psi.MvLetStmt
import org.move.lang.core.psi.MvPatBinding
import org.move.lang.core.psi.ext.bindingOwner
import org.move.lang.core.psi.ext.endOffset
import org.move.lang.core.psi.ext.hasAncestor
import org.move.lang.core.psi.ext.isMsl
import org.move.lang.core.types.infer.inferenceOwner
import org.move.lang.core.types.infer.inference
import org.move.lang.core.types.ty.*

class MvTypeInlayHintsProvider2: InlayHintsProvider {

override fun createCollector(file: PsiFile, editor: Editor): InlayHintsCollector = Collector()

private class Collector: SharedBypassCollector {
override fun collectFromElement(element: PsiElement, sink: InlayTreeSink) {
when (element) {
is MvPatBinding -> showTypeForPatBinding(element, sink)
}
}

private fun showTypeForPatBinding(patBinding: MvPatBinding, sink: InlayTreeSink) {
// skip private variables
if (patBinding.name.startsWith("_")) return

// only show bindings for let statements
if (patBinding.bindingOwner !is MvLetStmt) return

val contextInferenceOwner = patBinding.inferenceOwner() ?: return

val msl = patBinding.isMsl()
val ty = contextInferenceOwner.inference(msl).getBindingType(patBinding)
if (ty is TyUnknown) return

val pos = InlineInlayPosition(patBinding.endOffset, false)
val format = HintFormat.default.withFontSize(ABitSmallerThanInEditor)
sink.addPresentation(pos, hintFormat = format) {
text(": ")
MvTypeHintsFactory.typeHint(ty, this)
}
}
}

companion object {
const val PROVIDER_ID: String = "org.move.hints.types"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,14 @@ fun MvInferenceContextOwner.inference(msl: Boolean): InferenceResult {
}
}

fun MvElement.inferenceOwner(): MvInferenceContextOwner? = this.ancestorOrSelf()

fun MvElement.inference(msl: Boolean): InferenceResult? {
val contextOwner = this.ancestorOrSelf<MvInferenceContextOwner>() ?: return null
val contextOwner = inferenceOwner() ?: return null
return contextOwner.inference(msl)
}


//data class ResolvedPath(val element: MvElement, val isVisible: Boolean) {
// companion object {
// fun from(entry: ScopeEntry, context: MvElement): ResolvedPath {
Expand Down
11 changes: 9 additions & 2 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,15 @@
implementationClass="org.move.ide.hints.StructLitFieldsInfoHandler" />
<codeInsight.parameterNameHints language="Move"
implementationClass="org.move.ide.hints.MvInlayParameterHintsProvider" />
<codeInsight.inlayProvider language="Move"
implementationClass="org.move.ide.hints.type.MvInlayTypeHintsProvider" />

<!-- <codeInsight.inlayProvider language="Move"-->
<!-- implementationClass="org.move.ide.hints.type.MvInlayTypeHintsProvider" />-->
<codeInsight.declarativeInlayProvider group="TYPES_GROUP"
implementationClass="org.move.ide.hints.type.MvTypeInlayHintsProvider2"
isEnabledByDefault="true"
language="Move"
providerId="org.move.hints.types"
nameKey="inlay.hints.types"/>

<defaultLiveTemplates file="liveTemplates/Move.xml" />
<liveTemplateContext implementation="org.move.ide.liveTemplates.MvContextType$Generic" />
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/MvBundle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
inlay.hints.types=Types

advanced.setting.aptos.group=Aptos

advanced.setting.org.move.aptos.test.tool.window=Show test results in Test Tool Window
Expand Down
124 changes: 124 additions & 0 deletions src/test/kotlin/org/move/ide/hints/InlayTypeHintsProvider2Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.move.ide.hints

import com.intellij.testFramework.utils.inlays.declarative.DeclarativeInlayHintsProviderTestCase
import org.intellij.lang.annotations.Language
import org.move.ide.hints.type.MvTypeInlayHintsProvider2

class InlayTypeHintsProvider2Test: DeclarativeInlayHintsProviderTestCase() {
fun `test primitive type`() = checkByText("""
module 0x1::m {
fun call(): bool { true }
fun main() {
let a/*<# : |bool #>*/ = call();
}
}
""")

fun `test tuple type`() = checkByText("""
module 0x1::m {
fun call(): (bool, u8) { (true, 1) }
fun main() {
let t/*<# : |(|bool|, |u8|) #>*/ = call();
}
}
""")

fun `test tuple type destructured`() = checkByText("""
module 0x1::m {
fun call(): (bool, u8) { (true, 1) }
fun main() {
let (a/*<# : |bool #>*/, b/*<# : |u8 #>*/) = call();
}
}
""")

fun `test struct type`() = checkByText("""
module 0x1::m {
struct S {}
fun call(): S {}
fun main() {
let s/*<# : |S #>*/ = call();
}
}
""")

fun `test struct type with generics`() = checkByText("""
module 0x1::m {
struct S<R> {}
fun call<R>(): S<R> {}
fun main() {
let s/*<# : |S|<|u8|> #>*/ = call<u8>();
}
}
""")

fun `test vector type`() = checkByText("""
module 0x1::m {
fun main() {
let a/*<# : |vector|[|integer|] #>*/ = vector[1];
}
}
""")

fun `test uninferred vector type for unknown item type`() = checkByText(
"""
module 0x1::m {
fun main() {
let a/*<# : |vector|[|?|] #>*/ = vector[unknown()];
}
}
"""
)

fun `test vector uninferred type`() = checkByText(
"""
module 0x1::m {
fun main() {
let a/*<# : |vector|[|?|] #>*/ = vector[];
}
}
"""
)

fun `test no type hint for unknown type`() = checkByText(
"""
module 0x1::m {
fun main() {
let a = unknown();
}
}
"""
)

fun `test no type hints for function parameters`() = checkByText(
"""
module 0x1::m {
fun main(a: u8) {
}
}
"""
)

fun `test no hints for private variables`() = checkByText("""
module 0x1::m {
fun main() {
let _ = 1;
let _a = 1;
let (_a, _b) = (1, 1);
}
}
""")

fun `test let stmt without expr`() = checkByText("""
module 0x1::m {
fun main() {
let a/*<# : |integer #>*/;
a = 1;
}
}
""")

private fun checkByText(@Language("Move") code: String) {
doTestProvider("main.move", code, MvTypeInlayHintsProvider2())
}
}
Loading

0 comments on commit e1db952

Please sign in to comment.