Skip to content

Commit

Permalink
Merge pull request #268 from pontem-network/add-fields-completion-to-…
Browse files Browse the repository at this point in the history
…struct-lit

a number of fixes for struct field completion and completion priority
  • Loading branch information
mkurnikov authored Jan 15, 2025
2 parents 098638b + c5b7196 commit 18cc79d
Show file tree
Hide file tree
Showing 18 changed files with 205 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.move.lang.core.completion

import com.intellij.codeInsight.completion.CompletionConfidence
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.util.ThreeState
import org.move.lang.MvElementTypes.IDENTIFIER
import org.move.lang.core.psi.MvPatBinding
import org.move.lang.core.psi.MvLetStmt
import org.move.lang.core.psi.MvPatField
import org.move.lang.core.psi.ext.elementType
import org.move.lang.core.psi.ext.bindingOwner

Expand All @@ -17,8 +17,12 @@ class MvCompletionConfidence : CompletionConfidence() {
// If the identifier is uppercase, the user probably wants to type a destructuring pattern
// (`let Foo { ... }`), so we show the completion popup in this case
if (contextElement.elementType == IDENTIFIER) {
val parent = contextElement.parent
if (parent is MvPatBinding && parent.bindingOwner is MvLetStmt) {
val binding = contextElement.parent
if (binding is MvPatBinding && binding.bindingOwner is MvLetStmt) {
// let S { va/*caret*/ }
if (binding.parent is MvPatField) {
return ThreeState.UNSURE
}
val identText = contextElement.node.chars
if (identText.firstOrNull()?.isLowerCase() == true) {
return ThreeState.YES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.move.lang.core.resolve2.processFieldLookupResolveVariants
import org.move.lang.core.resolve2.processLabelResolveVariants
import org.move.lang.core.resolve2.processMethodResolveVariants
import org.move.lang.core.resolve2.processPatBindingResolveVariants
import org.move.lang.core.resolve2.processStructPatFieldResolveVariants
import org.move.lang.core.types.infer.InferenceContext
import org.move.lang.core.types.infer.substitute
import org.move.lang.core.types.ty.*
Expand All @@ -47,7 +48,7 @@ object CommonCompletionProvider: MvCompletionProvider() {

// handles dot expr
if (element is MvMethodOrField) {
addMethodOrFieldVariants(element, result)
addMethodOrFieldVariants(element, result, completionCtx)
}

addCompletionVariants(element, result, completionCtx)
Expand All @@ -69,6 +70,11 @@ object CommonCompletionProvider: MvCompletionProvider() {
val processor = skipAlreadyProvidedFields(element, processor0)
processPatBindingResolveVariants(element, true, processor)
}
// `let Res { my_f/*caret*/: field }`
is MvPatFieldFull -> {
val processor = skipAlreadyProvidedFields(element, processor0)
processStructPatFieldResolveVariants(element, processor)
}
// loop labels
is MvLabel -> processLabelResolveVariants(element, it)
// `spec ITEM {}` module items, where ITEM is a reference to the function/struct/enum
Expand All @@ -78,30 +84,26 @@ object CommonCompletionProvider: MvCompletionProvider() {
}

@VisibleForTesting
fun addMethodOrFieldVariants(element: MvMethodOrField, result: CompletionResultSet) {
val msl = element.isMsl()
val receiverTy = element.inferReceiverTy(msl).knownOrNull() ?: return
val expectedTy = getExpectedTypeForEnclosingPathOrDotExpr(element, msl)

val ctx = MvCompletionContext(element, msl, expectedTy)
fun addMethodOrFieldVariants(element: MvMethodOrField, result: CompletionResultSet, ctx: MvCompletionContext) {
val receiverTy = element.inferReceiverTy(ctx.msl).knownOrNull() ?: return

val tyAdt = receiverTy.derefIfNeeded() as? TyAdt
if (tyAdt != null) {
collectCompletionVariants(result, ctx, subst = tyAdt.substitution) {
processFieldLookupResolveVariants(element, tyAdt, msl, it)
processFieldLookupResolveVariants(element, tyAdt, ctx.msl, it)
}
}

processMethodResolveVariants(element, receiverTy, ctx.msl, createProcessor { e ->
val function = e.element as? MvFunction ?: return@createProcessor
val subst = function.tyVarsSubst
val declaredFuncTy = function.functionTy(msl).substitute(subst) as TyFunction
val declaredFuncTy = function.functionTy(ctx.msl).substitute(subst) as TyFunction
val declaredSelfTy = declaredFuncTy.paramTypes.first()
val autoborrowedReceiverTy =
TyReference.autoborrow(receiverTy, declaredSelfTy)
?: error("unreachable, references always compatible")

val inferenceCtx = InferenceContext(msl)
val inferenceCtx = InferenceContext(ctx.msl)
inferenceCtx.combineTypes(declaredSelfTy, autoborrowedReceiverTy)

result.addElement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ fun getExpectedTypeForEnclosingPathOrDotExpr(element: MvReferenceElement, msl: B
is MvPathType,
is MvPathExpr,
is MvDotExpr -> {
val inference = (ancestor as MvElement).inference(msl) ?: return TyUnknown
val inference = ancestor.inference(msl) ?: return TyUnknown
return inferExpectedTy(ancestor, inference)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
import org.move.lang.core.completion.MvCompletionContext
import org.move.lang.core.completion.createLookupElement
import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.*
import org.move.lang.core.resolve2.ref.FieldResolveVariant
Expand Down Expand Up @@ -41,17 +42,24 @@ object StructFieldsCompletionProvider: MvCompletionProvider() {
when (element) {
is MvPatField -> {
val patStruct = element.patStruct
// Path resolution is cached, but sometimes path changes so much that it can't be retrieved
// from cache anymore. In this case we need to get the old path.
// OLD: "safe" here means that if tree changes too much (=any of the ancestors of path are changed),
// then it's a no-op and we continue working with current path.
val struct = patStruct.path.maybeFieldsOwner ?: return
addFieldsToCompletion(
patStruct.path.maybeStruct ?: return,
struct,
patStruct.fieldNames,
result,
completionCtx
)
}
is MvStructLitField -> {
val structLit = element.parentStructLitExpr
// see MvPatField's comment above
val struct = structLit.path.maybeFieldsOwner ?: return
addFieldsToCompletion(
structLit.path.maybeStruct ?: return,
struct,
structLit.providedFieldNames,
result,
completionCtx
Expand All @@ -62,12 +70,12 @@ object StructFieldsCompletionProvider: MvCompletionProvider() {


private fun addFieldsToCompletion(
referredStruct: MvStruct,
fieldsOwner: MvFieldsOwner,
providedFieldNames: Set<String>,
result: CompletionResultSet,
completionContext: MvCompletionContext,
) {
for (field in referredStruct.namedFields.filter { it.name !in providedFieldNames }) {
for (field in fieldsOwner.namedFields.filter { it.name !in providedFieldNames }) {
val scopeEntry = FieldResolveVariant(field.name, field)
createLookupElement(scopeEntry, completionContext)
result.addElement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ private fun preferTrue(
property: (P) -> Boolean,
id: String
): MvCompletionWeigher = object : MvCompletionWeigher {
override fun weigh(element: LookupElement): Boolean =
if (element is MvLookupElement) !property(element.props) else true
override fun weigh(element: LookupElement): Boolean {
return if (element is MvLookupElement) !property(element.props) else true
}

override val id: String get() = id
}
Expand Down Expand Up @@ -173,8 +174,9 @@ private fun splitIntoGroups(weighersWithAnchors: List<Any>): List<AnchoredWeighe
private class RsCompletionWeigherAsLookupElementWeigher(
private val weigher: MvCompletionWeigher
) : LookupElementWeigher(weigher.id, /* negated = */ false, /* dependsOnPrefix = */ false) {

override fun weigh(element: LookupElement): Comparable<*> {
val rsElement = element.`as`(LookupElement::class.java)
return weigher.weigh(rsElement ?: element)
val mvLookupElement = element.`as`(MvLookupElement::class.java)
return weigher.weigh(mvLookupElement ?: element)
}
}
2 changes: 2 additions & 0 deletions src/main/kotlin/org/move/lang/core/psi/ext/MvPath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ val MvPath.identifierName: String? get() = identifier?.text

val MvPath.maybeStruct get() = reference?.resolveFollowingAliases() as? MvStruct

val MvPath.maybeFieldsOwner get() = reference?.resolveFollowingAliases() as? MvFieldsOwner

val MvPath.maybeSchema get() = reference?.resolveFollowingAliases() as? MvSchema

//fun MvPath.allowedNamespaces(isCompletion: Boolean = false): Set<Namespace> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package org.move.lang.core.psi.ext

import com.intellij.lang.ASTNode
import org.move.lang.MvElementTypes
import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.completion.safeGetOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.resolve.RsResolveProcessor
import org.move.lang.core.resolve.SimpleScopeEntry
Expand Down
21 changes: 11 additions & 10 deletions src/main/kotlin/org/move/lang/core/resolve2/NameResolution2.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.move.lang.core.resolve2

import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.*
import org.move.lang.core.resolve.*
Expand Down Expand Up @@ -66,9 +67,9 @@ fun processStructPatFieldResolveVariants(
patFieldFull: MvPatFieldFull,
processor: RsResolveProcessor
): Boolean {
val resolved = patFieldFull.patStruct.path.reference?.resolveFollowingAliases()
val resolvedStruct = resolved as? MvFieldsOwner ?: return false
return processNamedFieldDeclarations(resolvedStruct, processor)
// used in completion
val fieldsOwner = patFieldFull.patStruct.path.maybeFieldsOwner ?: return false
return processNamedFieldDeclarations(fieldsOwner, processor)
}

fun processPatBindingResolveVariants(
Expand All @@ -79,10 +80,10 @@ fun processPatBindingResolveVariants(
// field pattern shorthand
if (binding.parent is MvPatField) {
val parentPat = binding.parent.parent as MvPatStruct
val structItem = parentPat.path.reference?.resolveFollowingAliases()
val fieldsOwner = parentPat.path.maybeFieldsOwner
// can be null if unresolved
if (structItem is MvFieldsOwner) {
if (processNamedFieldDeclarations(structItem, originalProcessor)) return true
if (fieldsOwner != null) {
if (processNamedFieldDeclarations(fieldsOwner, originalProcessor)) return true
if (isCompletion) return false
}
}
Expand Down Expand Up @@ -254,14 +255,14 @@ fun walkUpThroughScopes(
return false
}

private fun processFieldDeclarations(item: MvFieldsOwner, processor: RsResolveProcessor): Boolean =
item.fields.any { field ->
private fun processFieldDeclarations(fieldsOwner: MvFieldsOwner, processor: RsResolveProcessor): Boolean =
fieldsOwner.fields.any { field ->
val name = field.name ?: return@any false
processor.process(name, NAMES, field)
}

private fun processNamedFieldDeclarations(struct: MvFieldsOwner, processor: RsResolveProcessor): Boolean =
struct.namedFields.any { field ->
private fun processNamedFieldDeclarations(fieldsOwner: MvFieldsOwner, processor: RsResolveProcessor): Boolean =
fieldsOwner.namedFields.any { field ->
val name = field.name
processor.process(name, NAMES, field)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.move.lang.core.resolve2.ref

import com.intellij.psi.ResolveResult
import org.move.cli.MoveProject
import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.*
import org.move.lang.core.resolve.*
Expand Down Expand Up @@ -64,7 +65,10 @@ class MvPath2ReferenceImpl(element: MvPath): MvPolyVariantReferenceBase<MvPath>(
}

private fun getResolvedPathFromInference(path: MvPath, msl: Boolean): List<RsPathResolveResult<MvElement>>? {
return path.inference(msl)?.getResolvedPath(path)
// Path resolution is cached, but sometimes path changes so much that it can't be retrieved
// from cache anymore. In this case we need to get the old path.
val originalPath = path.getOriginalOrSelf()
return originalPath.inference(msl)?.getResolvedPath(originalPath)
?.map {
RsPathResolveResult(it.element, it.isVisible)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.move.lang.core.types.infer

import com.intellij.psi.PsiElement
import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.elementType
import org.move.lang.core.types.ty.Ty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.intellij.psi.util.CachedValue
import org.jetbrains.annotations.TestOnly
import org.move.cli.settings.isDebugModeEnabled
import org.move.ide.formatter.impl.location
import org.move.lang.core.completion.getOriginalOrSelf
import org.move.lang.core.completion.safeGetOriginalOrSelf
import org.move.lang.core.psi.*
import org.move.lang.core.psi.ext.*
import org.move.lang.core.resolve.ScopeEntry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ class TypeInferenceWalker(
typeErrorEncountered = true
}
if (rightExpr != null) {
val rightTy = rightExpr.inferType()
val rightTy = rightExpr.inferType(leftTy)
if (!rightTy.supportsArithmeticOp()) {
ctx.reportTypeError(TypeError.UnsupportedBinaryOp(rightExpr, rightTy, op))
typeErrorEncountered = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.move.lang.completion

import com.intellij.testFramework.fixtures.CompletionAutoPopupTester
import com.intellij.util.ThrowableRunnable
import org.intellij.lang.annotations.Language
import org.move.utils.tests.NamedAddress
import org.move.utils.tests.completion.CompletionTestCase

class CompletionAutoPopupTest: CompletionTestCase() {
private lateinit var tester: CompletionAutoPopupTester

fun `test popup is not shown when typing variable name`() = checkPopupIsNotShownAfterTyping(
"""
module 0x1::m {
struct MyStruct { val: u8 }
fun main() {
let tr/*caret*/
}
}
""", "u"
)

fun `test popup is shown when typing name starting with upper case`() = checkPopupIsShownAfterTyping(
"""
module 0x1::m {
struct MyStruct { val: u8 }
fun main() {
let Str/*caret*/
}
}
""", "u"
)

fun `test popup is shown for struct pat fields`() = checkPopupIsShownAfterTyping(
"""
module 0x1::m {
struct MyStruct { val: u8 }
fun main() {
let MyStruct { v/*caret*/ };
}
}
""", "a"
)

override fun setUp() {
super.setUp()
tester = CompletionAutoPopupTester(myFixture)
}

override fun runTestRunnable(testRunnable: ThrowableRunnable<Throwable>) {
tester.runWithAutoPopupEnabled(testRunnable)
}

override fun runInDispatchThread(): Boolean = false

private fun checkPopupIsShownAfterTyping(@Language("Move") code: String, toType: String) {
configureAndType(code, toType)
assertNotNull(tester.lookup)
}

private fun checkPopupIsNotShownAfterTyping(@Language("Move") code: String, toType: String) {
configureAndType(code, toType)
assertNull(tester.lookup)
}

private fun configureAndType(code: String, toType: String) {
InlineFile(code).withCaret()
tester.typeWithPauses(toType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.move.lang.core.psi.MvQualNamedElement
import org.move.utils.tests.MoveV2
import org.move.utils.tests.completion.CompletionTestCase

class CompletionPrioritiesTest : CompletionTestCase() {
class CompletionPrioritiesTest: CompletionTestCase() {
fun `test local before builtin before unimported`() = checkCompletionsOrder(
listOf("borrow_local", "borrow_global", "borrow_global_mut", "borrow"),
"""
Expand Down Expand Up @@ -199,6 +199,17 @@ module 0x1::Main {
"""
)

fun `test use binary op types for completion sorting`() = checkCompletionsOrder(
listOf("def_val_2", "def_val"), """
module std::modules {
struct Ss { def_val: u8, def_val_2: u16 }
fun main(s: Ss) {
1u16 + s.de/*caret*/;
}
}
""".trimIndent()
)

private fun checkCompletionsOrder(listStart: List<String>, @Language("Move") code: String) {
val variants = completionFixture.invokeCompletion(code)
val lookupStrings = variants.map { it.lookupString }
Expand Down
Loading

0 comments on commit 18cc79d

Please sign in to comment.